feat(witness): Implement ephemeral polecat model for immediate recycling

Updates HandlePolecatDone to auto-nuke polecats immediately after MR submission
when cleanup_status=clean. This separates polecat lifecycle from MR lifecycle:

- Polecat lifecycle: spawning → working → mr_submitted → nuked
- MR lifecycle: created → queued → processed → merged (handled by Refinery)

Key changes:
- Try auto-nuke for ALL POLECAT_DONE messages regardless of MR status
- If cleanup_status=clean (branch pushed), nuke immediately
- If dirty state, create cleanup wisp for manual intervention
- Cleanup wisps are now exception handling, not the normal flow

Conflict resolution is handled by the Refinery, which creates NEW tasks
for NEW polecats when merge conflicts are detected.

(gt-si8rq.9)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nux
2026-01-03 12:55:15 -08:00
committed by Steve Yegge
parent 915f77ea03
commit 8d61c043cd

View File

@@ -29,7 +29,14 @@ type HandlerResult struct {
// HandlePolecatDone processes a POLECAT_DONE message from a polecat. // HandlePolecatDone processes a POLECAT_DONE message from a polecat.
// For ESCALATED/DEFERRED exits (no pending MR), auto-nukes if clean. // For ESCALATED/DEFERRED exits (no pending MR), auto-nukes if clean.
// For PHASE_COMPLETE exits, recycles the polecat (session ends, worktree kept). // For PHASE_COMPLETE exits, recycles the polecat (session ends, worktree kept).
// For exits with pending MR, creates a cleanup wisp to wait for MERGED. // For COMPLETED exits with MR and clean state, auto-nukes immediately (ephemeral model).
// For exits with pending MR but dirty state, creates cleanup wisp for manual intervention.
//
// Ephemeral Polecat Model:
// Polecats are truly ephemeral - done at MR submission, recyclable immediately.
// Once the branch is pushed (cleanup_status=clean), the polecat can be nuked.
// The MR lifecycle continues independently in the Refinery.
// If conflicts arise, Refinery creates a NEW conflict-resolution task for a NEW polecat.
func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResult { func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResult {
result := &HandlerResult{ result := &HandlerResult{
MessageID: msg.ID, MessageID: msg.ID,
@@ -59,23 +66,28 @@ func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResul
// ESCALATED/DEFERRED exits typically have no MR pending // ESCALATED/DEFERRED exits typically have no MR pending
hasPendingMR := payload.MRID != "" || payload.Exit == "COMPLETED" hasPendingMR := payload.MRID != "" || payload.Exit == "COMPLETED"
if !hasPendingMR { // Ephemeral model: try to auto-nuke immediately regardless of MR status
// No MR pending - can auto-nuke immediately if clean // If cleanup_status is clean, the branch is pushed and polecat is recyclable.
nukeResult := AutoNukeIfClean(workDir, rigName, payload.PolecatName) // The MR will be processed independently by the Refinery.
if nukeResult.Nuked { nukeResult := AutoNukeIfClean(workDir, rigName, payload.PolecatName)
result.Handled = true 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 { return result
// Nuke failed - fall through to create wisp for manual cleanup }
result.Error = nukeResult.Error if nukeResult.Error != nil {
} // Nuke failed - fall through to create wisp for manual cleanup
// Couldn't auto-nuke (dirty state or verification failed) - create wisp for manual intervention result.Error = nukeResult.Error
} }
// Create a cleanup wisp for this polecat // Couldn't auto-nuke (dirty state or verification failed) - create wisp for manual intervention
// Either waiting for MR to be merged, or needs manual cleanup // 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) wispID, err := createCleanupWisp(workDir, payload.PolecatName, payload.IssueID, payload.Branch)
if err != nil { if err != nil {
result.Error = fmt.Errorf("creating cleanup wisp: %w", err) result.Error = fmt.Errorf("creating cleanup wisp: %w", err)
@@ -85,9 +97,9 @@ func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResul
result.Handled = true result.Handled = true
result.WispCreated = wispID result.WispCreated = wispID
if hasPendingMR { if hasPendingMR {
result.Action = fmt.Sprintf("created cleanup wisp %s for %s (waiting for MR %s)", wispID, payload.PolecatName, payload.MRID) result.Action = fmt.Sprintf("created cleanup wisp %s for %s (MR=%s, needs intervention: %s)", wispID, payload.PolecatName, payload.MRID, nukeResult.Reason)
} else { } else {
result.Action = fmt.Sprintf("created cleanup wisp %s for %s (needs manual cleanup: dirty state)", wispID, payload.PolecatName) result.Action = fmt.Sprintf("created cleanup wisp %s for %s (needs manual cleanup: %s)", wispID, payload.PolecatName, nukeResult.Reason)
} }
return result return result