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
|
```bash
|
||||||
# Use --naked to skip tmux session creation
|
# 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:
|
# Output tells you how to start the agent manually:
|
||||||
# cd ~/gt/gastown/polecats/Toast
|
# cd ~/gt/gastown/polecats/<name>
|
||||||
# claude
|
# claude
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ ARGS (use these to guide execution):
|
|||||||
| Command | Purpose |
|
| Command | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `gt sling <bead> --args "..."` | Store args in bead, nudge gracefully |
|
| `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 prime` | Display attached work + args on startup |
|
||||||
| `gt mol status` | Show current work status including args |
|
| `gt mol status` | Show current work status including args |
|
||||||
| `bd show <bead>` | View raw bead with attached_args field |
|
| `bd show <bead>` | View raw bead with attached_args field |
|
||||||
|
|||||||
@@ -16,56 +16,43 @@ import (
|
|||||||
var slingCmd = &cobra.Command{
|
var slingCmd = &cobra.Command{
|
||||||
Use: "sling <bead-or-formula> [target]",
|
Use: "sling <bead-or-formula> [target]",
|
||||||
GroupID: GroupWork,
|
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.
|
Long: `Sling work onto an agent's hook and start working immediately.
|
||||||
|
|
||||||
Unlike 'gt handoff', sling does NOT restart the session. It:
|
This is THE command for assigning work in Gas Town. It handles:
|
||||||
1. Attaches the work to the hook (durability)
|
- Existing agents (mayor, crew, witness, refinery)
|
||||||
2. Injects a prompt to start working NOW
|
- 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:
|
Target Resolution:
|
||||||
- You've been chatting with an agent and want to kick off a workflow
|
gt sling gt-abc # Self (current agent)
|
||||||
- You want to assign work to another agent that has useful context
|
gt sling gt-abc crew # Crew worker in current rig
|
||||||
- You (Overseer) want to start work then attend to another window
|
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,
|
Spawning Options (when target is a rig):
|
||||||
but until the hook is changed or closed, that agent owns the work.
|
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:
|
Natural Language Args:
|
||||||
gt sling gt-abc # Hook bead and start now
|
gt sling gt-abc --args "patch release"
|
||||||
gt sling gt-abc -s "Fix the bug" # With context subject
|
gt sling code-review --args "focus on security"
|
||||||
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)
|
|
||||||
|
|
||||||
Auto-spawning polecats:
|
The --args string is stored in the bead and shown via gt prime. Since the
|
||||||
When target is a rig name (not a specific agent), sling automatically spawns
|
executor is an LLM, it interprets these instructions naturally.
|
||||||
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.
|
|
||||||
|
|
||||||
Standalone formula slinging:
|
Formula Slinging:
|
||||||
gt sling mol-town-shutdown mayor/ # Cook + wisp + attach + nudge
|
gt sling mol-release mayor/ # Cook + wisp + attach + nudge
|
||||||
gt sling towers-of-hanoi --var disks=3 # With formula variables
|
gt sling towers-of-hanoi --var disks=3
|
||||||
|
|
||||||
Natural language args (for LLM executor):
|
Formula-on-Bead (--on flag):
|
||||||
gt sling beads-release --args "patch release"
|
gt sling mol-review --on gt-abc # Apply formula to existing work
|
||||||
gt sling code-review gt-abc --args "focus on security issues"
|
gt sling shiny --on gt-abc crew # Apply formula, sling to crew
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Compare:
|
Compare:
|
||||||
gt hook <bead> # Just attach (no action)
|
gt hook <bead> # Just attach (no action)
|
||||||
@@ -84,6 +71,13 @@ var (
|
|||||||
slingOnTarget string // --on flag: target bead when slinging a formula
|
slingOnTarget string // --on flag: target bead when slinging a formula
|
||||||
slingVars []string // --var flag: formula variables (key=value)
|
slingVars []string // --var flag: formula variables (key=value)
|
||||||
slingArgs string // --args flag: natural language instructions for executor
|
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() {
|
func init() {
|
||||||
@@ -93,6 +87,14 @@ func init() {
|
|||||||
slingCmd.Flags().StringVar(&slingOnTarget, "on", "", "Apply formula to existing bead (implies wisp scaffolding)")
|
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().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')")
|
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)
|
rootCmd.AddCommand(slingCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,12 +156,21 @@ func runSling(cmd *cobra.Command, args []string) error {
|
|||||||
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)
|
||||||
|
if slingNaked {
|
||||||
|
fmt.Printf(" --naked: would skip tmux session\n")
|
||||||
|
}
|
||||||
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
|
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
|
||||||
targetPane = "<new-pane>"
|
targetPane = "<new-pane>"
|
||||||
} else {
|
} else {
|
||||||
// Spawn a fresh polecat in the rig
|
// Spawn a fresh polecat in the rig
|
||||||
fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName)
|
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 {
|
if spawnErr != nil {
|
||||||
return fmt.Errorf("spawning polecat: %w", spawnErr)
|
return fmt.Errorf("spawning polecat: %w", spawnErr)
|
||||||
}
|
}
|
||||||
@@ -460,12 +471,21 @@ func runSlingFormula(args []string) error {
|
|||||||
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)
|
||||||
|
if slingNaked {
|
||||||
|
fmt.Printf(" --naked: would skip tmux session\n")
|
||||||
|
}
|
||||||
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
|
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
|
||||||
targetPane = "<new-pane>"
|
targetPane = "<new-pane>"
|
||||||
} else {
|
} else {
|
||||||
// Spawn a fresh polecat in the rig
|
// Spawn a fresh polecat in the rig
|
||||||
fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName)
|
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 {
|
if spawnErr != nil {
|
||||||
return fmt.Errorf("spawning polecat: %w", spawnErr)
|
return fmt.Errorf("spawning polecat: %w", spawnErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ var spawnCmd = &cobra.Command{
|
|||||||
Use: "spawn [rig/polecat | rig]",
|
Use: "spawn [rig/polecat | rig]",
|
||||||
Aliases: []string{"sp"},
|
Aliases: []string{"sp"},
|
||||||
GroupID: GroupWork,
|
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.
|
Long: `Spawn a polecat with a work assignment.
|
||||||
|
|
||||||
Use 'gt spawn pending' to view spawns waiting to be triggered.
|
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 {
|
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 == "" {
|
if spawnIssue == "" && spawnMessage == "" {
|
||||||
return fmt.Errorf("must specify --issue or -m/--message")
|
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)
|
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.
|
// This is a lightweight spawn for sling - it doesn't assign issues or send mail.
|
||||||
// The caller (sling) handles hook attachment and nudging.
|
// 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
|
// Find workspace
|
||||||
townRoot, err := workspace.FindFromCwdOrError()
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -951,7 +966,7 @@ func SpawnPolecatForSling(rigName string, force bool) (*SpawnedPolecatInfo, erro
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
// Exists - recreate with fresh worktree
|
// Exists - recreate with fresh worktree
|
||||||
// Check for uncommitted work first
|
// Check for uncommitted work first
|
||||||
if !force {
|
if !opts.Force {
|
||||||
pGit := git.NewGit(existingPolecat.ClonePath)
|
pGit := git.NewGit(existingPolecat.ClonePath)
|
||||||
workStatus, checkErr := pGit.CheckUncommittedWork()
|
workStatus, checkErr := pGit.CheckUncommittedWork()
|
||||||
if checkErr == nil && !workStatus.Clean() {
|
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)
|
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)
|
return nil, fmt.Errorf("recreating polecat: %w", err)
|
||||||
}
|
}
|
||||||
} else if err == polecat.ErrPolecatNotFound {
|
} 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)
|
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
|
// Resolve account for Claude config
|
||||||
accountsPath := constants.MayorAccountsPath(townRoot)
|
accountsPath := constants.MayorAccountsPath(townRoot)
|
||||||
claudeConfigDir, accountHandle, err := config.ResolveAccountConfigDir(accountsPath, "")
|
claudeConfigDir, accountHandle, err := config.ResolveAccountConfigDir(accountsPath, opts.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("resolving account: %w", err)
|
return nil, fmt.Errorf("resolving account: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user