Skip to content

feat(argv): write the run= a declared completer answers - #890

Merged
jdx merged 6 commits into
agent/complete-callbacksfrom
agent/complete-run
Aug 15, 2026
Merged

feat(argv): write the run= a declared completer answers#890
jdx merged 6 commits into
agent/complete-callbacksfrom
agent/complete-run

Conversation

@jdx

@jdx jdx commented Aug 15, 2026

Copy link
Copy Markdown
Owner

A spec is what every other consumer reads — the usage CLI, another shell's
generator, a docs page — so a CLI whose completers live in Rust still has to say
what they are. It now writes them: complete "tool" run="mise __complete_word__ --candidates tool", generated from the completer rather than declared beside it,
so there is one place a completer is said to exist and no second declaration to
keep in step.

And the binary answers exactly that command, so what the KDL promises is a thing
that works rather than a string that looks plausible. A name nothing declares
answers empty rather than failing: a script generated against a newer version of a
CLI is stale, and a stale script should complete nothing rather than print into
somebody's prompt.

Found by the name a spec uses — the lowercased argument name, or a flag's value
placeholder — which is the rule the reference resolves a complete block by, so
both sides agree about which completer a run= belongs to. Deduplicated, because
six of mise's commands take a tool and a spec declares one block for it.

The round trip is the test: emit the KDL, parse it back with usage-lib, and check
it reads a run= for that argument — then ask the binary the same question and get
the answer, filtered by what was typed.

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


Stack created with GitHub Stacks CLIGive Feedback 💬


Note

Medium Risk
Changes completion protocol and spec output that external generators read; behavior is heavily covered by roundtrip and dispatch tests, but wrong resolution order would break mise-style multi-command tool completers.

Overview
Declared Rust completers are now written into emitted KDL as complete blocks whose run= invokes the same binary with __complete_word__ --candidates <name> --line '…', instead of leaving completion as a second hand-maintained declaration beside the Rust callbacks.

The binary implements that contract via new for_name resolution (aligned with usage-lib: root, then command reached on the line, then tree fallback; cursor-aware when one name maps to both a positional and a flag value) and derive support for --candidates, returning filtered candidates or an empty list when the name is unknown. Spec emission also adds completers_on, per-command write_completers, and shell-safe line interpolation (apostrophe escaping; no descriptions=#true so colon-heavy values are not split).

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

A spec is what every other consumer reads — the usage CLI, another shell's
generator, a docs page — so a CLI whose completers live in Rust still has to say
what they are. It now writes them: `complete "tool" run="mise __complete_word__
--candidates tool"`, generated from the completer rather than declared beside it,
so there is one place a completer is said to exist and no second declaration to
keep in step.

And the binary answers exactly that command, so what the KDL promises is a thing
that works rather than a string that looks plausible. A name nothing declares
answers empty rather than failing: a script generated against a newer version of a
CLI is stale, and a stale script should complete nothing rather than print into
somebody's prompt.

Found by the name a spec uses — the lowercased argument name, or a flag's value
placeholder — which is the rule the reference resolves a `complete` block by, so
both sides agree about which completer a `run=` belongs to. Deduplicated, because
six of mise's commands take a `tool` and a spec declares one block for it.

The round trip is the test: emit the KDL, parse it back with usage-lib, and check
it reads a `run=` for that argument — then ask the binary the same question and get
the answer, filtered by what was typed.

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

coderabbitai Bot commented Aug 15, 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: b432cb52-2bad-4056-9ce4-fd3473c214b8

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.

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

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR emits declared Rust completers as executable KDL complete blocks and adds named callback dispatch to the generated completion entrypoint.

  • Places completion blocks on the command that declares them and aligns root-versus-command precedence with usage-lib.
  • Passes the interpolated command line back to the binary so callbacks can inspect completion context.
  • Adds round-trip and callback-dispatch conformance coverage.

Confidence Score: 4/5

The PR is not yet safe to merge because attached flag values can still be answered by a same-named positional completer.

The generated --candidates path excludes the current attached-value token before walking the command line, leaving the flag unidentified and allowing at_cursor to choose the positional callback instead.

Files Needing Attention: argv/src/complete.rs, conformance/tests/spec_roundtrip.rs

Important Files Changed

Filename Overview
argv/src/complete.rs Adds name-based callback lookup and command-local completer discovery, but attached flag values still lose their flag identity before callback selection.
argv/src/spec.rs Emits command-scoped completion blocks with value-only output semantics and shell-safe line interpolation.
derive/src/codegen.rs Routes generated --candidates requests through the new named-completer path; this makes the outstanding attached-value dispatch defect externally reachable.
conformance/tests/spec_roundtrip.rs Covers KDL round trips, command ownership, precedence, and detached same-name fields, but not attached values through for_name.

Reviews (6): Last reviewed commit: "test(argv): pin that an attached value i..." | Re-trigger Greptile

Comment thread argv/src/complete.rs
Comment thread argv/src/spec.rs Outdated
…line

Three from review, all in what the emitted `run=` promises.

A `complete` block now sits inside the command that declares it, which is where the
reference looks for one first. Written at the top level, two sibling commands that
take a `TOOL` and mean different things by it — the installed ones against the
available ones — collapsed into a single block, and whichever came first in tree
order answered for both. `for_name` resolves the same way: the command the line
reached, and only then anywhere in the tree.

The line goes with the request. A caller running that command is the reference,
which interpolates `words` through tera before running it, so a completer reading
an earlier flag sees the same line it would have seen natively. Without it the
answer is computed against nothing, which for that completer is a wrong answer
rather than a missing one.

And `descriptions=#true` is gone, because it was not true: it tells the reference to
read a description after an unescaped colon, while this answers with values — so a
value containing a colon would have been split in half, and mise's task names are
full of them.

Found by greptile and Cursor Bugbot.

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

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▂▂▃▁▇█▇▇█ 175,733,287 → 175,834,233 +0.06% 16.07 → 15.96ms -0.71%
startup █▆▆▆▇▁▁▄▄ 1,221,955 → 1,221,953 -0.00% 0.96 → 0.98ms +2.01%

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 29956 5895173 196x
usage: argv -> struct                             839 ns      0.84 µs
clap: build tree + parse -> struct             493658 ns    493.66 µs
clap: parse -> struct, tree reused              23481 ns     23.48 µs
clap: build tree only                          306097 ns    306.10 µs

968892b5b8b0 vs b8068da1afb6 · measured on the runner, not pushed to the history.

jdx commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Three findings, all correct, all fixed in the head commit.

Completer context never reaches binary — right, and it would have been a wrong answer rather than a missing one: a completer that reads an earlier flag would compute against nothing and answer confidently. The run= now carries the line, interpolated by whoever runs it — which is the reference, through tera, the same way its own run="mise alias ls {{words[PREV]}}" works.

Deduplication conflates distinct callbacks — also right. A complete block now sits inside the command that declares it, which is where usage-lib looks for one first (complete_word.rs:511 checks cmd.complete after spec.complete), and for_name resolves the same way: the command the line reached, then anywhere in the tree. The test has two siblings that both take a TOOL and mean the installed ones against the available ones.

Description protocol mismatches output — correct, and the fix is to stop claiming it. descriptions=#true tells the reference to read a description after an unescaped colon; what this answers with is values, so a value containing a colon would have been split in half — and mise's task names are full of them.

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

Comment thread argv/src/complete.rs Outdated

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

Comment thread argv/src/spec.rs
The root's own first, then the command the line reached, then the tree. The order
matters because the reference reads `spec.complete.get(name).or(cmd.complete
.get(name))` — spec-level before command-level — and the root's completers are
exactly what a spec writes at its top level. Answering in a different order would
mean this binary and the reference disagreed about a spec they both read.

The fallback to tree order stays last, which is what keeps two sibling commands
meaning different things by one name.

Found by greptile.

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

jdx commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Two more, one fixed and one that matches the reference.

Completer lookup reverses precedence — correct. usage-lib reads spec.complete.get(name).or(cmd.complete.get(name)), spec-level before command-level, and the root's completers are exactly what a spec writes at the top level. So it now resolves the root's own first, then the command the line reached, then tree order as a fallback — which is what keeps two siblings meaning different things by one name. There is a test pinning the root winning, since that order is otherwise invisible.

Unescaped apostrophes break run line — real, and shared with the reference: its own run="mise alias ls {{words[PREV]}}" is unquoted, so a word with a space breaks it there. Single-quoting is strictly better than that and still not airtight, and tera has no shell-quote filter to make it so. Left as is rather than half-fixed, and worth saying plainly: a completion line containing an apostrophe will misparse on the run= path. The native path — the generated scripts calling the binary directly — is unaffected, and that is the path an adopter's users are on.

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

Comment thread argv/src/complete.rs
Comment thread argv/src/complete.rs
`--source=⌶` is a dash-prefixed token, so it is a flag position: the flag branches
come first and the positional's completer is never reached. Raised as a case where
it would be, and it is not — but it is worth holding, because the word being
completed is excluded from the walk, so the flag is not `awaiting_value` either and
the position would look like the argument's if that order ever changed.

The fixture's flag and argument now have *different* completers, which is what makes
which one answered visible at all.

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

jdx commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Checked, and this one does not hold: a dash-prefixed token never reaches the positional branch. The order in candidates is flags first —

`-`        → both forms
`--…`      → long flags
`-…`       → short flags
restart / awaiting value / positional

— so --source=⌶ is answered by long_flags filtered on --source=, which matches nothing, and the argument's completer is never consulted. Same for an attached short value.

Pinned anyway, with a fixture whose flag and argument have different completers so that which one answered is visible at all — because your reasoning about the walk is right even though the conclusion is not: the word being completed is excluded, so the flag is not awaiting_value either, and the position would look like the argument's if that branch order ever changed.

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

@jdx
jdx merged commit c62d8a6 into main Aug 15, 2026
9 checks passed
@jdx
jdx deleted the agent/complete-run branch August 15, 2026 15:13
jdx added a commit that referenced this pull request Aug 15, 2026
A spec is what every other consumer reads — the usage CLI, another
shell's
generator, a docs page — so a CLI whose completers live in Rust still
has to say
what they are. It now writes them: `complete "tool" run="mise
__complete_word__
--candidates tool"`, generated from the completer rather than declared
beside it,
so there is one place a completer is said to exist and no second
declaration to
keep in step.

And the binary answers exactly that command, so what the KDL promises is
a thing
that works rather than a string that looks plausible. A name nothing
declares
answers empty rather than failing: a script generated against a newer
version of a
CLI is stale, and a stale script should complete nothing rather than
print into
somebody's prompt.

Found by the name a spec uses — the lowercased argument name, or a
flag's value
placeholder — which is the rule the reference resolves a `complete`
block by, so
both sides agree about which completer a `run=` belongs to.
Deduplicated, because
six of mise's commands take a `tool` and a spec declares one block for
it.

The round trip is the test: emit the KDL, parse it back with usage-lib,
and check
it reads a `run=` for that argument — then ask the binary the same
question and get
the answer, filtered by what was typed.

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

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes completion protocol and spec output that external generators
read; behavior is heavily covered by roundtrip and dispatch tests, but
wrong resolution order would break mise-style multi-command `tool`
completers.
> 
> **Overview**
> Declared Rust completers are now **written into emitted KDL** as
`complete` blocks whose `run=` invokes the same binary with
`__complete_word__ --candidates <name> --line '…'`, instead of leaving
completion as a second hand-maintained declaration beside the Rust
callbacks.
> 
> The binary **implements that contract** via new `for_name` resolution
(aligned with usage-lib: root, then command reached on the line, then
tree fallback; cursor-aware when one name maps to both a positional and
a flag value) and derive support for **`--candidates`**, returning
filtered candidates or an empty list when the name is unknown. Spec
emission also adds **`completers_on`**, per-command `write_completers`,
and shell-safe line interpolation (apostrophe escaping; no
`descriptions=#true` so colon-heavy values are not split).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
968892b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 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