Remove StateIdle and idle polecat concept

The transient polecat model says: "Polecats exist only while working."
This removes the deprecated StateIdle and updates the codebase:

- Remove StateIdle from polecat/types.go (keep StateActive for legacy data)
- Update manager.go: Get() returns StateDone (not StateIdle) when no work
- Update manager.go: Add/Recreate return StateWorking (not StateIdle)
- Remove zombie scan logic from deacon.go (no idle polecats to scan for)
- Update tests to reflect new behavior

The correct lifecycle is now:
- Spawn: polecat created with work (StateWorking)
- Work: sessions cycle, sandbox persists
- Done: polecat signals completion (StateDone)
- Nuke: Witness destroys sandbox

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
prime
2026-01-04 14:36:23 -08:00
committed by Steve Yegge
parent 871410f157
commit af95b7b7f4
4 changed files with 29 additions and 324 deletions
+17 -17
View File
@@ -259,13 +259,13 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
fmt.Printf("Warning: could not create agent bead: %v\n", err)
}
// Return polecat with derived state (no issue assigned yet = idle)
// Return polecat with working state (transient model: polecats are spawned with work)
// State is derived from beads, not stored in state.json
now := time.Now()
polecat := &Polecat{
Name: name,
Rig: m.rig.Name,
State: StateIdle, // No issue assigned yet
State: StateWorking, // Transient model: polecat spawns with work
ClonePath: polecatPath,
Branch: branchName,
CreatedAt: now,
@@ -478,12 +478,12 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
fmt.Printf("Warning: could not create agent bead: %v\n", err)
}
// Return fresh polecat
// Return fresh polecat in working state (transient model: polecats are spawned with work)
now := time.Now()
return &Polecat{
Name: name,
Rig: m.rig.Name,
State: StateIdle,
State: StateWorking,
ClonePath: polecatPath,
Branch: branchName,
CreatedAt: now,
@@ -543,9 +543,8 @@ func (m *Manager) List() ([]*Polecat, error) {
// Get returns a specific polecat by name.
// State is derived from beads assignee field:
// - If an issue is assigned to this polecat and is open/in_progress: StateWorking
// - If an issue is assigned but closed: StateDone
// - If no issue assigned: StateIdle
// - If an issue is assigned to this polecat: StateWorking
// - If no issue assigned: StateDone (ready for cleanup - transient polecats should have work)
func (m *Manager) Get(name string) (*Polecat, error) {
if !m.exists(name) {
return nil, ErrPolecatNotFound
@@ -557,7 +556,7 @@ func (m *Manager) Get(name string) (*Polecat, error) {
// SetState updates a polecat's state.
// In the beads model, state is derived from issue status:
// - StateWorking/StateActive: issue status set to in_progress
// - StateDone/StateIdle: assignee cleared from issue
// - StateDone: assignee cleared from issue (polecat ready for cleanup)
// - StateStuck: issue status set to blocked (if supported)
// If beads is not available, this is a no-op.
func (m *Manager) SetState(name string, state State) error {
@@ -582,8 +581,8 @@ func (m *Manager) SetState(name string, state State) error {
return fmt.Errorf("setting issue status: %w", err)
}
}
case StateDone, StateIdle:
// Clear assignment when done/idle
case StateDone:
// Clear assignment when done (polecat ready for cleanup)
if issue != nil {
empty := ""
if err := m.beads.Update(issue.ID, beads.UpdateOptions{Assignee: &empty}); err != nil {
@@ -654,7 +653,8 @@ func (m *Manager) ClearIssue(name string) error {
}
// loadFromBeads gets polecat info from beads assignee field.
// State is simple: issue assigned → working, no issue → idle.
// State is simple: issue assigned → working, no issue → done (ready for cleanup).
// Transient polecats should always have work; no work means ready for Witness cleanup.
// We don't interpret issue status (ZFC: Go is transport, not decision-maker).
func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
polecatPath := m.polecatDir(name)
@@ -671,20 +671,20 @@ func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
assignee := m.assigneeID(name)
issue, beadsErr := m.beads.GetAssignedIssue(assignee)
if beadsErr != nil {
// If beads query fails, return basic polecat info
// This allows the system to work even if beads is not available
// If beads query fails, return basic polecat info as working
// (assume polecat is doing something if it exists)
return &Polecat{
Name: name,
Rig: m.rig.Name,
State: StateIdle,
State: StateWorking,
ClonePath: polecatPath,
Branch: branchName,
}, nil
}
// Simple rule: has issue = working, no issue = idle
// We don't interpret issue.Status - that's for Claude to decide
state := StateIdle
// Transient model: has issue = working, no issue = done (ready for cleanup)
// Polecats without work should be nuked by the Witness
state := StateDone
issueID := ""
if issue != nil {
issueID = issue.ID
+7 -8
View File
@@ -17,8 +17,7 @@ func TestStateIsActive(t *testing.T) {
{StateWorking, true},
{StateDone, false},
{StateStuck, false},
// Legacy states are treated as active
{StateIdle, true},
// Legacy active state is treated as active
{StateActive, true},
}
@@ -34,7 +33,6 @@ func TestStateIsWorking(t *testing.T) {
state State
working bool
}{
{StateIdle, false},
{StateActive, false},
{StateWorking, true},
{StateDone, false},
@@ -143,8 +141,9 @@ func TestAssigneeID(t *testing.T) {
// Note: State persistence tests removed - state is now derived from beads assignee field.
// Integration tests should verify beads-based state management.
func TestGetReturnsIdleWithoutBeads(t *testing.T) {
// When beads is not available, Get should return StateIdle
func TestGetReturnsWorkingWithoutBeads(t *testing.T) {
// When beads is not available, Get should return StateWorking
// (assume the polecat is doing something if it exists)
root := t.TempDir()
polecatDir := filepath.Join(root, "polecats", "Test")
if err := os.MkdirAll(polecatDir, 0755); err != nil {
@@ -163,7 +162,7 @@ func TestGetReturnsIdleWithoutBeads(t *testing.T) {
}
m := NewManager(r, git.NewGit(root))
// Get should return polecat with StateIdle (no beads = no assignment)
// Get should return polecat with StateWorking (assume active if beads unavailable)
polecat, err := m.Get("Test")
if err != nil {
t.Fatalf("Get: %v", err)
@@ -172,8 +171,8 @@ func TestGetReturnsIdleWithoutBeads(t *testing.T) {
if polecat.Name != "Test" {
t.Errorf("Name = %q, want Test", polecat.Name)
}
if polecat.State != StateIdle {
t.Errorf("State = %v, want StateIdle (beads not available)", polecat.State)
if polecat.State != StateWorking {
t.Errorf("State = %v, want StateWorking (beads not available)", polecat.State)
}
}
+5 -6
View File
@@ -19,10 +19,9 @@ const (
// StateStuck means the polecat needs assistance.
StateStuck State = "stuck"
// Legacy states for backward compatibility during transition.
// New code should not use these.
StateIdle State = "idle" // Deprecated: use StateWorking
StateActive State = "active" // Deprecated: use StateWorking
// StateActive is deprecated: use StateWorking.
// Kept only for backward compatibility with existing data.
StateActive State = "active"
)
// IsWorking returns true if the polecat is currently working.
@@ -32,9 +31,9 @@ func (s State) IsWorking() bool {
// IsActive returns true if the polecat session is actively working.
// For transient polecats, this is true for working state and
// legacy idle/active states (treated as working).
// legacy active state (treated as working).
func (s State) IsActive() bool {
return s == StateWorking || s == StateIdle || s == StateActive
return s == StateWorking || s == StateActive
}
// Polecat represents a worker agent in a rig.