polecat: fresh unique branches per run, add gc command (gt-ake0m)

Changed polecat branch model to use unique timestamped branches
(polecat/<name>-<timestamp>) instead of reusing persistent branches.
This prevents JSONL drift issues where stale polecat branches don't
have recently created beads.

Changes:
- Add(): create unique branch, simplified (no reuse logic)
- Recreate(): create fresh branch, old ones left for GC
- loadFromBeads(): read actual branch from git worktree
- CleanupStaleBranches(): remove orphaned polecat branches
- ListBranches(pattern): new git helper for branch enumeration
- gt polecat gc: new command to clean up stale branches

🤖 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-27 17:19:02 -08:00
parent 3aa0ba6e6b
commit 4730ac508f
3 changed files with 189 additions and 48 deletions

View File

@@ -197,8 +197,29 @@ var (
polecatSyncFromMain bool
polecatStatusJSON bool
polecatGitStateJSON bool
polecatGCDryRun 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 as polecats
are recreated.
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 gastown
gt polecat gc gastown --dry-run`,
Args: cobra.ExactArgs(1),
RunE: runPolecatGC,
}
var polecatGitStateCmd = &cobra.Command{
Use: "git-state <rig>/<polecat>",
Short: "Show git state for pre-kill verification",
@@ -238,6 +259,9 @@ func init() {
// 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")
// Add subcommands
polecatCmd.AddCommand(polecatListCmd)
polecatCmd.AddCommand(polecatAddCmd)
@@ -249,6 +273,7 @@ func init() {
polecatCmd.AddCommand(polecatSyncCmd)
polecatCmd.AddCommand(polecatStatusCmd)
polecatCmd.AddCommand(polecatGitStateCmd)
polecatCmd.AddCommand(polecatGCCmd)
rootCmd.AddCommand(polecatCmd)
}
@@ -998,6 +1023,72 @@ func getGitState(worktreePath string) (*GitState, error) {
return state, 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