Files
gastown/internal/cmd/crew.go
rictus 8ce52f166c feat(crew): Add gt crew stop command for stopping crew sessions
Implements gt crew stop with the following features:
- Stop individual crew members: gt crew stop dave
- Stop multiple: gt crew stop beads/emma beads/grip
- Stop all: gt crew stop --all
- Filter by rig: gt crew stop --all --rig beads
- Dry-run mode: gt crew stop --all --dry-run
- Force mode (skip output capture): gt crew stop dave --force

Uses same semantics as gt shutdown: captures output before stopping,
logs kill events to town log, provides clear status reporting.

(gt-q2am4)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 13:11:08 -08:00

366 lines
13 KiB
Go

package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// Crew command flags
var (
crewRig string
crewBranch bool
crewJSON bool
crewForce bool
crewNoTmux bool
crewDetached bool
crewMessage string
crewAccount string
crewAll bool
crewDryRun bool
)
var crewCmd = &cobra.Command{
Use: "crew",
GroupID: GroupWorkspace,
Short: "Manage crew workspaces (user-managed persistent workspaces)",
RunE: requireSubcommand,
Long: `Crew workers are user-managed persistent workspaces within a rig.
Unlike polecats which are witness-managed and transient, crew workers are:
- Persistent: Not auto-garbage-collected
- User-managed: Overseer controls lifecycle
- Long-lived identities: recognizable names like dave, emma, fred
- Gas Town integrated: Mail, handoff mechanics work
- Tmux optional: Can work in terminal directly
Commands:
gt crew start <name> Start a crew workspace (creates if needed)
gt crew stop <name> Stop crew workspace session(s)
gt crew add <name> Create a new crew workspace
gt crew list List crew workspaces with status
gt crew at <name> Attach to crew workspace session
gt crew remove <name> Remove a crew workspace
gt crew refresh <name> Context cycling with mail-to-self handoff
gt crew restart <name> Kill and restart session fresh (alias: rs)
gt crew status [<name>] Show detailed workspace status`,
}
var crewAddCmd = &cobra.Command{
Use: "add <name>",
Short: "Create a new crew workspace",
Long: `Create new crew workspace(s) with a clone of the rig repository.
Each workspace is created at <rig>/crew/<name>/ with:
- A full git clone of the project repository
- Mail directory for message delivery
- CLAUDE.md with crew worker prompting
- Optional feature branch (crew/<name>)
Examples:
gt crew add dave # Create single workspace
gt crew add murgen croaker goblin # Create multiple at once
gt crew add emma --rig greenplace # Create in specific rig
gt crew add fred --branch # Create with feature branch`,
Args: cobra.MinimumNArgs(1),
RunE: runCrewAdd,
}
var crewListCmd = &cobra.Command{
Use: "list",
Short: "List crew workspaces with status",
Long: `List all crew workspaces in a rig with their status.
Shows git branch, session state, and git status for each workspace.
Examples:
gt crew list # List in current rig
gt crew list --rig greenplace # List in specific rig
gt crew list --json # JSON output`,
RunE: runCrewList,
}
var crewAtCmd = &cobra.Command{
Use: "at [name]",
Aliases: []string{"attach"},
Short: "Attach to crew workspace session",
Long: `Start or attach to a tmux session for a crew workspace.
Creates a new tmux session if none exists, or attaches to existing.
Use --no-tmux to just print the directory path instead.
When run from inside tmux, the session is started but you stay in your
current pane. Use C-b s to switch to the new session.
When run from outside tmux, you are attached to the session (unless
--detached is specified).
Role Discovery:
If no name is provided, attempts to detect the crew workspace from the
current directory. If you're in <rig>/crew/<name>/, it will attach to
that workspace automatically.
Examples:
gt crew at dave # Attach to dave's session
gt crew at # Auto-detect from cwd
gt crew at dave --detached # Start session without attaching
gt crew at dave --no-tmux # Just print path`,
Args: cobra.MaximumNArgs(1),
RunE: runCrewAt,
}
var crewRemoveCmd = &cobra.Command{
Use: "remove <name...>",
Short: "Remove crew workspace(s)",
Long: `Remove one or more crew workspaces from the rig.
Checks for uncommitted changes and running sessions before removing.
Use --force to skip checks and remove anyway.
Examples:
gt crew remove dave # Remove with safety checks
gt crew remove dave emma fred # Remove multiple
gt crew remove beads/grip beads/fang # Remove from specific rig
gt crew remove dave --force # Force remove`,
Args: cobra.MinimumNArgs(1),
RunE: runCrewRemove,
}
var crewRefreshCmd = &cobra.Command{
Use: "refresh <name>",
Short: "Context cycling with mail-to-self handoff",
Long: `Cycle a crew workspace session with handoff.
Sends a handoff mail to the workspace's own inbox, then restarts the session.
The new session reads the handoff mail and resumes work.
Examples:
gt crew refresh dave # Refresh with auto-generated handoff
gt crew refresh dave -m "Working on gt-123" # Add custom message`,
Args: cobra.ExactArgs(1),
RunE: runCrewRefresh,
}
var crewStatusCmd = &cobra.Command{
Use: "status [<name>]",
Short: "Show detailed workspace status",
Long: `Show detailed status for crew workspace(s).
Displays session state, git status, branch info, and mail inbox status.
If no name given, shows status for all crew workers.
Examples:
gt crew status # Status of all crew workers
gt crew status dave # Status of specific worker
gt crew status --json # JSON output`,
RunE: runCrewStatus,
}
var crewRestartCmd = &cobra.Command{
Use: "restart [name...]",
Aliases: []string{"rs"},
Short: "Kill and restart crew workspace session(s)",
Long: `Kill the tmux session and restart fresh with Claude.
Useful when a crew member gets confused or needs a clean slate.
Unlike 'refresh', this does NOT send handoff mail - it's a clean start.
The command will:
1. Kill existing tmux session if running
2. Start fresh session with Claude
3. Run gt prime to reinitialize context
Use --all to restart all running crew sessions across all rigs.
Examples:
gt crew restart dave # Restart dave's session
gt crew restart dave emma fred # Restart multiple
gt crew restart beads/grip beads/fang # Restart from specific rig
gt crew rs emma # Same, using alias
gt crew restart --all # Restart all running crew sessions
gt crew restart --all --rig beads # Restart all crew in beads rig
gt crew restart --all --dry-run # Preview what would be restarted`,
Args: func(cmd *cobra.Command, args []string) error {
if crewAll {
if len(args) > 0 {
return fmt.Errorf("cannot specify both --all and a name")
}
return nil
}
if len(args) < 1 {
return fmt.Errorf("requires at least 1 argument (or --all)")
}
return nil
},
RunE: runCrewRestart,
}
var crewRenameCmd = &cobra.Command{
Use: "rename <old-name> <new-name>",
Short: "Rename a crew workspace",
Long: `Rename a crew workspace.
Kills any running session, renames the directory, and updates state.
The new session will use the new name (gt-<rig>-crew-<new-name>).
Examples:
gt crew rename dave david # Rename dave to david
gt crew rename madmax max # Rename madmax to max`,
Args: cobra.ExactArgs(2),
RunE: runCrewRename,
}
var crewPristineCmd = &cobra.Command{
Use: "pristine [<name>]",
Short: "Sync crew workspaces with remote",
Long: `Ensure crew workspace(s) are up-to-date.
Runs git pull and bd sync for the specified crew, or all crew workers.
Reports any uncommitted changes that may need attention.
Examples:
gt crew pristine # Pristine all crew workers
gt crew pristine dave # Pristine specific worker
gt crew pristine --json # JSON output`,
RunE: runCrewPristine,
}
var crewNextCmd = &cobra.Command{
Use: "next",
Short: "Switch to next crew session in same rig",
Hidden: true, // Internal command for tmux keybindings
RunE: runCrewNext,
}
var crewPrevCmd = &cobra.Command{
Use: "prev",
Short: "Switch to previous crew session in same rig",
Hidden: true, // Internal command for tmux keybindings
RunE: runCrewPrev,
}
var crewStartCmd = &cobra.Command{
Use: "start <rig> [name]",
Short: "Start crew worker(s) in a rig",
Long: `Start crew workers in a rig, creating workspaces if they don't exist.
Takes the rig name as the first argument. Optionally specify a crew member name
to start just that worker, or use --all to start all crew members in the rig.
The crew session starts in the background with Claude running and ready.
Examples:
gt crew start gastown joe # Start joe in gastown rig
gt crew start gastown --all # Start all crew in gastown rig
gt crew start beads # Error: specify name or --all
gt crew start beads grip fang # Start grip and fang in beads rig`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("requires at least 1 argument: the rig name")
}
if len(args) == 1 && !crewAll {
return fmt.Errorf("specify a crew member name or use --all to start all crew in the rig")
}
return nil
},
RunE: runCrewStart,
}
var crewStopCmd = &cobra.Command{
Use: "stop [name...]",
Short: "Stop crew workspace session(s)",
Long: `Stop one or more running crew workspace sessions.
Kills the tmux session(s) for the specified crew member(s). Use --all to
stop all running crew sessions across all rigs.
The name can include the rig in slash format (e.g., beads/emma).
If not specified, the rig is inferred from the current directory.
Output is captured before stopping for debugging purposes (use --force
to skip capture for faster shutdown).
Examples:
gt crew stop dave # Stop dave's session
gt crew stop beads/emma beads/grip # Stop multiple from specific rig
gt crew stop --all # Stop all running crew sessions
gt crew stop --all --rig beads # Stop all crew in beads rig
gt crew stop --all --dry-run # Preview what would be stopped
gt crew stop dave --force # Stop without capturing output`,
Args: func(cmd *cobra.Command, args []string) error {
if crewAll {
if len(args) > 0 {
return fmt.Errorf("cannot specify both --all and a name")
}
return nil
}
if len(args) < 1 {
return fmt.Errorf("requires at least 1 argument (or --all)")
}
return nil
},
RunE: runCrewStop,
}
func init() {
// Add flags
crewAddCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to create crew workspace in")
crewAddCmd.Flags().BoolVar(&crewBranch, "branch", false, "Create a feature branch (crew/<name>)")
crewListCmd.Flags().StringVar(&crewRig, "rig", "", "Filter by rig name")
crewListCmd.Flags().BoolVar(&crewJSON, "json", false, "Output as JSON")
crewAtCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use")
crewAtCmd.Flags().BoolVar(&crewNoTmux, "no-tmux", false, "Just print directory path")
crewAtCmd.Flags().BoolVarP(&crewDetached, "detached", "d", false, "Start session without attaching")
crewAtCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use (overrides default)")
crewRemoveCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use")
crewRemoveCmd.Flags().BoolVar(&crewForce, "force", false, "Force remove (skip safety checks)")
crewRefreshCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use")
crewRefreshCmd.Flags().StringVarP(&crewMessage, "message", "m", "", "Custom handoff message")
crewStatusCmd.Flags().StringVar(&crewRig, "rig", "", "Filter by rig name")
crewStatusCmd.Flags().BoolVar(&crewJSON, "json", false, "Output as JSON")
crewRenameCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use")
crewPristineCmd.Flags().StringVar(&crewRig, "rig", "", "Filter by rig name")
crewPristineCmd.Flags().BoolVar(&crewJSON, "json", false, "Output as JSON")
crewRestartCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use (filter when using --all)")
crewRestartCmd.Flags().BoolVar(&crewAll, "all", false, "Restart all running crew sessions")
crewRestartCmd.Flags().BoolVar(&crewDryRun, "dry-run", false, "Show what would be restarted without restarting")
crewStartCmd.Flags().BoolVar(&crewAll, "all", false, "Start all crew members in the rig")
crewStartCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use")
crewStopCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use (filter when using --all)")
crewStopCmd.Flags().BoolVar(&crewAll, "all", false, "Stop all running crew sessions")
crewStopCmd.Flags().BoolVar(&crewDryRun, "dry-run", false, "Show what would be stopped without stopping")
crewStopCmd.Flags().BoolVar(&crewForce, "force", false, "Skip output capture for faster shutdown")
// Add subcommands
crewCmd.AddCommand(crewAddCmd)
crewCmd.AddCommand(crewListCmd)
crewCmd.AddCommand(crewAtCmd)
crewCmd.AddCommand(crewRemoveCmd)
crewCmd.AddCommand(crewRefreshCmd)
crewCmd.AddCommand(crewStatusCmd)
crewCmd.AddCommand(crewRenameCmd)
crewCmd.AddCommand(crewPristineCmd)
crewCmd.AddCommand(crewRestartCmd)
// Add --session flag to next/prev commands for tmux key binding support
// When run via run-shell, tmux session context may be wrong, so we pass it explicitly
crewNextCmd.Flags().StringVarP(&crewCycleSession, "session", "s", "", "tmux session name (for key bindings)")
crewPrevCmd.Flags().StringVarP(&crewCycleSession, "session", "s", "", "tmux session name (for key bindings)")
crewCmd.AddCommand(crewNextCmd)
crewCmd.AddCommand(crewPrevCmd)
crewCmd.AddCommand(crewStartCmd)
crewCmd.AddCommand(crewStopCmd)
rootCmd.AddCommand(crewCmd)
}