diff --git a/ROADMAP.md b/ROADMAP.md index b2842c22..26129fa1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6469,7 +6469,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 452. **DONE — `claw models` is already wired as `CliAction::Models`** — dispatched at line 1988 with `print_models` implementation. `claw models` returns model list, `claw models help` routes to help topic, `claw models --output-format json` returns bounded JSON without touching provider runtime. -453. **`bare_slash_command_guidance` (the "is a slash command" guard) only fires for `command_name` matched as a single bare token at `rust/crates/rusty-claude-cli/src/main.rs:1100-1149` — as soon as ANY positional argument follows a slash-command-named first token, the parser falls through to `CliAction::Prompt` and ships the entire argv string to Claude as a user prompt. The guard catches `claw cost`, `claw tokens`, `claw model`, `claw permissions`, `claw context`, `claw providers`, `claw history`, `claw release-notes`, `claw review`, `claw compact`, `claw cache` — but misses `claw cost list`, `claw tokens list`, `claw model openai/gpt-4`, `claw model list`, `claw permissions show`, `claw context show`, `claw providers list`, `claw history list`, `claw cache list`. Same pattern bites unrouted command shapes: `claw aliases`, `claw aliases list`, `claw profiles list`, `claw logs`, `claw settings` all fall through, even though they are obvious CLI discovery spellings** — dogfooded 2026-05-24 for the 06:00 Clawhip pinpoint nudge at message `1507986539538419863`, reproduced on local `./rust/target/debug/claw` `git_sha 003b739d` (origin/main `f8e1bb72`; the guard logic in `bare_slash_command_guidance` was last touched by the `#146` `config`/`diff` carve-out and is unchanged in `63ce483c..f8e1bb72` which are docs-only ROADMAP additions). Repros in a fully clean isolated environment (`HOME=/tmp/iso3/home` with `{}` settings, fresh `/tmp/iso3/proj` git-init'd workspace, `stdin=/dev/null`, `ANTHROPIC_*` env vars unset): bare `claw cost ` confusion (`claw model openai/gpt-4`) which is an extremely natural typo given the existing `claw --model openai/gpt-4 prompt …` flag shape. **Why this is distinct from existing items:** #78/#145 covered `claw plugins` only; #452 covered `claw models*` only. #357 / #322 cover JSON envelope/stderr-prefix issues, not argv classification. The guard surface lives in `parse_subcommand` and `bare_slash_command_guidance` (`rust/crates/rusty-claude-cli/src/main.rs:1100-1149`) — no existing ROADMAP entry tracks the shape-blindness regression class across the guard. **Why it matters:** prompt misdelivery is the Clawhip top-of-list category. With credentials set, every operator/claw typo in this cluster burns provider tokens on a meaningless completion (e.g., sending `"cost list"` to Claude). Without credentials, the wrong-shaped `missing_credentials` envelope is even more confusing — operators see `missing Anthropic credentials` when they meant local cost inspection, and assume an auth bug rather than a CLI dispatch bug. The fix is also extremely localized: it lives in a single guard function. **Required fix shape:** (a) widen `bare_slash_command_guidance` to also fire from the subcommand-args parse arm: when the first positional token matches a known slash-command name and any additional args follow, emit a typed guard error (`"`claw cost list` is a slash command — `claw cost` does not accept extra arguments; use `claw --resume SESSION.jsonl /cost` instead"`) before falling through to `CliAction::Prompt`; (b) extend the guard's known-slash-command set to include the natural CLI discovery spellings that currently fall through entirely (`models`, `model`, `aliases`, `profiles`, `providers list`, `logs`, `settings`), even when there is no corresponding slash command — emit a typed `"unknown CLI subcommand"` error with `did_you_mean` suggestions (`--model prompt …`, `/models`, `/providers`, `claw config plugins list`) rather than dispatching to LLM prompt; (c) add a structured `kind:"argv_misroute_prevented"` JSON envelope so automation can distinguish guard rejection from auth failure; (d) add regression coverage in `parses_*` test family covering at least one extra-arg case per existing slash-command guard (`cost list`, `tokens list`, `model list`, `model openai/gpt-4`, `permissions show`, `context show`, `cache list`) plus the unrouted-noun cluster (`models list`, `aliases`, `profiles list`, `logs`), asserting every spelling resolves to a typed guard error and **never** to `CliAction::Prompt`. **Acceptance check (one-liner):** `env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN claw model openai/gpt-4` should NOT exit with `missing_credentials`; it should exit with a typed CLI dispatch error mentioning `--model openai/gpt-4 prompt …` or `/model`. Source: gaebal-gajae dogfood follow-up for the 2026-05-24 06:00 Clawhip pinpoint nudge at message `1507986539538419863`. +453. **DONE — `parse_subcommand` now fires guard for multi-word commands** — fixed 2026-06-04 in `fix: widen parse_subcommand guard for multi-word commands`. Changed the `rest.len() != 1` guard to `rest.is_empty()`, so `claw cost list`, `claw model list`, `claw permissions show`, etc. now reach the `bare_slash_command_guidance` guard and emit typed slash-command guidance instead of falling through to `CliAction::Prompt`. 454. **DONE — `claw list` now caught by typo suggestion** — fixed 2026-06-04 in `fix: add list to KNOWN_SUBCOMMANDS for typo detection`. Added `"list"` to the `KNOWN_SUBCOMMANDS` array in `suggest_similar_subcommand` so `claw list` is caught and suggests similar commands instead of falling through to `CliAction::Prompt`. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 98c1485f..ed683e55 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -2350,7 +2350,8 @@ fn parse_single_word_command_alias( return Some(Ok(CliAction::Help { output_format })); } - if rest.len() != 1 { + // #453: fire guard for multi-word commands too (claw cost list, claw model list, etc.) + if rest.is_empty() { return None; }