From df46e75a511e3f6dc053995f23fe7be65d467025 Mon Sep 17 00:00:00 2001 From: gastown/crew/joe Date: Tue, 30 Dec 2025 20:52:23 -0800 Subject: [PATCH] Fix: Unknown subcommands now error instead of silently showing help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/cmd/account.go | 1 + internal/cmd/convoy.go | 1 + internal/cmd/crew.go | 1 + internal/cmd/daemon.go | 1 + internal/cmd/deacon.go | 1 + internal/cmd/mail.go | 1 + internal/cmd/mayor.go | 1 + internal/cmd/molecule.go | 2 ++ internal/cmd/mq.go | 2 ++ internal/cmd/polecat.go | 1 + internal/cmd/refinery.go | 1 + internal/cmd/rig.go | 1 + internal/cmd/root.go | 12 ++++++++++++ internal/cmd/session.go | 1 + internal/cmd/swarm.go | 1 + internal/cmd/witness.go | 1 + 16 files changed, 29 insertions(+) diff --git a/internal/cmd/account.go b/internal/cmd/account.go index 07882b4d..5a75833d 100644 --- a/internal/cmd/account.go +++ b/internal/cmd/account.go @@ -24,6 +24,7 @@ var accountCmd = &cobra.Command{ Use: "account", GroupID: GroupConfig, Short: "Manage Claude Code accounts", + RunE: requireSubcommand, Long: `Manage multiple Claude Code accounts for Gas Town. This enables switching between accounts (e.g., personal vs work) with diff --git a/internal/cmd/convoy.go b/internal/cmd/convoy.go index 5c541e61..57e33f8e 100644 --- a/internal/cmd/convoy.go +++ b/internal/cmd/convoy.go @@ -37,6 +37,7 @@ var convoyCmd = &cobra.Command{ Use: "convoy", GroupID: GroupWork, Short: "Track batches of work across rigs", + RunE: requireSubcommand, Long: `Manage convoys - the primary unit for tracking batched work. A convoy is a persistent tracking unit that monitors related issues across diff --git a/internal/cmd/crew.go b/internal/cmd/crew.go index 1fac5491..0d9f4761 100644 --- a/internal/cmd/crew.go +++ b/internal/cmd/crew.go @@ -24,6 +24,7 @@ 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: diff --git a/internal/cmd/daemon.go b/internal/cmd/daemon.go index 7cdcedb3..175cbf5b 100644 --- a/internal/cmd/daemon.go +++ b/internal/cmd/daemon.go @@ -17,6 +17,7 @@ var daemonCmd = &cobra.Command{ Use: "daemon", GroupID: GroupServices, Short: "Manage the Gas Town daemon", + RunE: requireSubcommand, Long: `Manage the Gas Town background daemon. The daemon is a simple Go process that: diff --git a/internal/cmd/deacon.go b/internal/cmd/deacon.go index 9c6f0a83..33841d2e 100644 --- a/internal/cmd/deacon.go +++ b/internal/cmd/deacon.go @@ -26,6 +26,7 @@ var deaconCmd = &cobra.Command{ Aliases: []string{"dea"}, GroupID: GroupAgents, Short: "Manage the Deacon session", + RunE: requireSubcommand, Long: `Manage the Deacon tmux session. The Deacon is the hierarchical health-check orchestrator for Gas Town. diff --git a/internal/cmd/mail.go b/internal/cmd/mail.go index 8b15f12e..773037e0 100644 --- a/internal/cmd/mail.go +++ b/internal/cmd/mail.go @@ -46,6 +46,7 @@ var mailCmd = &cobra.Command{ Use: "mail", GroupID: GroupComm, Short: "Agent messaging system", + RunE: requireSubcommand, Long: `Send and receive messages between agents. The mail system allows Mayor, polecats, and the Refinery to communicate. diff --git a/internal/cmd/mayor.go b/internal/cmd/mayor.go index dff67c82..82557061 100644 --- a/internal/cmd/mayor.go +++ b/internal/cmd/mayor.go @@ -21,6 +21,7 @@ var mayorCmd = &cobra.Command{ Aliases: []string{"may"}, GroupID: GroupAgents, Short: "Manage the Mayor session", + RunE: requireSubcommand, Long: `Manage the Mayor tmux session. The Mayor is the global coordinator for Gas Town, running as a persistent diff --git a/internal/cmd/molecule.go b/internal/cmd/molecule.go index 2cc40d69..3344e252 100644 --- a/internal/cmd/molecule.go +++ b/internal/cmd/molecule.go @@ -14,6 +14,7 @@ var moleculeCmd = &cobra.Command{ Aliases: []string{"molecule"}, GroupID: GroupWork, Short: "Agent molecule workflow commands", + RunE: requireSubcommand, Long: `Agent-specific molecule workflow operations. These commands operate on the current agent's hook and attached molecules. @@ -204,6 +205,7 @@ a permanent (but compact) record.`, var moleculeStepCmd = &cobra.Command{ Use: "step", Short: "Molecule step operations", + RunE: requireSubcommand, Long: `Commands for working with molecule steps. A molecule is a DAG of steps. Each step is a beads issue with the molecule root diff --git a/internal/cmd/mq.go b/internal/cmd/mq.go index 3bd42550..2d97bdce 100644 --- a/internal/cmd/mq.go +++ b/internal/cmd/mq.go @@ -53,6 +53,7 @@ var mqCmd = &cobra.Command{ Use: "mq", GroupID: GroupWork, Short: "Merge queue operations", + RunE: requireSubcommand, Long: `Manage the merge queue for a rig. The merge queue tracks work branches from polecats waiting to be merged. @@ -168,6 +169,7 @@ Example: var mqIntegrationCmd = &cobra.Command{ Use: "integration", Short: "Manage integration branches for epics", + RunE: requireSubcommand, Long: `Manage integration branches for batch work on epics. Integration branches allow multiple MRs for an epic to target a shared diff --git a/internal/cmd/polecat.go b/internal/cmd/polecat.go index 77f4e580..0ad3f7b3 100644 --- a/internal/cmd/polecat.go +++ b/internal/cmd/polecat.go @@ -33,6 +33,7 @@ var polecatCmd = &cobra.Command{ Aliases: []string{"cat", "polecats"}, GroupID: GroupAgents, Short: "Manage polecats in rigs", + RunE: requireSubcommand, Long: `Manage polecat lifecycle in rigs. Polecats are worker agents that operate in their own git worktrees. diff --git a/internal/cmd/refinery.go b/internal/cmd/refinery.go index 75d75043..27aeaf63 100644 --- a/internal/cmd/refinery.go +++ b/internal/cmd/refinery.go @@ -25,6 +25,7 @@ var refineryCmd = &cobra.Command{ Aliases: []string{"ref"}, GroupID: GroupAgents, Short: "Manage the merge queue processor", + RunE: requireSubcommand, Long: `Manage the Refinery merge queue processor for a rig. The Refinery processes merge requests from polecats, merging their work diff --git a/internal/cmd/rig.go b/internal/cmd/rig.go index fb1d68cc..52fd60a1 100644 --- a/internal/cmd/rig.go +++ b/internal/cmd/rig.go @@ -26,6 +26,7 @@ var rigCmd = &cobra.Command{ Use: "rig", GroupID: GroupWorkspace, Short: "Manage rigs in the workspace", + RunE: requireSubcommand, Long: `Manage rigs (project containers) in the Gas Town workspace. A rig is a container for managing a project and its agents: diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 75ae1a11..2a95bdb0 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( + "fmt" "strings" "github.com/spf13/cobra" @@ -85,3 +86,14 @@ func buildCommandPath(cmd *cobra.Command) string { } return strings.Join(parts, " ") } + +// requireSubcommand returns a RunE function for parent commands that require +// a subcommand. Without this, Cobra silently shows help and exits 0 for +// unknown subcommands like "gt mol foobar", masking errors. +func requireSubcommand(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("requires a subcommand\n\nRun '%s --help' for usage", buildCommandPath(cmd)) + } + return fmt.Errorf("unknown command %q for %q\n\nRun '%s --help' for available commands", + args[0], buildCommandPath(cmd), buildCommandPath(cmd)) +} diff --git a/internal/cmd/session.go b/internal/cmd/session.go index 79ef57fd..6d7ff4e5 100644 --- a/internal/cmd/session.go +++ b/internal/cmd/session.go @@ -37,6 +37,7 @@ var sessionCmd = &cobra.Command{ Aliases: []string{"sess"}, GroupID: GroupAgents, Short: "Manage polecat sessions", + RunE: requireSubcommand, Long: `Manage tmux sessions for polecats. Sessions are tmux sessions running Claude for each polecat. diff --git a/internal/cmd/swarm.go b/internal/cmd/swarm.go index 67ca6f48..df92fb50 100644 --- a/internal/cmd/swarm.go +++ b/internal/cmd/swarm.go @@ -40,6 +40,7 @@ var swarmCmd = &cobra.Command{ GroupID: GroupWork, Short: "[DEPRECATED] Use 'gt convoy' instead", Deprecated: "Use 'gt convoy' for work tracking. A 'swarm' is now just the ephemeral workers on a convoy.", + RunE: requireSubcommand, Long: `DEPRECATED: Use 'gt convoy' instead. The term "swarm" now refers to the ephemeral set of workers on a convoy's issues, diff --git a/internal/cmd/witness.go b/internal/cmd/witness.go index bf528c15..34f5df66 100644 --- a/internal/cmd/witness.go +++ b/internal/cmd/witness.go @@ -29,6 +29,7 @@ var witnessCmd = &cobra.Command{ Use: "witness", GroupID: GroupAgents, Short: "Manage the polecat monitoring agent", + RunE: requireSubcommand, Long: `Manage the Witness monitoring agent for a rig. The Witness monitors polecats for stuck/idle state, nudges polecats