Parent commands (mol, mail, crew, polecat, etc.) previously showed help and exited 0 for unknown subcommands like "gt mol foobar". This masked errors in scripts and confused users. Added requireSubcommand() helper to root.go and applied it to all parent commands. Now unknown subcommands properly error with exit code 1. Example before: gt mol unhook → shows help, exits 0 Example after: gt mol unhook → "Error: unknown command "unhook"", exits 1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
321 lines
11 KiB
Go
321 lines
11 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 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 [name...]",
|
|
Short: "Start crew workspace(s) (creates if needed)",
|
|
Long: `Start one or more crew workspaces, creating them if they don't exist.
|
|
|
|
This is an alias for 'gt start crew'. It combines 'gt crew add' and 'gt crew at --detached'.
|
|
The crew session starts in the background with Claude running and ready.
|
|
|
|
The name can include the rig in slash format (e.g., greenplace/joe).
|
|
If not specified, the rig is inferred from the current directory.
|
|
|
|
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 start that
|
|
workspace automatically.
|
|
|
|
Examples:
|
|
gt crew start joe # Start joe in current rig
|
|
gt crew start greenplace/joe # Start joe in gastown rig
|
|
gt crew start beads/grip beads/fang # Start multiple crew members
|
|
gt crew start joe --rig beads # Start joe in beads rig
|
|
gt crew start # Auto-detect from cwd`,
|
|
RunE: runCrewStart,
|
|
}
|
|
|
|
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().StringVar(&crewRig, "rig", "", "Rig to use")
|
|
crewStartCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use")
|
|
|
|
// 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)
|
|
|
|
rootCmd.AddCommand(crewCmd)
|
|
}
|