From 9f2eefe9cede3f5166741037cb5e2d4c2f4e5bd7 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 21:50:14 -0800 Subject: [PATCH] fix: verify polecat branch pushed before cleanup (gt-gl6s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add BranchPushedToRemote() to git package that properly handles polecat branches without upstream tracking. Update verifyPolecatState in witness to check that branches are pushed before allowing cleanup. This prevents the scenario where polecats close issues without pushing their work, causing all commits to be lost when the worktree is deleted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/git/git.go | 46 +++++++++++++++++++++++++++++++++++++ internal/witness/manager.go | 15 +++++++++--- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/internal/git/git.go b/internal/git/git.go index 458c7d4c..3a5d64bd 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -639,3 +639,49 @@ func (g *Git) CheckUncommittedWork() (*UncommittedWorkStatus, error) { return status, nil } + +// BranchPushedToRemote checks if a branch has been pushed to the remote. +// Returns (pushed bool, unpushedCount int, err). +// This handles polecat branches that don't have upstream tracking configured. +func (g *Git) BranchPushedToRemote(localBranch, remote string) (bool, int, error) { + remoteBranch := remote + "/" + localBranch + + // First check if the remote branch exists + exists, err := g.RemoteBranchExists(remote, localBranch) + if err != nil { + return false, 0, fmt.Errorf("checking remote branch: %w", err) + } + + if !exists { + // Remote branch doesn't exist - count commits since origin/main (or HEAD if that fails) + count, err := g.run("rev-list", "--count", "origin/main..HEAD") + if err != nil { + // Fallback: just count all commits on HEAD + count, err = g.run("rev-list", "--count", "HEAD") + if err != nil { + return false, 0, fmt.Errorf("counting commits: %w", err) + } + } + var n int + _, err = fmt.Sscanf(count, "%d", &n) + if err != nil { + return false, 0, fmt.Errorf("parsing commit count: %w", err) + } + // If there are any commits since main, branch is not pushed + return n == 0, n, nil + } + + // Remote branch exists - check if local is ahead + count, err := g.run("rev-list", "--count", remoteBranch+"..HEAD") + if err != nil { + return false, 0, fmt.Errorf("counting unpushed commits: %w", err) + } + + var n int + _, err = fmt.Sscanf(count, "%d", &n) + if err != nil { + return false, 0, fmt.Errorf("parsing unpushed count: %w", err) + } + + return n == 0, n, nil +} diff --git a/internal/witness/manager.go b/internal/witness/manager.go index 2352a249..1af74c47 100644 --- a/internal/witness/manager.go +++ b/internal/witness/manager.go @@ -765,9 +765,18 @@ func (m *Manager) verifyPolecatState(polecatName string) error { // Note: beads changes would be reflected in git status above, // since beads files are tracked in git. - // Note: MR submission is now done automatically by polecat's handoff command, - // so we don't need to verify it here - the polecat wouldn't have requested - // shutdown if that step failed + // 2. Check that the polecat branch was pushed to remote + // This catches the case where a polecat closes an issue without pushing their work. + // Without this check, work can be lost when the polecat worktree is cleaned up. + branchName := "polecat/" + polecatName + pushed, unpushedCount, err := polecatGit.BranchPushedToRemote(branchName, "origin") + if err != nil { + // Log but don't fail - could be network issue + fmt.Printf(" Warning: could not verify branch push status: %v\n", err) + } else if !pushed { + return fmt.Errorf("branch %s has %d unpushed commit(s) - run 'git push origin %s' before closing", + branchName, unpushedCount, branchName) + } return nil }