Skip to content

fix(spec): apply default_subcommand only at the root - #850

Open
jdx wants to merge 1 commit into
agent/default-routingfrom
agent/lib-default-scope
Open

fix(spec): apply default_subcommand only at the root#850
jdx wants to merge 1 commit into
agent/default-routingfrom
agent/lib-default-scope

Conversation

@jdx

@jdx jdx commented Aug 12, 2026

Copy link
Copy Markdown
Owner

A spec declares one default_subcommand, once, at the top — and the parser looked that name
up at whichever command it happened to be standing on. So a command with an unrelated
subcommand of the same name acquired a default of its own: with default_subcommand "ls"
declared for the program, ex config zzz descended into config ls and bound zzz there.
Nothing declared that, and there is no way to declare it, which is what makes it a bug
rather than a feature.

Gated on still being at the root, which out.cmds already records — it holds just the root
until something descends.

Found while teaching usage-argv to route on the property: the two disagreed, and the corpus
recorded it as a divergence pending a decision. jdx's call was to fix it here. The vector
that recorded the difference is now an ordinary agreeing one, and deleting its label was not
optional — the reference test checks labels in both directions, so it failed with an
instruction to remove it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com


Stack created with GitHub Stacks CLIGive Feedback 💬


Note

Medium Risk
Changes core argv routing in lib/src/parse.rs, which affects how nested commands parse; scope is narrow and covered by corpus and unit tests.

Overview
Fixes incorrect default_subcommand routing in usage-lib so the single spec-level default is honored only at the root, not on nested commands that happen to share a subcommand name.

The parser now gates default routing on out.cmds.len() == 1 in addition to the existing one-shot flag. Before this change, ex config zzz with default_subcommand "ls" at the top could incorrectly descend into config ls and bind zzz there; it now correctly yields unexpected_arg when config has no argument.

The corpus vector default-is-declared-for-the-root is updated to expect that error and its usage-lib divergence label is removed. PLAN.md documents the fix instead of an open divergence, and a unit test covers both the nested non-routing and root routing cases.

Reviewed by Cursor Bugbot for commit 6bd131f. Bugbot is set up for automated code reviews on this repo. Configure here.

What was happening

default_subcommand is a property of the Spec, declared once at the top — there is no
syntax for a per-command one. But the parser looked that single name up against
out.cmd, whichever command it was standing on. So a command with an unrelated subcommand
of the same name silently acquired a default:

spec: default_subcommand "ls"; cmd ls; cmd config { cmd ls { arg "[WHAT]" } }

before: ["config", "zzz"] -> cmds ["ex", "config", "ls"], args [("WHAT", "zzz")]
after:  ["config", "zzz"] -> error: unexpected word: zzz

Nothing declared that behavior and nothing could have, which is what makes it a bug rather
than a feature. Gated on still being at the root, which out.cmds already records — it holds
just the root until something descends.

Verification

  • A usage-lib test asserting both halves: the nested command does not route, and the root
    still does. Mutation-checked — with the guard removed it fails.
  • All 351 existing usage-lib tests still pass, including the five that already covered
    default_subcommand (explicit selection, same-named child, nested subcommands via mount,
    and discovery precedence).
  • The corpus vector that recorded this as a divergence is now an ordinary agreeing vector.
    Deleting its label was not optional: reference_labels_are_accurate checks labels in
    both directions, so it failed with usage-lib now agrees — delete the label. That is the
    mechanism working as intended, and it is also the proof the fix took effect.

Provenance

Found while teaching usage-argv to route on this property (#848): the two implementations
disagreed, and rather than guess I recorded it as a divergence pending your decision. You
said fix it, so this is that fix.

AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fc117a8-3e4e-427c-9836-81a03c633509

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jdx
jdx force-pushed the agent/lib-default-scope branch from 8384add to 85f943d Compare August 12, 2026 23:40
@jdx
jdx force-pushed the agent/lib-default-scope branch from 85f943d to 363dbd7 Compare August 12, 2026 23:45
@jdx
jdx marked this pull request as ready for review August 12, 2026 23:49
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR restricts default_subcommand routing to the root command, preventing nested commands with a same-named child from acquiring an undeclared default.

  • Adds a root-depth guard to the usage-lib parser.
  • Adds regression coverage for nested rejection and preserved root routing.
  • Updates the conformance vector and planning documentation to reflect agreement between parsers.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
lib/src/parse.rs Restricts default-subcommand lookup to the root and adds focused regression coverage for both nested and root behavior.
corpus/09-default-subcommand.json Removes the obsolete divergence label now that usage-lib produces the corpus’s expected nested-command error.
PLAN.md Updates project documentation to record the corrected root-only routing behavior.

Reviews (3): Last reviewed commit: "fix(spec): apply default_subcommand only..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▁▁▁▁▁▁▁▁▁▂██ 152,857,235 → 152,957,608 +0.07% 14.64 → 14.37ms -1.83%
startup ▁███████████ 1,201,903 → 1,201,983 +0.01% 1.04 → 0.96ms -7.08%

No instruction-count regression above 1%.

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

Shadow comparison

Parsing mise use -g node@20 against a shadow of mise's committed spec.
Reported, not gated: the shadow grows as the derive learns to express more, so
what to watch is the ratio rather than either column.

usage clap ratio
instructions, cold parse 40221 5962046 148x
usage: argv -> struct                            1747 ns      1.75 µs
clap: build tree + parse -> struct             501981 ns    501.98 µs
clap: parse -> struct, tree reused              22927 ns     22.93 µs
clap: build tree only                          310938 ns    310.94 µs

6bd131f508e4 vs cca57930b536 · measured on the runner, not pushed to the history.

@jdx
jdx force-pushed the agent/lib-default-scope branch from 363dbd7 to 85f943d Compare August 13, 2026 00:29
A spec declares one `default_subcommand`, once, at the top — and the parser looked that name
up at whichever command it happened to be standing on. So a command with an unrelated
subcommand of the same name acquired a default of its own: with `default_subcommand "ls"`
declared for the program, `ex config zzz` descended into `config ls` and bound `zzz` there.
Nothing declared that, and there is no way to declare it, which is what makes it a bug
rather than a feature.

Gated on still being at the root, which `out.cmds` already records — it holds just the root
until something descends.

Found while teaching usage-argv to route on the property: the two disagreed, and the corpus
recorded it as a divergence pending a decision. jdx's call was to fix it here. The vector
that recorded the difference is now an ordinary agreeing one, and deleting its label was not
optional — the reference test checks labels in both directions, so it failed with an
instruction to remove it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jdx
jdx force-pushed the agent/lib-default-scope branch from 85f943d to 6bd131f Compare August 13, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant