feat(polecat): refuse to lose uncommitted work during cleanup (gt-8v8)

Add comprehensive uncommitted work checks before any polecat cleanup:
- Check for uncommitted changes (modified/untracked files)
- Check for stashes
- Check for unpushed commits

Affected commands:
- gt polecat remove: now refuses if uncommitted work exists
- gt rig shutdown: checks all polecats before shutdown
- Witness cleanup: refuses to clean polecats with uncommitted work
- gt spawn: warns if spawning to polecat with uncommitted work

Safety model:
- --force: bypasses uncommitted changes check only
- --nuclear: bypasses ALL safety checks (will lose work)

New git helpers:
- StashCount(): count stashes in repo
- UnpushedCommits(): count commits not pushed to upstream
- CheckUncommittedWork(): comprehensive work status check

🤖 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-20 15:55:05 -08:00
parent 2a9f31a02d
commit 4f21002132
5 changed files with 262 additions and 22 deletions

View File

@@ -182,6 +182,29 @@ func runSpawn(cmd *cobra.Command, args []string) error {
return fmt.Errorf("polecat '%s' is already working on %s", polecatName, pc.Issue)
}
// Check for uncommitted work in existing polecat (safety check)
pGit := git.NewGit(pc.ClonePath)
workStatus, err := pGit.CheckUncommittedWork()
if err == nil && !workStatus.Clean() {
fmt.Printf("\n%s Polecat has uncommitted work:\n", style.Warning.Render("⚠"))
if workStatus.HasUncommittedChanges {
fmt.Printf(" • %d uncommitted change(s)\n", len(workStatus.ModifiedFiles)+len(workStatus.UntrackedFiles))
}
if workStatus.StashCount > 0 {
fmt.Printf(" • %d stash(es)\n", workStatus.StashCount)
}
if workStatus.UnpushedCommits > 0 {
fmt.Printf(" • %d unpushed commit(s)\n", workStatus.UnpushedCommits)
}
fmt.Println()
if !spawnForce {
return fmt.Errorf("polecat '%s' has uncommitted work (%s)\nCommit or stash changes before spawning, or use --force to proceed anyway",
polecatName, workStatus.String())
}
fmt.Printf("%s Proceeding with --force (uncommitted work may be lost if polecat is cleaned up)\n",
style.Dim.Render("Warning:"))
}
// Check for unread mail in polecat's inbox (indicates existing unstarted work)
polecatAddress := fmt.Sprintf("%s/%s", rigName, polecatName)
router := mail.NewRouter(r.Path)