Files
gastown/internal/cmd/sling_batch.go
mayor 7924921d17 fix(sling): auto-attach work molecule and handle dead polecats
Combines three related sling improvements:

1. Auto-attach mol-polecat-work (Issue #288)
   - Automatically attach work molecule when slinging to polecats
   - Ensures polecats have standard guidance molecule attached

2. Fix polecat hook with molecule (Issue #197)
   - Use beads.ResolveHookDir() for correct directory resolution
   - Prevents bd cook from failing in polecat worktree

3. Spawn fresh polecat when target has no session
   - When slinging to a dead polecat, spawn fresh one instead of failing
   - Fixes stale convoys not progressing due to done polecats
2026-01-13 14:01:49 +01:00

160 lines
5.1 KiB
Go

package cmd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/style"
)
// runBatchSling handles slinging multiple beads to a rig.
// Each bead gets its own freshly spawned polecat.
func runBatchSling(beadIDs []string, rigName string, townBeadsDir string) error {
// Validate all beads exist before spawning any polecats
for _, beadID := range beadIDs {
if err := verifyBeadExists(beadID); err != nil {
return fmt.Errorf("bead '%s' not found", beadID)
}
}
if slingDryRun {
fmt.Printf("%s Batch slinging %d beads to rig '%s':\n", style.Bold.Render("🎯"), len(beadIDs), rigName)
for _, beadID := range beadIDs {
fmt.Printf(" Would spawn polecat for: %s\n", beadID)
}
return nil
}
fmt.Printf("%s Batch slinging %d beads to rig '%s'...\n", style.Bold.Render("🎯"), len(beadIDs), rigName)
// Track results for summary
type slingResult struct {
beadID string
polecat string
success bool
errMsg string
}
results := make([]slingResult, 0, len(beadIDs))
// Spawn a polecat for each bead and sling it
for i, beadID := range beadIDs {
fmt.Printf("\n[%d/%d] Slinging %s...\n", i+1, len(beadIDs), beadID)
// Check bead status
info, err := getBeadInfo(beadID)
if err != nil {
results = append(results, slingResult{beadID: beadID, success: false, errMsg: err.Error()})
fmt.Printf(" %s Could not get bead info: %v\n", style.Dim.Render("✗"), err)
continue
}
if info.Status == "pinned" && !slingForce {
results = append(results, slingResult{beadID: beadID, success: false, errMsg: "already pinned"})
fmt.Printf(" %s Already pinned (use --force to re-sling)\n", style.Dim.Render("✗"))
continue
}
// Spawn a fresh polecat
spawnOpts := SlingSpawnOptions{
Force: slingForce,
Account: slingAccount,
Create: slingCreate,
HookBead: beadID, // Set atomically at spawn time
Agent: slingAgent,
}
spawnInfo, err := SpawnPolecatForSling(rigName, spawnOpts)
if err != nil {
results = append(results, slingResult{beadID: beadID, success: false, errMsg: err.Error()})
fmt.Printf(" %s Failed to spawn polecat: %v\n", style.Dim.Render("✗"), err)
continue
}
targetAgent := spawnInfo.AgentID()
hookWorkDir := spawnInfo.ClonePath
// Auto-convoy: check if issue is already tracked
if !slingNoConvoy {
existingConvoy := isTrackedByConvoy(beadID)
if existingConvoy == "" {
convoyID, err := createAutoConvoy(beadID, info.Title)
if err != nil {
fmt.Printf(" %s Could not create auto-convoy: %v\n", style.Dim.Render("Warning:"), err)
} else {
fmt.Printf(" %s Created convoy 🚚 %s\n", style.Bold.Render("→"), convoyID)
}
} else {
fmt.Printf(" %s Already tracked by convoy %s\n", style.Dim.Render("○"), existingConvoy)
}
}
// Hook the bead. See: https://github.com/steveyegge/gastown/issues/148
townRoot := filepath.Dir(townBeadsDir)
hookCmd := exec.Command("bd", "--no-daemon", "update", beadID, "--status=hooked", "--assignee="+targetAgent)
hookCmd.Dir = beads.ResolveHookDir(townRoot, beadID, hookWorkDir)
hookCmd.Stderr = os.Stderr
if err := hookCmd.Run(); err != nil {
results = append(results, slingResult{beadID: beadID, polecat: spawnInfo.PolecatName, success: false, errMsg: "hook failed"})
fmt.Printf(" %s Failed to hook bead: %v\n", style.Dim.Render("✗"), err)
continue
}
fmt.Printf(" %s Work attached to %s\n", style.Bold.Render("✓"), spawnInfo.PolecatName)
// Log sling event
actor := detectActor()
_ = events.LogFeed(events.TypeSling, actor, events.SlingPayload(beadID, targetAgent))
// 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 {
fmt.Printf(" %s Could not store args: %v\n", style.Dim.Render("Warning:"), err)
}
}
// Nudge the polecat
if spawnInfo.Pane != "" {
if err := injectStartPrompt(spawnInfo.Pane, beadID, slingSubject, slingArgs); err != nil {
fmt.Printf(" %s Could not nudge (agent will discover via gt prime)\n", style.Dim.Render("○"))
} else {
fmt.Printf(" %s Start prompt sent\n", style.Bold.Render("▶"))
}
}
results = append(results, slingResult{beadID: beadID, polecat: spawnInfo.PolecatName, success: true})
}
// Wake witness and refinery once at the end
wakeRigAgents(rigName)
// Print summary
successCount := 0
for _, r := range results {
if r.success {
successCount++
}
}
fmt.Printf("\n%s Batch sling complete: %d/%d succeeded\n", style.Bold.Render("📊"), successCount, len(beadIDs))
if successCount < len(beadIDs) {
for _, r := range results {
if !r.success {
fmt.Printf(" %s %s: %s\n", style.Dim.Render("✗"), r.beadID, r.errMsg)
}
}
}
return nil
}