polecat

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 22, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package polecat provides polecat lifecycle management.

Package polecat provides polecat workspace and session management.

Package polecat provides polecat lifecycle management.

Index

Constants

View Source
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

View Source
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

View Source
var (
	ErrSessionRunning  = errors.New("session already running")
	ErrSessionNotFound = errors.New("session not found")
)

Session errors

View Source
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

func GetThemeNames(theme string) ([]string, error)

GetThemeNames returns the names in a specific theme.

func ListThemes

func ListThemes() []string

ListThemes returns the list of available built-in themes.

func PendingFile

func PendingFile(townRoot string) string

PendingFile returns the path to the pending spawns file.

func PruneStalePending

func PruneStalePending(townRoot string, maxAge time.Duration) (int, error)

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

func NewManager(r *rig.Rig, g *git.Git) *Manager

NewManager creates a new polecat manager.

func (*Manager) Add

func (m *Manager) Add(name string) (*Polecat, error)

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

func (m *Manager) AllocateName() (string, error)

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

func (m *Manager) AssignIssue(name, issue string) error

AssignIssue assigns an issue to a polecat by setting the issue's assignee in beads.

func (*Manager) CleanupStaleBranches

func (m *Manager) CleanupStaleBranches() (int, error)

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

func (m *Manager) ClearIssue(name string) error

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

func (m *Manager) Get(name string) (*Polecat, error)

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) List

func (m *Manager) List() ([]*Polecat, error)

List returns all polecats in the rig.

func (*Manager) PoolStatus

func (m *Manager) PoolStatus() (active int, names []string)

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

func (m *Manager) ReleaseName(name string)

ReleaseName releases a name back to the pool. This is called when a polecat is removed.

func (*Manager) Remove

func (m *Manager) Remove(name string, force bool) error

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

func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error

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

func (m *Manager) RepairWorktree(name string, force bool) (*Polecat, error)

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

func (m *Manager) SetState(name string, state State) 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: 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

func NewNamePool(rigPath, rigName string) *NamePool

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

func (p *NamePool) ActiveCount() int

ActiveCount returns the number of names currently in use from the pool.

func (*NamePool) ActiveNames

func (p *NamePool) ActiveNames() []string

ActiveNames returns a sorted list of names currently in use from the pool.

func (*NamePool) AddCustomName

func (p *NamePool) AddCustomName(name string)

AddCustomName adds a custom name to the pool.

func (*NamePool) Allocate

func (p *NamePool) Allocate() (string, error)

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) GetTheme

func (p *NamePool) GetTheme() string

GetTheme returns the current theme name.

func (*NamePool) IsPoolName

func (p *NamePool) IsPoolName(name string) bool

IsPoolName returns true if the name is a pool name (themed or numbered).

func (*NamePool) Load

func (p *NamePool) Load() error

Load loads the pool state from disk.

func (*NamePool) MarkInUse

func (p *NamePool) MarkInUse(name string)

MarkInUse marks a name as in use (for reconciling with existing polecats).

func (*NamePool) Reconcile

func (p *NamePool) Reconcile(existingPolecats []string)

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

func (p *NamePool) Release(name string)

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.

func (*NamePool) Save

func (p *NamePool) Save() error

Save persists the pool state to disk using atomic write.

func (*NamePool) SetTheme

func (p *NamePool) SetTheme(theme string) error

SetTheme sets the theme and resets the pool. Existing in-use names are preserved if they exist in the new theme.

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.

func (*Polecat) Summary

func (p *Polecat) Summary() Summary

Summary returns a Summary for this polecat.

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"
)

func (State) IsActive

func (s State) IsActive() bool

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 (State) IsWorking

func (s State) IsWorking() bool

IsWorking returns true if the polecat is currently working.

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL