feat(witness): delay polecat cleanup until MR merges (gt-12hwb)

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 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/gus
2026-01-06 13:11:30 -08:00
committed by Steve Yegge
parent b79e4a7c3b
commit 63af29284b
2 changed files with 58 additions and 21 deletions

View File

@@ -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/<rig>/polecats/<polecat-name>
```
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

View File

@@ -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
}