diff --git a/docs/no-tmux-mode.md b/docs/no-tmux-mode.md index da3171e6..f3d8ab25 100644 --- a/docs/no-tmux-mode.md +++ b/docs/no-tmux-mode.md @@ -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/ # claude ``` @@ -99,7 +99,7 @@ ARGS (use these to guide execution): | Command | Purpose | |---------|---------| | `gt sling --args "..."` | Store args in bead, nudge gracefully | -| `gt spawn --naked` | Assign work without tmux session | +| `gt sling --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 ` | View raw bead with attached_args field | diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 53dd004f..0fb13e5b 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -16,56 +16,43 @@ import ( var slingCmd = &cobra.Command{ Use: "sling [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 ' 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=) - 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 # 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/", rigName) targetPane = "" } 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/", rigName) targetPane = "" } 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) } diff --git a/internal/cmd/spawn.go b/internal/cmd/spawn.go index 8eadbab5..e4246241 100644 --- a/internal/cmd/spawn.go +++ b/internal/cmd/spawn.go @@ -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 # Auto-spawn polecat")) + fmt.Println(style.Dim.Render(" gt sling --naked # No-tmux mode")) + fmt.Println(style.Dim.Render(" gt sling --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) }