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.
// For ESCALATED/DEFERRED exits (no pending MR), auto-nukes if clean.
// 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 {
result := &HandlerResult{
MessageID: msg.ID,
@@ -59,23 +66,28 @@ func HandlePolecatDone(workDir, rigName string, msg *mail.Message) *HandlerResul
// ESCALATED/DEFERRED exits typically have no MR pending
hasPendingMR := payload.MRID != "" || payload.Exit == "COMPLETED"
if !hasPendingMR {
// No MR pending - can auto-nuke immediately if clean
nukeResult := AutoNukeIfClean(workDir, rigName, payload.PolecatName)
if nukeResult.Nuked {
result.Handled = true
// 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.
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)
return result
}
if nukeResult.Error != nil {
// Nuke failed - fall through to create wisp for manual cleanup
result.Error = nukeResult.Error
}
// Couldn't auto-nuke (dirty state or verification failed) - create wisp for manual intervention
return result
}
if nukeResult.Error != nil {
// Nuke failed - fall through to create wisp for manual cleanup
result.Error = nukeResult.Error
}
// Create a cleanup wisp for this polecat
// Either waiting for MR to be merged, or needs manual cleanup
// 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)
@@ -85,9 +97,9 @@ 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 (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 {
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