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 }