Resolve a bare numeric `cards <id>` to a clear error instead of silent exit-0

Merged
JE
2mo ago
+343-23

The defect

basecamp cards 12345 — a bare numeric positional on the group noun cards — returned exit 0 and the group help. To an agent that meant basecamp cards show 12345, that reads as success while nothing ran: a silent no-op masked as OK.

This is the residual from #416 ("Remove shortcut commands that shadow group nouns"). That PR fixed the top-level basecamp card 12345 case — it now errors non-zero with unknown command "card" … Did you mean cards?. But the same footgun survived one level down on the group nouns themselves.

Investigation showed it is systemic, not cards-only: cards, todos, messages, and comments all exited 0 + help on a bare positional. Root cause — Cobra's default legacyArgs rejects an unknown first arg only for the root command; every non-root group parent (no RunE) falls through to help with a zero exit.

Convention matched

The CLI already has a bare-group seam: isBareGroupWithFlags converts basecamp cards --in X (a group invoked with stray flags, no subcommand) into a non-zero usage error, and the root help function suppresses help output so Execute() can emit it. This change extends that exact seam to cover a stray positional the same way it already covers stray flags — mirroring #416's top-level fix one level down, and honoring the Agent Accessibility principle: no silent exit-0 that hides a no-op. Bare cards (no args) still shows help with exit 0; nothing about the command surface changes.

The fix

  • isBareGroupWithUnknownArg — a non-runnable group carrying a leftover positional (parallel to isBareGroupWithFlags). The root help function suppresses help for it; Execute() converts it to a usage error.
  • unknownSubcommandError returns output.ErrUsageHint (code usage, exit 1) that names the unknown command and points at the canonical path: for a numeric id it spells out Did you mean "basecamp cards show 12345"?; otherwise it offers Cobra's spelling suggestions, falling back to --help.

Because the fix lives at the shared seam, it resolves cards (the residual) and every sibling group noun uniformly.

$ basecamp cards 12345
{ "ok": false, "error": "unknown command \"12345\" for \"basecamp cards\"",
  "code": "usage", "hint": "Did you mean \"basecamp cards show 12345\"?" }   # exit 1

$ basecamp cards            # unchanged: help, exit 0
$ basecamp cards show 123   # unchanged: routes to the show subcommand

The error is classified usage (exit 1), not api_error — an agent branching on the code sees a usage mistake it must not retry, per the same reasoning behind TestTransformCobraErrorClassifiesArityAsUsage.

Test coverage

  • internal/cli/help_test.go: isBareGroupWithUnknownArg detects cards 12345 / todos 999 / comments 55 / a typo, with help suppressed; unknownSubcommandError asserts code, message, and both hint variants (numeric→show, typo→help); a routing test confirms cards show 123 resolves past the guard to the runnable leaf.
  • e2e/errors.bats: basecamp cards 12345 and basecamp messages shwo exit non-zero with the usage envelope and hint.

Tracking: Agent Accessibility card 10265932727 (residual from #416).


Summary by cubic

Group commands previously treated stray positional arguments as successful help; they now return a non-zero usage error. For example, basecamp cards 12345 suggests cards show 12345 instead of masking a no-op as success.

  • Applies to cards, todos, messages, comments, chat, and nested groups such as cards column and cards step.
  • Numeric hints target only identifier-taking show commands; singleton groups such as accounts fall back to --help.
  • Typos suggest the nearest subcommand, including aliases, while scoped calls such as cards 12345 --in myproject retain the specific hint.
  • Bare groups and explicit --help remain unchanged; --help=false still reports the stray argument.
  • Adds unit and end-to-end coverage for the usage envelope and routing behavior.