From e159489edbaf8bc61a4d467022b0f293d2cc25bc Mon Sep 17 00:00:00 2001 From: gastown/polecats/furiosa Date: Thu, 1 Jan 2026 18:49:09 -0800 Subject: [PATCH] fix(sling): Set hook_bead atomically at polecat spawn time (gt-h46pk) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/cmd/polecat_spawn.go | 19 +++++++++++++------ internal/cmd/sling.go | 9 +++++---- internal/polecat/manager.go | 22 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/internal/cmd/polecat_spawn.go b/internal/cmd/polecat_spawn.go index 2cba58a3..d43e5655 100644 --- a/internal/cmd/polecat_spawn.go +++ b/internal/cmd/polecat_spawn.go @@ -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 { diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 31c4234d..ecfc8006 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -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 { diff --git a/internal/polecat/manager.go b/internal/polecat/manager.go index bb296494..3a2a96ac 100644 --- a/internal/polecat/manager.go +++ b/internal/polecat/manager.go @@ -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/-). // 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)