From 63af29284b70cd1650961e7b6b103cb5e7c9411c Mon Sep 17 00:00:00 2001 From: gastown/crew/gus Date: Tue, 6 Jan 2026 13:11:30 -0800 Subject: [PATCH] feat(witness): delay polecat cleanup until MR merges (gt-12hwb) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 of local-only polecat branches: Handle conflict resolution edge case. Problem: If polecat worktree is nuked before MR merges, the local branch is gone and conflict resolution can't access it. Solution: Witness now defers cleanup for polecats with pending MRs: - HandlePolecatDone creates a cleanup wisp with "merge-requested" state - Polecat worktree preserved until MERGED signal arrives - HandleMerged then nukes the polecat (existing behavior) Also updated mol-polecat-conflict-resolve.formula.toml: - Removed fetch from origin (branches are local-only now) - Added instructions to fetch from source polecat's worktree - Added rig and source_polecat variables 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../mol-polecat-conflict-resolve.formula.toml | 37 +++++++++++++--- internal/witness/handlers.go | 42 ++++++++++++------- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/internal/formula/formulas/mol-polecat-conflict-resolve.formula.toml b/internal/formula/formulas/mol-polecat-conflict-resolve.formula.toml index 127e5078..86718453 100644 --- a/internal/formula/formulas/mol-polecat-conflict-resolve.formula.toml +++ b/internal/formula/formulas/mol-polecat-conflict-resolve.formula.toml @@ -145,20 +145,39 @@ git stash list # Should be empty If dirty, clean up first (stash or discard). -**2. Fetch latest state:** +**2. Fetch latest main:** ```bash git fetch origin -git fetch origin {{branch}}:refs/remotes/origin/{{branch}} ``` -**3. Checkout the branch:** +**3. Locate the source polecat's worktree:** + +The branch is local-only (not pushed to origin). Find the source polecat path +from the task metadata. The path follows the pattern: +``` +~/gt//polecats/ +``` + +Extract the source polecat name from the MR metadata: ```bash -git checkout -b temp-resolve origin/{{branch}} +bd show {{original_mr}} --json | jq -r '.description' | grep -oP 'Source polecat: \K\S+' +``` + +**4. Fetch the branch from the source polecat's worktree:** +```bash +# The source polecat's worktree still exists (cleanup is deferred until MR merges) +SOURCE_POLECAT_PATH="$HOME/gt/{{rig}}/polecats/{{source_polecat}}" +git fetch "$SOURCE_POLECAT_PATH" {{branch}}:{{branch}} +``` + +**5. Checkout the branch:** +```bash +git checkout -b temp-resolve {{branch}} ``` Using `temp-resolve` as the local branch name keeps things clear. -**4. Verify the branch state:** +**6. Verify the branch state:** ```bash git log --oneline -5 # Recent commits git log origin/main..HEAD # Commits not on main @@ -383,3 +402,11 @@ required = true [vars.branch] description = "The branch to rebase (extracted from task metadata)" required = true + +[vars.rig] +description = "The rig where the source polecat resides" +required = true + +[vars.source_polecat] +description = "The name of the polecat whose local branch contains the work (extracted from MR metadata)" +required = true diff --git a/internal/witness/handlers.go b/internal/witness/handlers.go index 7c0325b7..a67bcfa2 100644 --- a/internal/witness/handlers.go +++ b/internal/witness/handlers.go @@ -67,18 +67,34 @@ func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResul // ESCALATED/DEFERRED exits typically have no MR pending hasPendingMR := payload.MRID != "" || payload.Exit == "COMPLETED" - // Ephemeral model: try to auto-nuke immediately regardless of MR status - // If cleanup_status is clean, the branch is pushed and polecat is recyclable. - // The MR will be processed independently by the Refinery. + // Local-only branches model: if there's a pending MR, DON'T nuke. + // The polecat's local branch is needed for conflict resolution if merge fails. + // Once the MR merges (MERGED signal), HandleMerged will nuke the polecat. + if hasPendingMR { + // Create cleanup wisp to track this polecat is waiting for merge + wispID, err := createCleanupWisp(workDir, payload.PolecatName, payload.IssueID, payload.Branch) + if err != nil { + result.Error = fmt.Errorf("creating cleanup wisp: %w", err) + return result + } + + // Update wisp state to indicate it's waiting for merge + if err := UpdateCleanupWispState(workDir, wispID, "merge-requested"); err != nil { + // Non-fatal - wisp was created, just couldn't update state + result.Error = fmt.Errorf("updating wisp state: %w", err) + } + + result.Handled = true + result.WispCreated = wispID + result.Action = fmt.Sprintf("deferred cleanup for %s (pending MR=%s, local branch preserved for conflict resolution)", payload.PolecatName, payload.MRID) + return result + } + + // No pending MR - try to auto-nuke immediately nukeResult := AutoNukeIfClean(workDir, rigName, payload.PolecatName) if nukeResult.Nuked { result.Handled = true - if hasPendingMR { - // Ephemeral model: polecat nuked, MR continues in Refinery - result.Action = fmt.Sprintf("auto-nuked %s (ephemeral: exit=%s, MR=%s): %s", payload.PolecatName, payload.Exit, payload.MRID, nukeResult.Reason) - } else { - result.Action = fmt.Sprintf("auto-nuked %s (exit=%s, no MR): %s", payload.PolecatName, payload.Exit, nukeResult.Reason) - } + result.Action = fmt.Sprintf("auto-nuked %s (exit=%s, no MR): %s", payload.PolecatName, payload.Exit, nukeResult.Reason) return result } if nukeResult.Error != nil { @@ -87,8 +103,6 @@ func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResul } // Couldn't auto-nuke (dirty state or verification failed) - create wisp for manual intervention - // Note: Even with pending MR, if we can't auto-nuke it means something is wrong - // (uncommitted changes, unpushed commits, etc.) that needs attention. wispID, err := createCleanupWisp(workDir, payload.PolecatName, payload.IssueID, payload.Branch) if err != nil { result.Error = fmt.Errorf("creating cleanup wisp: %w", err) @@ -97,11 +111,7 @@ func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResul result.Handled = true result.WispCreated = wispID - if hasPendingMR { - result.Action = fmt.Sprintf("created cleanup wisp %s for %s (MR=%s, needs intervention: %s)", wispID, payload.PolecatName, payload.MRID, nukeResult.Reason) - } else { - result.Action = fmt.Sprintf("created cleanup wisp %s for %s (needs manual cleanup: %s)", wispID, payload.PolecatName, nukeResult.Reason) - } + result.Action = fmt.Sprintf("created cleanup wisp %s for %s (needs manual cleanup: %s)", wispID, payload.PolecatName, nukeResult.Reason) return result }