Documentation
¶
Overview ¶
Package polecat provides polecat lifecycle management.
Package polecat provides polecat workspace and session management.
Package polecat provides polecat lifecycle management.
Index ¶
- Constants
- Variables
- func GetThemeNames(theme string) ([]string, error)
- func ListThemes() []string
- func PendingFile(townRoot string) string
- func PruneStalePending(townRoot string, maxAge time.Duration) (int, error)
- func SavePending(townRoot string, pending []*PendingSpawn) error
- type AddOptions
- type CleanupStatus
- type Manager
- func (m *Manager) Add(name string) (*Polecat, error)
- func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
- func (m *Manager) AllocateName() (string, error)
- func (m *Manager) AssignIssue(name, issue string) error
- func (m *Manager) CleanupStaleBranches() (int, error)
- func (m *Manager) ClearIssue(name string) error
- func (m *Manager) DetectStalePolecats(threshold int) ([]*StalenessInfo, error)
- func (m *Manager) Get(name string) (*Polecat, error)
- func (m *Manager) List() ([]*Polecat, error)
- func (m *Manager) PoolStatus() (active int, names []string)
- func (m *Manager) ReconcilePool()
- func (m *Manager) ReleaseName(name string)
- func (m *Manager) Remove(name string, force bool) error
- func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error
- func (m *Manager) RepairWorktree(name string, force bool) (*Polecat, error)
- func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOptions) (*Polecat, error)
- func (m *Manager) SetState(name string, state State) error
- type NamePool
- func (p *NamePool) ActiveCount() int
- func (p *NamePool) ActiveNames() []string
- func (p *NamePool) AddCustomName(name string)
- func (p *NamePool) Allocate() (string, error)
- func (p *NamePool) GetTheme() string
- func (p *NamePool) IsPoolName(name string) bool
- func (p *NamePool) Load() error
- func (p *NamePool) MarkInUse(name string)
- func (p *NamePool) Reconcile(existingPolecats []string)
- func (p *NamePool) Release(name string)
- func (p *NamePool) Reset()
- func (p *NamePool) Save() error
- func (p *NamePool) SetTheme(theme string) error
- type PendingSpawn
- type Polecat
- type SessionInfo
- type SessionManager
- func (m *SessionManager) Attach(polecat string) error
- func (m *SessionManager) Capture(polecat string, lines int) (string, error)
- func (m *SessionManager) CaptureSession(sessionID string, lines int) (string, error)
- func (m *SessionManager) Inject(polecat, message string) error
- func (m *SessionManager) IsRunning(polecat string) (bool, error)
- func (m *SessionManager) List() ([]SessionInfo, error)
- func (m *SessionManager) SessionName(polecat string) string
- func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error
- func (m *SessionManager) Status(polecat string) (*SessionInfo, error)
- func (m *SessionManager) Stop(polecat string, force bool) error
- func (m *SessionManager) StopAll(force bool) error
- type SessionStartOptions
- type StalenessInfo
- type State
- type Summary
- type TriggerResult
- type UncommittedWorkError
Constants ¶
const ( // DefaultPoolSize is the number of reusable names in the pool. DefaultPoolSize = 50 // DefaultTheme is the default theme for new rigs. DefaultTheme = "mad-max" )
Variables ¶
var ( ErrPolecatExists = errors.New("polecat already exists") ErrPolecatNotFound = errors.New("polecat not found") ErrHasChanges = errors.New("polecat has uncommitted changes") ErrHasUncommittedWork = errors.New("polecat has uncommitted work") )
Common errors
var ( ErrSessionRunning = errors.New("session already running") ErrSessionNotFound = errors.New("session not found") )
Session errors
var BuiltinThemes = map[string][]string{
"mad-max": {
"furiosa", "nux", "slit", "rictus", "dementus",
"capable", "toast", "dag", "cheedo", "valkyrie",
"keeper", "morsov", "ace", "warboy", "imperator",
"organic", "coma", "splendid", "angharad", "max",
"immortan", "bullet", "toecutter", "goose", "nightrider",
"glory", "scrotus", "chumbucket", "corpus", "dinki",
"prime", "vuvalini", "rockryder", "wretched", "buzzard",
"gastown", "bullet-farmer", "citadel", "wasteland", "fury",
"road-warrior", "interceptor", "blackfinger", "wraith", "witness",
"chrome", "shiny", "mediocre", "guzzoline", "aqua-cola",
},
"minerals": {
"obsidian", "quartz", "jasper", "onyx", "opal",
"topaz", "garnet", "ruby", "amber", "jade",
"pearl", "flint", "granite", "basalt", "marble",
"shale", "slate", "pyrite", "mica", "agate",
"malachite", "turquoise", "lapis", "emerald", "sapphire",
"diamond", "amethyst", "citrine", "zircon", "peridot",
"coral", "jet", "moonstone", "sunstone", "bloodstone",
"rhodonite", "sodalite", "hematite", "magnetite", "calcite",
"fluorite", "selenite", "kyanite", "labradorite", "amazonite",
"chalcedony", "carnelian", "aventurine", "chrysoprase", "heliodor",
},
"wasteland": {
"rust", "chrome", "nitro", "guzzle", "witness",
"shiny", "fury", "thunder", "dust", "scavenger",
"radrat", "ghoul", "mutant", "raider", "vault",
"pipboy", "nuka", "brahmin", "deathclaw", "mirelurk",
"synth", "institute", "enclave", "brotherhood", "minuteman",
"railroad", "atom", "crater", "foundation", "refuge",
"settler", "wanderer", "courier", "lone", "chosen",
"tribal", "khan", "legion", "ncr", "ranger",
"overseer", "sentinel", "paladin", "scribe", "initiate",
"elder", "lancer", "knight", "squire", "proctor",
},
}
Built-in themes with themed polecat names.
Functions ¶
func GetThemeNames ¶
GetThemeNames returns the names in a specific theme.
func ListThemes ¶
func ListThemes() []string
ListThemes returns the list of available built-in themes.
func PendingFile ¶
PendingFile returns the path to the pending spawns file.
func PruneStalePending ¶
PruneStalePending removes pending spawns older than the given age. Spawns that are too old likely had their sessions die.
func SavePending ¶
func SavePending(townRoot string, pending []*PendingSpawn) error
SavePending saves the pending spawns to disk.
Types ¶
type AddOptions ¶
type AddOptions struct {
HookBead string // Bead ID to set as hook_bead at spawn time (atomic assignment)
}
AddOptions configures polecat creation.
type CleanupStatus ¶
type CleanupStatus string
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.
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" )
func (CleanupStatus) CanForceRemove ¶
func (s CleanupStatus) CanForceRemove() bool
CanForceRemove returns true if the status allows forced removal. Uncommitted changes can be force-removed, but stashes and unpushed commits cannot.
func (CleanupStatus) IsSafe ¶
func (s CleanupStatus) IsSafe() bool
IsSafe returns true if the status indicates it's safe to remove the worktree without losing any work.
func (CleanupStatus) RequiresRecovery ¶
func (s CleanupStatus) RequiresRecovery() bool
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.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager handles polecat lifecycle.
func NewManager ¶
NewManager creates a new polecat manager.
func (*Manager) Add ¶
Add creates a new polecat as a git worktree from the repo base. Uses the shared bare repo (.repo.git) if available, otherwise mayor/rig. This is much faster than a full clone and shares objects with all worktrees. Polecat state is derived from beads assignee field, not state.json.
Branch naming: Each polecat run gets a unique branch (polecat/<name>-<timestamp>). This prevents drift issues from stale branches and ensures a clean starting state. Old branches are ephemeral and never pushed to origin.
func (*Manager) AddWithOptions ¶
func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
AddWithOptions creates a new polecat with the specified options. This allows setting hook_bead atomically at creation time, avoiding cross-beads routing issues when slinging work to new polecats.
func (*Manager) AllocateName ¶
AllocateName allocates a name from the name pool. Returns a pooled name (polecat-01 through polecat-50) if available, otherwise returns an overflow name (rigname-N).
func (*Manager) AssignIssue ¶
AssignIssue assigns an issue to a polecat by setting the issue's assignee in beads.
func (*Manager) CleanupStaleBranches ¶
CleanupStaleBranches removes orphaned polecat branches that are no longer in use. This includes: - Branches for polecats that no longer exist - Old timestamped branches (keeps only the most recent per polecat name) Returns the number of branches deleted.
func (*Manager) ClearIssue ¶
ClearIssue removes the issue assignment from a polecat. In the transient model, this transitions to Done state for cleanup. This clears the assignee from the currently assigned issue in beads. If beads is not available, this is a no-op.
func (*Manager) DetectStalePolecats ¶
func (m *Manager) DetectStalePolecats(threshold int) ([]*StalenessInfo, error)
DetectStalePolecats identifies polecats that are candidates for cleanup. A polecat is considered stale if: - No active tmux session AND - Either: way behind main (>threshold commits) OR no agent bead/activity - Has no uncommitted work that could be lost
threshold: minimum commits behind main to consider "way behind" (e.g., 20)
func (*Manager) Get ¶
Get returns a specific polecat by name. State is derived from beads assignee field: - If an issue is assigned to this polecat: StateWorking - If no issue assigned: StateDone (ready for cleanup - transient polecats should have work)
func (*Manager) PoolStatus ¶
PoolStatus returns information about the name pool.
func (*Manager) ReconcilePool ¶
func (m *Manager) ReconcilePool()
ReconcilePool syncs pool state with existing polecat directories. This should be called to recover from crashes or stale state.
func (*Manager) ReleaseName ¶
ReleaseName releases a name back to the pool. This is called when a polecat is removed.
func (*Manager) Remove ¶
Remove deletes a polecat worktree. If force is true, removes even with uncommitted changes (but not stashes/unpushed). Use nuclear=true to bypass ALL safety checks.
func (*Manager) RemoveWithOptions ¶
RemoveWithOptions deletes a polecat worktree with explicit control over safety checks. force=true: bypass uncommitted changes check (legacy behavior) nuclear=true: bypass ALL safety checks including stashes and unpushed commits
ZFC #10: Uses cleanup_status from agent bead if available (polecat self-report), falls back to git check for backward compatibility.
func (*Manager) RepairWorktree ¶
RepairWorktree repairs a stale polecat by removing it and creating a fresh worktree. This is NOT for normal operation - it handles reconciliation when AllocateName returns a name that unexpectedly already exists (stale state recovery).
The polecat starts with the latest code from origin/<default-branch>. The name is preserved (not released to pool) since we're repairing immediately. force controls whether to bypass uncommitted changes check.
Branch naming: Each repair gets a unique branch (polecat/<name>-<timestamp>). Old branches are left for garbage collection - they're never pushed to origin.
func (*Manager) RepairWorktreeWithOptions ¶
func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOptions) (*Polecat, error)
RepairWorktreeWithOptions repairs a stale polecat and creates a fresh worktree with options. This is NOT for normal operation - see RepairWorktree for context. Allows setting hook_bead atomically at repair time.
func (*Manager) SetState ¶
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: 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.
type NamePool ¶
type NamePool struct {
// RigName is the rig this pool belongs to.
RigName string `json:"rig_name"`
// Theme is the current theme name (e.g., "mad-max", "minerals").
Theme string `json:"theme"`
// CustomNames allows overriding the built-in theme names.
CustomNames []string `json:"custom_names,omitempty"`
// InUse tracks which pool names are currently in use.
// Key is the name itself, value is true if in use.
InUse map[string]bool `json:"in_use"`
// OverflowNext is the next overflow sequence number.
// Starts at MaxSize+1 and increments.
OverflowNext int `json:"overflow_next"`
// MaxSize is the maximum number of themed names before overflow.
MaxSize int `json:"max_size"`
// contains filtered or unexported fields
}
NamePool manages a bounded pool of reusable polecat names. Names are drawn from a themed pool (mad-max by default). When the pool is exhausted, overflow names use rigname-N format.
func NewNamePool ¶
NewNamePool creates a new name pool for a rig.
func NewNamePoolWithConfig ¶
func NewNamePoolWithConfig(rigPath, rigName, theme string, customNames []string, maxSize int) *NamePool
NewNamePoolWithConfig creates a name pool with specific configuration.
func (*NamePool) ActiveCount ¶
ActiveCount returns the number of names currently in use from the pool.
func (*NamePool) ActiveNames ¶
ActiveNames returns a sorted list of names currently in use from the pool.
func (*NamePool) AddCustomName ¶
AddCustomName adds a custom name to the pool.
func (*NamePool) Allocate ¶
Allocate returns a name from the pool. It prefers names in order from the theme list, and falls back to overflow names when the pool is exhausted.
func (*NamePool) IsPoolName ¶
IsPoolName returns true if the name is a pool name (themed or numbered).
func (*NamePool) MarkInUse ¶
MarkInUse marks a name as in use (for reconciling with existing polecats).
func (*NamePool) Reconcile ¶
Reconcile updates the pool state based on existing polecat directories. This should be called on startup to sync pool state with reality.
func (*NamePool) Release ¶
Release returns a pooled name to the pool. For overflow names, this is a no-op (they are not reusable).
func (*NamePool) Reset ¶
func (p *NamePool) Reset()
Reset clears the pool state, releasing all names.
type PendingSpawn ¶
type PendingSpawn struct {
// Rig is the rig name (e.g., "gastown")
Rig string `json:"rig"`
// Polecat is the polecat name (e.g., "p-abc123")
Polecat string `json:"polecat"`
// Session is the tmux session name
Session string `json:"session"`
// Issue is the assigned issue ID
Issue string `json:"issue"`
// SpawnedAt is when the spawn was detected
SpawnedAt time.Time `json:"spawned_at"`
// MailID is the ID of the POLECAT_STARTED message
MailID string `json:"mail_id"`
}
PendingSpawn represents a polecat that has been spawned but not yet triggered.
func CheckInboxForSpawns ¶
func CheckInboxForSpawns(townRoot string) ([]*PendingSpawn, error)
CheckInboxForSpawns reads the Deacon's inbox for POLECAT_STARTED messages and adds them to the pending list.
func LoadPending ¶
func LoadPending(townRoot string) ([]*PendingSpawn, error)
LoadPending loads the pending spawns from disk.
type Polecat ¶
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"`
}
Polecat represents a worker agent in a rig.
type SessionInfo ¶
type SessionInfo struct {
// Polecat is the polecat name.
Polecat string `json:"polecat"`
// SessionID is the tmux session identifier.
SessionID string `json:"session_id"`
// Running indicates if the session is currently active.
Running bool `json:"running"`
// RigName is the rig this session belongs to.
RigName string `json:"rig_name"`
// Attached indicates if someone is attached to the session.
Attached bool `json:"attached,omitempty"`
// Created is when the session was created.
Created time.Time `json:"created,omitempty"`
// Windows is the number of tmux windows.
Windows int `json:"windows,omitempty"`
// LastActivity is when the session last had activity.
LastActivity time.Time `json:"last_activity,omitempty"`
}
SessionInfo contains information about a running polecat session.
type SessionManager ¶
type SessionManager struct {
// contains filtered or unexported fields
}
SessionManager handles polecat session lifecycle.
func NewSessionManager ¶
func NewSessionManager(t *tmux.Tmux, r *rig.Rig) *SessionManager
NewSessionManager creates a new polecat session manager for a rig.
func (*SessionManager) Attach ¶
func (m *SessionManager) Attach(polecat string) error
Attach attaches to a polecat session.
func (*SessionManager) Capture ¶
func (m *SessionManager) Capture(polecat string, lines int) (string, error)
Capture returns the recent output from a polecat session.
func (*SessionManager) CaptureSession ¶
func (m *SessionManager) CaptureSession(sessionID string, lines int) (string, error)
CaptureSession returns the recent output from a session by raw session ID.
func (*SessionManager) Inject ¶
func (m *SessionManager) Inject(polecat, message string) error
Inject sends a message to a polecat session.
func (*SessionManager) IsRunning ¶
func (m *SessionManager) IsRunning(polecat string) (bool, error)
IsRunning checks if a polecat session is active.
func (*SessionManager) List ¶
func (m *SessionManager) List() ([]SessionInfo, error)
List returns information about all polecat sessions for this rig.
func (*SessionManager) SessionName ¶
func (m *SessionManager) SessionName(polecat string) string
SessionName generates the tmux session name for a polecat.
func (*SessionManager) Start ¶
func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error
Start creates and starts a new session for a polecat.
func (*SessionManager) Status ¶
func (m *SessionManager) Status(polecat string) (*SessionInfo, error)
Status returns detailed status for a polecat session.
func (*SessionManager) Stop ¶
func (m *SessionManager) Stop(polecat string, force bool) error
Stop terminates a polecat session.
func (*SessionManager) StopAll ¶
func (m *SessionManager) StopAll(force bool) error
StopAll terminates all polecat sessions for this rig.
type SessionStartOptions ¶
type SessionStartOptions struct {
// WorkDir overrides the default working directory (polecat clone dir).
WorkDir string
// Issue is an optional issue ID to work on.
Issue string
// Command overrides the default "cursor-agent" command.
Command string
// Account specifies the account handle to use (overrides default).
Account string
// CursorConfigDir is resolved CURSOR_CONFIG_DIR for the account.
// If set, this is injected as an environment variable.
CursorConfigDir string
// Agent is the agent preset name (e.g., "cursor", "gemini", "codex").
// If empty, defaults to the town/rig default agent.
Agent string
}
SessionStartOptions configures polecat session startup.
type StalenessInfo ¶
type StalenessInfo struct {
Name string
CommitsBehind int // How many commits behind origin/main
HasActiveSession bool // Whether tmux session is running
HasUncommittedWork bool // Whether there's uncommitted or unpushed work
AgentState string // From agent bead (empty if no bead)
IsStale bool // Overall assessment: safe to clean up
Reason string // Why it's considered stale (or not)
}
StalenessInfo contains details about a polecat's staleness.
type State ¶
type State string
State represents the current state of a polecat. In the transient model, polecats exist only while working.
const ( // StateWorking means the polecat is actively working on an issue. // This is the initial and primary state for transient polecats. StateWorking State = "working" // StateDone means the polecat has completed its assigned work // and is ready for cleanup by the Witness. StateDone State = "done" // StateStuck means the polecat needs assistance. StateStuck State = "stuck" // StateActive is deprecated: use StateWorking. // Kept only for backward compatibility with existing data. StateActive State = "active" )
type Summary ¶
type Summary struct {
Name string `json:"name"`
State State `json:"state"`
Issue string `json:"issue,omitempty"`
}
Summary provides a concise view of polecat status.
type TriggerResult ¶
type TriggerResult struct {
Spawn *PendingSpawn
Triggered bool
Error error
}
TriggerResult holds the result of attempting to trigger a pending spawn.
func TriggerPendingSpawns ¶
func TriggerPendingSpawns(townRoot string, timeout time.Duration) ([]TriggerResult, error)
TriggerPendingSpawns polls each pending spawn and triggers when ready. Returns the spawns that were successfully triggered.
type UncommittedWorkError ¶
type UncommittedWorkError struct {
PolecatName string
Status *git.UncommittedWorkStatus
}
UncommittedWorkError provides details about uncommitted work.
func (*UncommittedWorkError) Error ¶
func (e *UncommittedWorkError) Error() string
func (*UncommittedWorkError) Unwrap ¶
func (e *UncommittedWorkError) Unwrap() error