Skip to content

feat(cli): declare what each usage command does to the world - #751

Merged
jdx merged 1 commit into
mainfrom
claude/usage-own-effects
Jul 27, 2026
Merged

feat(cli): declare what each usage command does to the world#751
jdx merged 1 commit into
mainfrom
claude/usage-own-effects

Conversation

@jdx

@jdx jdx commented Jul 27, 2026

Copy link
Copy Markdown
Owner

usage added effect=, shipped it to mise, hk, pitchfork, aube and communique — and declared nothing for its own 16 commands. Anything pointed at usage's spec sees empty fields on the tool that invented the feature.

command effect flags that raise it
complete-word read
generate read
generate completion read
generate completion-init read
generate fig read --out-file → write
generate json read
generate manpage read --out-file → write
generate markdown read --out-dir, --out-file → write
generate sdk write
lint read
sponsors read

Why the shape is interesting

Most of these print to stdout and take an optional flag to write to a file instead, so they sit at read with the flag raising them. That's the composition rule earning its place rather than being asserted in a doc — usage g markdown -f x.kdl only reads; the same command with --out-file writes.

generate sdk is the exception. Its -o --output is required, so there is no read-only way to invoke it and the effect belongs on the command, not a flag.

Unclassified, deliberately

bash, fish, zsh, powershell and exec run a script the user supplied, so their effect is whatever that script does. Labeling them would be a lie in whichever direction it was labeled, and read in particular would be dangerous. A test asserts every unclassified command has an entry in UNCLASSIFIED with its reason, so a new command can't go unlabeled by accident.

How

clap can't express this, so it's applied to the derived spec through clap_usage::spec (#743), the same shape as mise's command_effects. Five tests cover stale command paths, stale flag names, the unclassified guard, and both composition cases above.

Also emits min_usage_version "4.0" — an older usage rejects a spec carrying effect= with "unsupported cmd prop effect", so the declaration and the floor move together. mise does the same.

The generated docs pick it up for free: usage generate sdk now reads Effect: modifies state, and --out-file on markdown reads Effect: modifies state under a read-only command.

This PR was generated by an AI coding assistant.


Note

Low Risk
Metadata and spec-generation path changes only; no runtime command behavior changes beyond richer --usage-spec output.

Overview
Adds command_effects so the usage CLI’s own --usage-spec output carries effect=read / effect=write on commands and flags (clap can’t express this; it’s patched onto the derived spec like mise/hk).

Most generators are read; --out-file, --out-dir, etc. raise matching flags to write. generate sdk is write at the command level because output is required. bash, exec, and other script runners stay unclassified, with tests guarding stale paths and accidental gaps.

usage_spec::generate now builds the spec via clap_usage::spec, runs apply, and emits min_usage_version "4.0" alongside the KDL. Regenerated usage.usage.kdl, commands.json, and CLI reference docs show read-only vs modifies-state in docs.

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

Summary by CodeRabbit

  • New Features

    • Added effect metadata to CLI commands and output flags, distinguishing read-only operations from actions that modify files or state.
    • Added minimum usage specification version metadata.
  • Documentation

    • Updated CLI reference documentation and command metadata to display read-only and state-modifying effects.
    • Clarified effects for generation commands and file output options.

usage added `effect=` and shipped it to mise, hk, pitchfork, aube and
communique, and then declared nothing for its own 16 commands. Anything
pointed at usage's spec — its docs, `usage mcp` — sees empty fields on
the tool that invented the feature.

    complete-word          read
    generate               read
    generate completion    read
    generate fig           read   --out-file  write
    generate manpage       read   --out-file  write
    generate markdown      read   --out-dir   write
                                  --out-file  write
    generate sdk           write
    lint                   read
    sponsors               read

Most of these print to stdout and take an optional flag to write to a
file instead, so they sit at `read` with the flag raising them. That is
the composition rule earning its place rather than being asserted in a
doc: `usage g markdown -f x.kdl` only reads, and the same command with
`--out-file` writes.

`generate sdk` is the exception. Its `-o --output` is required, so there
is no read-only way to invoke it and the effect belongs on the command.

The five shell commands — `bash`, `fish`, `zsh`, `powershell`, `exec` —
are deliberately unset, because they run a script the user supplied and
their effect is whatever that script does. `read` in particular would be
dangerous. A test asserts every unclassified command has an entry in
`UNCLASSIFIED` with its reason, so nothing goes unlabeled by accident.

Applied to the derived spec via `clap_usage::spec` (#743) since clap
cannot express it, the same shape as mise's `command_effects`. Also
emits `min_usage_version "4.0"`, because an older `usage` rejects a spec
carrying `effect=` with "unsupported cmd prop effect".

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

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI now classifies command and output-flag effects, applies them during usage-spec generation, emits usage version metadata, and synchronizes the resulting effect annotations across usage specifications and CLI reference documentation.

Changes

CLI effect metadata

Layer / File(s) Summary
Effect classification and validation
cli/src/command_effects.rs
Adds command and flag effect mappings, nested command traversal, an unclassified allowlist, and tests validating configured paths, flags, and effect behavior.
Usage spec generation
cli/src/lib.rs, cli/src/usage_spec.rs, cli/usage.usage.kdl
Wires effect application into explicit spec generation and adds min_usage_version "4.0" with read/write command and output-flag metadata.
Reference metadata synchronization
docs/cli/reference/commands.json, docs/cli/reference/*.md, docs/cli/reference/generate/*.md
Documents command effects, output-flag effects, and the minimum usage version in generated CLI references.

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

Sequence Diagram(s)

sequenceDiagram
  participant CliMetadata
  participant UsageSpec
  participant CommandEffects
  CliMetadata->>UsageSpec: build spec from CLI metadata
  UsageSpec->>CommandEffects: apply command and flag effects
  CommandEffects-->>UsageSpec: classified spec
  UsageSpec-->>CliMetadata: emit usage version and spec
Loading

Poem

I’m a rabbit with effects in my hat,
Read here, write there—imagine that!
Flags hop neatly into their place,
Specs bloom with metadata grace.
Documentation follows the trail,
And every stale path gets caught by the rail.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding effect declarations for usage commands.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds effect metadata to the usage CLI’s generated specification and documentation.

  • Classifies read-only and state-modifying commands and output flags.
  • Leaves script-executing commands deliberately unclassified and adds coverage tests for command and flag mappings.
  • Raises the generated specification’s minimum supported usage version to 4.0.

Confidence Score: 5/5

The PR appears safe to merge with no concrete correctness or security issues identified.

The declared effects match the reachable command implementations and output paths, the generated specification remains structurally valid, and the checked-in documentation consistently reflects the new metadata.

Important Files Changed

Filename Overview
cli/src/command_effects.rs Adds centralized command and flag effect mappings with tests covering stale paths, missing flags, unclassified commands, and effect composition.
cli/src/usage_spec.rs Applies effect metadata to the derived clap specification and emits the required minimum usage version before serialization.
cli/src/lib.rs Registers the new internal command-effects module without changing CLI dispatch behavior.
cli/usage.usage.kdl Regenerates the dogfooding CLI specification with matching command and flag effects and a 4.0 compatibility floor.
docs/cli/reference/commands.json Propagates the new effect and minimum-version metadata into the generated JSON reference.
docs/cli/reference/generate/markdown.md Documents the read-only default and state-modifying output flags for Markdown generation.
docs/cli/reference/generate/sdk.md Documents SDK generation as state-modifying because every valid invocation writes to its required output directory.

Reviews (1): Last reviewed commit: "feat(cli): declare what each usage comma..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▁▁█ 101,818,810 → 102,924,733 +1.09% ⚠️ 17.84 → 17.32ms -2.89%
startup ▄▁█ 1,156,928 → 1,160,431 +0.30% 1.21 → 1.17ms -2.89%

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

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.

9ee7a0214831 vs c66aba6a7d67 · measured on this runner, not pushed to the history.

@jdx

jdx commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

The markdown benchmark is +1.09%, over the 1% gate. It isn't a code regression — the benchmark's input is the file this PR is meant to grow.

tak.toml points the markdown bench at cli/usage.usage.kdl, and this PR adds 11 effect= props and 4 flag effects to it. Measured with the unchanged main binary against both spec files:

bytes in bytes of markdown out
main spec 9,418 12,208
this PR's spec 9,627 12,589 (+3.1%)

Same binary, more input, more output. And no code on the g markdown path changed at all — the Rust diff is cli/src/command_effects.rs (new), cli/src/lib.rs (one mod line) and cli/src/usage_spec.rs, and command_effects::apply is reachable only from --usage-spec. The renderer is byte-identical to main.

So the gate fired on the corpus growing, which means it will fire on any PR that adds a command or a flag — for doing its job. That's a property of pointing the bench at a file that changes with the CLI, not of this change.

Two ways forward, and I'd rather you picked than have me quietly widen your gate:

  1. Merge this as-is. The +1.09% is real work a user asked for: 15 more lines of rendered docs. Nothing got slower per unit of output.
  2. Freeze the bench input first. Copy today's spec to a fixture, point tak.toml at that, and the bench measures "did the renderer get slower" instead of "did the CLI grow". Refreshed deliberately when someone wants the corpus updated. I'll open that as its own PR if you want it — it stands on its own merits and unblocks every future spec-growing change, but it does drop the signal that a growing spec costs users time, so it's your call.

This comment was generated by an AI coding assistant.

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

🧹 Nitpick comments (1)
cli/src/command_effects.rs (1)

153-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert declared unclassified commands remain unclassified.

This only checks actual_unclassified - UNCLASSIFIED. If a user-script command is added to EFFECTS, it disappears from accidental and the test still passes. Compare the actual unset-path set with UNCLASSIFIED in both directions.

Proposed fix
-        let deliberate: HashSet<_> = UNCLASSIFIED.iter().map(|(path, _)| *path).collect();
-        let accidental: Vec<_> = commands()
+        let deliberate: HashSet<_> = UNCLASSIFIED
+            .iter()
+            .map(|(path, _)| (*path).to_owned())
+            .collect();
+        let actual: HashSet<_> = commands()
             .into_iter()
-            .filter(|(path, classified)| !classified && !deliberate.contains(path.as_str()))
+            .filter(|(_, classified)| !classified)
             .map(|(path, _)| path)
             .collect();
-        assert!(
-            accidental.is_empty(),
-            "unclassified with no entry in UNCLASSIFIED: {accidental:?}"
-        );
+        assert_eq!(actual, deliberate, "unclassified commands must match UNCLASSIFIED");
🤖 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 `@cli/src/command_effects.rs` around lines 153 - 165, The
nothing_is_unclassified_by_accident test must compare the complete set of
commands with unset effects against UNCLASSIFIED in both directions. Build the
actual unclassified path set from commands(), assert it equals the declared
UNCLASSIFIED set, and retain the existing diagnostic context for mismatches.
🤖 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.

Nitpick comments:
In `@cli/src/command_effects.rs`:
- Around line 153-165: The nothing_is_unclassified_by_accident test must compare
the complete set of commands with unset effects against UNCLASSIFIED in both
directions. Build the actual unclassified path set from commands(), assert it
equals the declared UNCLASSIFIED set, and retain the existing diagnostic context
for mismatches.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: d9ff4fbc-177e-4773-b026-eaa6cf09faea

📥 Commits

Reviewing files that changed from the base of the PR and between c66aba6 and 9ee7a02.

📒 Files selected for processing (16)
  • cli/src/command_effects.rs
  • cli/src/lib.rs
  • cli/src/usage_spec.rs
  • cli/usage.usage.kdl
  • docs/cli/reference/commands.json
  • docs/cli/reference/complete-word.md
  • docs/cli/reference/generate.md
  • docs/cli/reference/generate/completion-init.md
  • docs/cli/reference/generate/completion.md
  • docs/cli/reference/generate/fig.md
  • docs/cli/reference/generate/json.md
  • docs/cli/reference/generate/manpage.md
  • docs/cli/reference/generate/markdown.md
  • docs/cli/reference/generate/sdk.md
  • docs/cli/reference/lint.md
  • docs/cli/reference/sponsors.md

@jdx
jdx merged commit accc8a4 into main Jul 27, 2026
6 of 7 checks passed
@jdx
jdx deleted the claude/usage-own-effects branch July 27, 2026 17:56
jdx added a commit that referenced this pull request Jul 27, 2026
`list_commands` emitted `"daemons remove"` while `describe_command`
answered with `"pitchfork daemons remove"`. An agent doing the obvious
thing — read the list, describe an entry, describe something the first
response mentioned — got a tool error on a command that exists, because
`find_chain` read the binary name as a subcommand.

`describe_command` now reports the path alone and carries `bin` beside
it, the way `list_commands` already does. A test walks every row of
`list_commands` through `describe_command` and asserts the name comes
back unchanged, so the two ends cannot drift apart again.

A leading binary name is also accepted now, since an agent that has seen
the CLI in a shell writes the whole line. Only skipped when the root has
no subcommand by that name, so a CLI with a `usage usage` keeps
resolving its own command rather than losing it to the prefix.

Rebased onto main, which brought #751. Its `nothing_is_unclassified_by_accident`
test failed immediately on `mcp`, which is what it is for; classified
`read`, since every tool this serves only reads the spec it was handed.
Unlike `mise mcp`, which stays unclassified because it serves a tool that
runs tasks, nothing here can act on the CLI it describes.

Reported by cursor on #746.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Aug 11, 2026
⚠️ **CAUTION: this is a major update, indicating a breaking change!** ⚠️

This MR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [usage](https://github.com/jdx/usage) | tools | major | `3.5.6` → `5.1.0` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>jdx/usage (usage)</summary>

### [`v5.1.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#510---2026-08-09)

[Compare Source](jdx/usage@v5.0.0...v5.1.0)

##### 🚀 Features

- **(spec)** parse usage comments from strings by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;782](jdx/usage#782)

##### 🐛 Bug Fixes

- **(spec)** avoid inferred metadata from included specs by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;786](jdx/usage#786)

##### 🧪 Testing

- **(windows)** make the suite runnable on Windows by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;771](jdx/usage#771)

##### 📦️ Dependency Updates

- update rust crate rmcp to v3 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;780](jdx/usage#780)

### [`v5.0.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#500---2026-08-02)

[Compare Source](jdx/usage@v4.1.0...v5.0.0)

##### 🚀 Features

- **(cli)** allow overriding the shell program with USAGE\_SHELL\_<SHELL> by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;767](jdx/usage#767)

##### 🐛 Bug Fixes

- **(cli)** forward parsed args to WSL bash via WSLENV on windows by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;764](jdx/usage#764)
- **(cli)** let generate markdown write to stdout by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;766](jdx/usage#766)
- **(complete)** use `type -P` so the CLI-presence guard ignores shell functions by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;760](jdx/usage#760)
- **(parse)** enforce double\_dash="required" for positional args by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;762](jdx/usage#762)
- **(windows)** run `run=` scripts with sh when available by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;765](jdx/usage#765)

##### 🎨 Styling

- fix clippy and deprecation warnings in test and bench targets by [@&#8203;JamBalaya56562](https://github.com/JamBalaya56562) in [#&#8203;763](jdx/usage#763)

### [`v4.1.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#410---2026-07-30)

[Compare Source](jdx/usage@v4.0.0...v4.1.0)

##### 🚀 Features

- **(cli)** declare what each usage command does to the world by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;751](jdx/usage#751)
- **(mcp)** serve a usage spec to an agent over stdio by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;746](jdx/usage#746)
- **(spec)** add a top-level `repository` field by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;747](jdx/usage#747)

##### 🐛 Bug Fixes

- **(parse)** keep a re-declared global's aliases on one flag by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;752](jdx/usage#752)
- complete repeated variadic args by [@&#8203;Jai-JAP](https://github.com/Jai-JAP) in [#&#8203;753](jdx/usage#753)

##### New Contributors

- [@&#8203;Jai-JAP](https://github.com/Jai-JAP) made their first contribution in [#&#8203;753](jdx/usage#753)

### [`v4.0.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#400---2026-07-25)

[Compare Source](jdx/usage@v3.6.0...v4.0.0)

##### 🚀 Features

- **(spec)** allow effect= on flags and args by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;742](jdx/usage#742)

### [`v3.6.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#360---2026-07-25)

[Compare Source](jdx/usage@v3.5.7...v3.6.0)

##### 🚀 Features

- **(spec)** add effect= to declare what a command does to the world by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;739](jdx/usage#739)

##### 🚜 Refactor

- **(spec)** make missed SpecCommand fields a compile error, and fix the four that were already missed by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;740](jdx/usage#740)

### [`v3.5.7`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#357---2026-07-25)

[Compare Source](jdx/usage@v3.5.6...v3.5.7)

##### 🐛 Bug Fixes

- **(parse)** don't leak the mounting CLI's flags into mounted commands; scan past non-global flags by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;738](jdx/usage#738)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODguMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWFqb3IiXX0=-->
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