feat(sling): Support batch slinging multiple beads to a rig
Allows slinging multiple beads in a single command: gt sling gt-abc gt-def gt-ghi gastown Each bead gets its own freshly spawned polecat. This parallelizes work dispatch without running gt sling N times manually. Changes: - Updated Args from RangeArgs(1,2) to MinimumNArgs(1) - Added batch mode detection when len(args)>2 and last arg is a rig - Added runBatchSling() to handle multiple beads with progress tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,8 +84,14 @@ Compare:
|
|||||||
gt sling <bead> # Attach + start now (keep context)
|
gt sling <bead> # Attach + start now (keep context)
|
||||||
gt handoff <bead> # Attach + restart (fresh context)
|
gt handoff <bead> # Attach + restart (fresh context)
|
||||||
|
|
||||||
The propulsion principle: if it's on your hook, YOU RUN IT.`,
|
The propulsion principle: if it's on your hook, YOU RUN IT.
|
||||||
Args: cobra.RangeArgs(1, 2),
|
|
||||||
|
Batch Slinging:
|
||||||
|
gt sling gt-abc gt-def gt-ghi gastown # Sling multiple beads to a rig
|
||||||
|
|
||||||
|
When multiple beads are provided with a rig target, each bead gets its own
|
||||||
|
polecat. This parallelizes work dispatch without running gt sling N times.`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: runSling,
|
RunE: runSling,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +152,16 @@ func runSling(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("--var cannot be used with --on (formula-on-bead mode doesn't support variables)")
|
return fmt.Errorf("--var cannot be used with --on (formula-on-bead mode doesn't support variables)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch mode detection: multiple beads with rig target
|
||||||
|
// Pattern: gt sling gt-abc gt-def gt-ghi gastown
|
||||||
|
// When len(args) > 2 and last arg is a rig, sling each bead to its own polecat
|
||||||
|
if len(args) > 2 {
|
||||||
|
lastArg := args[len(args)-1]
|
||||||
|
if rigName, isRig := IsRigName(lastArg); isRig {
|
||||||
|
return runBatchSling(args[:len(args)-1], rigName, townBeadsDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --quality is shorthand for formula-on-bead with polecat workflow
|
// --quality is shorthand for formula-on-bead with polecat workflow
|
||||||
// Convert: gt sling gp-abc greenplace --quality=shiny
|
// Convert: gt sling gp-abc greenplace --quality=shiny
|
||||||
// To: gt sling mol-polecat-shiny --on gt-abc gastown
|
// To: gt sling mol-polecat-shiny --on gt-abc gastown
|
||||||
@@ -1233,3 +1249,150 @@ func createAutoConvoy(beadID, beadTitle string) (string, error) {
|
|||||||
|
|
||||||
return convoyID, nil
|
return convoyID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
if slingNaked {
|
||||||
|
fmt.Printf(" --naked: would skip tmux sessions\n")
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
Naked: slingNaked,
|
||||||
|
Account: slingAccount,
|
||||||
|
Create: slingCreate,
|
||||||
|
HookBead: beadID, // Set atomically at spawn time
|
||||||
|
}
|
||||||
|
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
|
||||||
|
hookCmd := exec.Command("bd", "update", beadID, "--status=hooked", "--assignee="+targetAgent)
|
||||||
|
hookCmd.Env = append(os.Environ(), "BEADS_DIR="+townBeadsDir)
|
||||||
|
if hookWorkDir != "" {
|
||||||
|
hookCmd.Dir = 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)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user