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 Start a crew workspace (creates if needed) gt crew stop Stop crew workspace session(s) gt crew add Create a new crew workspace gt crew list List crew workspaces with status gt crew at Attach to crew workspace session gt crew remove Remove a crew workspace gt crew refresh Context cycling with mail-to-self handoff gt crew restart Kill and restart session fresh (alias: rs) gt crew status [] Show detailed workspace status`, } var crewAddCmd = &cobra.Command{ Use: "add ", Short: "Create a new crew workspace", Long: `Create new crew workspace(s) with a clone of the rig repository. Each workspace is created at /crew// with: - A full git clone of the project repository - Mail directory for message delivery - CLAUDE.md with crew worker prompting - Optional feature branch (crew/) 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 /crew//, 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 ", 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 ", 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 []", 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 ", 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--crew-). 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 []", 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 [name]", Aliases: []string{"spawn"}, 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/)") 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) }