//go:build !windows package infra import ( "fmt" "os/exec" "strings" "syscall" "time" ) // AgentCleanupWorktree tears down a worktree previously created by // AgentLaunchWorktree: kills the claude PID (SIGTERM, then SIGKILL after 1s), // removes the git worktree (force) and deletes the branch. // // All three steps are best-effort; we only return an error when ALL three // fail, so callers can call this on partially-initialised runs safely. // // Impure: signals processes, runs git, touches the filesystem. func AgentCleanupWorktree(repoRoot, branch, worktreePath string, pid int) error { var killErr, wtErr, brErr error // 1. Kill the process tree if a PID was provided. if pid > 0 { if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { killErr = err } else { // Give it ~1s to exit gracefully, then SIGKILL if still alive. time.Sleep(1 * time.Second) if alive := syscall.Kill(pid, 0); alive == nil { if err := syscall.Kill(pid, syscall.SIGKILL); err != nil { killErr = err } } } } // 2. Remove worktree (force). if worktreePath != "" { out, err := exec.Command("git", "-C", repoRoot, "worktree", "remove", "--force", worktreePath).CombinedOutput() if err != nil { wtErr = fmt.Errorf("worktree remove: %v: %s", err, strings.TrimSpace(string(out))) } } // 3. Delete the branch. if branch != "" { out, err := exec.Command("git", "-C", repoRoot, "branch", "-D", branch).CombinedOutput() if err != nil { brErr = fmt.Errorf("branch -D: %v: %s", err, strings.TrimSpace(string(out))) } } // Only error out if every requested step failed. Individual failures are // expected (e.g. cleanup called twice, dangling branch already gone). if killErr != nil && wtErr != nil && brErr != nil { return fmt.Errorf("cleanup failed: kill=%v; worktree=%v; branch=%v", killErr, wtErr, brErr) } return nil }