fix(sling): Set hook_bead atomically at polecat spawn time (gt-h46pk)

When slinging work to a rig (auto-spawning a polecat), the hook_bead
is now set atomically during agent bead creation rather than in a
separate updateAgentHookBead call after spawn.

This fixes cross-beads routing issues when town beads (hq-*) are slung
to rig polecats (gt-* agent beads). By setting hook_bead at creation
time within the polecat manager context, the correct beads routing is
used.

Changes:
- Add AddOptions struct with HookBead field to polecat.Manager
- Add AddWithOptions() and RecreateWithOptions() functions
- Pass HookBead through SlingSpawnOptions in cmd/polecat_spawn.go
- Pass beadID as HookBead in cmd/sling.go for rig target spawns

Note: updateAgentHookBead() is kept for non-spawn targets (existing
agents) and formula-on-bead mode (updates hook to wisp root after
creation).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/polecats/furiosa
2026-01-01 18:49:09 -08:00
committed by Steve Yegge
parent a85fae73a5
commit e159489edb
3 changed files with 40 additions and 10 deletions

View File

@@ -34,10 +34,11 @@ func (s *SpawnedPolecatInfo) AgentID() string {
// SlingSpawnOptions contains options for spawning a polecat via sling.
type SlingSpawnOptions struct {
Force bool // Force spawn even if polecat has uncommitted work
Naked bool // No-tmux mode: skip session creation
Account string // Claude Code account handle to use
Create bool // Create polecat if it doesn't exist (currently always true for sling)
Force bool // Force spawn even if polecat has uncommitted work
Naked bool // No-tmux mode: skip session creation
Account string // Claude Code account handle to use
Create bool // Create polecat if it doesn't exist (currently always true for sling)
HookBead string // Bead ID to set as hook_bead at spawn time (atomic assignment)
}
// SpawnPolecatForSling creates a fresh polecat and optionally starts its session.
@@ -77,6 +78,12 @@ func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolec
// Check if polecat already exists (shouldn't, since we allocated fresh)
existingPolecat, err := polecatMgr.Get(polecatName)
// Build add options with hook_bead set atomically at spawn time
addOpts := polecat.AddOptions{
HookBead: opts.HookBead,
}
if err == nil {
// Exists - recreate with fresh worktree
// Check for uncommitted work first
@@ -89,13 +96,13 @@ func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolec
}
}
fmt.Printf("Recreating polecat %s with fresh worktree...\n", polecatName)
if _, err = polecatMgr.Recreate(polecatName, opts.Force); err != nil {
if _, err = polecatMgr.RecreateWithOptions(polecatName, opts.Force, addOpts); err != nil {
return nil, fmt.Errorf("recreating polecat: %w", err)
}
} else if err == polecat.ErrPolecatNotFound {
// Create new polecat
fmt.Printf("Creating polecat %s...\n", polecatName)
if _, err = polecatMgr.Add(polecatName); err != nil {
if _, err = polecatMgr.AddWithOptions(polecatName, addOpts); err != nil {
return nil, fmt.Errorf("creating polecat: %w", err)
}
} else {

View File

@@ -238,10 +238,11 @@ func runSling(cmd *cobra.Command, args []string) error {
// Spawn a fresh polecat in the rig
fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName)
spawnOpts := SlingSpawnOptions{
Force: slingForce,
Naked: slingNaked,
Account: slingAccount,
Create: slingCreate,
Force: slingForce,
Naked: slingNaked,
Account: slingAccount,
Create: slingCreate,
HookBead: beadID, // Set atomically at spawn time
}
spawnInfo, spawnErr := SpawnPolecatForSling(rigName, spawnOpts)
if spawnErr != nil {

View File

@@ -175,6 +175,11 @@ func (m *Manager) exists(name string) bool {
return err == nil
}
// AddOptions configures polecat creation.
type AddOptions struct {
HookBead string // Bead ID to set as hook_bead at spawn time (atomic assignment)
}
// 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.
@@ -184,6 +189,13 @@ func (m *Manager) exists(name string) bool {
// This prevents drift issues from stale branches and ensures a clean starting state.
// Old branches are ephemeral and never pushed to origin.
func (m *Manager) Add(name string) (*Polecat, error) {
return m.AddWithOptions(name, AddOptions{})
}
// 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 (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error) {
if m.exists(name) {
return nil, ErrPolecatExists
}
@@ -226,12 +238,14 @@ func (m *Manager) Add(name string) (*Polecat, error) {
// Create agent bead for ZFC compliance (self-report state).
// State starts as "spawning" - will be updated to "working" when Claude starts.
// HookBead is set atomically at creation time if provided (avoids cross-beads routing issues).
agentID := m.agentBeadID(name)
_, err = m.beads.CreateAgentBead(agentID, agentID, &beads.AgentFields{
RoleType: "polecat",
Rig: m.rig.Name,
AgentState: "spawning",
RoleBead: "gt-polecat-role",
HookBead: opts.HookBead, // Set atomically at spawn time
})
if err != nil {
// Non-fatal - log warning but continue
@@ -372,6 +386,12 @@ func (m *Manager) ReleaseName(name string) {
// Branch naming: Each recreation gets a unique branch (polecat/<name>-<timestamp>).
// Old branches are left for garbage collection - they're never pushed to origin.
func (m *Manager) Recreate(name string, force bool) (*Polecat, error) {
return m.RecreateWithOptions(name, force, AddOptions{})
}
// RecreateWithOptions removes an existing polecat and creates a fresh worktree with options.
// This allows setting hook_bead atomically at recreation time.
func (m *Manager) RecreateWithOptions(name string, force bool, opts AddOptions) (*Polecat, error) {
if !m.exists(name) {
return nil, ErrPolecatNotFound
}
@@ -433,11 +453,13 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) {
}
// Create fresh agent bead for ZFC compliance
// HookBead is set atomically at recreation time if provided.
_, err = m.beads.CreateAgentBead(agentID, agentID, &beads.AgentFields{
RoleType: "polecat",
Rig: m.rig.Name,
AgentState: "spawning",
RoleBead: "gt-polecat-role",
HookBead: opts.HookBead, // Set atomically at spawn time
})
if err != nil {
fmt.Printf("Warning: could not create agent bead: %v\n", err)