polecat

package
v0.3.0 Latest Latest
Warning

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

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

Documentation

Overview

Package polecat provides polecat lifecycle 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 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 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) 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 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