feat(spec): add effect= to declare what a command does to the world#739
Conversation
Commands can now declare `effect="read"`, `"write"` or `"destructive"`:
cmd "ls" effect="read"
cmd "use" effect="write"
cmd "uninstall" effect="destructive"
This is a coarse classification, not a permission model. Several consumers
keep reinventing the same distinction and each has to hand-maintain its own
list: docs want to mark destructive commands, wrapper scripts want to
confirm before running one, and AI coding agents want an allowlist of
read-only commands rather than prompting on every invocation. Declaring it
once in the spec means it renders everywhere, like every other spec field.
It is deliberately not inherited by subcommands — `git remote` and
`git remote remove` do different things, and inheriting would make the least
safe reading of a spec the default. An unset effect means unknown, not safe.
Also accepted as a child node (`effect "destructive"`) so integrations that
generate KDL do not have to emit props.
Generated markdown renders it as an "Effect" line next to Usage and Aliases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 33 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
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. Comment |
Greptile SummaryAdds command-effect metadata throughout the spec pipeline.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported merge omission and Markdown coverage gap are both addressed at the current head. Important Files Changed
Reviews (3): Last reviewed commit: "fix(spec): drop dead ParseEffectError, u..." | Re-trigger Greptile |
| {%- if cmd.effect %} | ||
| - **Effect**: {% if cmd.effect == "read" %}read-only{% elif cmd.effect == "destructive" %}destructive — may delete or irreversibly overwrite{% else %}modifies state{% endif %} | ||
| {%- endif %} |
There was a problem hiding this comment.
Effect rendering lacks snapshot coverage
The new template branches for read, write, and destructive are not exercised by the existing Markdown snapshots because the shared test spec declares no effects. Add cases for all three values so changes to enum serialization or template comparisons cannot silently produce incorrect labels.
Knowledge Base Used: Docs generation: rendering a Spec into markdown, manpages, and CLI help
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
merge is how included and mounted specs are composed onto a command, and it omitted the new field, so an overlay that said nothing about effects erased one already declared. Also add markdown snapshot coverage for all three effect labels plus a command with no effect, since the shared kitchen-sink spec declares none and left the template branches unexercised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both findings were real. Fixed in f41891d. effect dropped by Worth noting what the test turned up while I was writing it: at the root level Template branches unexercised — also correct, the kitchen-sink spec declares no effects. Added a snapshot covering all three labels plus a command with none, so a change to enum serialization or the template comparisons can't silently produce a wrong label. While fixing the merge gap I audited every
Happy to open a follow-up. Making This comment was generated by an AI coding assistant. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f41891d. Configure here.
- ParseEffectError was never constructed; strum's FromStr plus bail_parse! already produce the parse error, so the type was dead on arrival - .cmd.rs.pending-snap was committed by accident and rewritten by the test run, breaking the "render produces no diff" CI check; untrack it and gitignore the pattern so it cannot happen again - reformat the docs table with prettier to match lint:prettier Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both correct, fixed: Unused Insta pending snapshot committed — worse than noise: it isn't on Also ran This comment was generated by an AI coding assistant. |
usage 3.6 added `effect=`, which lets a spec say whether a command only inspects state, changes it, or destroys something (jdx/usage#739). Declare it for all 172 mise commands: 89 read, 71 write, 7 destructive. clap cannot express this, so the classification lives in one table in src/cli/command_effects.rs and is applied to the derived spec. One list is also the right shape for review: a safety classification is easier to audit as a single file than as annotations spread over sixty command files. Ten commands are deliberately unclassified, each with its reason recorded. `run`, `exec`, `watch`, `mcp`, `en` and friends execute code the user supplies, so their effect is whatever that code does; an unset effect means "ask", which is both honest and the safe default. Three tests keep the table honest: every command must be classified, no entry may refer to a command that no longer exists, and nothing may be classified twice. Coverage includes hidden commands — `mise global`, `bootstrap systemd apply` and `hook-not-found` are hidden but still change state, and the last one installs a tool when not_found_auto_install is set. Also required: - bump min_usage_version to 3.6, since older `usage` CLIs reject the spec with "unsupported cmd prop effect" - pass the argument to `is starting_with(...)` by name in mise-extra.usage.kdl. tera 2.1, which usage picked up in v3.5.7, rejects positional test arguments, so rendering docs with usage >= 3.5.7 fails with "Found string but expected identifier" regardless of this change. main already requires 3.5.7, so this is live as soon as mise.lock moves off the 3.5.2 pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BREAKING CHANGE: SpecArg, SpecFlag, SpecChoices, SpecComplete, SpecConfig, SpecConfigProp, SpecExample and SpecMount are now #[non_exhaustive], so downstream crates can no longer build them with a struct literal. Use the builders and constructors instead; every field stays pub, so `Builder::build()` followed by field assignment covers cases that map from optional data. Adding `effect` to SpecFlag and SpecArg failed cargo-semver-checks with constructible_struct_adds_field: both are all-pub, so any new field is a major. SpecCommand escaped the same fate in #739 only because it happens to have a private cache field, and Spec was already non_exhaustive. Eight of the twelve spec structs had the problem. Since this release is a major either way, spend it once: after this, adding a spec field is no longer breaking. Two fields have been added in a day and there will be a third. non_exhaustive is only useful if the types stay constructible, so this also fills the gaps: - SpecFlagBuilder/SpecArgBuilder gain usage, help_first_line and effect, the three fields they could not previously set - SpecMount::new, SpecChoices::new, SpecComplete::new (+ run, descriptions), SpecConfig::new, SpecConfigProp::new (+ env, help, default_value) - SpecExample::new becomes public and gains header, help and lang Verified against the real consumer: mise builds and emits a byte-identical spec after migrating its seven struct literals to builders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Lets a spec declare what running a command does:
readwritedestructiveWhy in the spec
Several consumers keep reinventing this same distinction, and each one hand-maintains its own list:
--helpwant to mark destructive commandsallowed-toolsfield that is exactly this list, currently written by handThat is the usual argument for putting something in a spec: one annotation, many renderings, same as
helporhidetoday. It's structured data, so there's nothing to keep in sync and no prose to go stale.Design notes
Not inherited by subcommands.
git remoteandgit remote removedo different things. Inheriting would make the least safe reading of a spec the default one, which is the wrong direction for a field whose whole purpose is flagging danger.Unset means unknown, not safe. Consumers should treat a missing
effectas "ask". This keeps the field additive — existing specs are unaffected, and a partially annotated spec degrades to today's behavior.Coarse on purpose. Three values, not a capability model. Anything finer becomes a policy language, and that belongs in the consumer.
Changes
SpecCommandEffectenum inlib/src/spec/effect.rs, exported asusage::SpecCommandEffectSpecCommand::effect: Option<SpecCommandEffect>, plus a builder methodeffect="read") or a child node (effect "read") — the latter so framework integrations generating KDL don't have to emit props; unknown values are a parse error pointing at the span- **Effect**:line next to Usage and AliasesNot in this PR
usage generate skillconsuming it to emitallowed-tools. That's the payoff, but it's a separate discussion about output format and shouldn't ride along with the spec primitive.hide.Verified
cargo test -p usage-libpasses (275 + doctests). Four new tests cover prop parsing, child-node parsing, non-inheritance, KDL round-trip, and the error on an unknown value. Markdown output checked by hand against a sample spec.cargo test --workspacehas two pre-existing failures inusage-cli --test examples(test_usage_double_slash_execution,..._old) — confirmed present on a clean checkout ofmain, unrelated to this change.This PR was generated by an AI coding assistant.
Note
Low Risk
Additive spec field with optional semantics; no changes to parsing or runtime CLI behavior beyond new metadata and doc generation.
Overview
Adds an optional
effecton commands so specs can classify side effects as read, write, or destructive (viaeffect="…"or a childeffectnode). Missingeffectstays unknown; values are not inherited by subcommands.The library introduces
SpecCommandEffect, wires it throughSpecCommand(parse, KDL serialize,merge),SpecCommandBuilder::effect, and the public API. Generated markdown adds a- **Effect**:line when set. Docs indocs/spec/index.mddescribe semantics and consumers (docs, wrappers, agents).Tests cover parsing, non-inheritance, KDL round-trip, merge behavior, invalid values, and markdown rendering.
.gitignoreignores*.pending-snapfrom insta.Reviewed by Cursor Bugbot for commit e0e5192. Bugbot is set up for automated code reviews on this repo. Configure here.