Polecats have exactly three operating conditions - there is no idle pool: - Working: session active, doing assigned work - Stalled: session stopped unexpectedly, never nudged back - Zombie: gt done called but cleanup failed Key clarifications: - These are SESSION states; polecat identity persists across sessions - "Stalled" and "zombie" are detected conditions, not stored states - The status:idle label only applies to persistent agents, not polecats Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
147 lines
5.0 KiB
Go
147 lines
5.0 KiB
Go
// Package polecat provides polecat lifecycle management.
|
|
package polecat
|
|
|
|
import "time"
|
|
|
|
// State represents the current session state of a polecat.
|
|
//
|
|
// IMPORTANT: There is NO idle state. Polecats have three operating conditions:
|
|
//
|
|
// - Working: Session active, doing assigned work (normal operation)
|
|
// - Stalled: Session stopped unexpectedly, was never nudged back to life
|
|
// - Zombie: Session called 'gt done' but cleanup failed - tried to die but couldn't
|
|
//
|
|
// The distinction matters: zombies completed their work; stalled polecats did not.
|
|
// Neither is "idle" - stalled polecats are SUPPOSED to be working, zombies are
|
|
// SUPPOSED to be dead. There is no idle pool where polecats wait for work.
|
|
//
|
|
// Note: These are SESSION states. The polecat IDENTITY (CV chain, mailbox, work
|
|
// history) persists across sessions. A stalled or zombie session doesn't destroy
|
|
// the polecat's identity - it just means the session needs intervention.
|
|
//
|
|
// "Stalled" and "zombie" are detected conditions, not stored states. The Witness
|
|
// detects them through monitoring (tmux state, age in StateDone, etc.).
|
|
type State string
|
|
|
|
const (
|
|
// StateWorking means the polecat session is actively working on an issue.
|
|
// This is the initial and primary state for transient polecats.
|
|
// Working is the ONLY healthy operating state - there is no idle pool.
|
|
StateWorking State = "working"
|
|
|
|
// StateDone means the polecat has completed its assigned work and called
|
|
// 'gt done'. This is normally a transient state - the session should exit
|
|
// immediately after. If a polecat remains in StateDone, it's a "zombie":
|
|
// the cleanup failed and the session is stuck.
|
|
StateDone State = "done"
|
|
|
|
// StateStuck means the polecat has explicitly signaled it needs assistance.
|
|
// This is an intentional request for help from the polecat itself.
|
|
// Different from "stalled" (detected externally when session stops working).
|
|
StateStuck State = "stuck"
|
|
|
|
// 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.
|
|
func (s State) IsWorking() bool {
|
|
return s == StateWorking
|
|
}
|
|
|
|
// IsActive returns true if the polecat session is actively working.
|
|
// For transient polecats, this is true for working state and
|
|
// legacy active state (treated as working).
|
|
func (s State) IsActive() bool {
|
|
return s == StateWorking || s == StateActive
|
|
}
|
|
|
|
// Polecat represents a worker agent in a rig.
|
|
type Polecat struct {
|
|
// Name is the polecat identifier.
|
|
Name string `json:"name"`
|
|
|
|
// Rig is the rig this polecat belongs to.
|
|
Rig string `json:"rig"`
|
|
|
|
// State is the current lifecycle state.
|
|
State State `json:"state"`
|
|
|
|
// ClonePath is the path to the polecat's clone of the rig.
|
|
ClonePath string `json:"clone_path"`
|
|
|
|
// Branch is the current git branch.
|
|
Branch string `json:"branch"`
|
|
|
|
// Issue is the currently assigned issue ID (if any).
|
|
Issue string `json:"issue,omitempty"`
|
|
|
|
// CreatedAt is when the polecat was created.
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// UpdatedAt is when the polecat was last updated.
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Summary provides a concise view of polecat status.
|
|
type Summary struct {
|
|
Name string `json:"name"`
|
|
State State `json:"state"`
|
|
Issue string `json:"issue,omitempty"`
|
|
}
|
|
|
|
// Summary returns a Summary for this polecat.
|
|
func (p *Polecat) Summary() Summary {
|
|
return Summary{
|
|
Name: p.Name,
|
|
State: p.State,
|
|
Issue: p.Issue,
|
|
}
|
|
}
|
|
|
|
// CleanupStatus represents the git state of a polecat for cleanup decisions.
|
|
// The Witness uses this to determine whether it's safe to nuke a polecat worktree.
|
|
type CleanupStatus string
|
|
|
|
const (
|
|
// CleanupClean means the worktree has no uncommitted work and is safe to remove.
|
|
CleanupClean CleanupStatus = "clean"
|
|
|
|
// CleanupUncommitted means there are uncommitted changes in the worktree.
|
|
CleanupUncommitted CleanupStatus = "has_uncommitted"
|
|
|
|
// CleanupStash means there are stashed changes that would be lost.
|
|
CleanupStash CleanupStatus = "has_stash"
|
|
|
|
// CleanupUnpushed means there are commits not pushed to the remote.
|
|
CleanupUnpushed CleanupStatus = "has_unpushed"
|
|
|
|
// CleanupUnknown means the status could not be determined.
|
|
CleanupUnknown CleanupStatus = "unknown"
|
|
)
|
|
|
|
// IsSafe returns true if the status indicates it's safe to remove the worktree
|
|
// without losing any work.
|
|
func (s CleanupStatus) IsSafe() bool {
|
|
return s == CleanupClean
|
|
}
|
|
|
|
// RequiresRecovery returns true if the status indicates there is work that
|
|
// needs to be recovered before removal. This includes uncommitted changes,
|
|
// stashes, and unpushed commits.
|
|
func (s CleanupStatus) RequiresRecovery() bool {
|
|
switch s {
|
|
case CleanupUncommitted, CleanupStash, CleanupUnpushed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// CanForceRemove returns true if the status allows forced removal.
|
|
// Uncommitted changes can be force-removed, but stashes and unpushed commits cannot.
|
|
func (s CleanupStatus) CanForceRemove() bool {
|
|
return s == CleanupClean || s == CleanupUncommitted
|
|
}
|