refactor(cmd): extract polecat helpers to reduce duplication
Split polecat.go (1635 lines) into: - polecat.go (1359 lines): cobra commands and handlers - polecat_helpers.go (260 lines): shared helper functions Extracted: - resolvePolecatTargets(): shared list-building logic from remove/nuke - checkPolecatSafety(): safety check logic for destructive operations - displaySafetyCheckBlocked(): blocked polecat display - displayDryRunSafetyCheck(): dry-run safety status display Reduces duplication between runPolecatRemove and runPolecatNuke. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -449,71 +449,14 @@ func runPolecatAdd(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runPolecatRemove(cmd *cobra.Command, args []string) error {
|
func runPolecatRemove(cmd *cobra.Command, args []string) error {
|
||||||
// Build list of polecats to remove
|
targets, err := resolvePolecatTargets(args, polecatRemoveAll)
|
||||||
type polecatToRemove struct {
|
if err != nil {
|
||||||
rigName string
|
return err
|
||||||
polecatName string
|
|
||||||
mgr *polecat.Manager
|
|
||||||
r *rig.Rig
|
|
||||||
}
|
}
|
||||||
var toRemove []polecatToRemove
|
|
||||||
|
|
||||||
if polecatRemoveAll {
|
if len(targets) == 0 {
|
||||||
// --all flag: first arg is just the rig name
|
fmt.Println("No polecats to remove.")
|
||||||
rigName := args[0]
|
return nil
|
||||||
// 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
|
// Remove each polecat
|
||||||
@@ -521,7 +464,7 @@ func runPolecatRemove(cmd *cobra.Command, args []string) error {
|
|||||||
var removeErrors []string
|
var removeErrors []string
|
||||||
removed := 0
|
removed := 0
|
||||||
|
|
||||||
for _, p := range toRemove {
|
for _, p := range targets {
|
||||||
// Check if session is running
|
// Check if session is running
|
||||||
if !polecatForce {
|
if !polecatForce {
|
||||||
polecatMgr := polecat.NewSessionManager(t, p.r)
|
polecatMgr := polecat.NewSessionManager(t, p.r)
|
||||||
@@ -1168,187 +1111,28 @@ func splitLines(s string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runPolecatNuke(cmd *cobra.Command, args []string) error {
|
func runPolecatNuke(cmd *cobra.Command, args []string) error {
|
||||||
// Build list of polecats to nuke
|
targets, err := resolvePolecatTargets(args, polecatNukeAll)
|
||||||
type polecatToNuke struct {
|
if err != nil {
|
||||||
rigName string
|
return err
|
||||||
polecatName string
|
|
||||||
mgr *polecat.Manager
|
|
||||||
r *rig.Rig
|
|
||||||
}
|
}
|
||||||
var toNuke []polecatToNuke
|
|
||||||
|
|
||||||
if polecatNukeAll {
|
if len(targets) == 0 {
|
||||||
// --all flag: first arg is just the rig name
|
fmt.Println("No polecats to nuke.")
|
||||||
rigName := args[0]
|
return nil
|
||||||
// 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
|
// 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 {
|
if !polecatNukeForce && !polecatNukeDryRun {
|
||||||
type blockReason struct {
|
var blocked []*SafetyCheckResult
|
||||||
polecat string
|
for _, p := range targets {
|
||||||
reasons []string
|
result := checkPolecatSafety(p)
|
||||||
}
|
if result.Blocked {
|
||||||
var blocked []blockReason
|
blocked = append(blocked, result)
|
||||||
|
|
||||||
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 {
|
if len(blocked) > 0 {
|
||||||
fmt.Printf("%s Cannot nuke the following polecats:\n\n", style.Error.Render("Error:"))
|
displaySafetyCheckBlocked(blocked)
|
||||||
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))
|
return fmt.Errorf("blocked: %d polecat(s) have active work", len(blocked))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1358,7 +1142,7 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
|
|||||||
var nukeErrors []string
|
var nukeErrors []string
|
||||||
nuked := 0
|
nuked := 0
|
||||||
|
|
||||||
for _, p := range toNuke {
|
for _, p := range targets {
|
||||||
if polecatNukeDryRun {
|
if polecatNukeDryRun {
|
||||||
fmt.Printf("Would nuke %s/%s:\n", p.rigName, p.polecatName)
|
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(" - Kill session: gt-%s-%s\n", p.rigName, p.polecatName)
|
||||||
@@ -1366,67 +1150,7 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
|
|||||||
fmt.Printf(" - Delete branch (if exists)\n")
|
fmt.Printf(" - Delete branch (if exists)\n")
|
||||||
fmt.Printf(" - Close agent bead: %s\n", beads.PolecatBeadID(p.rigName, p.polecatName))
|
fmt.Printf(" - Close agent bead: %s\n", beads.PolecatBeadID(p.rigName, p.polecatName))
|
||||||
|
|
||||||
// Show safety check status in dry-run
|
displayDryRunSafetyCheck(p)
|
||||||
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()
|
fmt.Println()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1499,7 +1223,7 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// Report results
|
// Report results
|
||||||
if polecatNukeDryRun {
|
if polecatNukeDryRun {
|
||||||
fmt.Printf("\n%s Would nuke %d polecat(s).\n", style.Info.Render("ℹ"), len(toNuke))
|
fmt.Printf("\n%s Would nuke %d polecat(s).\n", style.Info.Render("ℹ"), len(targets))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
260
internal/cmd/polecat_helpers.go
Normal file
260
internal/cmd/polecat_helpers.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
|
"github.com/steveyegge/gastown/internal/polecat"
|
||||||
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
// polecatTarget represents a polecat to operate on.
|
||||||
|
type polecatTarget struct {
|
||||||
|
rigName string
|
||||||
|
polecatName string
|
||||||
|
mgr *polecat.Manager
|
||||||
|
r *rig.Rig
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePolecatTargets builds a list of polecats from command args.
|
||||||
|
// If useAll is true, the first arg is treated as a rig name and all polecats in it are returned.
|
||||||
|
// Otherwise, args are parsed as rig/polecat addresses.
|
||||||
|
func resolvePolecatTargets(args []string, useAll bool) ([]polecatTarget, error) {
|
||||||
|
var targets []polecatTarget
|
||||||
|
|
||||||
|
if useAll {
|
||||||
|
// --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 nil, fmt.Errorf("with --all, provide just the rig name (e.g., 'gt polecat <cmd> %s --all')", strings.Split(rigName, "/")[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, r, err := getPolecatManager(rigName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
polecats, err := mgr.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listing polecats: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range polecats {
|
||||||
|
targets = append(targets, polecatTarget{
|
||||||
|
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 nil, 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 nil, fmt.Errorf("invalid address '%s': %w", arg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, r, err := getPolecatManager(rigName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targets = append(targets, polecatTarget{
|
||||||
|
rigName: rigName,
|
||||||
|
polecatName: polecatName,
|
||||||
|
mgr: mgr,
|
||||||
|
r: r,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafetyCheckResult holds the result of safety checks for a polecat.
|
||||||
|
type SafetyCheckResult struct {
|
||||||
|
Polecat string
|
||||||
|
Blocked bool
|
||||||
|
Reasons []string
|
||||||
|
CleanupStatus polecat.CleanupStatus
|
||||||
|
HookBead string
|
||||||
|
HookStale bool // true if hooked bead is closed
|
||||||
|
OpenMR string
|
||||||
|
GitState *GitState
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPolecatSafety performs safety checks before destructive operations.
|
||||||
|
// Returns nil if the polecat is safe to operate on, or a SafetyCheckResult with reasons if blocked.
|
||||||
|
func checkPolecatSafety(target polecatTarget) *SafetyCheckResult {
|
||||||
|
result := &SafetyCheckResult{
|
||||||
|
Polecat: fmt.Sprintf("%s/%s", target.rigName, target.polecatName),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get polecat info for branch name
|
||||||
|
polecatInfo, infoErr := target.mgr.Get(target.polecatName)
|
||||||
|
|
||||||
|
// Check 1: Unpushed commits via cleanup_status or git state
|
||||||
|
bd := beads.New(target.r.Path)
|
||||||
|
agentBeadID := beads.PolecatBeadID(target.rigName, target.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)
|
||||||
|
result.GitState = gitState
|
||||||
|
if gitErr != nil {
|
||||||
|
result.Reasons = append(result.Reasons, "cannot check git state")
|
||||||
|
} else if !gitState.Clean {
|
||||||
|
if gitState.UnpushedCommits > 0 {
|
||||||
|
result.Reasons = append(result.Reasons, fmt.Sprintf("has %d unpushed commit(s)", gitState.UnpushedCommits))
|
||||||
|
} else if len(gitState.UncommittedFiles) > 0 {
|
||||||
|
result.Reasons = append(result.Reasons, fmt.Sprintf("has %d uncommitted file(s)", len(gitState.UncommittedFiles)))
|
||||||
|
} else if gitState.StashCount > 0 {
|
||||||
|
result.Reasons = append(result.Reasons, fmt.Sprintf("has %d stash(es)", gitState.StashCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check cleanup_status from agent bead
|
||||||
|
result.CleanupStatus = polecat.CleanupStatus(fields.CleanupStatus)
|
||||||
|
switch result.CleanupStatus {
|
||||||
|
case polecat.CleanupClean:
|
||||||
|
// OK
|
||||||
|
case polecat.CleanupUnpushed:
|
||||||
|
result.Reasons = append(result.Reasons, "has unpushed commits")
|
||||||
|
case polecat.CleanupUncommitted:
|
||||||
|
result.Reasons = append(result.Reasons, "has uncommitted changes")
|
||||||
|
case polecat.CleanupStash:
|
||||||
|
result.Reasons = append(result.Reasons, "has stashed changes")
|
||||||
|
case polecat.CleanupUnknown, "":
|
||||||
|
result.Reasons = append(result.Reasons, "cleanup status unknown")
|
||||||
|
default:
|
||||||
|
result.Reasons = append(result.Reasons, fmt.Sprintf("cleanup status: %s", result.CleanupStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 3: Work on hook
|
||||||
|
hookBead := agentIssue.HookBead
|
||||||
|
if hookBead == "" {
|
||||||
|
hookBead = fields.HookBead
|
||||||
|
}
|
||||||
|
if hookBead != "" {
|
||||||
|
result.HookBead = hookBead
|
||||||
|
// Check if hooked bead is still active (not closed)
|
||||||
|
hookedIssue, err := bd.Show(hookBead)
|
||||||
|
if err == nil && hookedIssue != nil {
|
||||||
|
if hookedIssue.Status != "closed" {
|
||||||
|
result.Reasons = append(result.Reasons, fmt.Sprintf("has work on hook (%s)", hookBead))
|
||||||
|
} else {
|
||||||
|
result.HookStale = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.Reasons = append(result.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 {
|
||||||
|
result.OpenMR = mr.ID
|
||||||
|
result.Reasons = append(result.Reasons, fmt.Sprintf("has open MR (%s)", mr.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Blocked = len(result.Reasons) > 0
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// displaySafetyCheckBlocked prints blocked polecats and guidance.
|
||||||
|
func displaySafetyCheckBlocked(blocked []*SafetyCheckResult) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayDryRunSafetyCheck shows safety check status for dry-run mode.
|
||||||
|
func displayDryRunSafetyCheck(target polecatTarget) {
|
||||||
|
fmt.Printf("\n Safety checks:\n")
|
||||||
|
polecatInfo, infoErr := target.mgr.Get(target.polecatName)
|
||||||
|
bd := beads.New(target.r.Path)
|
||||||
|
agentBeadID := beads.PolecatBeadID(target.rigName, target.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 != "" {
|
||||||
|
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)"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user