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:
Steve Yegge
2025-12-26 18:33:22 -08:00
parent ff22c84cd6
commit 0ad427e4a8
13 changed files with 227 additions and 1115 deletions

View File

@@ -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

View File

@@ -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)
```

View File

@@ -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`

View File

@@ -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

View File

@@ -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`.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View 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
}

View File

@@ -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