feat(orphans): make kill command handle both commits and processes

The gt orphans kill command now performs a unified cleanup that removes
orphaned commits via git gc AND kills orphaned Claude processes in one
operation, with a single confirmation prompt.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dag
2026-01-16 15:32:43 -08:00
committed by beads/crew/emma
parent 5178fa7f0a
commit eea3dd564d

View File

@@ -54,20 +54,22 @@ var (
// Commit orphan kill command // Commit orphan kill command
var orphansKillCmd = &cobra.Command{ var orphansKillCmd = &cobra.Command{
Use: "kill", Use: "kill",
Short: "Remove orphaned commits permanently", Short: "Remove all orphans (commits and processes)",
Long: `Remove orphaned commits by running git garbage collection. Long: `Remove orphaned commits and kill orphaned Claude processes.
This command finds orphaned commits and then runs 'git gc --prune=now' This command performs a complete orphan cleanup:
to permanently delete unreachable objects from the repository. 1. Finds and removes orphaned commits via 'git gc --prune=now'
2. Finds and kills orphaned Claude processes (PPID=1)
WARNING: This operation is irreversible. Once commits are pruned, WARNING: This operation is irreversible. Once commits are pruned,
they cannot be recovered. they cannot be recovered.
The command will: The command will:
1. Find orphaned commits (same as 'gt orphans') 1. Find orphaned commits (same as 'gt orphans')
2. Show what will be removed 2. Find orphaned Claude processes (same as 'gt orphans procs')
3. Ask for confirmation (unless --force) 3. Show what will be removed/killed
4. Run git gc --prune=now 4. Ask for confirmation (unless --force)
5. Run git gc and kill processes
Examples: Examples:
gt orphans kill # Kill orphans from last 7 days (default) gt orphans kill # Kill orphans from last 7 days (default)
@@ -345,7 +347,7 @@ func formatAge(t time.Time) string {
return fmt.Sprintf("%d days ago", days) return fmt.Sprintf("%d days ago", days)
} }
// runOrphansKill removes orphaned commits by running git gc // runOrphansKill removes orphaned commits and kills orphaned processes
func runOrphansKill(cmd *cobra.Command, args []string) error { func runOrphansKill(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError() townRoot, err := workspace.FindFromCwdOrError()
if err != nil { if err != nil {
@@ -358,36 +360,59 @@ func runOrphansKill(cmd *cobra.Command, args []string) error {
} }
mayorPath := r.Path + "/mayor/rig" mayorPath := r.Path + "/mayor/rig"
fmt.Printf("Scanning for orphaned commits in %s...\n\n", rigName)
orphans, err := findOrphanCommits(mayorPath) // Find orphaned commits
fmt.Printf("Scanning for orphaned commits in %s...\n", rigName)
commitOrphans, err := findOrphanCommits(mayorPath)
if err != nil { if err != nil {
return fmt.Errorf("finding orphans: %w", err) return fmt.Errorf("finding orphan commits: %w", err)
}
if len(orphans) == 0 {
fmt.Printf("%s No orphaned commits found\n", style.Bold.Render("✓"))
return nil
} }
// Filter commits by date
cutoff := time.Now().AddDate(0, 0, -orphansKillDays) cutoff := time.Now().AddDate(0, 0, -orphansKillDays)
var filtered []OrphanCommit var filteredCommits []OrphanCommit
for _, o := range orphans { for _, o := range commitOrphans {
if orphansKillAll || o.Date.After(cutoff) { if orphansKillAll || o.Date.After(cutoff) {
filtered = append(filtered, o) filteredCommits = append(filteredCommits, o)
} }
} }
if len(filtered) == 0 { // Find orphaned processes
fmt.Printf("%s No orphaned commits in the last %d days\n", style.Bold.Render("✓"), orphansKillDays) fmt.Printf("Scanning for orphaned Claude processes...\n\n")
fmt.Printf("%s Use --days=N or --all to target older orphans\n", style.Dim.Render("Hint:")) procOrphans, err := findOrphanProcesses()
if err != nil {
return fmt.Errorf("finding orphan processes: %w", err)
}
// Check if there's anything to do
if len(filteredCommits) == 0 && len(procOrphans) == 0 {
fmt.Printf("%s No orphans found\n", style.Bold.Render("✓"))
return nil return nil
} }
fmt.Printf("%s Found %d orphaned commit(s) to remove:\n\n", style.Warning.Render("⚠"), len(filtered)) // Show orphaned commits
for _, o := range filtered { if len(filteredCommits) > 0 {
fmt.Printf(" %s %s\n", style.Bold.Render(o.SHA[:8]), o.Subject) fmt.Printf("%s Found %d orphaned commit(s) to remove:\n\n", style.Warning.Render("⚠"), len(filteredCommits))
fmt.Printf(" %s by %s\n\n", style.Dim.Render(formatAge(o.Date)), o.Author) for _, o := range filteredCommits {
fmt.Printf(" %s %s\n", style.Bold.Render(o.SHA[:8]), o.Subject)
fmt.Printf(" %s by %s\n\n", style.Dim.Render(formatAge(o.Date)), o.Author)
}
} else if len(commitOrphans) > 0 {
fmt.Printf("%s No orphaned commits in the last %d days (use --days=N or --all)\n\n",
style.Dim.Render(""), orphansKillDays)
}
// Show orphaned processes
if len(procOrphans) > 0 {
fmt.Printf("%s Found %d orphaned Claude process(es) to kill:\n\n", style.Warning.Render("⚠"), len(procOrphans))
for _, o := range procOrphans {
displayArgs := o.Args
if len(displayArgs) > 80 {
displayArgs = displayArgs[:77] + "..."
}
fmt.Printf(" %s %s\n", style.Bold.Render(fmt.Sprintf("PID %d", o.PID)), displayArgs)
}
fmt.Println()
} }
if orphansKillDryRun { if orphansKillDryRun {
@@ -395,9 +420,11 @@ func runOrphansKill(cmd *cobra.Command, args []string) error {
return nil return nil
} }
// Confirmation
if !orphansKillForce { if !orphansKillForce {
fmt.Printf("%s\n", style.Warning.Render("WARNING: This operation is irreversible!")) fmt.Printf("%s\n", style.Warning.Render("WARNING: This operation is irreversible!"))
fmt.Printf("Remove %d orphaned commit(s)? [y/N] ", len(filtered)) total := len(filteredCommits) + len(procOrphans)
fmt.Printf("Remove %d orphan(s)? [y/N] ", total)
var response string var response string
_, _ = fmt.Scanln(&response) _, _ = fmt.Scanln(&response)
if strings.ToLower(strings.TrimSpace(response)) != "y" { if strings.ToLower(strings.TrimSpace(response)) != "y" {
@@ -406,16 +433,53 @@ func runOrphansKill(cmd *cobra.Command, args []string) error {
} }
} }
fmt.Printf("\nRunning git gc --prune=now...\n") // Kill orphaned commits
gcCmd := exec.Command("git", "gc", "--prune=now") if len(filteredCommits) > 0 {
gcCmd.Dir = mayorPath fmt.Printf("\nRunning git gc --prune=now...\n")
gcCmd.Stdout = os.Stdout gcCmd := exec.Command("git", "gc", "--prune=now")
gcCmd.Stderr = os.Stderr gcCmd.Dir = mayorPath
if err := gcCmd.Run(); err != nil { gcCmd.Stdout = os.Stdout
return fmt.Errorf("git gc failed: %w", err) gcCmd.Stderr = os.Stderr
if err := gcCmd.Run(); err != nil {
return fmt.Errorf("git gc failed: %w", err)
}
fmt.Printf("%s Removed %d orphaned commit(s)\n", style.Bold.Render("✓"), len(filteredCommits))
} }
fmt.Printf("\n%s Removed %d orphaned commit(s)\n", style.Bold.Render("✓"), len(filtered)) // Kill orphaned processes
if len(procOrphans) > 0 {
fmt.Printf("\nKilling orphaned processes...\n")
var killed, failed int
for _, o := range procOrphans {
proc, err := os.FindProcess(o.PID)
if err != nil {
fmt.Printf(" %s PID %d: %v\n", style.Error.Render("✗"), o.PID, err)
failed++
continue
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
if err == os.ErrProcessDone {
fmt.Printf(" %s PID %d: already terminated\n", style.Dim.Render("○"), o.PID)
continue
}
fmt.Printf(" %s PID %d: %v\n", style.Error.Render("✗"), o.PID, err)
failed++
continue
}
fmt.Printf(" %s PID %d killed\n", style.Bold.Render("✓"), o.PID)
killed++
}
fmt.Printf("%s %d process(es) killed", style.Bold.Render("✓"), killed)
if failed > 0 {
fmt.Printf(", %d failed", failed)
}
fmt.Println()
}
fmt.Printf("\n%s Orphan cleanup complete\n", style.Bold.Render("✓"))
return nil return nil
} }