Skip to content

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

Merged
jdx merged 1 commit into
agent/default-routingfrom
agent/lib-default-scope
Aug 13, 2026
Merged

fix(spec): apply default_subcommand only at the root#850
jdx merged 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 argv routing for nested commands when a subcommand name matches the root default; behavior is now aligned with the spec and corpus, but it is a user-visible parse change for those edge cases.

Overview
default_subcommand is spec-wide and root-only, but usage-lib was resolving the declared name against whatever command the parser was on. That let nested commands inherit a default when they merely had a subcommand with the same name (e.g. top-level default_subcommand "ls" made ex config zzz route into config ls).

The discovery-phase guard now requires out.cmds.len() == 1 in addition to the existing once-per-parse flag, so default routing runs only at the root. ex config zzz becomes unexpected_arg instead of silently descending; root-level routing is unchanged.

The corpus case default-is-declared-for-the-root drops its divergence label and expects agreement. PLAN.md records the fix in usage-lib rather than an open decision. A focused unit test covers nested vs root behavior.

Reviewed by Cursor Bugbot for commit f1c2af9. 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: 5f64ba83-0d4b-4d9d-8be1-865e3c176317

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 the root-path guard and focused parser coverage.
  • Updates the conformance vector to remove the resolved implementation divergence.
  • Documents the corrected routing behavior in the implementation plan.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
lib/src/parse.rs Gates default-subcommand routing on the parser remaining at the root and tests both nested rejection and root routing.
corpus/09-default-subcommand.json Converts the previously documented parser divergence into a shared unexpected-argument expectation.
PLAN.md Records that root-only default-subcommand behavior is now aligned across parser implementations.

Fix All in Greploop

Reviews (4): 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,797,288 → 152,861,514 +0.04% 14.15 → 14.21ms +0.38%
startup ▆▆▆█▆▆▆▆▆▆▃▁ 1,201,844 → 1,201,800 -0.00% 0.96 → 0.96ms -0.73%

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                            1740 ns      1.74 µs
clap: build tree + parse -> struct             499189 ns    499.19 µs
clap: parse -> struct, tree reused              23604 ns     23.60 µs
clap: build tree only                          309310 ns    309.31 µs

f1c2af98b7ff vs 076fe59bdc97 · measured on the runner, not pushed to the history.

@jdx
jdx force-pushed the agent/lib-default-scope branch 2 times, most recently from 85f943d to 6bd131f Compare August 13, 2026 00:54
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 6bd131f to f1c2af9 Compare August 13, 2026 02:38
@jdx
jdx merged commit a3d9e62 into main Aug 13, 2026
9 of 11 checks passed
@jdx
jdx deleted the agent/lib-default-scope branch August 13, 2026 03:14
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