Skip to content

feat(spec): add help_heading, and render it - #802

Merged
jdx merged 3 commits into
mainfrom
agent/spec-help-heading
Aug 11, 2026
Merged

feat(spec): add help_heading, and render it#802
jdx merged 3 commits into
mainfrom
agent/spec-help-heading

Conversation

@jdx

@jdx jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Stacked on #801 — review that first; this PR's diff will include it until it merges.

Closes the gap #801 turned up: writing the spec emitter, I reached for help_heading and found the spec has no such field.

Why it matters more than it looks

A CLI with dozens of flags is unreadable without sections, and clap has had help_heading for years. Because the spec could not record one, From<&clap::Arg> was silently dropping it — so every CLI in the fleet that groups its flags has been losing the grouping on the way into its spec. mise groups its entire watch passthrough set that way (31 uses, all in one file).

That is the part worth noting: this was not a missing feature so much as a leak that was invisible because nothing downstream could have shown it.

The spec side

  • help_heading on SpecFlag and SpecArg, as a property (help_heading="Filtering") or a child node for longer text, written back out.
  • Mapped from clap's get_help_heading() for both flags and positionals.
  • Builder setters, docs in the flag and arg references, tests for the round trip and the clap conversion.
  • usage-argv carries it in both FlagMeta and ArgMeta, which is what prompted this: the derive cannot emit what the spec cannot express, so per the canonicality rule the spec went first.

The rendering side

Help output and generated markdown both group by heading now — a field nothing displays is half a feature.

Grouping happens in the docs models, not in a template: Tera can filter on an attribute's value but cannot partition on one, and "everything without a heading" is not expressible as a filter. Behaviour:

  • Unheaded entries keep the default section title (Flags: / Arguments:) and come first.
  • Each heading gets its own section, in the order the headings first appear.
  • A heading with nothing visible in it produces no section, and a CLI that heads every flag gets no empty Flags:.
  • Positionals group too, since the spec field is on both.

One thing worth knowing if you touch this: the groups hold clones, and render_md mutates the flag and arg lists after the model is built — so grouping at construction published copies without their rendered markdown. That cost me a debugging round; groups are rebuilt at the end of render_md and the method that does it says why.

Every existing snapshot is unchanged — output is byte-identical when nothing has a heading — and four new ones cover the grouped case across both renderers, including hidden entries. mise run render produces no diff.

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


Note

Low Risk
Presentational metadata and docs/help rendering only; parsing behavior is unchanged and output without headings remains identical.

Overview
Adds help_heading to flags and positionals in the usage spec (KDL parse/serialize, builders, reference docs) and threads it through usage-argv metadata and KDL emission so derive output can record section titles losslessly.

Clap bridge fix: From<&clap::Arg> now maps get_help_heading(), so grouped flags are no longer dropped when converting to a spec.

Rendering: CLI help (short/long templates) and generated markdown partition flags and args by heading via flag_groups / arg_groups in the docs models. Unheaded items stay under default Arguments / Flags and appear first; custom headings follow in first-seen order; sections with only hidden entries are omitted. Markdown regroups after render_md so grouped clones keep rendered help text.

Conformance roundtrip tests and new snapshot tests cover grouped help and global flags; existing output stays byte-identical when no headings are set.

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

Summary by CodeRabbit

  • New Features
    • Added configurable help headings for flags and arguments.
    • Help and Markdown output now groups entries under their headings while preserving declaration order.
    • Empty sections and hidden entries are omitted from generated documentation.
    • Global and local flags are displayed in separate grouped sections.
  • Documentation
    • Added specification examples and reference documentation for configuring help headings.
  • Bug Fixes
    • Improved consistency between generated help output and Markdown documentation.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds optional help_heading metadata for flags and arguments. The metadata survives KDL and Clap conversion. CLI and Markdown output now groups visible entries by heading while preserving order and omitting empty sections.

Changes

Help heading support

Layer / File(s) Summary
Specification metadata and serialization
argv/src/spec.rs, lib/src/spec/flag.rs, lib/src/spec/arg.rs, lib/src/spec/builder.rs
Flags and arguments now store, parse, serialize, and build optional help_heading values. Clap conversion copies the heading.
Grouped documentation models
lib/src/docs/models.rs
SpecCommand now exposes ordered argument and flag groups. Grouping places unheaded entries first and rebuilds groups after Markdown rendering.
Grouped help and Markdown rendering
lib/src/docs/cli/templates/*, lib/src/docs/markdown/templates/cmd_template.md.tera, PLAN.md
Templates render nonempty groups with configurable headings and default labels. Markdown separates global and local flags.
Roundtrip, integration, and rendering coverage
conformance/tests/spec_roundtrip.rs, lib/src/docs/cli/mod.rs, lib/src/docs/markdown/cmd.rs, lib/src/spec/flag.rs, docs/spec/reference/*
Tests cover parsing, serialization, Clap transfer, ordering, hidden entries, empty sections, and global flags. Reference examples document the syntax.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Clap
  participant Spec
  participant DocsModel
  participant HelpTemplate
  participant MarkdownTemplate
  Clap->>Spec: provide flag and argument help_heading
  Spec->>DocsModel: convert metadata
  DocsModel->>DocsModel: group entries by heading
  DocsModel->>HelpTemplate: provide arg_groups and flag_groups
  DocsModel->>MarkdownTemplate: provide grouped visible entries
Loading

Possibly related PRs

  • jdx/usage#746: Both modify specification metadata and documentation models for command descriptions.
  • jdx/usage#801: This change implements the related specification gap for help_heading.

Poem

I’m a rabbit with headings to share,
Flags hop to sections with care.
Args join the queue,
Empty groups stay few,
And Markdown grows tidy and fair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding help_heading support and rendering grouped output.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

Comment thread argv/src/spec.rs
@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds help_heading metadata for flags and positional arguments and uses it to group terminal-help and Markdown sections.

  • Carries headings through clap conversion, usage-argv metadata, KDL parsing, serialization, builders, and documentation.
  • Groups visible arguments and flags under default or custom headings while preserving declaration order.
  • Adds round-trip, conversion, and renderer snapshot coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
argv/src/spec.rs Adds help_heading to both metadata structures and correctly emits it for flags and positional arguments, completing the previously reported positional path.
lib/src/spec/arg.rs Parses, serializes, and imports positional help headings from clap.
lib/src/spec/flag.rs Parses, serializes, and imports flag help headings, with round-trip and clap-conversion tests.
lib/src/docs/models.rs Introduces ordered heading groups and rebuilds cloned groups after Markdown rendering to keep rendered fields current.
lib/src/docs/markdown/templates/cmd_template.md.tera Renders visible positional and flag groups under default or custom headings, including global-flag handling.
lib/src/docs/cli/templates/spec_template_long.tera Switches long terminal help from flat argument and flag lists to grouped sections.
lib/src/docs/cli/templates/spec_template_short.tera Switches short terminal help from flat argument and flag lists to grouped sections.

Reviews (5): Last reviewed commit: "fix(docs): group global flags by heading..." | Re-trigger Greptile

Comment thread argv/src/spec.rs
@jdx
jdx force-pushed the agent/spec-help-heading branch 3 times, most recently from d340027 to e388012 Compare August 11, 2026 02:12

jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Both findings — ArgMeta and write_arg missing help_heading — are fixed in the current commit. The spec field is on flags and arguments, so the metadata had to be too; the fixture now puts a heading on the root's positional and a_help_heading_survives asserts both survive the round trip.

Rebased on the latest #801, which picked up its four fixes as well.

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

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ 110,538,625 → 148,152,129 +34.03% ⚠️ 11.08 → 15.00ms +35.41%
startup ▁▃▅██▅▅▆▆▇▆▆▆▆▆▆▆▆▆█ 1,201,545 → 1,204,001 +0.20% 0.95 → 1.08ms +13.03%

1 benchmark(s) above the 1% gate: markdown +34.03%

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.

da412c73921f vs cf17ee51a47b · measured on the runner, not pushed to the history.

@jdx jdx changed the title feat(spec): add help_heading to flags and args feat(spec): add help_heading, and render it Aug 11, 2026

@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 cc568bc. Configure here.

Comment thread lib/src/docs/markdown/templates/cmd_template.md.tera
@jdx
jdx force-pushed the agent/spec-help-heading branch from cc568bc to 8fed243 Compare August 11, 2026 13:55
Base automatically changed from agent/argv-spec-emit to main August 11, 2026 15:47
jdx and others added 3 commits August 11, 2026 15:47
A CLI with dozens of flags is unreadable without sections, clap has had
help_heading for years, and the spec had no way to record one — so the
clap bridge was dropping it. Every CLI in the fleet that groups its flags
lost the grouping on the way into its spec; mise groups its whole watch
passthrough set that way.

Now a field on both flags and arguments, accepted as a property or a child
node, written back out, and mapped from clap's own help_heading. usage-argv
carries it too, which is what the change was for: the derive cannot emit
something the spec cannot express, so the spec goes first.

Nothing renders a heading yet — grouping flags in help and markdown output
needs the docs models and templates to change, which is its own PR and is
now a box in PLAN.md. The field is not idle in the meantime: a clap-derived
spec stops losing information the moment this lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A spec field nothing displays is half a feature, so help output and
generated markdown both group by heading now.

Grouping happens in the docs models rather than in a template, because
Tera can filter on an attribute's value but cannot partition on one, and
"everything without a heading" is not expressible as a filter. Unheaded
entries keep the default section title and come first; a heading with
nothing visible in it produces no section, so a CLI that heads every flag
does not get an empty "Flags:".

The groups hold clones, which bit once already: render_md mutates the flag
and arg lists after the model is built, so groups made before that point
published copies without their rendered markdown. They are rebuilt at the
end of render_md, and the method that does it says why.

Every existing snapshot is unchanged — output is byte-identical when
nothing has a heading — and four new ones cover the grouped case in both
renderers, including hidden entries and a heading whose only entry is
hidden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated markdown renders global flags in their own section, built from
the flat list, so a `help_heading` on a global flag was ignored there while
help output honored it — the same CLI documented two different ways.

A heading now beats the default title, so a grouped global flag lands in the
section it belongs to and only ungrouped ones fall under "Global Flags".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jdx
jdx force-pushed the agent/spec-help-heading branch from 8fed243 to da412c7 Compare August 11, 2026 15:47
@jdx
jdx merged commit 5b917ad into main Aug 11, 2026
7 of 8 checks passed
@jdx
jdx deleted the agent/spec-help-heading branch August 11, 2026 15:50

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/docs/models.rs`:
- Around line 295-296: Filter out entries with hide set to true from both flags
and args before passing them to group_by_heading in the constructor, and apply
the same filtering in regroup. Ensure grouped items and headings containing only
hidden entries are excluded from CLI help output.

In `@lib/src/spec/builder.rs`:
- Around line 250-257: Correct the doc comments on the builder methods: in
help_heading, document the help-output heading, and in env, document that it
sets the environment variable name. Keep the method implementations unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 913863e3-5e4f-4465-98dc-c75bf95ed3db

📥 Commits

Reviewing files that changed from the base of the PR and between cf17ee5 and da412c7.

⛔ Files ignored due to path filters (1)
  • conformance/tests/snapshots/spec_roundtrip__the_emitted_spec_is_stable.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • PLAN.md
  • argv/src/spec.rs
  • conformance/tests/spec_roundtrip.rs
  • docs/spec/reference/arg.md
  • docs/spec/reference/flag.md
  • lib/src/docs/cli/mod.rs
  • lib/src/docs/cli/templates/spec_template_long.tera
  • lib/src/docs/cli/templates/spec_template_short.tera
  • lib/src/docs/markdown/cmd.rs
  • lib/src/docs/markdown/templates/cmd_template.md.tera
  • lib/src/docs/models.rs
  • lib/src/spec/arg.rs
  • lib/src/spec/builder.rs
  • lib/src/spec/flag.rs

Comment thread lib/src/docs/models.rs
Comment on lines +295 to +296
flag_groups: group_by_heading(&flags, |f| f.help_heading.as_deref()),
arg_groups: group_by_heading(&args, |a| a.help_heading.as_deref()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude hidden entries before building groups.

args and flags include entries where hide is true. The CLI templates render every grouped item. Hidden entries therefore appear in help output, and headings that contain only hidden entries are not omitted.

Filter hidden flags and arguments before grouping in this constructor and in regroup at Lines 465-466.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/docs/models.rs` around lines 295 - 296, Filter out entries with hide
set to true from both flags and args before passing them to group_by_heading in
the constructor, and apply the same filtering in regroup. Ensure grouped items
and headings containing only hidden entries are excluded from CLI help output.

Comment thread lib/src/spec/builder.rs
Comment on lines 250 to 257
/// Set environment variable name
/// Heading to list this under in help output.
pub fn help_heading(mut self, help_heading: impl Into<String>) -> Self {
self.inner.help_heading = Some(help_heading.into());
self
}

pub fn env(mut self, env: impl Into<String>) -> Self {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the environment-variable documentation to env.

help_heading now documents itself as setting an environment variable. env has no documentation. Put the heading documentation on help_heading. Put Set environment variable name on env.

Also applies to: 395-402

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/spec/builder.rs` around lines 250 - 257, Correct the doc comments on
the builder methods: in help_heading, document the help-output heading, and in
env, document that it sets the environment variable name. Keep the method
implementations unchanged.

jdx added a commit that referenced this pull request Aug 11, 2026
Stacked on #802 (which is stacked on #801) — review those first; this
diff includes them until they merge.

The piece the last three PRs were building toward: one Rust type in, a
parser and a spec out.

```rust
/// A tool that does things
#[derive(usage::Cli)]
#[usage(bin = "ex", version = "1.0")]
struct Cli {
    /// How many jobs to run at once
    #[usage(short = 'j', long, env = "EX_JOBS", default = "4")]
    jobs: Option<String>,

    /// Colorize output
    #[usage(long, negate = "--no-color", default = "true")]
    color: bool,

    /// Files to process
    files: Vec<String>,
}
```

That gives you `Cli::parse_from(argv)`, `Cli::command()`, `Cli::spec()`,
and `Cli::to_kdl()` — so the same declaration feeds `usage g
markdown|manpage`, the completion generators, and grouped help output
from #802.

## What's generated

Three things, and the split is the design:

- **`static` parse tables** — all a successful parse reads.
- **`static` metadata** — what spec emission and help need, which a
parse never touches.
- **a parse function** — a `match` on table keys assigning straight into
the struct's fields. No map to build and read back, nothing allocated
that does not end up in the result.

`command()` returns a `&'static`, so there is no command tree to
construct before parsing starts. A test asserts the pointer is the same
every call, which is the property the whole project exists for.

A field with `long` or `short` is a flag; anything else is positional.
Help comes from the doc comment — first paragraph short, whole comment
long.

## Scope, stated plainly

**One command per struct.** Subcommands need an enum of variants,
cross-type table references, and a nested path through the parse
function; that is its own PR and a box in `PLAN.md`.

**Values are text** — `bool`, `String`, `Option<String>`, `Vec<String>`,
or an unsigned integer with `count`. Converting to other types is also
where `env`, required-ness, and `choices` get enforced, and that layer
does not exist yet. So `Option<u32>` is a *compile error* explaining
exactly that, rather than something that silently half-works.

## The error messages got real attention

They are the surface an author actually interacts with, so:

- `short = "j"` → *a short flag is a character: write `short = 'j'`*
- a duplicate `--flag` → points at both declarations, second first
- an argument after a variadic one → *can never be filled, because the
variadic takes every remaining word*
- an unknown option → lists the ones that exist
- `count` on a `String` → says it has to be an unsigned integer

## Tests

Twelve, over a deliberately awkward CLI: attached and bundled shorts,
`--flag=value`, repeated flags, a negation turning off a default, a
hidden flag that still parses, `--` passthrough, and a typo reported
rather than bound. Then the same declaration is checked to emit a spec
usage-lib accepts field by field, render as markdown and a manpage, and
group by heading. Two more CLIs cover the empty cases — no flags, no
positionals — which is where generated code tends to break on an unused
variable or an empty `match`.

Not published: a CLI framework that cannot express subcommands is not
one to depend on by accident. The version tracks the workspace so it is
ready the moment it can.

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Large new proc-macro surface that defines CLI parsing behavior for
adopters; mitigated by extensive conformance tests but still pre-v1 (no
subcommands, limited types, env not enforced at parse time).
> 
> **Overview**
> Adds the **`usage-derive`** workspace crate and **`#[derive(Cli)]`**,
so a single struct with `#[usage(...)]` attributes becomes a
**`usage-argv` parser**, **static spec metadata**, and **KDL** for
docs/completions.
> 
> Generated code exposes **`parse_from` / `parse`**, **`command()`** and
**`spec()`** as `&'static` tables, and **`to_kdl()`**. Parsing is a
direct `match` on flag/arg keys into prefixed locals (avoids field-name
clashes). The model layer rejects invalid declarations at compile time
(duplicate flags, `var` vs `variadic`, unsupported types, dashed
long/name normalization, etc.) with targeted errors.
> 
> **v0 scope:** one command per struct; text-ish field types only
(`bool`, `String`, `Option<String>`, `Vec<String>`, counting integers).
Subcommands and typed value conversion are explicitly deferred in
**PLAN.md**.
> 
> **Conformance** gains **`conformance/tests/derive.rs`** end-to-end
tests (parsing, spec round-trip, markdown/manpage/help). Release tooling
includes **`usage-derive`** in publish and git-cliff paths.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
973a60a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added `usage-derive`, enabling CLI definitions through a
`#[derive(Cli)]` macro.
* Supports flags, positional arguments, defaults, aliases, repeatable
values, negation, help text, environment settings, and generated command
specifications.
* Added parsing, help/documentation rendering, and KDL serialization for
derived CLI types.
* Added compile-time diagnostics for unsupported or invalid
declarations.

* **Documentation**
* Documented supported attributes, value types, limitations, and the
current roadmap.

* **Tests**
* Added comprehensive end-to-end coverage for parsing, help output,
specifications, errors, and CLI configurations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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