feat: Unify gt sling and gt spawn - sling is THE work dispatch command (gt-1py3y)

Complete unification of work assignment commands:

- Add spawn flags to sling: --naked, --create, --molecule, --force, --account
- SpawnPolecatForSling now accepts SlingSpawnOptions struct
- Deprecate gt spawn with warning pointing to gt sling
- Update no-tmux-mode.md to use sling examples

gt sling now handles:
- Existing agents (mayor, crew, witness, refinery)
- Auto-spawning polecats when target is a rig
- Formula instantiation and wisp creation
- No-tmux mode for manual agent operation

🤖 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:19:14 -08:00
parent 9462208f79
commit 6de8f80ea9
3 changed files with 107 additions and 53 deletions

View File

@@ -34,10 +34,10 @@ gt sling gt-abc --args "patch release"
```bash
# Use --naked to skip tmux session creation
gt spawn gastown/Toast --issue gt-abc --naked
gt sling gt-abc gastown --naked
# Output tells you how to start the agent manually:
# cd ~/gt/gastown/polecats/Toast
# cd ~/gt/gastown/polecats/<name>
# claude
```
@@ -99,7 +99,7 @@ ARGS (use these to guide execution):
| Command | Purpose |
|---------|---------|
| `gt sling <bead> --args "..."` | Store args in bead, nudge gracefully |
| `gt spawn --naked` | Assign work without tmux session |
| `gt sling <bead> <rig> --naked` | Assign work without tmux session |
| `gt prime` | Display attached work + args on startup |
| `gt mol status` | Show current work status including args |
| `bd show <bead>` | View raw bead with attached_args field |

View File

@@ -16,56 +16,43 @@ import (
var slingCmd = &cobra.Command{
Use: "sling <bead-or-formula> [target]",
GroupID: GroupWork,
Short: "Hook work and start immediately (no restart)",
Short: "Assign work to an agent (THE unified work dispatch command)",
Long: `Sling work onto an agent's hook and start working immediately.
Unlike 'gt handoff', sling does NOT restart the session. It:
1. Attaches the work to the hook (durability)
2. Injects a prompt to start working NOW
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
- Formula instantiation and wisp creation
- No-tmux mode for manual agent operation
This preserves current context while kicking off work. Use when:
- You've been chatting with an agent and want to kick off a workflow
- You want to assign work to another agent that has useful context
- You (Overseer) want to start work then attend to another window
Target Resolution:
gt sling gt-abc # Self (current agent)
gt sling gt-abc crew # Crew worker in current rig
gt sling gt-abc gastown # Auto-spawn polecat in rig
gt sling gt-abc gastown/Toast # Specific polecat
gt sling gt-abc mayor # Mayor
The hook provides durability - the agent can restart, compact, or hand off,
but until the hook is changed or closed, that agent owns the work.
Spawning Options (when target is a rig):
gt sling gt-abc gastown --molecule mol-review # Use specific workflow
gt sling gt-abc gastown --create # Create polecat if missing
gt sling gt-abc gastown --naked # No-tmux (manual start)
gt sling gt-abc gastown --force # Ignore unread mail
gt sling gt-abc gastown --account work # Use specific Claude account
Examples:
gt sling gt-abc # Hook bead and start now
gt sling gt-abc -s "Fix the bug" # With context subject
gt sling gt-abc crew # Sling bead to crew worker
gt sling gt-abc gastown/crew/max # Sling bead to specific agent
gt sling gt-abc gastown # Auto-spawn polecat in rig (light spawn)
Natural Language Args:
gt sling gt-abc --args "patch release"
gt sling code-review --args "focus on security"
Auto-spawning polecats:
When target is a rig name (not a specific agent), sling automatically spawns
a fresh polecat and slings work to it. This is a light spawn - the polecat
starts with just the hook. For full molecule workflow with crash recovery,
use 'gt spawn --issue <bead> <rig>' instead.
The --args string is stored in the bead and shown via gt prime. Since the
executor is an LLM, it interprets these instructions naturally.
Standalone formula slinging:
gt sling mol-town-shutdown mayor/ # Cook + wisp + attach + nudge
gt sling towers-of-hanoi --var disks=3 # With formula variables
Formula Slinging:
gt sling mol-release mayor/ # Cook + wisp + attach + nudge
gt sling towers-of-hanoi --var disks=3
Natural language args (for LLM executor):
gt sling beads-release --args "patch release"
gt sling code-review gt-abc --args "focus on security issues"
The --args string is injected into the prompt and shown to the executor.
Since the executor is an LLM, it interprets these instructions naturally.
When the first argument is a formula (not a bead), sling will:
1. Cook the formula (bd cook)
2. Create a wisp instance (bd wisp create)
3. Pin the wisp to the target (bd update --status=pinned --assignee=<target>)
4. Nudge the target to start
Formula-on-bead scaffolding (--on flag):
gt sling shiny --on gt-abc # Apply shiny formula to existing work
gt sling mol-review --on gt-abc crew # Apply review formula, sling to crew
When --on is specified, the formula shapes execution of the target bead.
Formula-on-Bead (--on flag):
gt sling mol-review --on gt-abc # Apply formula to existing work
gt sling shiny --on gt-abc crew # Apply formula, sling to crew
Compare:
gt hook <bead> # Just attach (no action)
@@ -84,6 +71,13 @@ var (
slingOnTarget string // --on flag: target bead when slinging a formula
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
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
slingForce bool // --force: force spawn even if polecat has unread mail
slingAccount string // --account: Claude Code account handle to use
)
func init() {
@@ -93,6 +87,14 @@ func init() {
slingCmd.Flags().StringVar(&slingOnTarget, "on", "", "Apply formula to existing bead (implies wisp scaffolding)")
slingCmd.Flags().StringArrayVar(&slingVars, "var", nil, "Formula variable (key=value), can be repeated")
slingCmd.Flags().StringVarP(&slingArgs, "args", "a", "", "Natural language instructions for the executor (e.g., 'patch release')")
// Flags for polecat spawning (when target is a rig)
slingCmd.Flags().BoolVar(&slingNaked, "naked", false, "No-tmux mode: assign work but skip session creation (manual start)")
slingCmd.Flags().BoolVar(&slingCreate, "create", false, "Create polecat if it doesn't exist")
slingCmd.Flags().StringVar(&slingMolecule, "molecule", "", "Molecule workflow to instantiate on the bead")
slingCmd.Flags().BoolVar(&slingForce, "force", false, "Force spawn even if polecat has unread mail")
slingCmd.Flags().StringVar(&slingAccount, "account", "", "Claude Code account handle to use")
rootCmd.AddCommand(slingCmd)
}
@@ -154,12 +156,21 @@ func runSling(cmd *cobra.Command, args []string) error {
if slingDryRun {
// Dry run - just indicate what would happen
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
if slingNaked {
fmt.Printf(" --naked: would skip tmux session\n")
}
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
targetPane = "<new-pane>"
} else {
// Spawn a fresh polecat in the rig
fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName)
spawnInfo, spawnErr := SpawnPolecatForSling(rigName, false)
spawnOpts := SlingSpawnOptions{
Force: slingForce,
Naked: slingNaked,
Account: slingAccount,
Create: slingCreate,
}
spawnInfo, spawnErr := SpawnPolecatForSling(rigName, spawnOpts)
if spawnErr != nil {
return fmt.Errorf("spawning polecat: %w", spawnErr)
}
@@ -460,12 +471,21 @@ func runSlingFormula(args []string) error {
if slingDryRun {
// Dry run - just indicate what would happen
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
if slingNaked {
fmt.Printf(" --naked: would skip tmux session\n")
}
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
targetPane = "<new-pane>"
} else {
// Spawn a fresh polecat in the rig
fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName)
spawnInfo, spawnErr := SpawnPolecatForSling(rigName, false)
spawnOpts := SlingSpawnOptions{
Force: slingForce,
Naked: slingNaked,
Account: slingAccount,
Create: slingCreate,
}
spawnInfo, spawnErr := SpawnPolecatForSling(rigName, spawnOpts)
if spawnErr != nil {
return fmt.Errorf("spawning polecat: %w", spawnErr)
}

View File

@@ -46,7 +46,7 @@ var spawnCmd = &cobra.Command{
Use: "spawn [rig/polecat | rig]",
Aliases: []string{"sp"},
GroupID: GroupWork,
Short: "Spawn a polecat with work assignment",
Short: "[DEPRECATED] Use 'gt sling' instead - spawn a polecat with work",
Long: `Spawn a polecat with a work assignment.
Use 'gt spawn pending' to view spawns waiting to be triggered.
@@ -133,6 +133,13 @@ type BeadsIssue struct {
}
func runSpawn(cmd *cobra.Command, args []string) error {
// Deprecation warning - prefer gt sling
fmt.Println(style.Warning.Render("DEPRECATED: 'gt spawn' is deprecated. Use 'gt sling' instead:"))
fmt.Println(style.Dim.Render(" gt sling <bead> <rig> # Auto-spawn polecat"))
fmt.Println(style.Dim.Render(" gt sling <bead> <rig> --naked # No-tmux mode"))
fmt.Println(style.Dim.Render(" gt sling <bead> <rig> --args '...' # With natural language args"))
fmt.Println()
if spawnIssue == "" && spawnMessage == "" {
return fmt.Errorf("must specify --issue or -m/--message")
}
@@ -911,10 +918,18 @@ func (s *SpawnedPolecatInfo) AgentID() string {
return fmt.Sprintf("%s/polecats/%s", s.RigName, s.PolecatName)
}
// SpawnPolecatForSling creates a fresh polecat and starts its session.
// 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 a lightweight spawn for sling - it doesn't assign issues or send mail.
// The caller (sling) handles hook attachment and nudging.
func SpawnPolecatForSling(rigName string, force bool) (*SpawnedPolecatInfo, error) {
func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolecatInfo, error) {
// Find workspace
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
@@ -951,7 +966,7 @@ func SpawnPolecatForSling(rigName string, force bool) (*SpawnedPolecatInfo, erro
if err == nil {
// Exists - recreate with fresh worktree
// Check for uncommitted work first
if !force {
if !opts.Force {
pGit := git.NewGit(existingPolecat.ClonePath)
workStatus, checkErr := pGit.CheckUncommittedWork()
if checkErr == nil && !workStatus.Clean() {
@@ -960,7 +975,7 @@ func SpawnPolecatForSling(rigName string, force bool) (*SpawnedPolecatInfo, erro
}
}
fmt.Printf("Recreating polecat %s with fresh worktree...\n", polecatName)
if _, err = polecatMgr.Recreate(polecatName, force); err != nil {
if _, err = polecatMgr.Recreate(polecatName, opts.Force); err != nil {
return nil, fmt.Errorf("recreating polecat: %w", err)
}
} else if err == polecat.ErrPolecatNotFound {
@@ -979,9 +994,28 @@ func SpawnPolecatForSling(rigName string, force bool) (*SpawnedPolecatInfo, erro
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, "")
claudeConfigDir, accountHandle, err := config.ResolveAccountConfigDir(accountsPath, opts.Account)
if err != nil {
return nil, fmt.Errorf("resolving account: %w", err)
}