Skip to content

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

Merged
jeremy merged 4 commits into
mainfrom
cards-bare-numeric-error
Sep 2, 2026
Merged

Resolve a bare numeric cards <id> to a clear error instead of silent exit-0#678
jeremy merged 4 commits into
mainfrom
cards-bare-numeric-error

Conversation

@jeremy

@jeremy jeremy commented Sep 2, 2026

Copy link
Copy Markdown
Member

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.

Written for commit 11011ee. Summary will update on new commits.

Review in cubic

Copilot AI balanced review requested due to automatic review settings September 2, 2026 18:14
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T19:37:23.806685Z 11011ee New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the tests Tests (unit and e2e) label Sep 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Existing end-to-end expectations will fail, and typo suggestions do not currently use Cobra’s configured distance.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Prevents bare positional arguments on command groups from silently exiting successfully.

Changes:

  • Converts unknown group arguments into usage errors with hints.
  • Suppresses misleading help output.
  • Adds unit and end-to-end coverage.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File summaries
File Description
internal/cli/root.go Creates structured unknown-subcommand errors and hints.
internal/cli/help.go Detects unknown positional arguments on groups.
internal/cli/help_test.go Tests detection, hints, and valid routing.
e2e/errors.bats Verifies non-zero structured errors.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/cli/help.go
if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil && helpFlag.Changed {
return false
}
return len(cmd.Flags().Args()) > 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right call — these nested groups carry the identical silent-exit-0 footgun, so I extended the contract rather than narrowing the predicate. chat foobar, cards column foobar, and cards step foobar now return the same usage error, and I updated their expectations in e2e/chat.bats and e2e/cards_columns_steps.bats. Ran the full e2e suite to be sure — those three were the only affected cases. Fixed in e1fd344.

Comment thread internal/cli/root.go
Comment thread internal/cli/help.go Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/cli/help.go">

<violation number="1" location="internal/cli/help.go:23">
P3: `basecamp help cards 12345` now prints nothing and exits 0. Cobra's `help` subcommand calls cards.Help() directly (no --help flag set), so isBareGroupWithUnknownArg(cards) fires in rootHelpFunc and suppresses output, while Execute() won't convert it because executedCmd is the runnable `help` command. This reintroduces the exact silent exit-0 no-op the PR targets, but on the explicit help path. Gate the suppression so it doesn't apply when the group is reached through the `help` subcommand.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/cli/help_test.go
Comment thread internal/cli/help.go
Comment thread internal/cli/help.go
// help output so Execute() can emit a usage error with non-zero exit.
// Must run before machine-help branches to avoid mixed output.
if isBareGroupWithFlags(cmd) {
if isBareGroupWithFlags(cmd) || isBareGroupWithUnknownArg(cmd) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: basecamp help cards 12345 now prints nothing and exits 0. Cobra's help subcommand calls cards.Help() directly (no --help flag set), so isBareGroupWithUnknownArg(cards) fires in rootHelpFunc and suppresses output, while Execute() won't convert it because executedCmd is the runnable help command. This reintroduces the exact silent exit-0 no-op the PR targets, but on the explicit help path. Gate the suppression so it doesn't apply when the group is reached through the help subcommand.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/cli/help.go, line 23:

<comment>`basecamp help cards 12345` now prints nothing and exits 0. Cobra's `help` subcommand calls cards.Help() directly (no --help flag set), so isBareGroupWithUnknownArg(cards) fires in rootHelpFunc and suppresses output, while Execute() won't convert it because executedCmd is the runnable `help` command. This reintroduces the exact silent exit-0 no-op the PR targets, but on the explicit help path. Gate the suppression so it doesn't apply when the group is reached through the `help` subcommand.</comment>

<file context>
@@ -20,7 +20,7 @@ func rootHelpFunc() func(*cobra.Command, []string) {
 		// help output so Execute() can emit a usage error with non-zero exit.
 		// Must run before machine-help branches to avoid mixed output.
-		if isBareGroupWithFlags(cmd) {
+		if isBareGroupWithFlags(cmd) || isBareGroupWithUnknownArg(cmd) {
 			return
 		}
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. In the explicit basecamp help cards 12345 route the help subcommand renders cards' help itself without parsing cards' flags, so cmd.Flags().Args() is empty there and the guard does not fire — verified it still prints help and exits 0, and added an e2e regression test (help for a group with a stray id still renders help). 809e8b5.

Copilot AI review requested due to automatic review settings September 2, 2026 18:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Numeric hints can recommend show commands that do not accept or use an identifier.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread internal/cli/root.go Outdated
Comment on lines +696 to +697
if isAllDigits(arg) && hasSubcommandNamed(cmd, "show") {
return output.ErrUsageHint(msg, fmt.Sprintf("Did you mean %q?", cmd.CommandPath()+" show "+arg))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 809e8b5 — the numeric show <id> hint is now gated on the group's show subcommand actually declaring an identifier positional (via ParseArgs). accounts, hillcharts, and notes fall back to --help; cards, todos, docs, etc. keep the hint. Added a singleton regression case in both the unit table and e2e.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1fd344fa2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cli/root.go Outdated
Comment on lines +696 to +697
if isAllDigits(arg) && hasSubcommandNamed(cmd, "show") {
return output.ErrUsageHint(msg, fmt.Sprintf("Did you mean %q?", cmd.CommandPath()+" show "+arg))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Only suggest show <id> when the subcommand accepts an ID

For numeric input, checking only that a show child exists produces invalid remediation for singleton resources. For example, basecamp accounts 123 now suggests basecamp accounts show 123, but newAccountsShowCmd (internal/commands/accounts.go:171-183) shows the current account and does not consume an ID; because it also lacks an argument validator, following the hint can return plausible details for the wrong account. hillcharts has the same shape. Restrict this hint to groups whose show command actually accepts an ID, otherwise fall back to help.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 809e8b5 — restricted the show <id> hint to groups whose show accepts an identifier (checked via ParseArgs), so accounts 123 and hillcharts 3 now fall back to --help instead of steering to a show that ignores the id. Thanks for naming the wrong-record risk.

Comment thread internal/cli/help.go Outdated
Copilot AI review requested due to automatic review settings September 2, 2026 18:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation is focused, preserves documented help behavior, and has comprehensive regression coverage.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

A group noun handed a positional that matches no subcommand — "cards
12345", "todos 999" — stopped at the group and rendered help with a zero
exit. An agent that meant "cards show 12345" read that success while
nothing ran: a silent no-op masked as OK.

Cobra only rejects an unknown first arg for the root command; every
non-root group falls through to help. Catch the leftover positional at
the same seam that already converts a bare group with stray flags into a
usage error, and return a non-zero usage error that names the unknown
command and points at the canonical path — spelling out "<group> show
<id>" for a numeric id, offering spelling suggestions otherwise.

Bare "cards" and "cards --help" still show help; "cards show 123" still
routes to the subcommand.
Borrow the root's SuggestionsMinimumDistance so a mistyped subcommand
("messages shwo") suggests "show" — group commands leave the threshold at
zero, which matches only an exact prefix.

The guard already covers every non-runnable group, so unknown positionals
on nested groups ("chat foobar", "cards column foobar", "cards step
foobar") now return the same usage error; update their expectations from
silent help to the error contract. Add a regression test proving explicit
--help still renders help past the guard.
Suggest "<group> show <id>" only when that group's show subcommand
actually takes an identifier. Singleton reads (accounts, hillcharts,
notes) have a show that ignores a positional, so steering a numeric arg
there would silently return the wrong record; those now fall back to the
group's help.

Decide the --help exemption from the parsed flag value rather than
whether it changed, so an explicit --help=false no longer slips a stray
positional past the guard. Fold the shared non-runnable-group test into
one isBareGroup helper so the flag and positional predicates can't drift.
Copilot AI review requested due to automatic review settings September 2, 2026 19:21
@jeremy
jeremy force-pushed the cards-bare-numeric-error branch from 809e8b5 to 6fbf3de Compare September 2, 2026 19:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The reviewed changes have comprehensive coverage and no unresolved issues.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fbf3dee4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cli/root.go Outdated
Comment thread internal/cli/root.go
Check the stray-positional case before the bare-flags case so a scoped
invocation ("cards 12345 --in myproject") keeps the specific "did you
mean cards show 12345" hint instead of collapsing to the generic
"subcommand required".

Match the group's show action by alias as well as primary name, so a
command like chat's id-taking "line" that exposes "show" as an alias
still earns the numeric show hint.
Copilot AI review requested due to automatic review settings September 2, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The reviewed changes have comprehensive regression coverage and no unresolved issues.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@jeremy
jeremy merged commit e62fb54 into main Sep 2, 2026
26 checks passed
@jeremy
jeremy deleted the cards-bare-numeric-error branch September 2, 2026 20:36
@robzolkos robzolkos added the bug Something isn't working label Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants