Skip to content

feat(complete): complete config keys and values from the spec - #840

Merged
jdx merged 1 commit into
agent/config-schemafrom
agent/config-complete
Aug 13, 2026
Merged

feat(complete): complete config keys and values from the spec#840
jdx merged 1 commit into
agent/config-schemafrom
agent/config-complete

Conversation

@jdx

@jdx jdx commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Two reserved complete types — config_keys and config_values — so a config get/config set pair completes from the spec's config block. Fifth and last PR of stack #836 on the spec side; depends on the vocabulary in #835.

Every CLI in the fleet writes this by hand today as a run= that shells out to the binary for its own settings list. The spec already says what the keys are and what each accepts, so the completion needs no subprocess:

complete "key" type="config_keys"
complete "value" type="config_values"
$ mycli config get <TAB>
cache_dir       Where to keep the cache
color           Colorize output
jobs            Number of parallel jobs
log_level       How much to say
old_jobs        deprecated — How many jobs
python.compile  Compile python from source
task.output     How to print task output

$ mycli config set log_level <TAB>
error  only failures
warn   failures and warnings
info   the usual
debug  every decision

config_keys

Every prop, dotted keys and all, with its help as the description.

hide filters here — which is where it belongs, and the difference from the JSON schema in #839. A hidden setting is still settable, so a schema that rejected it would be wrong; but nothing should suggest it. Same declaration, opposite treatment, for reasons specific to each consumer.

A deprecated setting is still offered, with its description saying so. A config file in the wild names it, and a user has to be able to complete the key they are removing.

config_values

Looks back along the command line for the last word that names a setting, rather than assuming the key sits next to the cursor — where the key sits is the CLI's business:

$ mycli config set --global log_level <TAB>   # key next to the cursor
$ mycli config set log_level --global <TAB>   # and not

Offers that setting's choices with their own help, or true/false for a boolean — including option<bool> and the boolean side of a bool|string union, via the type grammar's simplified(). For anything else it says nothing, which lets the existing file fallback do the obvious thing for a path-valued setting rather than reimplementing path completion.

Also

complete's reference page never documented type= at all, so the built-in file/path/dir are written down alongside the two new ones.

Verification

Four tests over a new examples/config.usage.kdl, each mutation-checked. Worth reporting: the backward-scan mutation survived the first time — my test put the flag before the key, so the word next to the cursor was the key anyway and the scan was never exercised. Rather than accept a passing test, I added the case with a flag between the key and the cursor, which is the one that needs the scan, and re-ran the mutation to confirm it now fails.

cargo test --workspace --all-features green, clippy --all-targets -D warnings clean, cargo fmt, prettier, mise run render applied.

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


Note

Low Risk
Changes are localized to complete_word completion behavior plus docs/tests; closed-vs-open fallback is the main behavioral shift beyond new config completers.

Overview
Adds reserved complete types config_keys and config_values so config get/set can complete from the spec’s config block without a run= subprocess.

config_keys lists visible prop keys (respecting hide, marking deprecated) with one-line help. config_values resolves the active key from partial-parse bindings on the argument wired to config_keys (not raw word scans), then offers choices help, booleans (including unions/options via holds_a_boolean / accepts_unenumerable_values), or defers to path/file fallback when values aren’t enumerable.

Completion plumbing now threads a Ctx (tera + spec + parse output), and builtins/args return a closed flag so enumerated completers suppress the working-directory file fallback on no match—while bool|path-style unions stay open so paths still complete.

Docs cover built-in type= values; examples/config.usage.kdl and integration tests exercise the edge cases.

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

@coderabbitai

coderabbitai Bot commented Aug 12, 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: 722cedf4-0a20-4304-b613-4137e486253d

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 cli/src/cli/complete_word.rs Outdated
@jdx
jdx force-pushed the agent/config-complete branch from d71dc80 to 575c469 Compare August 12, 2026 22:34
@jdx
jdx force-pushed the agent/config-complete branch from 575c469 to 1b12aa8 Compare August 12, 2026 22:38
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▁▁▁▁▁▁▁▁▁▂▂▂█ 152,897,001 → 175,086,787 +14.51% ⚠️ 14.41 → 16.34ms +13.41%
startup ▁▁▁▁▁▁▁▁▁▁▁▁█ 1,201,844 → 1,222,069 +1.68% ⚠️ 0.97 → 0.97ms +0.05%

2 benchmark(s) above the 1% gate: markdown +14.51%, startup +1.68%

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 29823 5962046 199x
usage: argv -> struct                             858 ns      0.86 µs
clap: build tree + parse -> struct             499802 ns    499.80 µs
clap: parse -> struct, tree reused              23893 ns     23.89 µs
clap: build tree only                          310840 ns    310.84 µs

748770cd2feb vs ebea8955d430 · measured on the runner, not pushed to the history.

@jdx
jdx marked this pull request as ready for review August 12, 2026 22:58
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds spec-driven completion for configuration keys and values without invoking subprocesses.

  • Adds config_keys and config_values reserved completion types.
  • Uses parser bindings to associate values with the appropriate configuration key.
  • Supports choices, booleans, open unions, hidden and deprecated properties, and multiline-description normalization.
  • Documents the completion types and adds comprehensive integration fixtures and tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
cli/src/cli/complete_word.rs Implements spec-driven config completion and resolves the previously reported key-selection, boolean-union, and path-fallback failures.
cli/tests/complete_word.rs Adds focused coverage for parser-bound key selection, multiple and variadic keys, boolean unions, open fallback behavior, and closed candidate sets.
docs/spec/reference/complete.md Documents built-in completion types and the behavior of config key and value completion.
docs/spec/reference/config.md Links configuration declarations to the new completion functionality.
examples/config.usage.kdl Provides a representative configuration spec used to exercise the new completion behavior and prior edge cases.

Fix All in Greploop

Reviews (14): Last reviewed commit: "feat(complete): complete config keys and..." | Re-trigger Greptile

Comment thread cli/src/cli/complete_word.rs Outdated
Comment thread cli/src/cli/complete_word.rs Outdated
@jdx
jdx force-pushed the agent/config-complete branch from 1b12aa8 to 40230a1 Compare August 12, 2026 23:07

jdx commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Bugbot's finding is right, and it is the loose end I had left in the doc comment rather than fixed: config_keys and a closed config_values returned an empty list for an unmatched prefix, and an empty builtin result was treated as "unhandled", so the generic file fallback ran. Tabbing a mistyped setting name offered the contents of the working directory — which tells the user a filename is a setting.

complete_builtin now reports whether the set it drew from is closed, and that travels through complete_arg into the same has_explicit_choices flag declared choices already used. Closed means the completer knows the whole candidate set, so no match means no matches:

  • config_keys — closed.
  • config_values — closed when the spec enumerates the values (choices, or a boolean's two words); open for a path, a number or free text, where the file fallback is exactly what the user wants. Bugbot drew that line correctly.
  • file/path/dir — open, because they are the fallback: an empty result there means the directory had no match, not that nothing is valid.

Getting this verified took two passes worth reporting. My first test used prefixes matching no file in the test's working directory, so the fallback produced nothing anyway and two of three mutations passed against it — the test could not distinguish the behaviours it was written to pin. The prefix is now s, which matches no setting and no declared value but does match a directory in the crate the test runs in, plus a control asserting a path-valued setting still falls back. All four mutations now fail as they should.

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

Comment thread cli/src/cli/complete_word.rs Outdated
@jdx
jdx force-pushed the agent/config-complete branch from 40230a1 to f73f481 Compare August 12, 2026 23:18

jdx commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Both of Greptile's findings were real; fixed in the amended head.

The key scan could pick up a flag's value. My reverse scan over raw words could not tell a positional from the value of a flag, so on config set jobs --tag color <TAB> it found color — a setting in its own right — and offered its booleans for a line that is setting jobs. The fix removes the scan rather than patching it: the parser already binds positionals separately from flag values, so the key now comes from parsed.args (last one first, and the last element of a variadic). That makes the whole class unreachable rather than handling the one spelling I thought of.

A union's boolean depended on member order. simplified() returns a union's first member, so bool|string offered true/false and string|bool — the same type, written the other way round — fell through to filenames. Now a boolean anywhere in the type counts, recursively, including through option<…>. A list of booleans deliberately does not: what goes on the command line there is a list, and offering true would be the wrong shape.

Both mutation-verified. The second took two tries to verify honestly: my filtered test run (--test complete_word config) didn't match the new test's name, so the mutation looked survivable when the test had simply never run. Running the whole file showed it fails as it should.

One knock-on: threading the parse output through pushed complete_positional to eight arguments, which clippy refuses and this repo does not silence. The three values that always travel together — the tera context, the spec, and the parse output — are now one Ctx parameter, which takes the helpers to 6, 5 and 4 arguments.

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

Comment thread cli/src/cli/complete_word.rs
@jdx
jdx force-pushed the agent/config-complete branch from f73f481 to 596b77f Compare August 12, 2026 23:49

jdx commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Two more findings from the later round, both real and both fixed.

A union that also takes free-form values is not a closed set. holds_a_boolean marked bool|path closed after considering true and false, so a path prefix like src/ completed to nothing — for a setting whose whole point is that it can be a path. Closedness now asks whether the type accepts values no list could enumerate; bool|path offers the two words and keeps the file fallback.

A declared choices list stays closed whatever the base type, which is the other half of the same question: a spec that enumerates its values has said what they are. mise's python.uv_venv_auto is bool|string and lists all four.

A description is one row. Choice help was copied whole into the description column while config_keys already took the first line. Candidates reach the shell one per line with tab-separated columns, so a newline in a description splits one candidate into several rows of nonsense. Both go through the same one-line helper now.

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

@jdx
jdx force-pushed the agent/config-complete branch from 596b77f to 7a79ead Compare August 12, 2026 23:59
@jdx
jdx force-pushed the agent/config-complete branch from 7a79ead to b80f737 Compare August 13, 2026 00:20
@jdx
jdx force-pushed the agent/config-complete branch from b80f737 to 00b1f03 Compare August 13, 2026 01:00
@jdx
jdx force-pushed the agent/config-complete branch from 00b1f03 to b00446d Compare August 13, 2026 01:07
@jdx
jdx force-pushed the agent/config-complete branch from b00446d to 419e4ea Compare August 13, 2026 02:38
Comment thread cli/src/cli/complete_word.rs Outdated
@jdx
jdx force-pushed the agent/config-complete branch from 419e4ea to 0018df1 Compare August 13, 2026 03:27

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Right. The reverse scan I put in to fix the flag-value case had the mirror-image problem: on a variadic value argument, the last positional value is one of the values being typed, so set-many log_level color <TAB> found color — a setting in its own right — and offered its booleans while completing another of log_level's values.

The key comes before the values it is given, so the scan now goes forward and takes the first positional that names a setting (and a variadic's first element, not its last). The example spec gained a variadic set-many command so the case is actually exercised; mutation-verified by restoring the reverse scan.

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

Comment thread cli/src/cli/complete_word.rs Outdated
@jdx
jdx force-pushed the agent/config-complete branch from 0018df1 to e26d3df Compare August 13, 2026 03:39

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Right, and it is the mirror of the last one — which is the signal that both were guesses. Scanning backwards took a variadic's own value; scanning forwards takes an unrelated positional that happens to name a setting.

So the guessing is gone: the key is the argument the spec says holds one, the one declared with complete "key" type="config_keys". There is nothing to infer from position at all now. The example spec gained a for-profile <profile> <key> <value> command whose profile is called color — a real setting — and a set-many whose variadic values include one; both complete the right setting, and the mutation that drops the completer filter fails.

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

Comment thread cli/src/cli/complete_word.rs Outdated
@jdx
jdx force-pushed the agent/config-complete branch from e26d3df to 44f1f84 Compare August 13, 2026 03:53

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Right, and this is the mutation my own check flagged as surviving last round — I judged it untestable because no CLI had two key arguments, which was a statement about my example spec rather than about the code. The nearest key governs now, the same rule as a variadic's last element, and the example gained a two-key move-to <from_key> <to_key> <value> command so both directions are pinned:

$ mycli config move-to color log_level <TAB>   → log_level's choices
$ mycli config move-to log_level color <TAB>   → color's true/false

Mutation-verified: taking the first key again fails.

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

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

Comment thread cli/src/cli/complete_word.rs Outdated
@jdx
jdx force-pushed the agent/config-complete branch from 44f1f84 to b3ee906 Compare August 13, 2026 04:02

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Right. Walking past the nearest key when it names no setting offered a different key's values for a line whose own key is a typo — while a single unknown key correctly offers nothing. Only the nearest is consulted now.

Worth recording that my first test for this could not have caught it: I used the prefix s, and none of log_level's choices start with s, so both behaviours printed nothing and the mutation survived. With an empty prefix the difference is visible and the mutation fails.

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

Comment thread cli/src/cli/complete_word.rs
Two reserved `complete` types, `config_keys` and `config_values`, so a
`config get`/`config set` pair completes from the `config` block with no `run=`
and no subprocess. Every CLI in the fleet writes this by hand today as a shell
command that asks the binary for its own settings list; the spec already says
what the keys are and what each one accepts.

`config_keys` offers every prop, dotted keys and all, with its help as the
description. `hide` filters here — which is exactly where it belongs, and the
difference from the JSON schema: a hidden setting is still settable, so a
schema must accept it, but nothing should suggest it. A deprecated setting is
still offered, because a config file in the wild names it and a user has to be
able to complete what they are removing, with its description saying so.

`config_values` looks back along the line for the last word naming a setting,
rather than assuming the key sits next to the cursor — `config set --global
log_level <TAB>` and `config set log_level --global <TAB>` both work. It offers
the setting's choices with their own help, or true/false for a boolean
(including `option<bool>` and the boolean side of a union). For anything else
it says nothing, which lets the existing file fallback do the obvious thing for
a path-valued setting.

`complete`'s reference page never documented `type=` at all, so the built-in
`file`/`path`/`dir` are written down here too.

Four tests over a new `examples/config.usage.kdl`, each mutation-checked. One
of those mutations survived at first — the backward scan was never exercised
because the flag in the test came *before* the key — so the case with a flag
between the key and the cursor was added, which is the one that needs it.
@jdx
jdx force-pushed the agent/config-complete branch from b3ee906 to 748770c Compare August 13, 2026 04:12

jdx commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Investigated this one and did not add the guard, because I could not make the situation happen.

The concern is that a config_keys argument bound from its own default= rather than from the command line would win the nearest-wins rule. I built exactly that spec — a with-default-key <key> <value> [defaulted_key] command whose optional key defaults to color, a real setting, positioned after the value being completed, where such a binding would be the nearest one — and compared the output with and without a "only words the user typed" filter:

$ mycli config with-default-key log_level <TAB>
error   only failures
warn    failures and warnings
info    the usual
debug   every decision        # identical either way

A partial parse does not bind an argument from its default, so the filter changed nothing. Unreachable defensive code in a completion path is worse than the case it defends against — it is the same reason I removed a collision guard from the JSON schema generator earlier in this stack when its mutation proved unreachable.

What I did add is that spec and a test asserting the typed key governs. It passes today by construction; it is there so that if a partial parse ever starts filling defaults, this fails rather than silently offering values for a setting nobody wrote. The comment at the call site records the measurement so the next reader does not have to redo it.

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

@jdx
jdx merged commit a28de61 into main Aug 13, 2026
8 of 9 checks passed
@jdx
jdx deleted the agent/config-complete branch August 13, 2026 13:49
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