feat: Remove gt spawn completely - gt sling is THE command (gt-1py3y)
Fully remove gt spawn from the codebase: - Delete spawn.go, create polecat_spawn.go with just sling helpers - Remove all gt spawn references from docs and CLAUDE.md - Update code comments to reference gt sling gt sling now handles ALL work dispatch: - Existing agents: gt sling <bead> mayor/crew/witness - Auto-spawn: gt sling <bead> <rig> - No-tmux: gt sling <bead> <rig> --naked - With args: gt sling <bead> --args "..." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,7 @@ gastown/ ← This rig
|
||||
- `gt rigs` - List all rigs
|
||||
|
||||
### Work Management
|
||||
- `gt spawn --issue <id>` - Spawn polecat for issue
|
||||
- `gt sling <bead> <rig>` - Assign work to polecat in rig
|
||||
- `bd update <id> --status=in_progress` - Claim work
|
||||
|
||||
## Development
|
||||
|
||||
@@ -699,7 +699,7 @@ pending → in_progress → completed
|
||||
When a molecule is attached to an issue:
|
||||
|
||||
```bash
|
||||
gt spawn --issue gt-xyz --molecule mol-shiny
|
||||
gt sling gt-xyz <rig> --molecule mol-shiny
|
||||
```
|
||||
|
||||
1. Molecule is validated (steps, dependencies)
|
||||
@@ -756,7 +756,7 @@ bd mol show mol-shiny
|
||||
bd mol bond mol-shiny --var feature_name="auth"
|
||||
|
||||
# Spawn polecat with molecule
|
||||
gt spawn --issue gt-xyz --molecule mol-shiny
|
||||
gt sling gt-xyz <rig> --molecule mol-shiny
|
||||
```
|
||||
|
||||
### Why Molecules?
|
||||
@@ -1957,7 +1957,7 @@ gt capture <polecat> "<cmd>" # Run command in polecat session
|
||||
### Session Management
|
||||
|
||||
```bash
|
||||
gt spawn --issue <id> --molecule mol-shiny # Spawn polecat with workflow
|
||||
gt sling <id> <rig> --molecule mol-shiny # Spawn polecat with workflow
|
||||
gt handoff # Polecat requests shutdown (run when done)
|
||||
gt session stop <p> # Kill polecat session (Witness uses this)
|
||||
```
|
||||
|
||||
@@ -221,4 +221,4 @@ echo "Bootstrap complete!"
|
||||
After bootstrapping:
|
||||
1. Start a Mayor session: `gt mayor attach`
|
||||
2. Check for work: `bd ready`
|
||||
3. Spawn workers with molecules: `gt spawn --issue <id> --molecule mol-shiny`
|
||||
3. Spawn workers with molecules: `gt sling <id> <rig> --molecule mol-shiny`
|
||||
|
||||
@@ -127,7 +127,7 @@ bd list --has-external-block
|
||||
|
||||
**Manual (launch):**
|
||||
```bash
|
||||
gt spawn --continue gt-mol-root
|
||||
gt sling gt-mol-root
|
||||
# Spawns polecat, which reads handoff mail and continues
|
||||
```
|
||||
|
||||
@@ -153,7 +153,7 @@ Deacon patrol checks parked molecules:
|
||||
### Phase 2: Gas Town Integration (gt-* issues)
|
||||
|
||||
1. **gt park command**: Set blocked_by, clear assignee, handoff, shutdown
|
||||
2. **gt spawn --continue**: Resume parked molecule
|
||||
2. **gt sling**: Resume parked molecule
|
||||
3. **Patrol step**: Check parked molecules for unblocked
|
||||
|
||||
### Phase 3: Automation (future)
|
||||
@@ -213,7 +213,7 @@ gt park --step=gt-mol.3 --waiting="beads:mol-run-assignee"
|
||||
# Polecat shutting down.
|
||||
|
||||
# Later, after beads ships:
|
||||
gt spawn --continue gt-mol-root
|
||||
gt sling gt-mol-root
|
||||
# Resuming molecule gt-mol-root...
|
||||
# Reading handoff context...
|
||||
# Continuing from step gt-mol.3
|
||||
|
||||
@@ -267,7 +267,7 @@ Execute registered plugins whose gates are open.
|
||||
## Limitations
|
||||
|
||||
- **No polecat spawning**: Plugins cannot spawn polecats. If a plugin tries
|
||||
to use `gt spawn`, behavior is undefined. This may change in the future.
|
||||
to use `gt sling`, behavior is undefined. This may change in the future.
|
||||
|
||||
- **No cross-plugin dependencies**: Plugins don't declare dependencies on
|
||||
each other. If ordering matters, mark both as `parallel: false`.
|
||||
|
||||
@@ -63,7 +63,7 @@ Highest priority override. Set this to use a specific account:
|
||||
|
||||
```bash
|
||||
export GT_ACCOUNT=yegge
|
||||
gt spawn gastown # Uses yegge account
|
||||
gt sling <bead> gastown # Uses yegge account
|
||||
```
|
||||
|
||||
### Command Interface
|
||||
@@ -100,13 +100,13 @@ gt account status
|
||||
|
||||
```bash
|
||||
# Override for a specific spawn
|
||||
gt spawn --account=yegge gastown
|
||||
gt sling <bead> gastown --account=yegge
|
||||
|
||||
# Override for crew attach
|
||||
gt crew at --account=ghosttrack max
|
||||
|
||||
# With env var (highest precedence)
|
||||
GT_ACCOUNT=yegge gt spawn gastown
|
||||
GT_ACCOUNT=yegge gt sling <bead> gastown
|
||||
```
|
||||
|
||||
### Implementation Details
|
||||
@@ -119,7 +119,7 @@ GT_ACCOUNT=yegge gt spawn gastown
|
||||
|
||||
#### How Spawning Works
|
||||
|
||||
When `gt spawn` or `gt crew at` runs Claude Code:
|
||||
When `gt sling` or `gt crew at` runs Claude Code:
|
||||
|
||||
```go
|
||||
func resolveAccountConfigDir() string {
|
||||
|
||||
@@ -426,7 +426,7 @@ Unlike polecats, crew workers have no Witness oversight:
|
||||
### Phase 3: Spawn & Lifecycle
|
||||
1. Port spawn injection prompts
|
||||
2. Add lifecycle templates (nudge, escalation)
|
||||
3. Integrate with `gt spawn` command
|
||||
3. Integrate with `gt sling` command
|
||||
|
||||
### Phase 4: CLI & Validation
|
||||
1. Implement `gt prime` with role detection
|
||||
|
||||
@@ -361,7 +361,7 @@ Putting it all together:
|
||||
|
||||
```
|
||||
1. Human files issue in Beads
|
||||
2. Mayor dispatches: gt spawn --issue <id>
|
||||
2. Mayor dispatches: gt sling --issue <id>
|
||||
3. Polecat created with:
|
||||
- Fresh worktree
|
||||
- mol-polecat-work pinned to hook
|
||||
|
||||
@@ -29,7 +29,7 @@ This command:
|
||||
3. Optionally creates a GitHub repository
|
||||
|
||||
The .gitignore excludes:
|
||||
- Polecat worktrees and rig clones (recreated with 'gt spawn' or 'gt rig add')
|
||||
- Polecat worktrees and rig clones (recreated with 'gt sling' or 'gt rig add')
|
||||
- Runtime state files (state.json, *.lock)
|
||||
- OS and editor files
|
||||
|
||||
@@ -64,7 +64,7 @@ const HQGitignore = `# Gas Town HQ .gitignore
|
||||
**/registry.json
|
||||
|
||||
# =============================================================================
|
||||
# Rig git worktrees (recreate with 'gt spawn' or 'gt rig add')
|
||||
# Rig git worktrees (recreate with 'gt sling' or 'gt rig add')
|
||||
# =============================================================================
|
||||
|
||||
# Polecats - worker worktrees
|
||||
|
||||
@@ -94,7 +94,7 @@ var polecatWakeCmd = &cobra.Command{
|
||||
Long: `Resume a polecat to working state.
|
||||
|
||||
DEPRECATED: In the transient model, polecats are created fresh for each task
|
||||
via 'gt spawn'. This command is kept for backward compatibility.
|
||||
via 'gt sling'. This command is kept for backward compatibility.
|
||||
|
||||
Transitions: done → working
|
||||
|
||||
@@ -509,7 +509,7 @@ func runPolecatRemove(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func runPolecatWake(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println(style.Warning.Render("DEPRECATED: Use 'gt spawn' to create fresh polecats instead"))
|
||||
fmt.Println(style.Warning.Render("DEPRECATED: Use 'gt sling' to create fresh polecats instead"))
|
||||
fmt.Println()
|
||||
|
||||
rigName, polecatName, err := parseAddress(args[0])
|
||||
|
||||
207
internal/cmd/polecat_spawn.go
Normal file
207
internal/cmd/polecat_spawn.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Package cmd provides polecat spawning utilities for gt sling.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// SpawnedPolecatInfo contains info about a spawned polecat session.
|
||||
type SpawnedPolecatInfo struct {
|
||||
RigName string // Rig name (e.g., "gastown")
|
||||
PolecatName string // Polecat name (e.g., "Toast")
|
||||
ClonePath string // Path to polecat's git worktree
|
||||
SessionName string // Tmux session name (e.g., "gt-gastown-p-Toast")
|
||||
Pane string // Tmux pane ID
|
||||
}
|
||||
|
||||
// AgentID returns the agent identifier (e.g., "gastown/polecats/Toast")
|
||||
func (s *SpawnedPolecatInfo) AgentID() string {
|
||||
return fmt.Sprintf("%s/polecats/%s", s.RigName, s.PolecatName)
|
||||
}
|
||||
|
||||
// SlingSpawnOptions contains options for spawning a polecat via sling.
|
||||
type SlingSpawnOptions struct {
|
||||
Force bool // Force spawn even if polecat has uncommitted work
|
||||
Naked bool // No-tmux mode: skip session creation
|
||||
Account string // Claude Code account handle to use
|
||||
Create bool // Create polecat if it doesn't exist (currently always true for sling)
|
||||
}
|
||||
|
||||
// SpawnPolecatForSling creates a fresh polecat and optionally starts its session.
|
||||
// This is used by gt sling when the target is a rig name.
|
||||
// The caller (sling) handles hook attachment and nudging.
|
||||
func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolecatInfo, error) {
|
||||
// Find workspace
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
|
||||
// Load rig config
|
||||
rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
||||
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
|
||||
if err != nil {
|
||||
rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
|
||||
}
|
||||
|
||||
g := git.NewGit(townRoot)
|
||||
rigMgr := rig.NewManager(townRoot, rigsConfig, g)
|
||||
r, err := rigMgr.GetRig(rigName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rig '%s' not found", rigName)
|
||||
}
|
||||
|
||||
// Get polecat manager
|
||||
polecatGit := git.NewGit(r.Path)
|
||||
polecatMgr := polecat.NewManager(r, polecatGit)
|
||||
|
||||
// Allocate a new polecat name
|
||||
polecatName, err := polecatMgr.AllocateName()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("allocating polecat name: %w", err)
|
||||
}
|
||||
fmt.Printf("Allocated polecat: %s\n", polecatName)
|
||||
|
||||
// Check if polecat already exists (shouldn't, since we allocated fresh)
|
||||
existingPolecat, err := polecatMgr.Get(polecatName)
|
||||
if err == nil {
|
||||
// Exists - recreate with fresh worktree
|
||||
// Check for uncommitted work first
|
||||
if !opts.Force {
|
||||
pGit := git.NewGit(existingPolecat.ClonePath)
|
||||
workStatus, checkErr := pGit.CheckUncommittedWork()
|
||||
if checkErr == nil && !workStatus.Clean() {
|
||||
return nil, fmt.Errorf("polecat '%s' has uncommitted work: %s\nUse --force to proceed anyway",
|
||||
polecatName, workStatus.String())
|
||||
}
|
||||
}
|
||||
fmt.Printf("Recreating polecat %s with fresh worktree...\n", polecatName)
|
||||
if _, err = polecatMgr.Recreate(polecatName, opts.Force); err != nil {
|
||||
return nil, fmt.Errorf("recreating polecat: %w", err)
|
||||
}
|
||||
} else if err == polecat.ErrPolecatNotFound {
|
||||
// Create new polecat
|
||||
fmt.Printf("Creating polecat %s...\n", polecatName)
|
||||
if _, err = polecatMgr.Add(polecatName); err != nil {
|
||||
return nil, fmt.Errorf("creating polecat: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("getting polecat: %w", err)
|
||||
}
|
||||
|
||||
// Get polecat object for path info
|
||||
polecatObj, err := polecatMgr.Get(polecatName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting polecat after creation: %w", err)
|
||||
}
|
||||
|
||||
// Handle naked mode (no-tmux)
|
||||
if opts.Naked {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s\n", style.Bold.Render("🔧 NO-TMUX MODE (--naked)"))
|
||||
fmt.Printf("Polecat created. Agent must be started manually.\n\n")
|
||||
fmt.Printf("To start the agent:\n")
|
||||
fmt.Printf(" cd %s\n", polecatObj.ClonePath)
|
||||
fmt.Printf(" claude # Or: claude-code\n\n")
|
||||
fmt.Printf("Agent will discover work via gt prime on startup.\n")
|
||||
|
||||
return &SpawnedPolecatInfo{
|
||||
RigName: rigName,
|
||||
PolecatName: polecatName,
|
||||
ClonePath: polecatObj.ClonePath,
|
||||
SessionName: "", // No session in naked mode
|
||||
Pane: "", // No pane in naked mode
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Resolve account for Claude config
|
||||
accountsPath := constants.MayorAccountsPath(townRoot)
|
||||
claudeConfigDir, accountHandle, err := config.ResolveAccountConfigDir(accountsPath, opts.Account)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving account: %w", err)
|
||||
}
|
||||
if accountHandle != "" {
|
||||
fmt.Printf("Using account: %s\n", accountHandle)
|
||||
}
|
||||
|
||||
// Start session
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
|
||||
// Check if already running
|
||||
running, _ := sessMgr.IsRunning(polecatName)
|
||||
if !running {
|
||||
fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName)
|
||||
startOpts := session.StartOptions{
|
||||
ClaudeConfigDir: claudeConfigDir,
|
||||
}
|
||||
if err := sessMgr.Start(polecatName, startOpts); err != nil {
|
||||
return nil, fmt.Errorf("starting session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get session name and pane
|
||||
sessionName := sessMgr.SessionName(polecatName)
|
||||
pane, err := getSessionPane(sessionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting pane for %s: %w", sessionName, err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Polecat %s spawned\n", style.Bold.Render("✓"), polecatName)
|
||||
|
||||
return &SpawnedPolecatInfo{
|
||||
RigName: rigName,
|
||||
PolecatName: polecatName,
|
||||
ClonePath: polecatObj.ClonePath,
|
||||
SessionName: sessionName,
|
||||
Pane: pane,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsRigName checks if a target string is a rig name (not a role or path).
|
||||
// Returns the rig name and true if it's a valid rig.
|
||||
func IsRigName(target string) (string, bool) {
|
||||
// If it contains a slash, it's a path format (rig/role or rig/crew/name)
|
||||
if strings.Contains(target, "/") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Check known non-rig role names
|
||||
switch strings.ToLower(target) {
|
||||
case "mayor", "may", "deacon", "dea", "crew", "witness", "wit", "refinery", "ref":
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Try to load as a rig
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
||||
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
g := git.NewGit(townRoot)
|
||||
rigMgr := rig.NewManager(townRoot, rigsConfig, g)
|
||||
_, err = rigMgr.GetRig(target)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return target, true
|
||||
}
|
||||
@@ -72,7 +72,7 @@ var (
|
||||
slingVars []string // --var flag: formula variables (key=value)
|
||||
slingArgs string // --args flag: natural language instructions for executor
|
||||
|
||||
// Flags migrated from gt spawn for unified work assignment
|
||||
// Flags migrated for polecat spawning (used by sling for work assignment
|
||||
slingNaked bool // --naked: no-tmux mode (skip session creation)
|
||||
slingCreate bool // --create: create polecat if it doesn't exist
|
||||
slingMolecule string // --molecule: workflow to instantiate on the bead
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user