package cmd

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/steveyegge/gastown/internal/beads"
	"github.com/steveyegge/gastown/internal/git"
	"github.com/steveyegge/gastown/internal/polecat"
	"github.com/steveyegge/gastown/internal/rig"
	"github.com/steveyegge/gastown/internal/runtime"
	"github.com/steveyegge/gastown/internal/style"
	"github.com/steveyegge/gastown/internal/tmux"
)

// Polecat command flags
var (
	polecatListJSON  bool
	polecatListAll   bool
	polecatForce     bool
	polecatRemoveAll bool
)

var polecatCmd = &cobra.Command{
	Use:     "polecat",
	Aliases: []string{"cat", "polecats"},
	GroupID: GroupAgents,
	Short:   "Manage polecats in rigs",
	RunE:    requireSubcommand,
	Long: `Manage polecat lifecycle in rigs.

Polecats are worker agents that operate in their own git worktrees.
Use the subcommands to add, remove, list, wake, and sleep polecats.`,
}

var polecatListCmd = &cobra.Command{
	Use:   "list [rig]",
	Short: "List polecats in a rig",
	Long: `List polecats in a rig or all rigs.

In the transient model, polecats exist only while working. The list shows
all currently active polecats with their states:
  - working: Actively working on an issue
  - done: Completed work, waiting for cleanup
  - stuck: Needs assistance

Examples:
  gt polecat list greenplace
  gt polecat list --all
  gt polecat list greenplace --json`,
	RunE: runPolecatList,
}

var polecatAddCmd = &cobra.Command{
	Use:   "add <rig> <name>",
	Short: "Add a new polecat to a rig",
	Long: `Add a new polecat to a rig.

Creates a polecat directory, clones the rig repo, creates a work branch,
and initializes state.

Example:
  gt polecat add greenplace Toast`,
	Args: cobra.ExactArgs(2),
	RunE: runPolecatAdd,
}

var polecatRemoveCmd = &cobra.Command{
	Use:   "remove <rig>/<polecat>... | <rig> --all",
	Short: "Remove polecats from a rig",
	Long: `Remove one or more polecats from a rig.

Fails if session is running (stop first).
Warns if uncommitted changes exist.
Use --force to bypass checks.

Examples:
  gt polecat remove greenplace/Toast
  gt polecat remove greenplace/Toast greenplace/Furiosa
  gt polecat remove greenplace --all
  gt polecat remove greenplace --all --force`,
	Args: cobra.MinimumNArgs(1),
	RunE: runPolecatRemove,
}

var polecatSyncCmd = &cobra.Command{
	Use:   "sync <rig>/<polecat>",
	Short: "Sync beads for a polecat",
	Long: `Sync beads for a polecat's worktree.

Runs 'bd sync' in the polecat's worktree to push local beads changes
to the shared sync branch and pull remote changes.

Use --all to sync all polecats in a rig.
Use --from-main to only pull (no push).

Examples:
  gt polecat sync greenplace/Toast
  gt polecat sync greenplace --all
  gt polecat sync greenplace/Toast --from-main`,
	Args: cobra.MaximumNArgs(1),
	RunE: runPolecatSync,
}

var polecatStatusCmd = &cobra.Command{
	Use:   "status <rig>/<polecat>",
	Short: "Show detailed status for a polecat",
	Long: `Show detailed status for a polecat.

Displays comprehensive information including:
  - Current lifecycle state (working, done, stuck, idle)
  - Assigned issue (if any)
  - Session status (running/stopped, attached/detached)
  - Session creation time
  - Last activity time

Examples:
  gt polecat status greenplace/Toast
  gt polecat status greenplace/Toast --json`,
	Args: cobra.ExactArgs(1),
	RunE: runPolecatStatus,
}

var (
	polecatSyncAll           bool
	polecatSyncFromMain      bool
	polecatStatusJSON        bool
	polecatGitStateJSON      bool
	polecatGCDryRun          bool
	polecatNukeAll           bool
	polecatNukeDryRun        bool
	polecatNukeForce         bool
	polecatCheckRecoveryJSON bool
)

var polecatGCCmd = &cobra.Command{
	Use:   "gc <rig>",
	Short: "Garbage collect stale polecat branches",
	Long: `Garbage collect stale polecat branches in a rig.

Polecats use unique timestamped branches (polecat/<name>-<timestamp>) to
prevent drift issues. Over time, these branches accumulate when stale
polecats are repaired.

This command removes orphaned branches:
  - Branches for polecats that no longer exist
  - Old timestamped branches (keeps only the current one per polecat)

Examples:
  gt polecat gc greenplace
  gt polecat gc greenplace --dry-run`,
	Args: cobra.ExactArgs(1),
	RunE: runPolecatGC,
}

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)

SAFETY CHECKS: The command refuses to nuke a polecat if:
  - Worktree has unpushed/uncommitted changes
  - Polecat has an open merge request (MR bead)
  - Polecat has work on its hook

Use --force to bypass safety checks (LOSES WORK).
Use --dry-run to see what would happen and safety check status.

Examples:
  gt polecat nuke greenplace/Toast
  gt polecat nuke greenplace/Toast greenplace/Furiosa
  gt polecat nuke greenplace --all
  gt polecat nuke greenplace --all --dry-run
  gt polecat nuke greenplace/Toast --force  # bypass safety checks`,
	Args: cobra.MinimumNArgs(1),
	RunE: runPolecatNuke,
}

var polecatGitStateCmd = &cobra.Command{
	Use:   "git-state <rig>/<polecat>",
	Short: "Show git state for pre-kill verification",
	Long: `Show git state for a polecat's worktree.

Used by the Witness for pre-kill verification to ensure no work is lost.
Returns whether the worktree is clean (safe to kill) or dirty (needs cleanup).

Checks:
  - Working tree: uncommitted changes
  - Unpushed commits: commits ahead of origin/main
  - Stashes: stashed changes

Examples:
  gt polecat git-state greenplace/Toast
  gt polecat git-state greenplace/Toast --json`,
	Args: cobra.ExactArgs(1),
	RunE: runPolecatGitState,
}

var polecatCheckRecoveryCmd = &cobra.Command{
	Use:   "check-recovery <rig>/<polecat>",
	Short: "Check if polecat needs recovery vs safe to nuke",
	Long: `Check recovery status of a polecat based on cleanup_status in agent bead.

Used by the Witness to determine appropriate cleanup action:
  - SAFE_TO_NUKE: cleanup_status is 'clean' - no work at risk
  - NEEDS_RECOVERY: cleanup_status indicates unpushed/uncommitted work

This prevents accidental data loss when cleaning up dormant polecats.
The Witness should escalate NEEDS_RECOVERY cases to the Mayor.

Examples:
  gt polecat check-recovery greenplace/Toast
  gt polecat check-recovery greenplace/Toast --json`,
	Args: cobra.ExactArgs(1),
	RunE: runPolecatCheckRecovery,
}

var (
	polecatStaleJSON      bool
	polecatStaleThreshold int
	polecatStaleCleanup   bool
)

var polecatStaleCmd = &cobra.Command{
	Use:   "stale <rig>",
	Short: "Detect stale polecats that may need cleanup",
	Long: `Detect stale polecats in a rig that are candidates for cleanup.

A polecat is considered stale if:
  - No active tmux session
  - Way behind main (>threshold commits) OR no agent bead
  - Has no uncommitted work that could be lost

The default threshold is 20 commits behind main.

Use --cleanup to automatically nuke stale polecats that are safe to remove.
Use --dry-run with --cleanup to see what would be cleaned.

Examples:
  gt polecat stale greenplace
  gt polecat stale greenplace --threshold 50
  gt polecat stale greenplace --json
  gt polecat stale greenplace --cleanup
  gt polecat stale greenplace --cleanup --dry-run`,
	Args: cobra.ExactArgs(1),
	RunE: runPolecatStale,
}

func init() {
	// List flags
	polecatListCmd.Flags().BoolVar(&polecatListJSON, "json", false, "Output as JSON")
	polecatListCmd.Flags().BoolVar(&polecatListAll, "all", false, "List polecats in all rigs")

	// Remove flags
	polecatRemoveCmd.Flags().BoolVarP(&polecatForce, "force", "f", false, "Force removal, bypassing checks")
	polecatRemoveCmd.Flags().BoolVar(&polecatRemoveAll, "all", false, "Remove all polecats in the rig")

	// Sync flags
	polecatSyncCmd.Flags().BoolVar(&polecatSyncAll, "all", false, "Sync all polecats in the rig")
	polecatSyncCmd.Flags().BoolVar(&polecatSyncFromMain, "from-main", false, "Pull only, no push")

	// Status flags
	polecatStatusCmd.Flags().BoolVar(&polecatStatusJSON, "json", false, "Output as JSON")

	// Git-state flags
	polecatGitStateCmd.Flags().BoolVar(&polecatGitStateJSON, "json", false, "Output as JSON")

	// 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")
	polecatNukeCmd.Flags().BoolVarP(&polecatNukeForce, "force", "f", false, "Force nuke, bypassing all safety checks (LOSES WORK)")

	// Check-recovery flags
	polecatCheckRecoveryCmd.Flags().BoolVar(&polecatCheckRecoveryJSON, "json", false, "Output as JSON")

	// Stale flags
	polecatStaleCmd.Flags().BoolVar(&polecatStaleJSON, "json", false, "Output as JSON")
	polecatStaleCmd.Flags().IntVar(&polecatStaleThreshold, "threshold", 20, "Commits behind main to consider stale")
	polecatStaleCmd.Flags().BoolVar(&polecatStaleCleanup, "cleanup", false, "Automatically nuke stale polecats")

	// Add subcommands
	polecatCmd.AddCommand(polecatListCmd)
	polecatCmd.AddCommand(polecatAddCmd)
	polecatCmd.AddCommand(polecatRemoveCmd)
	polecatCmd.AddCommand(polecatSyncCmd)
	polecatCmd.AddCommand(polecatStatusCmd)
	polecatCmd.AddCommand(polecatGitStateCmd)
	polecatCmd.AddCommand(polecatCheckRecoveryCmd)
	polecatCmd.AddCommand(polecatGCCmd)
	polecatCmd.AddCommand(polecatNukeCmd)
	polecatCmd.AddCommand(polecatStaleCmd)

	rootCmd.AddCommand(polecatCmd)
}

// PolecatListItem represents a polecat in list output.
type PolecatListItem struct {
	Rig            string        `json:"rig"`
	Name           string        `json:"name"`
	State          polecat.State `json:"state"`
	Issue          string        `json:"issue,omitempty"`
	SessionRunning bool          `json:"session_running"`
}

// getPolecatManager creates a polecat manager for the given rig.
func getPolecatManager(rigName string) (*polecat.Manager, *rig.Rig, error) {
	_, r, err := getRig(rigName)
	if err != nil {
		return nil, nil, err
	}

	polecatGit := git.NewGit(r.Path)
	mgr := polecat.NewManager(r, polecatGit)

	return mgr, r, nil
}

func runPolecatList(cmd *cobra.Command, args []string) error {
	var rigs []*rig.Rig

	if polecatListAll {
		// List all rigs
		allRigs, _, err := getAllRigs()
		if err != nil {
			return err
		}
		rigs = allRigs
	} else {
		// Need a rig name
		if len(args) < 1 {
			return fmt.Errorf("rig name required (or use --all)")
		}
		_, r, err := getPolecatManager(args[0])
		if err != nil {
			return err
		}
		rigs = []*rig.Rig{r}
	}

	// Collect polecats from all rigs
	t := tmux.NewTmux()
	var allPolecats []PolecatListItem

	for _, r := range rigs {
		polecatGit := git.NewGit(r.Path)
		mgr := polecat.NewManager(r, polecatGit)
		polecatMgr := polecat.NewSessionManager(t, r)

		polecats, err := mgr.List()
		if err != nil {
			fmt.Fprintf(os.Stderr, "warning: failed to list polecats in %s: %v\n", r.Name, err)
			continue
		}

		for _, p := range polecats {
			running, _ := polecatMgr.IsRunning(p.Name)
			allPolecats = append(allPolecats, PolecatListItem{
				Rig:            r.Name,
				Name:           p.Name,
				State:          p.State,
				Issue:          p.Issue,
				SessionRunning: running,
			})
		}
	}

	// Output
	if polecatListJSON {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		return enc.Encode(allPolecats)
	}

	if len(allPolecats) == 0 {
		fmt.Println("No active polecats found.")
		return nil
	}

	fmt.Printf("%s\n\n", style.Bold.Render("Active Polecats"))
	for _, p := range allPolecats {
		// Session indicator
		sessionStatus := style.Dim.Render("○")
		if p.SessionRunning {
			sessionStatus = style.Success.Render("●")
		}

		// Display actual state (no normalization - idle means idle)
		displayState := p.State

		// State color
		stateStr := string(displayState)
		switch displayState {
		case polecat.StateWorking:
			stateStr = style.Info.Render(stateStr)
		case polecat.StateStuck:
			stateStr = style.Warning.Render(stateStr)
		case polecat.StateDone:
			stateStr = style.Success.Render(stateStr)
		default:
			stateStr = style.Dim.Render(stateStr)
		}

		fmt.Printf("  %s %s/%s  %s\n", sessionStatus, p.Rig, p.Name, stateStr)
		if p.Issue != "" {
			fmt.Printf("    %s\n", style.Dim.Render(p.Issue))
		}
	}

	return nil
}

func runPolecatAdd(cmd *cobra.Command, args []string) error {
	rigName := args[0]
	polecatName := args[1]

	mgr, _, err := getPolecatManager(rigName)
	if err != nil {
		return err
	}

	fmt.Printf("Adding polecat %s to rig %s...\n", polecatName, rigName)

	p, err := mgr.Add(polecatName)
	if err != nil {
		return fmt.Errorf("adding polecat: %w", err)
	}

	fmt.Printf("%s Polecat %s added.\n", style.SuccessPrefix, p.Name)
	fmt.Printf("  %s\n", style.Dim.Render(p.ClonePath))
	fmt.Printf("  Branch: %s\n", style.Dim.Render(p.Branch))

	return nil
}

func runPolecatRemove(cmd *cobra.Command, args []string) error {
	// Build list of polecats to remove
	type polecatToRemove struct {
		rigName     string
		polecatName string
		mgr         *polecat.Manager
		r           *rig.Rig
	}
	var toRemove []polecatToRemove

	if polecatRemoveAll {
		// --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 remove greenplace --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 remove.")
			return nil
		}

		for _, p := range polecats {
			toRemove = append(toRemove, polecatToRemove{
				rigName:     rigName,
				polecatName: p.Name,
				mgr:         mgr,
				r:           r,
			})
		}
	} else {
		// Multiple rig/polecat arguments - require explicit rig/polecat format
		for _, arg := range args {
			// Validate format: must contain "/" to avoid misinterpreting rig names as polecat names
			if !strings.Contains(arg, "/") {
				return fmt.Errorf("invalid address '%s': must be in 'rig/polecat' format (e.g., 'gastown/Toast')", arg)
			}

			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
			}

			toRemove = append(toRemove, polecatToRemove{
				rigName:     rigName,
				polecatName: polecatName,
				mgr:         mgr,
				r:           r,
			})
		}
	}

	// Remove each polecat
	t := tmux.NewTmux()
	var removeErrors []string
	removed := 0

	for _, p := range toRemove {
		// Check if session is running
		if !polecatForce {
			polecatMgr := polecat.NewSessionManager(t, p.r)
			running, _ := polecatMgr.IsRunning(p.polecatName)
			if running {
				removeErrors = append(removeErrors, fmt.Sprintf("%s/%s: session is running (stop first or use --force)", p.rigName, p.polecatName))
				continue
			}
		}

		fmt.Printf("Removing polecat %s/%s...\n", p.rigName, p.polecatName)

		if err := p.mgr.Remove(p.polecatName, polecatForce); err != nil {
			if errors.Is(err, polecat.ErrHasChanges) {
				removeErrors = append(removeErrors, fmt.Sprintf("%s/%s: has uncommitted changes (use --force)", p.rigName, p.polecatName))
			} else {
				removeErrors = append(removeErrors, fmt.Sprintf("%s/%s: %v", p.rigName, p.polecatName, err))
			}
			continue
		}

		fmt.Printf("  %s removed\n", style.Success.Render("✓"))
		removed++
	}

	// Report results
	if len(removeErrors) > 0 {
		fmt.Printf("\n%s Some removals failed:\n", style.Warning.Render("Warning:"))
		for _, e := range removeErrors {
			fmt.Printf("  - %s\n", e)
		}
	}

	if removed > 0 {
		fmt.Printf("\n%s Removed %d polecat(s).\n", style.SuccessPrefix, removed)
	}

	if len(removeErrors) > 0 {
		return fmt.Errorf("%d removal(s) failed", len(removeErrors))
	}

	return nil
}

func runPolecatSync(cmd *cobra.Command, args []string) error {
	if len(args) < 1 {
		return fmt.Errorf("rig or rig/polecat address required")
	}

	// Parse address - could be "rig" or "rig/polecat"
	rigName, polecatName, err := parseAddress(args[0])
	if err != nil {
		// Might just be a rig name
		rigName = args[0]
		polecatName = ""
	}

	mgr, _, err := getPolecatManager(rigName)
	if err != nil {
		return err
	}

	// Get list of polecats to sync
	var polecatsToSync []string
	if polecatSyncAll || polecatName == "" {
		polecats, err := mgr.List()
		if err != nil {
			return fmt.Errorf("listing polecats: %w", err)
		}
		for _, p := range polecats {
			polecatsToSync = append(polecatsToSync, p.Name)
		}
	} else {
		polecatsToSync = []string{polecatName}
	}

	if len(polecatsToSync) == 0 {
		fmt.Println("No polecats to sync.")
		return nil
	}

	// Sync each polecat
	var syncErrors []string
	for _, name := range polecatsToSync {
		// Get polecat to get correct clone path (handles old vs new structure)
		p, err := mgr.Get(name)
		if err != nil {
			syncErrors = append(syncErrors, fmt.Sprintf("%s: %v", name, err))
			continue
		}

		// Check directory exists
		if _, err := os.Stat(p.ClonePath); os.IsNotExist(err) {
			syncErrors = append(syncErrors, fmt.Sprintf("%s: directory not found", name))
			continue
		}

		// Build sync command
		syncArgs := []string{"sync"}
		if polecatSyncFromMain {
			syncArgs = append(syncArgs, "--from-main")
		}

		fmt.Printf("Syncing %s/%s...\n", rigName, name)

		syncCmd := exec.Command("bd", syncArgs...)
		syncCmd.Dir = p.ClonePath
		output, err := syncCmd.CombinedOutput()
		if err != nil {
			syncErrors = append(syncErrors, fmt.Sprintf("%s: %v", name, err))
			if len(output) > 0 {
				fmt.Printf("  %s\n", style.Dim.Render(string(output)))
			}
		} else {
			fmt.Printf("  %s\n", style.Success.Render("✓ synced"))
		}
	}

	if len(syncErrors) > 0 {
		fmt.Printf("\n%s Some syncs failed:\n", style.Warning.Render("Warning:"))
		for _, e := range syncErrors {
			fmt.Printf("  - %s\n", e)
		}
		return fmt.Errorf("%d sync(s) failed", len(syncErrors))
	}

	return nil
}

// PolecatStatus represents detailed polecat status for JSON output.
type PolecatStatus struct {
	Rig            string        `json:"rig"`
	Name           string        `json:"name"`
	State          polecat.State `json:"state"`
	Issue          string        `json:"issue,omitempty"`
	ClonePath      string        `json:"clone_path"`
	Branch         string        `json:"branch"`
	SessionRunning bool          `json:"session_running"`
	SessionID      string        `json:"session_id,omitempty"`
	Attached       bool          `json:"attached,omitempty"`
	Windows        int           `json:"windows,omitempty"`
	CreatedAt      string        `json:"created_at,omitempty"`
	LastActivity   string        `json:"last_activity,omitempty"`
}

func runPolecatStatus(cmd *cobra.Command, args []string) error {
	rigName, polecatName, err := parseAddress(args[0])
	if err != nil {
		return err
	}

	mgr, r, err := getPolecatManager(rigName)
	if err != nil {
		return err
	}

	// Get polecat info
	p, err := mgr.Get(polecatName)
	if err != nil {
		return fmt.Errorf("polecat '%s' not found in rig '%s'", polecatName, rigName)
	}

	// Get session info
	t := tmux.NewTmux()
	polecatMgr := polecat.NewSessionManager(t, r)
	sessInfo, err := polecatMgr.Status(polecatName)
	if err != nil {
		// Non-fatal - continue without session info
		sessInfo = &polecat.SessionInfo{
			Polecat: polecatName,
			Running: false,
		}
	}

	// JSON output
	if polecatStatusJSON {
		status := PolecatStatus{
			Rig:            rigName,
			Name:           polecatName,
			State:          p.State,
			Issue:          p.Issue,
			ClonePath:      p.ClonePath,
			Branch:         p.Branch,
			SessionRunning: sessInfo.Running,
			SessionID:      sessInfo.SessionID,
			Attached:       sessInfo.Attached,
			Windows:        sessInfo.Windows,
		}
		if !sessInfo.Created.IsZero() {
			status.CreatedAt = sessInfo.Created.Format("2006-01-02 15:04:05")
		}
		if !sessInfo.LastActivity.IsZero() {
			status.LastActivity = sessInfo.LastActivity.Format("2006-01-02 15:04:05")
		}
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		return enc.Encode(status)
	}

	// Human-readable output
	fmt.Printf("%s\n\n", style.Bold.Render(fmt.Sprintf("Polecat: %s/%s", rigName, polecatName)))

	// State with color
	stateStr := string(p.State)
	switch p.State {
	case polecat.StateWorking:
		stateStr = style.Info.Render(stateStr)
	case polecat.StateStuck:
		stateStr = style.Warning.Render(stateStr)
	case polecat.StateDone:
		stateStr = style.Success.Render(stateStr)
	default:
		stateStr = style.Dim.Render(stateStr)
	}
	fmt.Printf("  State:         %s\n", stateStr)

	// Issue
	if p.Issue != "" {
		fmt.Printf("  Issue:         %s\n", p.Issue)
	} else {
		fmt.Printf("  Issue:         %s\n", style.Dim.Render("(none)"))
	}

	// Clone path and branch
	fmt.Printf("  Clone:         %s\n", style.Dim.Render(p.ClonePath))
	fmt.Printf("  Branch:        %s\n", style.Dim.Render(p.Branch))

	// Session info
	fmt.Println()
	fmt.Printf("%s\n", style.Bold.Render("Session"))

	if sessInfo.Running {
		fmt.Printf("  Status:        %s\n", style.Success.Render("running"))
		fmt.Printf("  Session ID:    %s\n", style.Dim.Render(sessInfo.SessionID))

		if sessInfo.Attached {
			fmt.Printf("  Attached:      %s\n", style.Info.Render("yes"))
		} else {
			fmt.Printf("  Attached:      %s\n", style.Dim.Render("no"))
		}

		if sessInfo.Windows > 0 {
			fmt.Printf("  Windows:       %d\n", sessInfo.Windows)
		}

		if !sessInfo.Created.IsZero() {
			fmt.Printf("  Created:       %s\n", sessInfo.Created.Format("2006-01-02 15:04:05"))
		}

		if !sessInfo.LastActivity.IsZero() {
			// Show relative time for activity
			ago := formatActivityTime(sessInfo.LastActivity)
			fmt.Printf("  Last Activity: %s (%s)\n",
				sessInfo.LastActivity.Format("15:04:05"),
				style.Dim.Render(ago))
		}
	} else {
		fmt.Printf("  Status:        %s\n", style.Dim.Render("not running"))
	}

	return nil
}

// formatActivityTime returns a human-readable relative time string.
func formatActivityTime(t time.Time) string {
	d := time.Since(t)
	switch {
	case d < time.Minute:
		return fmt.Sprintf("%d seconds ago", int(d.Seconds()))
	case d < time.Hour:
		return fmt.Sprintf("%d minutes ago", int(d.Minutes()))
	case d < 24*time.Hour:
		return fmt.Sprintf("%d hours ago", int(d.Hours()))
	default:
		return fmt.Sprintf("%d days ago", int(d.Hours()/24))
	}
}

// GitState represents the git state of a polecat's worktree.
type GitState struct {
	Clean            bool     `json:"clean"`
	UncommittedFiles []string `json:"uncommitted_files"`
	UnpushedCommits  int      `json:"unpushed_commits"`
	StashCount       int      `json:"stash_count"`
}

func runPolecatGitState(cmd *cobra.Command, args []string) error {
	rigName, polecatName, err := parseAddress(args[0])
	if err != nil {
		return err
	}

	mgr, r, err := getPolecatManager(rigName)
	if err != nil {
		return err
	}

	// Verify polecat exists
	p, err := mgr.Get(polecatName)
	if err != nil {
		return fmt.Errorf("polecat '%s' not found in rig '%s'", polecatName, rigName)
	}

	// Get git state from the polecat's worktree
	state, err := getGitState(p.ClonePath)
	if err != nil {
		return fmt.Errorf("getting git state: %w", err)
	}

	// JSON output
	if polecatGitStateJSON {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		return enc.Encode(state)
	}

	// Human-readable output
	fmt.Printf("%s\n\n", style.Bold.Render(fmt.Sprintf("Git State: %s/%s", r.Name, polecatName)))

	// Working tree status
	if len(state.UncommittedFiles) == 0 {
		fmt.Printf("  Working Tree:  %s\n", style.Success.Render("clean"))
	} else {
		fmt.Printf("  Working Tree:  %s\n", style.Warning.Render("dirty"))
		fmt.Printf("  Uncommitted:   %s\n", style.Warning.Render(fmt.Sprintf("%d files", len(state.UncommittedFiles))))
		for _, f := range state.UncommittedFiles {
			fmt.Printf("                 %s\n", style.Dim.Render(f))
		}
	}

	// Unpushed commits
	if state.UnpushedCommits == 0 {
		fmt.Printf("  Unpushed:      %s\n", style.Success.Render("0 commits"))
	} else {
		fmt.Printf("  Unpushed:      %s\n", style.Warning.Render(fmt.Sprintf("%d commits ahead", state.UnpushedCommits)))
	}

	// Stashes
	if state.StashCount == 0 {
		fmt.Printf("  Stashes:       %s\n", style.Dim.Render("0"))
	} else {
		fmt.Printf("  Stashes:       %s\n", style.Warning.Render(fmt.Sprintf("%d", state.StashCount)))
	}

	// Verdict
	fmt.Println()
	if state.Clean {
		fmt.Printf("  Verdict:       %s\n", style.Success.Render("CLEAN (safe to kill)"))
	} else {
		fmt.Printf("  Verdict:       %s\n", style.Error.Render("DIRTY (needs cleanup)"))
	}

	return nil
}

// getGitState checks the git state of a worktree.
func getGitState(worktreePath string) (*GitState, error) {
	state := &GitState{
		Clean:            true,
		UncommittedFiles: []string{},
	}

	// Check for uncommitted changes (git status --porcelain)
	statusCmd := exec.Command("git", "status", "--porcelain")
	statusCmd.Dir = worktreePath
	output, err := statusCmd.Output()
	if err != nil {
		return nil, fmt.Errorf("git status: %w", err)
	}
	if len(output) > 0 {
		lines := splitLines(string(output))
		for _, line := range lines {
			if line != "" {
				// Extract filename (skip the status prefix)
				if len(line) > 3 {
					state.UncommittedFiles = append(state.UncommittedFiles, line[3:])
				} else {
					state.UncommittedFiles = append(state.UncommittedFiles, line)
				}
			}
		}
		state.Clean = false
	}

	// Check for unpushed commits (git log origin/main..HEAD)
	// We check commits first, then verify if content differs.
	// After squash merge, commits may differ but content may be identical.
	mainRef := "origin/main"
	logCmd := exec.Command("git", "log", mainRef+"..HEAD", "--oneline")
	logCmd.Dir = worktreePath
	output, err = logCmd.Output()
	if err != nil {
		// origin/main might not exist - try origin/master
		mainRef = "origin/master"
		logCmd = exec.Command("git", "log", mainRef+"..HEAD", "--oneline")
		logCmd.Dir = worktreePath
		output, _ = logCmd.Output() // non-fatal: might be a new repo without remote tracking
	}
	if len(output) > 0 {
		lines := splitLines(string(output))
		count := 0
		for _, line := range lines {
			if line != "" {
				count++
			}
		}
		if count > 0 {
			// Commits exist that aren't on main. But after squash merge,
			// the content may actually be on main with different commit SHAs.
			// Check if there's any actual diff between HEAD and main.
			diffCmd := exec.Command("git", "diff", mainRef, "HEAD", "--quiet")
			diffCmd.Dir = worktreePath
			diffErr := diffCmd.Run()
			if diffErr == nil {
				// Exit code 0 means no diff - content IS on main (squash merged)
				// Don't count these as unpushed
				state.UnpushedCommits = 0
			} else {
				// Exit code 1 means there's a diff - truly unpushed work
				state.UnpushedCommits = count
				state.Clean = false
			}
		}
	}

	// Check for stashes (git stash list)
	stashCmd := exec.Command("git", "stash", "list")
	stashCmd.Dir = worktreePath
	output, err = stashCmd.Output()
	if err != nil {
		// Ignore stash errors
		output = nil
	}
	if len(output) > 0 {
		lines := splitLines(string(output))
		count := 0
		for _, line := range lines {
			if line != "" {
				count++
			}
		}
		state.StashCount = count
		if count > 0 {
			state.Clean = false
		}
	}

	return state, nil
}

// RecoveryStatus represents whether a polecat needs recovery or is safe to nuke.
type RecoveryStatus struct {
	Rig           string                `json:"rig"`
	Polecat       string                `json:"polecat"`
	CleanupStatus polecat.CleanupStatus `json:"cleanup_status"`
	NeedsRecovery bool                  `json:"needs_recovery"`
	Verdict       string                `json:"verdict"` // SAFE_TO_NUKE or NEEDS_RECOVERY
	Branch        string                `json:"branch,omitempty"`
	Issue         string                `json:"issue,omitempty"`
}

func runPolecatCheckRecovery(cmd *cobra.Command, args []string) error {
	rigName, polecatName, err := parseAddress(args[0])
	if err != nil {
		return err
	}

	mgr, r, err := getPolecatManager(rigName)
	if err != nil {
		return err
	}

	// Verify polecat exists and get info
	p, err := mgr.Get(polecatName)
	if err != nil {
		return fmt.Errorf("polecat '%s' not found in rig '%s'", polecatName, rigName)
	}

	// Get cleanup_status from agent bead
	// We need to read it directly from beads since manager doesn't expose it
	rigPath := r.Path
	bd := beads.New(rigPath)
	agentBeadID := beads.PolecatBeadID(rigName, polecatName)
	_, fields, err := bd.GetAgentBead(agentBeadID)

	status := RecoveryStatus{
		Rig:     rigName,
		Polecat: polecatName,
		Branch:  p.Branch,
		Issue:   p.Issue,
	}

	if err != nil || fields == nil {
		// No agent bead or no cleanup_status - fall back to git check
		// This handles polecats that haven't self-reported yet
		gitState, gitErr := getGitState(p.ClonePath)
		if gitErr != nil {
			status.CleanupStatus = polecat.CleanupUnknown
			status.NeedsRecovery = true
			status.Verdict = "NEEDS_RECOVERY"
		} else if gitState.Clean {
			status.CleanupStatus = polecat.CleanupClean
			status.NeedsRecovery = false
			status.Verdict = "SAFE_TO_NUKE"
		} else if gitState.UnpushedCommits > 0 {
			status.CleanupStatus = polecat.CleanupUnpushed
			status.NeedsRecovery = true
			status.Verdict = "NEEDS_RECOVERY"
		} else if gitState.StashCount > 0 {
			status.CleanupStatus = polecat.CleanupStash
			status.NeedsRecovery = true
			status.Verdict = "NEEDS_RECOVERY"
		} else {
			status.CleanupStatus = polecat.CleanupUncommitted
			status.NeedsRecovery = true
			status.Verdict = "NEEDS_RECOVERY"
		}
	} else {
		// Use cleanup_status from agent bead
		status.CleanupStatus = polecat.CleanupStatus(fields.CleanupStatus)
		if status.CleanupStatus.IsSafe() {
			status.NeedsRecovery = false
			status.Verdict = "SAFE_TO_NUKE"
		} else {
			// RequiresRecovery covers uncommitted, stash, unpushed
			// Unknown/empty also treated conservatively
			status.NeedsRecovery = true
			status.Verdict = "NEEDS_RECOVERY"
		}
	}

	// JSON output
	if polecatCheckRecoveryJSON {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		return enc.Encode(status)
	}

	// Human-readable output
	fmt.Printf("%s\n\n", style.Bold.Render(fmt.Sprintf("Recovery Status: %s/%s", rigName, polecatName)))
	fmt.Printf("  Cleanup Status:  %s\n", status.CleanupStatus)
	if status.Branch != "" {
		fmt.Printf("  Branch:          %s\n", status.Branch)
	}
	if status.Issue != "" {
		fmt.Printf("  Issue:           %s\n", status.Issue)
	}
	fmt.Println()

	if status.NeedsRecovery {
		fmt.Printf("  Verdict:         %s\n", style.Error.Render("NEEDS_RECOVERY"))
		fmt.Println()
		fmt.Printf("  %s This polecat has unpushed/uncommitted work.\n", style.Warning.Render("⚠"))
		fmt.Println("  Escalate to Mayor for recovery before cleanup.")
	} else {
		fmt.Printf("  Verdict:         %s\n", style.Success.Render("SAFE_TO_NUKE"))
		fmt.Println()
		fmt.Printf("  %s Safe to nuke - no work at risk.\n", style.Success.Render("✓"))
	}

	return nil
}

func runPolecatGC(cmd *cobra.Command, args []string) error {
	rigName := args[0]

	mgr, r, err := getPolecatManager(rigName)
	if err != nil {
		return err
	}

	fmt.Printf("Garbage collecting stale polecat branches in %s...\n\n", r.Name)

	if polecatGCDryRun {
		// Dry run - list branches that would be deleted
		repoGit := git.NewGit(r.Path)

		// List all polecat branches
		branches, err := repoGit.ListBranches("polecat/*")
		if err != nil {
			return fmt.Errorf("listing branches: %w", err)
		}

		if len(branches) == 0 {
			fmt.Println("No polecat branches found.")
			return nil
		}

		// Get current branches
		polecats, err := mgr.List()
		if err != nil {
			return fmt.Errorf("listing polecats: %w", err)
		}

		currentBranches := make(map[string]bool)
		for _, p := range polecats {
			currentBranches[p.Branch] = true
		}

		// Show what would be deleted
		toDelete := 0
		for _, branch := range branches {
			if !currentBranches[branch] {
				fmt.Printf("  Would delete: %s\n", style.Dim.Render(branch))
				toDelete++
			} else {
				fmt.Printf("  Keep (in use): %s\n", style.Success.Render(branch))
			}
		}

		fmt.Printf("\nWould delete %d branch(es), keep %d\n", toDelete, len(branches)-toDelete)
		return nil
	}

	// Actually clean up
	deleted, err := mgr.CleanupStaleBranches()
	if err != nil {
		return fmt.Errorf("cleanup failed: %w", err)
	}

	if deleted == 0 {
		fmt.Println("No stale branches to clean up.")
	} else {
		fmt.Printf("%s Deleted %d stale branch(es).\n", style.SuccessPrefix, deleted)
	}

	return nil
}

// splitLines splits a string into non-empty lines.
func splitLines(s string) []string {
	var lines []string
	for _, line := range filepath.SplitList(s) {
		if line != "" {
			lines = append(lines, line)
		}
	}
	// filepath.SplitList doesn't work for newlines, use strings.Split instead
	lines = nil
	for _, line := range strings.Split(s, "\n") {
		lines = append(lines, line)
	}
	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 greenplace --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 - require explicit rig/polecat format
		for _, arg := range args {
			// Validate format: must contain "/" to avoid misinterpreting rig names as polecat names
			if !strings.Contains(arg, "/") {
				return fmt.Errorf("invalid address '%s': must be in 'rig/polecat' format (e.g., 'gastown/Toast')", arg)
			}

			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,
			})
		}
	}

	// Safety checks: refuse to nuke polecats with active work unless --force is set
	// Checks:
	// 1. Unpushed commits - worktree has uncommitted/unpushed changes
	// 2. Open MR beads - polecat has open merge requests pending
	// 3. Work on hook - polecat has work assigned to its hook
	if !polecatNukeForce && !polecatNukeDryRun {
		type blockReason struct {
			polecat string
			reasons []string
		}
		var blocked []blockReason

		for _, p := range toNuke {
			var reasons []string

			// Get polecat info for branch name
			polecatInfo, infoErr := p.mgr.Get(p.polecatName)

			// Check 1: Unpushed commits via cleanup_status or git state
			bd := beads.New(p.r.Path)
			agentBeadID := beads.PolecatBeadID(p.rigName, p.polecatName)
			agentIssue, fields, err := bd.GetAgentBead(agentBeadID)

			if err != nil || fields == nil {
				// No agent bead - fall back to git check
				if infoErr == nil && polecatInfo != nil {
					gitState, gitErr := getGitState(polecatInfo.ClonePath)
					if gitErr != nil {
						reasons = append(reasons, "cannot check git state")
					} else if !gitState.Clean {
						if gitState.UnpushedCommits > 0 {
							reasons = append(reasons, fmt.Sprintf("has %d unpushed commit(s)", gitState.UnpushedCommits))
						} else if len(gitState.UncommittedFiles) > 0 {
							reasons = append(reasons, fmt.Sprintf("has %d uncommitted file(s)", len(gitState.UncommittedFiles)))
						} else if gitState.StashCount > 0 {
							reasons = append(reasons, fmt.Sprintf("has %d stash(es)", gitState.StashCount))
						}
					}
				}
			} else {
				// Check cleanup_status from agent bead
				cleanupStatus := polecat.CleanupStatus(fields.CleanupStatus)
				switch cleanupStatus {
				case polecat.CleanupClean:
					// OK
				case polecat.CleanupUnpushed:
					reasons = append(reasons, "has unpushed commits")
				case polecat.CleanupUncommitted:
					reasons = append(reasons, "has uncommitted changes")
				case polecat.CleanupStash:
					reasons = append(reasons, "has stashed changes")
				case polecat.CleanupUnknown, "":
					reasons = append(reasons, "cleanup status unknown")
				default:
					reasons = append(reasons, fmt.Sprintf("cleanup status: %s", cleanupStatus))
				}

				// Check 3: Work on hook (check both Issue.HookBead from slot and fields.HookBead)
				// Only flag as blocking if the hooked bead is still in an active status.
				// If the hooked bead was closed externally (gt-jc7bq), don't block nuke.
				hookBead := agentIssue.HookBead
				if hookBead == "" {
					hookBead = fields.HookBead
				}
				if hookBead != "" {
					// Check if hooked bead is still active (not closed)
					hookedIssue, err := bd.Show(hookBead)
					if err == nil && hookedIssue != nil {
						// Only block if bead is still active (not closed)
						if hookedIssue.Status != "closed" {
							reasons = append(reasons, fmt.Sprintf("has work on hook (%s)", hookBead))
						}
						// If closed, the hook is stale - don't block nuke
					} else {
						// Can't verify hooked bead - be conservative
						reasons = append(reasons, fmt.Sprintf("has work on hook (%s, unverified)", hookBead))
					}
				}
			}

			// Check 2: Open MR beads for this branch
			if infoErr == nil && polecatInfo != nil && polecatInfo.Branch != "" {
				mr, mrErr := bd.FindMRForBranch(polecatInfo.Branch)
				if mrErr == nil && mr != nil {
					reasons = append(reasons, fmt.Sprintf("has open MR (%s)", mr.ID))
				}
			}

			if len(reasons) > 0 {
				blocked = append(blocked, blockReason{
					polecat: fmt.Sprintf("%s/%s", p.rigName, p.polecatName),
					reasons: reasons,
				})
			}
		}

		if len(blocked) > 0 {
			fmt.Printf("%s Cannot nuke the following polecats:\n\n", style.Error.Render("Error:"))
			var polecatList []string
			for _, b := range blocked {
				fmt.Printf("  %s:\n", style.Bold.Render(b.polecat))
				for _, r := range b.reasons {
					fmt.Printf("    - %s\n", r)
				}
				polecatList = append(polecatList, b.polecat)
			}
			fmt.Println()
			fmt.Println("Safety checks failed. Resolve issues before nuking, or use --force.")
			fmt.Println("Options:")
			fmt.Printf("  1. Complete work: gt done (from polecat session)\n")
			fmt.Printf("  2. Push changes: git push (from polecat worktree)\n")
			fmt.Printf("  3. Escalate: gt mail send mayor/ -s \"RECOVERY_NEEDED\" -m \"...\"\n")
			fmt.Printf("  4. Force nuke (LOSES WORK): gt polecat nuke --force %s\n", strings.Join(polecatList, " "))
			fmt.Println()
			return fmt.Errorf("blocked: %d polecat(s) have active work", len(blocked))
		}
	}

	// 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: %s\n", beads.PolecatBeadID(p.rigName, p.polecatName))

			// Show safety check status in dry-run
			fmt.Printf("\n  Safety checks:\n")
			polecatInfo, infoErr := p.mgr.Get(p.polecatName)
			bd := beads.New(p.r.Path)
			agentBeadID := beads.PolecatBeadID(p.rigName, p.polecatName)
			agentIssue, fields, err := bd.GetAgentBead(agentBeadID)

			// Check 1: Git state
			if err != nil || fields == nil {
				if infoErr == nil && polecatInfo != nil {
					gitState, gitErr := getGitState(polecatInfo.ClonePath)
					if gitErr != nil {
						fmt.Printf("    - Git state: %s\n", style.Warning.Render("cannot check"))
					} else if gitState.Clean {
						fmt.Printf("    - Git state: %s\n", style.Success.Render("clean"))
					} else {
						fmt.Printf("    - Git state: %s\n", style.Error.Render("dirty"))
					}
				} else {
					fmt.Printf("    - Git state: %s\n", style.Dim.Render("unknown (no polecat info)"))
				}
				fmt.Printf("    - Hook: %s\n", style.Dim.Render("unknown (no agent bead)"))
			} else {
				cleanupStatus := polecat.CleanupStatus(fields.CleanupStatus)
				if cleanupStatus.IsSafe() {
					fmt.Printf("    - Git state: %s\n", style.Success.Render("clean"))
				} else if cleanupStatus.RequiresRecovery() {
					fmt.Printf("    - Git state: %s (%s)\n", style.Error.Render("dirty"), cleanupStatus)
				} else {
					fmt.Printf("    - Git state: %s\n", style.Warning.Render("unknown"))
				}

				hookBead := agentIssue.HookBead
				if hookBead == "" {
					hookBead = fields.HookBead
				}
				if hookBead != "" {
					// Check if hooked bead is still active
					hookedIssue, err := bd.Show(hookBead)
					if err == nil && hookedIssue != nil && hookedIssue.Status == "closed" {
						fmt.Printf("    - Hook: %s (%s, closed - stale)\n", style.Warning.Render("stale"), hookBead)
					} else {
						fmt.Printf("    - Hook: %s (%s)\n", style.Error.Render("has work"), hookBead)
					}
				} else {
					fmt.Printf("    - Hook: %s\n", style.Success.Render("empty"))
				}
			}

			// Check 2: Open MR
			if infoErr == nil && polecatInfo != nil && polecatInfo.Branch != "" {
				mr, mrErr := bd.FindMRForBranch(polecatInfo.Branch)
				if mrErr == nil && mr != nil {
					fmt.Printf("    - Open MR: %s (%s)\n", style.Error.Render("yes"), mr.ID)
				} else {
					fmt.Printf("    - Open MR: %s\n", style.Success.Render("none"))
				}
			} else {
				fmt.Printf("    - Open MR: %s\n", style.Dim.Render("unknown (no branch info)"))
			}

			fmt.Println()
			continue
		}

		if polecatNukeForce {
			fmt.Printf("%s Nuking %s/%s (--force)...\n", style.Warning.Render("⚠"), p.rigName, p.polecatName)
		} else {
			fmt.Printf("Nuking %s/%s...\n", p.rigName, p.polecatName)
		}

		// Step 1: Kill session (force mode - no graceful shutdown)
		polecatMgr := polecat.NewSessionManager(t, p.r)
		running, _ := polecatMgr.IsRunning(p.polecatName)
		if running {
			if err := polecatMgr.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 := beads.PolecatBeadID(p.rigName, p.polecatName)
		closeArgs := []string{"close", agentBeadID, "--reason=nuked"}
		if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
			closeArgs = append(closeArgs, "--session="+sessionID)
		}
		closeCmd := exec.Command("bd", closeArgs...)
		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
}

func runPolecatStale(cmd *cobra.Command, args []string) error {
	rigName := args[0]
	mgr, r, err := getPolecatManager(rigName)
	if err != nil {
		return err
	}

	fmt.Printf("Detecting stale polecats in %s (threshold: %d commits behind main)...\n\n", r.Name, polecatStaleThreshold)

	staleInfos, err := mgr.DetectStalePolecats(polecatStaleThreshold)
	if err != nil {
		return fmt.Errorf("detecting stale polecats: %w", err)
	}

	if len(staleInfos) == 0 {
		fmt.Println("No polecats found.")
		return nil
	}

	// JSON output
	if polecatStaleJSON {
		return json.NewEncoder(os.Stdout).Encode(staleInfos)
	}

	// Summary counts
	var staleCount, safeCount int
	for _, info := range staleInfos {
		if info.IsStale {
			staleCount++
		} else {
			safeCount++
		}
	}

	// Display results
	for _, info := range staleInfos {
		statusIcon := style.Success.Render("●")
		statusText := "active"
		if info.IsStale {
			statusIcon = style.Warning.Render("○")
			statusText = "stale"
		}

		fmt.Printf("%s %s (%s)\n", statusIcon, style.Bold.Render(info.Name), statusText)

		// Session status
		if info.HasActiveSession {
			fmt.Printf("    Session: %s\n", style.Success.Render("running"))
		} else {
			fmt.Printf("    Session: %s\n", style.Dim.Render("stopped"))
		}

		// Commits behind
		if info.CommitsBehind > 0 {
			behindStyle := style.Dim
			if info.CommitsBehind >= polecatStaleThreshold {
				behindStyle = style.Warning
			}
			fmt.Printf("    Behind main: %s\n", behindStyle.Render(fmt.Sprintf("%d commits", info.CommitsBehind)))
		}

		// Agent state
		if info.AgentState != "" {
			fmt.Printf("    Agent state: %s\n", info.AgentState)
		} else {
			fmt.Printf("    Agent state: %s\n", style.Dim.Render("no bead"))
		}

		// Uncommitted work
		if info.HasUncommittedWork {
			fmt.Printf("    Uncommitted: %s\n", style.Error.Render("yes"))
		}

		// Reason
		fmt.Printf("    Reason: %s\n", info.Reason)
		fmt.Println()
	}

	// Summary
	fmt.Printf("Summary: %d stale, %d active\n", staleCount, safeCount)

	// Cleanup if requested
	if polecatStaleCleanup && staleCount > 0 {
		fmt.Println()
		if polecatNukeDryRun {
			fmt.Printf("Would clean up %d stale polecat(s):\n", staleCount)
			for _, info := range staleInfos {
				if info.IsStale {
					fmt.Printf("  - %s: %s\n", info.Name, info.Reason)
				}
			}
		} else {
			fmt.Printf("Cleaning up %d stale polecat(s)...\n", staleCount)
			nuked := 0
			for _, info := range staleInfos {
				if !info.IsStale {
					continue
				}
				fmt.Printf("  Nuking %s...", info.Name)
				if err := mgr.RemoveWithOptions(info.Name, true, false); err != nil {
					fmt.Printf(" %s (%v)\n", style.Error.Render("failed"), err)
				} else {
					fmt.Printf(" %s\n", style.Success.Render("done"))
					nuked++
				}
			}
			fmt.Printf("\n%s Nuked %d stale polecat(s).\n", style.SuccessPrefix, nuked)
		}
	}

	return nil
}
