Skip to content

feat(argv): render the usage line, byte-identical to usage-lib's - #854

Merged
jdx merged 4 commits into
agent/faithful-metadatafrom
agent/usage-line
Aug 13, 2026
Merged

feat(argv): render the usage line, byte-identical to usage-lib's#854
jdx merged 4 commits into
agent/faithful-metadatafrom
agent/usage-line

Conversation

@jdx

@jdx jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Usage: mise use [OPTIONS] <TOOL@VERSION>… — the line at the top of --help, and the first
thing a CLI framework has to be able to produce. Built from the same &'static metadata a
parse ignores, so a binary that never asks for help pays nothing for being able to.

usage-lib renders this through a tera template over a runtime model. This crate has no Spec
at run time, only tables, so the rules are reimplemented — and reimplemented rules drift. The
test runs both over mise's real spec and compares all 211 lines, because an adopter's help
text changing is a visible regression even when the change is one bracket. The shadow is
generated from the same KDL usage-lib is handed, so the two describe the same CLI by
construction rather than by a fixture kept in step by hand.

Two things it caught:

An argument that only takes what follows a -- is bracketed around the separator —
[-- COMMAND]…, one optional thing — where I had written -- [COMMAND]…, a literal --
followed by an optional word. Five of mise's commands read that way.

A flag is named after the form it answers to, not after the Rust field holding it. usage-lib
derives the name from the first long form, and the derive was kebab-casing the field ident —
so type_ gave a flag called type-, which help printed as type-: -t --type and errors
reported as type-. An explicit name still wins, and a flag with no long form keeps its
short, as usage-lib does. Selectors resolve by form rather than by name, so nothing that
refers to a flag changes.

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


Stack created with GitHub Stacks CLIGive Feedback 💬


Note

Low Risk
Cold-path help formatting behind spec, with broad parity tests; derive naming changes align emitted KDL/help with usage-lib and do not alter parse-time selector matching.

Overview
Adds usage_argv::help::usage_line behind the existing spec feature so the top-of-help invocation summary is built from static CommandMeta tables without a runtime spec model.

Parity: The gate bench compares every mise command’s line (211) against usage-lib from the same KDL; PLAN.md marks the usage line as done.

Rendering fixes: Defaulted flags/args render as optional (required && default.is_empty()), not raw required. -- positionals use [-- NAME]… with brackets outside the separator. Flags/args collapse to [FLAGS] / [ARGS]… past an inline limit of 2.

Derive: Flags without an explicit name are named from the first long or short form (fixes type_type-); short-only value flags keep the field ident as value_name.

Tests: Gate help.rs, conformance derive / metadata for defaulted and renamed flags.

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

The standard

All 211 of mise's commands render byte-identically to usage-lib.

usage-lib builds this through a tera template over a runtime model. usage-argv has no Spec at
run time — only &'static tables — so the rules are reimplemented here, and reimplemented rules
drift. The test runs both over mise's real spec and compares every line, because an adopter's
help text changing is a visible regression even when the change is one bracket. The shadow is
generated from the same KDL usage-lib is handed, so the two describe the same CLI by
construction rather than by a fixture kept in step by hand.

The parity test is also what found the four metadata gaps: three in #853 below this, and one
here.

What it caught here

A -- argument is bracketed around the separator. usage-lib writes [-- COMMAND]… — one
optional thing — where I had -- [COMMAND]…, a literal -- followed by an optional word. Five
of mise's commands read that way.

A flag is named after the form it answers to, not the field holding it. usage-lib derives
the name from the first long form; the derive was kebab-casing the Rust ident. So type_ gave a
flag called type-, printed by help as type-: -t --type and reported by errors as type-.
An explicit name still wins, and a flag with no long keeps its short, as usage-lib does.
Selectors resolve by form rather than by name, so nothing that refers to a flag changes — and
error messages get better for free.

Cost

None on the parse path: this reads the cold metadata a successful parse never touches, and lives
behind the existing spec feature. A binary that never renders help does not carry the
formatting code.

Not yet

The line only. The sections below it — about, arguments, flags grouped by heading, subcommands,
examples — and the --help/-h wiring in the derive are the next two PRs. The line comes first
because everything else is arranged around it, and because it is the piece with an exact oracle.

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

@coderabbitai

coderabbitai Bot commented Aug 13, 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: 3e647973-95d4-46d3-84c8-a5d691adae5d

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.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a feature-gated usage-line renderer backed by static command metadata and aligns derived flag names with their command-line forms.

  • Adds usage-line formatting for flags, positional arguments, defaults, hidden entries, subcommands, and double-dash arguments.
  • Adds parity coverage against usage-lib across mise’s generated command tree.
  • Updates derive metadata naming and conformance coverage for renamed, short-only, and defaulted fields.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
argv/src/help.rs Adds the feature-gated static-metadata usage-line renderer; no eligible blocking follow-up issue was established.
derive/src/model.rs Changes implicit flag names to follow their first long or short form while preserving explicit names and descriptive short-only value placeholders.
benches/gate/tests/help.rs Adds command-tree parity checks comparing rendered lines against usage-lib using mise’s canonical generated shadow.
conformance/tests/derive.rs Adds end-to-end coverage proving defaulted required-by-type fields render as optional and remain omittable during parsing.
conformance/tests/metadata.rs Adds coverage for short-only and explicitly renamed flag metadata, placeholders, and binding behavior.

Fix All in Greploop

Reviews (7): Last reviewed commit: "fix(derive): take a flag's placeholder f..." | Re-trigger Greptile

Comment thread argv/src/help.rs
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▁▁▁▁▁▂▂▂██ 175,245,063 → 175,051,067 -0.11% 16.93 → 16.00ms -5.54%
startup ▁▁▁▁▁▁▁▁██ 1,222,129 → 1,222,069 -0.00% 1.00 → 0.95ms -4.33%

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 29877 5960254 199x
usage: argv -> struct                             881 ns      0.88 µs
clap: build tree + parse -> struct             500818 ns    500.82 µs
clap: parse -> struct, tree reused              22953 ns     22.95 µs
clap: build tree only                          311818 ns    311.82 µs

32ff41b59316 vs fe13b0b9c7d8 · measured on the runner, not pushed to the history.

@jdx
jdx force-pushed the agent/usage-line branch from 179a3bc to df6f4e8 Compare August 13, 2026 14:22

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Real, and it goes to the heart of what this PR claims. usage-lib clears required while parsing a spec that declares a default, then renders the usage line from required alone — so it writes [dir] where this wrote <dir>, for an argument the parser is perfectly happy to omit. The same signal decides whether a collapsed list reads <ARGS>… or [ARGS]…, so a command of entirely defaulted arguments was marked required there too.

Fixed where the line is rendered rather than by clearing the flag in the metadata: the metadata is what the emitted spec is built from, and required there means what the author wrote.

Worth explaining why the 211-line parity test sails past this. Every defaulted positional in mise's spec is written required=#false default=…, so there is nothing to normalise and both sides already agree. A derived argument is required by its type and defaulted by its attribute — that is the shape that diverges, and mise's spec contains none. So the new test compares the two renderers over one derived declaration, and then parses it with no arguments at all to show the brackets are telling the truth.

Both call sites mutation-checked. The collapse threshold needed a case of its own: INLINE_LIMIT is 2, so it takes three defaulted arguments to reach the branch, and without one the mutation survived.

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

@jdx
jdx force-pushed the agent/usage-line branch from df6f4e8 to 57088ae Compare August 13, 2026 14:46
Comment thread argv/src/help.rs
@jdx
jdx force-pushed the agent/usage-line branch 2 times, most recently from ecc6bb6 to 6586ea7 Compare August 13, 2026 14:55

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Right — I fixed arguments and left flags reading required on its own, which is the same divergence in the other half of the line. usage-lib clears required on a flag that declares a default too, so a derived value flag with one was shown as <--out> when the parser fills it happily.

Both the brackets and the collapse threshold now use the same rule, and the fixture grew a defaulted flag plus three more so the collapsed [FLAGS] case is reached — without that third flag the threshold mutation survived, exactly as it did for arguments.

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

jdx and others added 2 commits August 13, 2026 16:17
`Usage: mise use [OPTIONS] <TOOL@VERSION>…` — the line at the top of `--help`, and the first
thing a CLI framework has to be able to produce. Built from the same `&'static` metadata a
parse ignores, so a binary that never asks for help pays nothing for being able to.

usage-lib renders this through a tera template over a runtime model. This crate has no `Spec`
at run time, only tables, so the rules are reimplemented — and reimplemented rules drift. The
test runs both over mise's real spec and compares all 211 lines, because an adopter's help
text changing is a visible regression even when the change is one bracket. The shadow is
generated from the same KDL usage-lib is handed, so the two describe the same CLI by
construction rather than by a fixture kept in step by hand.

Two things it caught:

An argument that only takes what follows a `--` is bracketed *around* the separator —
`[-- COMMAND]…`, one optional thing — where I had written `-- [COMMAND]…`, a literal `--`
followed by an optional word. Five of mise's commands read that way.

A flag is named after the form it answers to, not after the Rust field holding it. usage-lib
derives the name from the first long form, and the derive was kebab-casing the field ident —
so `type_` gave a flag called `type-`, which help printed as `type-: -t --type` and errors
reported as `type-`. An explicit `name` still wins, and a flag with no long form keeps its
short, as usage-lib does. Selectors resolve by form rather than by name, so nothing that
refers to a flag changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rs it

The usage line took optionality from `required` alone. usage-lib clears
`required` while *parsing* a spec that declares a default — a defaulted argument
is one the user may leave out — and then renders from `required`, so it writes
`[dir]` where this wrote `<dir>`, for an argument the parser is perfectly happy
to omit. The same signal decides whether a collapsed list reads `<ARGS>…` or
`[ARGS]…`, so a command of entirely defaulted arguments was marked required
there too.

Applied where the line is rendered rather than by clearing the flag in the
metadata: the metadata is what the emitted spec is built from, and `required`
there means what the author wrote.

Worth saying why the 211-line parity test did not catch it. Every defaulted
positional in mise's spec is written `required=#false default=…`, so there is
nothing to normalize and both sides agree. A *derived* argument is required by
its type and defaulted by its attribute, which is the shape that diverges — so
the new test compares the two renderers over one derived declaration, and also
parses it to show the brackets are telling the truth.

Both call sites mutation-checked; the collapse threshold needed a case of its
own, since three defaulted arguments are what it takes to reach it.
@jdx
jdx force-pushed the agent/usage-line branch from 6586ea7 to 0ec1bb1 Compare August 13, 2026 16:17
Comment thread derive/src/model.rs

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Real, and worth doing — but the code it names is not in this PR's own commits, and I have deliberately not touched it.

The block that sets name from shorts.first() for a short-only flag arrived in a later commit on the branch this stack has since moved onto (agent/help-short/agent/help-body), and another session is actively editing that worktree — it committed under me while I was reading this file. Two of us writing there is how work gets lost, so here is the analysis instead of a half-applied fix:

The name is right and should stay: usage-lib derives it the same way, so -j alone is called j on both sides, and changing that would break the parity this stack exists for. The problem is only that value_name falls back to name, so the placeholder becomes <j> rather than something a reader can act on. The derive knows the field's own name, so defaulting value_name to it for a flag with no long form and no explicit value_name fixes the placeholder without touching what the flag is called — and a test wants a short-only value flag, checking both the emitted KDL and the usage line.

Happy to apply it here if that branch goes idle, or it can be picked up by whoever owns it.

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

jdx added a commit that referenced this pull request Aug 13, 2026
…'s about

Two findings from Cursor Bugbot, both regressions from this stack.

A flag is named after the form it answers to, and for a short-only flag that form is one
character — right for the flag's name and useless as the name of its *value*, since help and the
KDL both fall back to `flag.name` for the placeholder. `-j <j>` where the field is called
`jobs`. The descriptive name is kept as the value name before the flag's own is overwritten.

And when a program's two descriptions are independent, the generator skipped the root's doc
comment and wrote them into the *usage* attribute list only — which the clap dialect never
emits, so the clap shadow stopped describing the program at all. clap takes an independent
`about` and `long_about`, and a fixture for comparing two frameworks cannot have one of them
missing the CLI's own description.

Both mutation-checked. `-h` parity is unchanged at 211 of 211, which is what says the value-name
fix did not move the rendering.

Two older findings on #854 were already answered by the `demanded`/`flag_demanded` helpers: a
defaulted argument or flag is one the parser fills, so the usage line brackets it as optional.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A flag is named after the form it answers to, which for a short-only flag is one character.
That is right for the flag's name and useless as the name of its *value*: help and the KDL both
fall back to `flag.name` for the placeholder, so `-j <j>` where the field is called `jobs`.

The descriptive name is kept as the value name before the flag's own is overwritten.

Fixed here rather than further up the stack, which is where I had put it — the finding is
against this PR's change, so it belongs in this PR. A fix that only exists in a descendant
leaves the branch it was reported on still carrying the bug, and this one merges on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Aug 13, 2026
…'s about

Two findings from Cursor Bugbot, both regressions from this stack.

A flag is named after the form it answers to, and for a short-only flag that form is one
character — right for the flag's name and useless as the name of its *value*, since help and the
KDL both fall back to `flag.name` for the placeholder. `-j <j>` where the field is called
`jobs`. The descriptive name is kept as the value name before the flag's own is overwritten.

And when a program's two descriptions are independent, the generator skipped the root's doc
comment and wrote them into the *usage* attribute list only — which the clap dialect never
emits, so the clap shadow stopped describing the program at all. clap takes an independent
`about` and `long_about`, and a fixture for comparing two frameworks cannot have one of them
missing the CLI's own description.

Both mutation-checked. `-h` parity is unchanged at 211 of 211, which is what says the value-name
fix did not move the rendering.

Two older findings on #854 were already answered by the `demanded`/`flag_demanded` helpers: a
defaulted argument or flag is one the parser fills, so the usage line brackets it as optional.

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

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c2d4575. Configure here.

Comment thread derive/src/model.rs
The value name falls back to the flag's name, and a flag is named after the form it answers to —
so keeping the field's kebab-cased ident as the placeholder was only right for a flag that has no
long form. With one, the long form *is* the descriptive name, and a field called `type_` rendered
`--type <type->`.

Narrowed to the case it was for: a short-only flag, where the name becomes one character and
`-j <j>` says nothing.

Found by Cursor Bugbot on #854.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Aug 13, 2026
…'s about

Two findings from Cursor Bugbot, both regressions from this stack.

A flag is named after the form it answers to, and for a short-only flag that form is one
character — right for the flag's name and useless as the name of its *value*, since help and the
KDL both fall back to `flag.name` for the placeholder. `-j <j>` where the field is called
`jobs`. The descriptive name is kept as the value name before the flag's own is overwritten.

And when a program's two descriptions are independent, the generator skipped the root's doc
comment and wrote them into the *usage* attribute list only — which the clap dialect never
emits, so the clap shadow stopped describing the program at all. clap takes an independent
`about` and `long_about`, and a fixture for comparing two frameworks cannot have one of them
missing the CLI's own description.

Both mutation-checked. `-h` parity is unchanged at 211 of 211, which is what says the value-name
fix did not move the rendering.

Two older findings on #854 were already answered by the `demanded`/`flag_demanded` helpers: a
defaulted argument or flag is one the parser fills, so the usage line brackets it as optional.

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

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Right, and the fix belongs here rather than in a descendant — copying the field ident into the value name ran for every unnamed value flag, so a field called `type_` with `long = "type"` rendered `--type `. Narrowed to the case it was for: a short-only flag, where the name becomes one character and `-j ` says nothing. A long form is already the descriptive name.

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

@jdx
jdx merged commit acb00ff into main Aug 13, 2026
9 checks passed
@jdx
jdx deleted the agent/usage-line branch August 13, 2026 20:18
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