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"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"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/events"
|
||||||
"github.com/steveyegge/gastown/internal/session"
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"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:
|
This is THE command for assigning work in Gas Town. It handles:
|
||||||
- Existing agents (mayor, crew, witness, refinery)
|
- Existing agents (mayor, crew, witness, refinery)
|
||||||
- Auto-spawning polecats when target is a rig
|
- Auto-spawning polecats when target is a rig
|
||||||
|
- Dispatching to dogs (Deacon's helper workers)
|
||||||
- Formula instantiation and wisp creation
|
- Formula instantiation and wisp creation
|
||||||
- No-tmux mode for manual agent operation
|
- 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 # Auto-spawn polecat in rig
|
||||||
gt sling gt-abc gastown/Toast # Specific polecat
|
gt sling gt-abc gastown/Toast # Specific polecat
|
||||||
gt sling gt-abc mayor # Mayor
|
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):
|
Spawning Options (when target is a rig):
|
||||||
gt sling gt-abc gastown --molecule mol-review # Use specific workflow
|
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 {
|
if len(args) > 1 {
|
||||||
target := args[1]
|
target := args[1]
|
||||||
|
|
||||||
|
// 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)
|
// Check if target is a rig name (auto-spawn polecat)
|
||||||
if rigName, isRig := IsRigName(target); isRig {
|
|
||||||
if slingDryRun {
|
if slingDryRun {
|
||||||
// Dry run - just indicate what would happen
|
// Dry run - just indicate what would happen
|
||||||
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
|
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
|
||||||
@@ -593,8 +622,31 @@ func runSlingFormula(args []string) error {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if target != "" {
|
if target != "" {
|
||||||
|
// 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)
|
// Check if target is a rig name (auto-spawn polecat)
|
||||||
if rigName, isRig := IsRigName(target); isRig {
|
|
||||||
if slingDryRun {
|
if slingDryRun {
|
||||||
// Dry run - just indicate what would happen
|
// Dry run - just indicate what would happen
|
||||||
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
|
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)
|
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