diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 22098d20..ddd77ce9 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" @@ -253,7 +254,37 @@ func runSling(cmd *cobra.Command, args []string) error { var targetWorkDir string targetAgent, targetPane, targetWorkDir, err = resolveTargetAgent(target) if err != nil { - return fmt.Errorf("resolving target: %w", err) + // Check if this is a dead polecat (no active session) + // If so, spawn a fresh polecat instead of failing + if isPolecatTarget(target) { + // Extract rig name from polecat target (format: rig/polecats/name) + parts := strings.Split(target, "/") + if len(parts) >= 3 && parts[1] == "polecats" { + rigName := parts[0] + fmt.Printf("Target polecat has no active session, spawning fresh polecat in rig '%s'...\n", rigName) + spawnOpts := SlingSpawnOptions{ + Force: slingForce, + Account: slingAccount, + Create: slingCreate, + HookBead: beadID, + Agent: slingAgent, + } + spawnInfo, spawnErr := SpawnPolecatForSling(rigName, spawnOpts) + if spawnErr != nil { + return fmt.Errorf("spawning polecat to replace dead polecat: %w", spawnErr) + } + targetAgent = spawnInfo.AgentID() + targetPane = spawnInfo.Pane + hookWorkDir = spawnInfo.ClonePath + + // Wake witness and refinery to monitor the new polecat + wakeRigAgents(rigName) + } else { + return fmt.Errorf("resolving target: %w", err) + } + } else { + return fmt.Errorf("resolving target: %w", err) + } } // Use target's working directory for bd commands (needed for redirect-based routing) if targetWorkDir != "" { @@ -422,6 +453,15 @@ func runSling(cmd *cobra.Command, args []string) error { // Update agent bead's hook_bead field (ZFC: agents track their current work) updateAgentHookBead(targetAgent, beadID, hookWorkDir, townBeadsDir) + // Auto-attach mol-polecat-work to polecat agent beads + // This ensures polecats have the standard work molecule attached for guidance + if strings.Contains(targetAgent, "/polecats/") { + if err := attachPolecatWorkMolecule(targetAgent, hookWorkDir, townRoot); err != nil { + // Warn but don't fail - polecat will still work without molecule + fmt.Printf("%s Could not attach work molecule: %v\n", style.Dim.Render("Warning:"), err) + } + } + // Store dispatcher in bead description (enables completion notification to dispatcher) if err := storeDispatcherInBead(beadID, actor); err != nil { // Warn but don't fail - polecat will still complete work diff --git a/internal/cmd/sling_batch.go b/internal/cmd/sling_batch.go index 8d941d95..1dc4f64a 100644 --- a/internal/cmd/sling_batch.go +++ b/internal/cmd/sling_batch.go @@ -111,6 +111,11 @@ func runBatchSling(beadIDs []string, rigName string, townBeadsDir string) error // Update agent bead state updateAgentHookBead(targetAgent, beadID, hookWorkDir, townBeadsDir) + // Auto-attach mol-polecat-work molecule to polecat agent bead + if err := attachPolecatWorkMolecule(targetAgent, hookWorkDir, townRoot); err != nil { + fmt.Printf(" %s Could not attach work molecule: %v\n", style.Dim.Render("Warning:"), err) + } + // Store args if provided if slingArgs != "" { if err := storeArgsInBead(beadID, slingArgs); err != nil { diff --git a/internal/cmd/sling_helpers.go b/internal/cmd/sling_helpers.go index 1afd6840..ae4092d1 100644 --- a/internal/cmd/sling_helpers.go +++ b/internal/cmd/sling_helpers.go @@ -9,7 +9,9 @@ import ( "time" "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" + "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" ) @@ -373,3 +375,67 @@ func wakeRigAgents(rigName string) { _ = t.NudgeSession(witnessSession, "Polecat dispatched - check for work") _ = t.NudgeSession(refinerySession, "Polecat dispatched - check for merge requests") } + +// isPolecatTarget checks if the target string refers to a polecat. +// Returns true if the target format is "rig/polecats/name". +// This is used to determine if we should respawn a dead polecat +// instead of failing when slinging work. +func isPolecatTarget(target string) bool { + parts := strings.Split(target, "/") + return len(parts) >= 3 && parts[1] == "polecats" +} + +// attachPolecatWorkMolecule attaches the mol-polecat-work molecule to a polecat's agent bead. +// This ensures all polecats have the standard work molecule attached for guidance. +// The molecule is attached by storing it in the agent bead's description using attachment fields. +// +// Per issue #288: gt sling should auto-attach mol-polecat-work when slinging to polecats. +func attachPolecatWorkMolecule(targetAgent, hookWorkDir, townRoot string) error { + // Parse the polecat name from targetAgent (format: "rig/polecats/name") + parts := strings.Split(targetAgent, "/") + if len(parts) != 3 || parts[1] != "polecats" { + return fmt.Errorf("invalid polecat agent format: %s", targetAgent) + } + rigName := parts[0] + polecatName := parts[2] + + // Get the polecat's agent bead ID + // Format: "--polecat-" (e.g., "gt-gastown-polecat-Toast") + prefix := config.GetRigPrefix(townRoot, rigName) + agentBeadID := beads.PolecatBeadIDWithPrefix(prefix, rigName, polecatName) + + // Resolve the rig directory for running bd commands. + // Use ResolveHookDir to ensure we run bd from the correct rig directory + // (not from the polecat's worktree, which doesn't have a .beads directory). + // This fixes issue #197: polecat fails to hook when slinging with molecule. + rigDir := beads.ResolveHookDir(townRoot, prefix+"-"+polecatName, hookWorkDir) + + b := beads.New(rigDir) + + // Check if molecule is already attached (avoid duplicate attach) + attachment, err := b.GetAttachment(agentBeadID) + if err == nil && attachment != nil && attachment.AttachedMolecule != "" { + // Already has a molecule attached - skip + return nil + } + + // Cook the mol-polecat-work formula to ensure the proto exists + // This is safe to run multiple times - cooking is idempotent + cookCmd := exec.Command("bd", "--no-daemon", "cook", "mol-polecat-work") + cookCmd.Dir = rigDir + cookCmd.Stderr = os.Stderr + if err := cookCmd.Run(); err != nil { + return fmt.Errorf("cooking mol-polecat-work formula: %w", err) + } + + // Attach the molecule to the polecat's agent bead + // The molecule ID is the formula name "mol-polecat-work" + moleculeID := "mol-polecat-work" + _, err = b.AttachMolecule(agentBeadID, moleculeID) + if err != nil { + return fmt.Errorf("attaching molecule %s to %s: %w", moleculeID, agentBeadID, err) + } + + fmt.Printf("%s Attached %s to %s\n", style.Bold.Render("✓"), moleculeID, agentBeadID) + return nil +}