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:
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user