Add gt sling support for deacon/dogs target (gt-0x5og.3)
Extends gt sling to dispatch work to dogs (Deacon helper workers): - gt sling <work> deacon/dogs - Auto-dispatch to idle dog from pool - gt sling <work> deacon/dogs/alpha - Dispatch to specific dog - Pool management: --create flag to spawn dogs if pool is empty - Dog state tracking (marks dog as working on dispatch) New functions: - IsDogTarget: Detect dog target patterns - DispatchToDog: Find/spawn dog and prepare for work - generateDogName: Create unique names for new dogs Dogs are reusable workers for infrastructure tasks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,10 +5,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/dog"
|
||||
"github.com/steveyegge/gastown/internal/events"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
@@ -25,6 +28,7 @@ var slingCmd = &cobra.Command{
|
||||
This is THE command for assigning work in Gas Town. It handles:
|
||||
- Existing agents (mayor, crew, witness, refinery)
|
||||
- Auto-spawning polecats when target is a rig
|
||||
- Dispatching to dogs (Deacon's helper workers)
|
||||
- Formula instantiation and wisp creation
|
||||
- No-tmux mode for manual agent operation
|
||||
|
||||
@@ -34,6 +38,8 @@ Target Resolution:
|
||||
gt sling gt-abc gastown # Auto-spawn polecat in rig
|
||||
gt sling gt-abc gastown/Toast # Specific polecat
|
||||
gt sling gt-abc mayor # Mayor
|
||||
gt sling gt-abc deacon/dogs # Auto-dispatch to idle dog
|
||||
gt sling gt-abc deacon/dogs/alpha # Specific dog
|
||||
|
||||
Spawning Options (when target is a rig):
|
||||
gt sling gt-abc gastown --molecule mol-review # Use specific workflow
|
||||
@@ -177,8 +183,31 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 1 {
|
||||
target := args[1]
|
||||
|
||||
// Check if target is a rig name (auto-spawn polecat)
|
||||
if rigName, isRig := IsRigName(target); isRig {
|
||||
// Check if target is a dog target (deacon/dogs or deacon/dogs/<name>)
|
||||
if dogName, isDog := IsDogTarget(target); isDog {
|
||||
if slingDryRun {
|
||||
if dogName == "" {
|
||||
fmt.Printf("Would dispatch to idle dog in kennel\n")
|
||||
} else {
|
||||
fmt.Printf("Would dispatch to dog '%s'\n", dogName)
|
||||
}
|
||||
targetAgent = fmt.Sprintf("deacon/dogs/%s", dogName)
|
||||
if dogName == "" {
|
||||
targetAgent = "deacon/dogs/<idle>"
|
||||
}
|
||||
targetPane = "<dog-pane>"
|
||||
} else {
|
||||
// Dispatch to dog
|
||||
dispatchInfo, dispatchErr := DispatchToDog(dogName, slingCreate)
|
||||
if dispatchErr != nil {
|
||||
return fmt.Errorf("dispatching to dog: %w", dispatchErr)
|
||||
}
|
||||
targetAgent = dispatchInfo.AgentID
|
||||
targetPane = dispatchInfo.Pane
|
||||
fmt.Printf("Dispatched to dog %s\n", dispatchInfo.DogName)
|
||||
}
|
||||
} else if rigName, isRig := IsRigName(target); isRig {
|
||||
// Check if target is a rig name (auto-spawn polecat)
|
||||
if slingDryRun {
|
||||
// Dry run - just indicate what would happen
|
||||
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
|
||||
@@ -593,8 +622,31 @@ func runSlingFormula(args []string) error {
|
||||
var err error
|
||||
|
||||
if target != "" {
|
||||
// Check if target is a rig name (auto-spawn polecat)
|
||||
if rigName, isRig := IsRigName(target); isRig {
|
||||
// Check if target is a dog target (deacon/dogs or deacon/dogs/<name>)
|
||||
if dogName, isDog := IsDogTarget(target); isDog {
|
||||
if slingDryRun {
|
||||
if dogName == "" {
|
||||
fmt.Printf("Would dispatch to idle dog in kennel\n")
|
||||
} else {
|
||||
fmt.Printf("Would dispatch to dog '%s'\n", dogName)
|
||||
}
|
||||
targetAgent = fmt.Sprintf("deacon/dogs/%s", dogName)
|
||||
if dogName == "" {
|
||||
targetAgent = "deacon/dogs/<idle>"
|
||||
}
|
||||
targetPane = "<dog-pane>"
|
||||
} else {
|
||||
// Dispatch to dog
|
||||
dispatchInfo, dispatchErr := DispatchToDog(dogName, slingCreate)
|
||||
if dispatchErr != nil {
|
||||
return fmt.Errorf("dispatching to dog: %w", dispatchErr)
|
||||
}
|
||||
targetAgent = dispatchInfo.AgentID
|
||||
targetPane = dispatchInfo.Pane
|
||||
fmt.Printf("Dispatched to dog %s\n", dispatchInfo.DogName)
|
||||
}
|
||||
} else if rigName, isRig := IsRigName(target); isRig {
|
||||
// Check if target is a rig name (auto-spawn polecat)
|
||||
if slingDryRun {
|
||||
// Dry run - just indicate what would happen
|
||||
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
|
||||
@@ -842,3 +894,144 @@ func qualityToFormula(quality string) (string, error) {
|
||||
return "", fmt.Errorf("invalid quality level '%s' (use: basic, shiny, or chrome)", quality)
|
||||
}
|
||||
}
|
||||
|
||||
// IsDogTarget checks if target is a dog target pattern.
|
||||
// Returns the dog name (or empty for pool dispatch) and true if it's a dog target.
|
||||
// Patterns:
|
||||
// - "deacon/dogs" -> ("", true) - dispatch to any idle dog
|
||||
// - "deacon/dogs/alpha" -> ("alpha", true) - dispatch to specific dog
|
||||
func IsDogTarget(target string) (dogName string, isDog bool) {
|
||||
target = strings.ToLower(target)
|
||||
|
||||
// Check for exact "deacon/dogs" (pool dispatch)
|
||||
if target == "deacon/dogs" {
|
||||
return "", true
|
||||
}
|
||||
|
||||
// Check for "deacon/dogs/<name>" (specific dog)
|
||||
if strings.HasPrefix(target, "deacon/dogs/") {
|
||||
name := strings.TrimPrefix(target, "deacon/dogs/")
|
||||
if name != "" && !strings.Contains(name, "/") {
|
||||
return name, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// DogDispatchInfo contains information about a dog dispatch.
|
||||
type DogDispatchInfo struct {
|
||||
DogName string // Name of the dog
|
||||
AgentID string // Agent ID format (deacon/dogs/<name>)
|
||||
Pane string // Tmux pane (empty if no session)
|
||||
Spawned bool // True if dog was spawned (new)
|
||||
}
|
||||
|
||||
// DispatchToDog finds or spawns a dog for work dispatch.
|
||||
// If dogName is empty, finds an idle dog from the pool.
|
||||
// If create is true and no dogs exist, creates one.
|
||||
func DispatchToDog(dogName string, create bool) (*DogDispatchInfo, error) {
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding town root: %w", err)
|
||||
}
|
||||
|
||||
rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
||||
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading rigs config: %w", err)
|
||||
}
|
||||
|
||||
mgr := dog.NewManager(townRoot, rigsConfig)
|
||||
|
||||
var targetDog *dog.Dog
|
||||
|
||||
if dogName != "" {
|
||||
// Specific dog requested
|
||||
targetDog, err = mgr.Get(dogName)
|
||||
if err != nil {
|
||||
if create {
|
||||
// Create the dog if it doesn't exist
|
||||
targetDog, err = mgr.Add(dogName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating dog %s: %w", dogName, err)
|
||||
}
|
||||
fmt.Printf("✓ Created dog %s\n", dogName)
|
||||
} else {
|
||||
return nil, fmt.Errorf("dog %s not found (use --create to add)", dogName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pool dispatch - find an idle dog
|
||||
targetDog, err = mgr.GetIdleDog()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding idle dog: %w", err)
|
||||
}
|
||||
|
||||
if targetDog == nil {
|
||||
if create {
|
||||
// No idle dogs - create one
|
||||
newName := generateDogName(mgr)
|
||||
targetDog, err = mgr.Add(newName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating dog %s: %w", newName, err)
|
||||
}
|
||||
fmt.Printf("✓ Created dog %s (pool was empty)\n", newName)
|
||||
} else {
|
||||
return nil, fmt.Errorf("no idle dogs available (use --create to add)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark dog as working
|
||||
if err := mgr.SetState(targetDog.Name, dog.StateWorking); err != nil {
|
||||
return nil, fmt.Errorf("setting dog state: %w", err)
|
||||
}
|
||||
|
||||
// Build agent ID
|
||||
agentID := fmt.Sprintf("deacon/dogs/%s", targetDog.Name)
|
||||
|
||||
// Try to find tmux session for the dog (dogs may run in tmux like polecats)
|
||||
sessionName := fmt.Sprintf("gt-deacon-%s", targetDog.Name)
|
||||
t := tmux.NewTmux()
|
||||
var pane string
|
||||
if has, _ := t.HasSession(sessionName); has {
|
||||
// Get the pane from the session
|
||||
pane, _ = getSessionPane(sessionName)
|
||||
}
|
||||
|
||||
return &DogDispatchInfo{
|
||||
DogName: targetDog.Name,
|
||||
AgentID: agentID,
|
||||
Pane: pane,
|
||||
Spawned: false, // TODO: track if we spawned a new session
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateDogName creates a unique dog name for pool expansion.
|
||||
func generateDogName(mgr *dog.Manager) string {
|
||||
// Use Greek alphabet for dog names
|
||||
names := []string{"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel"}
|
||||
|
||||
dogs, _ := mgr.List()
|
||||
existing := make(map[string]bool)
|
||||
for _, d := range dogs {
|
||||
existing[d.Name] = true
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
if !existing[name] {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: numbered dogs
|
||||
for i := 1; i <= 100; i++ {
|
||||
name := fmt.Sprintf("dog%d", i)
|
||||
if !existing[name] {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("dog%d", len(dogs)+1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user