Compare commits

..

1 Commits

Author SHA1 Message Date
Yeachan-Heo
667772e3b8 Keep local pre-push gate output machine-clean
The ROADMAP #694 local pre-push gate should catch stale Rust build breakage without polluting stdout that callers may reserve for structured output. Route the roadmap ID pre-check through stderr like the build gate messages.

Constraint: ROADMAP #693-#695 verification already covers typed analog phase errors, the cargo build gate, and startup preflight warnings; this change only fixes the failing pre-push hook contract found during G013 validation.
Rejected: Reworking hook installation or branch-protection policy | outside the local repository change surface available from this worktree.
Confidence: high
Scope-risk: narrow
Directive: Keep pre-push status/progress output on stderr so stdout stays available for machine callers.
Tested: python3 -m pytest tests/test_pre_push_hook_contract.py -q; cargo test --manifest-path rust/Cargo.toml -p claw-analog rag_response -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime startup_preflight -- --nocapture; python3 scripts/validate_cc2_board.py --board .omx/cc2/board.json; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo build --manifest-path rust/Cargo.toml --workspace --locked
Not-tested: full cargo test --workspace
2026-05-27 00:38:00 +00:00
5 changed files with 46 additions and 1374 deletions

View File

@@ -13,7 +13,7 @@ cd "$repo_root"
if [[ -x scripts/roadmap-check-ids.sh ]]; then if [[ -x scripts/roadmap-check-ids.sh ]]; then
echo "pre-push: scripts/roadmap-check-ids.sh" >&2 echo "pre-push: scripts/roadmap-check-ids.sh" >&2
scripts/roadmap-check-ids.sh scripts/roadmap-check-ids.sh >&2
fi fi
if [[ "${SKIP_CLAW_PRE_PUSH_BUILD:-}" == "1" ]]; then if [[ "${SKIP_CLAW_PRE_PUSH_BUILD:-}" == "1" ]]; then

View File

@@ -7735,46 +7735,3 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
783. **`claw --output-format json init` success envelope was missing `hint` field; idempotent re-init was not structurally detectable** — dogfooded 2026-05-27 on `32c9276f`. The init JSON envelope had no `hint` field (absent, not null), and no field to distinguish a fresh init from a re-init without checking `created.len() == 0`. Orchestrators had to inspect `created` array length to detect idempotent behavior. Fix: (1) added `hint` field to init JSON envelope — fresh path points at `CLAUDE.md + doctor`; idempotent path says "already initialised, run doctor"; (2) added `already_initialized: bool` field — `true` when `created` and `updated` are both empty (all artifacts skipped). Both test cases (fresh + re-init) covered by `init_json_envelope_has_hint_and_already_initialized_783`. 42 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori init-envelope probe on `32c9276f`, 2026-05-27. 783. **`claw --output-format json init` success envelope was missing `hint` field; idempotent re-init was not structurally detectable** — dogfooded 2026-05-27 on `32c9276f`. The init JSON envelope had no `hint` field (absent, not null), and no field to distinguish a fresh init from a re-init without checking `created.len() == 0`. Orchestrators had to inspect `created` array length to detect idempotent behavior. Fix: (1) added `hint` field to init JSON envelope — fresh path points at `CLAUDE.md + doctor`; idempotent path says "already initialised, run doctor"; (2) added `already_initialized: bool` field — `true` when `created` and `updated` are both empty (all artifacts skipped). Both test cases (fresh + re-init) covered by `init_json_envelope_has_hint_and_already_initialized_783`. 42 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori init-envelope probe on `32c9276f`, 2026-05-27.
784. **`claw export` had two opaque arg-error paths returning `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `81fe0ccb` (pinpoint by Gaebal-gajae). `claw export --output` (missing flag value) emitted plain `"missing value for --output"` with no typed prefix; `claw export a.md b.md` (extra positional) emitted plain `"unexpected export argument: second.md"`. Both classified as `unknown+null`. Fix: (1) `--output` missing-value error now uses `missing_flag_value:` prefix + `\n` usage hint; (2) extra positional now uses `unexpected_extra_args:` prefix + `\n` usage hint; (3) classifier `unexpected_extra_args` arm extended to match both `starts_with("unexpected extra arguments")` (prose form, #766) and `starts_with("unexpected_extra_args:")` (typed prefix form, #784). Integration test `export_arg_errors_have_typed_kind_and_hint_784` covers both paths. 43 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `81fe0ccb`, 2026-05-27. 784. **`claw export` had two opaque arg-error paths returning `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `81fe0ccb` (pinpoint by Gaebal-gajae). `claw export --output` (missing flag value) emitted plain `"missing value for --output"` with no typed prefix; `claw export a.md b.md` (extra positional) emitted plain `"unexpected export argument: second.md"`. Both classified as `unknown+null`. Fix: (1) `--output` missing-value error now uses `missing_flag_value:` prefix + `\n` usage hint; (2) extra positional now uses `unexpected_extra_args:` prefix + `\n` usage hint; (3) classifier `unexpected_extra_args` arm extended to match both `starts_with("unexpected extra arguments")` (prose form, #766) and `starts_with("unexpected_extra_args:")` (typed prefix form, #784). Integration test `export_arg_errors_have_typed_kind_and_hint_784` covers both paths. 43 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `81fe0ccb`, 2026-05-27.
785. **`claw dump` (typo/near-miss for dump-manifests) returned `error_kind:"unknown"` — no classifier arm for `"unknown subcommand:"` prose prefix** — dogfooded 2026-05-27 on `e628b4bb`. Any unknown top-level subcommand that triggers the suggestion path emitted `"unknown subcommand: <x>.\nDid you mean <y>"` but `classify_error_kind` had no arm for that prefix; all fell to the `"unknown"` catch-all. The hint was non-null (the suggestion text was extracted by `split_error_hint`) but `error_kind` was undifferentiated. Fix: added `starts_with("unknown subcommand:")``"unknown_subcommand"` arm. Unit test assertion + integration test `unknown_subcommand_returns_typed_kind_785` using `claw dump` as the trigger. 44 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori subcommand-classifier probe on `e628b4bb`, 2026-05-27.
786. **`claw dump-manifests --manifests-dir` (missing value) and `--manifests-dir=` (empty) both returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `87f43347` (pinpoint by Gaebal-gajae). Both missing `--manifests-dir` branches in `parse_dump_manifests_args` emitted plain `"--manifests-dir requires a path"` with no typed prefix; `classify_error_kind` had no matching arm so they fell to `"unknown"`. Fix: both branches now use `missing_flag_value:` prefix + `\n` usage hint. Integration test `dump_manifests_missing_dir_has_typed_kind_and_hint_786` covers both cases. 45 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `87f43347`, 2026-05-27.
787. **`claw --resume /tmp` (directory path) returned `error_kind:"session_load_failed"` + `hint:null`; resume error emission sites didn't apply `fallback_hint_for_error_kind`** — dogfooded 2026-05-27 on `22b423b6`. Two gaps: (1) the OS error `"Is a directory (os error 21)"` had no classifier arm, falling to generic `session_load_failed`; (2) both resume error emission paths (session load at line 3338, command execution at line 3484) called `split_error_hint` but not `fallback_hint_for_error_kind`, so API-layer errors with no `\n` always got `hint:null`. Fix: added `session_path_is_directory` classifier arm for `"Is a directory"` / `"os error 21"` messages; added `fallback_hint_for_error_kind` fallback to both resume error sites; added `session_path_is_directory` and `session_load_failed` to `fallback_hint_for_error_kind`. Unit test + integration test `resume_directory_path_returns_typed_kind_and_hint_787`. 46 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori resume-path probe on `22b423b6`, 2026-05-27.
788. **`claw --output-format json skills show <not-found>` emitted two JSON objects — one from the skills handler, one duplicate from the top-level error path** — dogfooded 2026-05-27 on `113145a4`. `print_skills` in JSON mode called `println!` to emit the `skill_not_found` error envelope, then returned `Err(...)`. The `?` propagation triggered the top-level error handler which emitted a second `action:"abort"` JSON envelope on stderr. Callers reading both stdout and stderr got two JSON objects with the same `error_kind` but different `action` fields — the first was the authoritative response, the second was a duplicate. Fix: replaced `return Err(...)` with `std::process::exit(1)` after the skills error JSON is emitted, mirroring the existing `is_help_action` guard pattern. Integration test `skills_show_not_found_emits_single_json_object_788` asserts exactly 1 JSON object on stdout and no JSON on stderr. 47 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori skills double-emission probe on `113145a4`, 2026-05-27.
789. **`claw --output-format json agents show <not-found>` and `plugins show <not-found>` both returned exit 0 despite `status:"error"` in the JSON** — dogfooded 2026-05-27 on `abdbf61a`. Skills was fixed in #788 (exit 1 via process::exit). Agents and plugins had the identical gap: `print_agents` had no error check at all (just println + Ok(())); `print_plugins`'s not-found branch used `return Ok(())`. MCP was already fixed in an earlier cycle (#68). Fix: added `is_error` check in `print_agents` JSON path (exit 1 when status=="error"); changed plugins not-found branch from `return Ok(())` to `std::process::exit(1)`. Existing `inventory_commands_emit_structured_json_when_requested` test updated to use `run_claw` directly for the not-found case. Two new tests added: `agents_show_not_found_exits_nonzero_789`, `plugins_show_not_found_exits_nonzero_789`. 49 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori exit-code consistency probe on `abdbf61a`, 2026-05-27.
790. **`claw --output-format json system-prompt <unknown-option>` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `e4c3c1aa`. The unknown-option branch in `parse_print_system_prompt_args` emitted plain `"unknown system-prompt option: {other}"` for all unrecognised options except `--json` (which appended a `\n`-delimited suggestion). All non-`--json` cases fell to `unknown+null`. Fix: replaced bare format string with `unknown_option: ... \n<usage hint>` format for all unknown options; `--json` special case preserves its `--output-format json` suggestion in the hint prefix. Integration test `system_prompt_unknown_option_returns_typed_kind_790` covers both paths. 50 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori system-prompt option probe on `e4c3c1aa`, 2026-05-27.
791. **`claw config show <extra>` and `claw config set <extra1> <extra2>` returned `unexpected_extra_args` + `hint:null`** — dogfooded 2026-05-27 on `9968a27e`. The config arg parser emitted `"unexpected extra arguments after `claw config {}`: {}"` with no `\n` delimiter, so `split_error_hint` returned `None` and `fallback_hint_for_error_kind("unexpected_extra_args")` also returns `None`. Fix: appended `\nUsage: claw config [env|hooks|model|plugins|mcp|settings]` to the error format string. Integration test `config_extra_args_have_non_null_hint_791` covers both paths. 51 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori config-arg probe on `9968a27e`, 2026-05-27.
792. **`claw agents list --bogus-flag` and `claw skills list --bogus-flag` silently returned `status:"ok" count:0` instead of an error** — dogfooded 2026-05-27 on `93a159dc`. The `list <filter>` arm in both handlers treated flag-shaped tokens (`--something`) as name substring filters. Since no agents/skills have `--bogus` in their name, result was empty success list — a false positive that masks typos and unknown flags. Fix: added flag-prefix guard at the top of both `list <args>` arms in `commands/src/lib.rs`; detected filter tokens starting with `-` return `unknown_option` + usage hint. Two new integration tests `agents_list_flag_shaped_filter_returns_unknown_option_792`, `skills_list_flag_shaped_filter_returns_unknown_option_792`. 53 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori agents/skills list probe on `93a159dc`, 2026-05-27.
793. **`claw plugins list --bogus-flag` silent empty success + `plugins uninstall <not-found>` had `hint:null`** — dogfooded 2026-05-27 on `abfa2e4c`. Two gaps: (1) `plugins list` filter branch in `print_plugins` treated `--bogus-flag` as an id substring filter, found no matches, returned `status:"ok"` empty list — same false-positive as #792 for agents/skills. (2) `plugins uninstall no-such` propagated `plugin_not_found` error via `?` with no `\n` delimiter; `plugin_not_found` was missing from `fallback_hint_for_error_kind` table. Fix: (1) added flag-prefix guard in `print_plugins` `is_list_action` branch (detects tokens starting with `-`, returns `unknown_option` + usage hint, exits 1); (2) added `"plugin_not_found"``"Run 'claw plugins list' to see installed plugins."` to fallback table. Two new tests `plugins_list_flag_shaped_filter_returns_unknown_option_793`, `plugins_uninstall_not_found_has_hint_793`. 55 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori plugins lifecycle probe on `abfa2e4c`, 2026-05-27.
794. **`claw plugins install /nonexistent/path` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `57a57ef7`. The error message `"plugin source '/path' was not found"` had no classifier arm, falling to `"unknown"`. Fix: added `plugin_source_not_found` classifier arm (`message.contains("plugin source") && message.contains("was not found")`); added `"plugin_source_not_found"``"Check that the path or URL is correct..."` to `fallback_hint_for_error_kind`. Unit test assertion added to `test_classify_error_kind`; integration test `plugins_install_not_found_path_returns_typed_kind_794` added. 56 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori plugins install probe on `57a57ef7`, 2026-05-27.
795. **`claw skills install /nonexistent` returned `skill_not_found + hint:null` and `claw skills uninstall x` returned `unsupported_skills_action + hint:null`** — dogfooded 2026-05-27 on `491f179a`. Both error kinds were missing from `fallback_hint_for_error_kind` table, so even though classify returned a typed kind, the hint field was always null. Fix: added `"skill_not_found"` → hint suggesting `claw skills list` / `claw skills install`; added `"unsupported_skills_action"` → hint listing supported actions. Integration test `skills_install_not_found_and_unsupported_action_have_hints_795` covers both paths. 57 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori skills lifecycle probe on `491f179a`, 2026-05-27.
796. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` returned confusing `agent_not_found`/`skill_not_found` for the concatenated "name extra" string** — dogfooded 2026-05-27 on `18b4cee5`. `join_optional_args` passes all tokens as a space-joined string; both `show` handlers called `split_once(' ')` to extract the name but did not check if the remainder (after the first split) contained additional tokens. Extra positional args (including `--flags`) became part of the "name", silently mangling the lookup. Fix: added second `split_once(' ')` on the extracted name; if the result has two parts, return `unexpected_extra_args` with a usage hint. Valid single-name lookups are unaffected. Two new integration tests `agents_show_extra_positional_arg_returns_unexpected_extra_796`, `skills_show_extra_positional_arg_returns_unexpected_extra_796`. 59 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori agents/skills show extra-arg probe on `18b4cee5`, 2026-05-27.
797. **Installed `claw version --output-format json` reports `git_sha:null` / `Git SHA unknown`, so dogfood cannot tie the binary under test to a source revision** — dogfooded 2026-05-27 from `#clawcode-building-in-public` using the installed `/home/bellman/.cargo/bin/claw` binary in a clean `ultraworkers/claw-code` checkout. `claw version --output-format json` returned `{"kind":"version","version":"0.1.0","git_sha":null,"target":null,"build_date":"2026-03-31"...}` while `claw status --output-format json` only reported workspace state (`git_branch`, clean/dirty counts) and did not provide any executable-vs-workspace provenance comparison. This is a clawability gap in event/log opacity and stale-binary confusion: an operator can run `doctor/status/version` successfully but still cannot prove which commit the installed CLI came from, whether it matches `origin/main`, or whether the observed behavior is from a stale packaged binary. **Required fix shape:** (a) embed build git SHA/target/build provenance in installed/release binaries whenever the source tree is available; (b) when provenance is missing, emit a typed `binary_provenance.status:"unknown"` rather than only `git_sha:null`; (c) have `status`/`doctor` include a redaction-safe comparison between executable provenance and workspace HEAD when running inside a git checkout; (d) add regression/packaging coverage proving release/local install paths preserve or explicitly classify provenance. **Why this matters:** dogfood reports and automation need to distinguish current-source failures from stale or unknown binary lineage before opening/rebasing/closing PRs. Source: gaebal-gajae live dogfood on 2026-05-27; active repo checkout had open PR #3124 DIRTY with no checks and PR #3125 CLEAN, but the installed binary itself could not identify its source revision.
798. **`claw plugins show <name> <extra-arg>` returned `unexpected_extra_args` + `hint:null`** — dogfooded 2026-05-27 on `9976585f`. The plugins arg parser at the top level emitted `"unexpected extra arguments after 'claw plugins show ...': ..."` with no `\n` delimiter (parity gap with #791 config fix). Fix: appended `\nUsage: claw plugins [list|show <id>|...]` to the error format string. Integration test `plugins_extra_args_have_non_null_hint_797`. Committed as `bff37000`. 60 CLI contract tests pass. [SCOPE: claw-code]
799. **`claw --output-format json ""` and `claw " "` returned `empty_prompt` + `hint:null`** — dogfooded 2026-05-27 on `bff37000`. The empty-prompt guard at the fallthrough path emitted `"empty prompt: provide a subcommand..."` with no `\n` delimiter. Fix: added `\n` + usage hint. Integration test `empty_prompt_has_non_null_hint_798`. Committed as `efb1542a`. 61 CLI contract tests pass. [SCOPE: claw-code]
800. **`classify_error_kind` unit test coverage gap: `invalid_history_count` and `unknown_option` arms had zero assertions** — found 2026-05-27 on `efb1542a`. Audit of 39 distinct classifier return values vs 37 unit test assertions revealed 2 untested arms. Fix: added 3 `assert_eq!` covering both arms (invalid_history_count prefix + contains paths, unknown_option prefix). Committed as `6ee67d6c`. All 39 return values now have unit test coverage. [SCOPE: claw-code]
801. **`claw --output-format json diff` in a non-git directory was missing `error_kind`, `hint`, and `message` fields** — dogfooded 2026-05-27 on `1201dc60`. The diff handler's no-git-repo JSON branch constructed a custom object with only `status:"error"` + `result:"no_git_repo"` + `detail`, violating the error envelope contract that every error has `error_kind` + `hint`. Fix: added `error_kind: "no_git_repo"`, `hint: "Run git init..."`, and `message` fields. Integration test `diff_non_git_dir_has_error_kind_and_hint_801`. 62 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori non-git-dir probe on `1201dc60`, 2026-05-27.
802. **Four `status:"error"` JSON sites in resume-mode and broad-cwd handlers were missing `hint` field** — found 2026-05-27 on `53953a81` via source audit of all `"status": "error"` sites in main.rs. The resume `unsupported_command` (L3433), `unsupported_resumed_command` (L3455), `cli_parse` (L3474), and `broad_cwd` (L4838) handlers all emitted JSON error envelopes with `error_kind` but no `hint` field. Fix: added contextual `hint` string to all four sites. Source audit now shows 0 `status:"error"` JSON objects missing `hint` across entire main.rs. 62 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori source-level audit of all error JSON sites, 2026-05-27.
803. **`claw agents list --bogus`, `skills list --bogus`, and `plugins list --bogus` in text mode silently returned empty success** — dogfooded 2026-05-27 on `fcebf644`. The JSON-mode flag guards added in #792/#793 only covered the JSON branch; the text-mode path through `handle_agents_slash_command`, `handle_skills_slash_command`, and `print_plugins` still passed flag-shaped tokens as substring filters. Fix: added flag-prefix guards to all three text-mode list handlers (agents and skills in `commands/src/lib.rs`, plugins in `main.rs print_plugins`). Also removed the now-redundant JSON-only guard from print_plugins (the early guard catches both modes). Updated `plugins_list_flag_shaped_filter_returns_unknown_option_793` test to check stderr. 62 CLI contract tests pass. [SCOPE: claw-code]
804. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` in text mode returned wrong `agent_not_found`/silent empty instead of catching extra args** — dogfooded 2026-05-27 on `bad1b97f`. Parity gap with JSON-mode fix #796: the text-mode show handlers in `commands/src/lib.rs` still used single-split `split_once(' ')` without checking for spaces in the extracted name. Fix: added `contains(' ')` guard to both text-mode show arms; extra tokens now return `unexpected extra arguments` with usage hint. 62 CLI contract tests pass. [SCOPE: claw-code]
805. **`claw skills show <not-found>` in text mode silently returned "No skills found." instead of an error** — dogfooded 2026-05-27 on `2c3c0f60`. The text-mode show handler in `handle_skills_slash_command` returned `render_skills_report(&matched)` with an empty vec instead of checking for empty match and returning an error. JSON mode already returned `skill_not_found` since #706. Fix: added `matched.is_empty()` guard with `skill_not_found` error + `\n` hint suggesting `claw skills list`. 62 CLI contract tests pass. [SCOPE: claw-code]
806. **`claw plugins show <not-found>` in text mode returned "No plugins installed." instead of an error** — dogfooded 2026-05-27 on `ae6a207d`. The text-mode path in `print_plugins` printed `payload.message` (the full list render) without checking if the requested plugin existed. JSON mode correctly returned `plugin_not_found`. Fix: added show-action filtering + not-found guard to text-mode path; added `starts_with("plugin_not_found:")` arm to classifier for the new error prefix. 63 CLI contract tests pass. [SCOPE: claw-code]

View File

@@ -2365,13 +2365,6 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
} }
Some(args) if args.starts_with("list ") => { Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase(); let filter = args["list ".len()..].trim().to_lowercase();
// #803: reject flag-shaped tokens in text mode too (JSON guard was added in #792)
if filter.starts_with('-') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown option for `agents list`: {filter}\nUsage: claw agents list [<filter>]\nFilters are name substrings, not flags."),
));
}
let roots = discover_definition_roots(cwd, "agents"); let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?; let agents = load_agents_from_roots(&roots)?;
let filtered: Vec<_> = agents let filtered: Vec<_> = agents
@@ -2390,30 +2383,22 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|| args.starts_with("info ") || args.starts_with("info ")
|| args.starts_with("describe ") => || args.starts_with("describe ") =>
{ {
let name_raw = args let name = args
.split_once(' ') .split_once(' ')
.map(|(_, name)| name) .map(|(_, name)| name)
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.to_lowercase(); .to_lowercase();
// #804: detect extra positional args (parity with JSON-mode fix #796)
if name_raw.contains(' ') {
let extra = name_raw.split_once(' ').map(|(_, e)| e).unwrap_or("");
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unexpected extra arguments after agent name\nUsage: claw agents show <name>\nUnexpected extra: '{extra}'"),
));
}
let roots = discover_definition_roots(cwd, "agents"); let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?; let agents = load_agents_from_roots(&roots)?;
let matched: Vec<_> = agents let matched: Vec<_> = agents
.into_iter() .into_iter()
.filter(|a| a.name.to_lowercase() == name_raw) .filter(|a| a.name.to_lowercase() == name)
.collect(); .collect();
if matched.is_empty() { if matched.is_empty() {
return Err(std::io::Error::new( return Err(std::io::Error::new(
std::io::ErrorKind::NotFound, std::io::ErrorKind::NotFound,
format!("agent not found: {name_raw}"), format!("agent not found: {name}"),
)); ));
} }
Ok(render_agents_report(&matched)) Ok(render_agents_report(&matched))
@@ -2444,18 +2429,6 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
} }
Some(args) if args.starts_with("list ") => { Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase(); let filter = args["list ".len()..].trim().to_lowercase();
// #792: unknown flags (--something) silently became filter strings, returning
// empty success list instead of an error. Detect and reject flag-shaped tokens.
if filter.starts_with('-') {
return Ok(serde_json::json!({
"kind": "agents",
"action": "list",
"status": "error",
"error_kind": "unknown_option",
"unexpected": filter,
"hint": "Usage: claw agents list [<filter>]\nFilters are name substrings, not flags.",
}));
}
let roots = discover_definition_roots(cwd, "agents"); let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?; let agents = load_agents_from_roots(&roots)?;
let filtered: Vec<_> = agents let filtered: Vec<_> = agents
@@ -2474,29 +2447,12 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|| args.starts_with("info ") || args.starts_with("info ")
|| args.starts_with("describe ") => || args.starts_with("describe ") =>
{ {
let name_raw = args let name = args
.split_once(' ') .split_once(' ')
.map(|(_, name)| name) .map(|(_, name)| name)
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.to_lowercase(); .to_lowercase();
// #796: extra positional args after the name (e.g. `agents show foo extra`)
// produced a confusing agent_not_found for "foo extra" instead of flagging
// the unexpected extra argument.
let (name, extra) = name_raw
.split_once(' ')
.map(|(n, e)| (n.to_string(), Some(e.to_string())))
.unwrap_or_else(|| (name_raw.clone(), None));
if let Some(extra_token) = extra {
return Ok(serde_json::json!({
"kind": "agents",
"action": "show",
"status": "error",
"error_kind": "unexpected_extra_args",
"unexpected": extra_token,
"hint": format!("Usage: claw agents show <name>\nUnexpected extra: '{extra_token}'"),
}));
}
let roots = discover_definition_roots(cwd, "agents"); let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?; let agents = load_agents_from_roots(&roots)?;
let matched: Vec<_> = agents let matched: Vec<_> = agents
@@ -2561,13 +2517,6 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
} }
Some(args) if args.starts_with("list ") => { Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase(); let filter = args["list ".len()..].trim().to_lowercase();
// #803: reject flag-shaped tokens in text mode too (JSON guard was added in #792)
if filter.starts_with('-') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown option for `skills list`: {filter}\nUsage: claw skills list [<filter>]\nFilters are name substrings, not flags."),
));
}
let roots = discover_skill_roots(cwd); let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?; let skills = load_skills_from_roots(&roots)?;
let filtered: Vec<_> = skills let filtered: Vec<_> = skills
@@ -2586,33 +2535,18 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|| args.starts_with("info ") || args.starts_with("info ")
|| args.starts_with("describe ") => || args.starts_with("describe ") =>
{ {
let name_raw = args let name = args
.split_once(' ') .split_once(' ')
.map(|(_, name)| name) .map(|(_, name)| name)
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.to_lowercase(); .to_lowercase();
// #804: detect extra positional args (parity with JSON-mode fix #796)
if name_raw.contains(' ') {
let extra = name_raw.split_once(' ').map(|(_, e)| e).unwrap_or("");
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unexpected extra arguments after skill name\nUsage: claw skills show <name>\nUnexpected extra: '{extra}'"),
));
}
let roots = discover_skill_roots(cwd); let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?; let skills = load_skills_from_roots(&roots)?;
let matched: Vec<_> = skills let matched: Vec<_> = skills
.into_iter() .into_iter()
.filter(|s| s.name.to_lowercase() == name_raw) .filter(|s| s.name.to_lowercase() == name)
.collect(); .collect();
// #805: text-mode show must return an error when skill not found (parity with JSON)
if matched.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("skill '{name_raw}' not found\nRun `claw skills list` to see available skills."),
));
}
Ok(render_skills_report(&matched)) Ok(render_skills_report(&matched))
} }
Some("install") => Ok(render_skills_usage(Some("install"))), Some("install") => Ok(render_skills_usage(Some("install"))),
@@ -2648,18 +2582,6 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
} }
Some(args) if args.starts_with("list ") => { Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase(); let filter = args["list ".len()..].trim().to_lowercase();
// #792: flag-shaped tokens silently became filter strings, returning
// empty success list instead of an error. Detect and reject them.
if filter.starts_with('-') {
return Ok(serde_json::json!({
"kind": "skills",
"action": "list",
"status": "error",
"error_kind": "unknown_option",
"unexpected": filter,
"hint": "Usage: claw skills list [<filter>]\nFilters are name substrings, not flags.",
}));
}
let roots = discover_skill_roots(cwd); let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?; let skills = load_skills_from_roots(&roots)?;
let filtered: Vec<_> = skills let filtered: Vec<_> = skills
@@ -2678,29 +2600,12 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|| args.starts_with("info ") || args.starts_with("info ")
|| args.starts_with("describe ") => || args.starts_with("describe ") =>
{ {
let name_raw = args let name = args
.split_once(' ') .split_once(' ')
.map(|(_, name)| name) .map(|(_, name)| name)
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.to_lowercase(); .to_lowercase();
// #796: extra positional args after the name (e.g. `skills show foo extra`)
// produced a confusing skill_not_found for "foo extra" instead of flagging
// the unexpected extra argument.
let (name, extra) = name_raw
.split_once(' ')
.map(|(n, e)| (n.to_string(), Some(e.to_string())))
.unwrap_or_else(|| (name_raw.clone(), None));
if let Some(extra_token) = extra {
return Ok(json!({
"kind": "skills",
"action": "show",
"status": "error",
"error_kind": "unexpected_extra_args",
"unexpected": extra_token,
"hint": format!("Usage: claw skills show <name>\nUnexpected extra: '{extra_token}'"),
}));
}
let roots = discover_skill_roots(cwd); let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?; let skills = load_skills_from_roots(&roots)?;
let matched: Vec<_> = skills let matched: Vec<_> = skills

View File

@@ -283,9 +283,6 @@ fn classify_error_kind(message: &str) -> &'static str {
// error message is "failed to restore session: legacy session is missing workspace // error message is "failed to restore session: legacy session is missing workspace
// binding: ...", so the specific arm must be checked first. // binding: ...", so the specific arm must be checked first.
"legacy_session_no_workspace_binding" "legacy_session_no_workspace_binding"
} else if message.contains("Is a directory") || message.contains("os error 21") {
// #787: --resume given a directory path instead of a .jsonl file
"session_path_is_directory"
} else if message.contains("failed to restore session") { } else if message.contains("failed to restore session") {
"session_load_failed" "session_load_failed"
} else if message.contains("unsupported ACP invocation") { } else if message.contains("unsupported ACP invocation") {
@@ -335,11 +332,8 @@ fn classify_error_kind(message: &str) -> &'static str {
"unknown_agents_subcommand" "unknown_agents_subcommand"
} else if message.starts_with("agent not found:") { } else if message.starts_with("agent not found:") {
"agent_not_found" "agent_not_found"
} else if message.contains("is not installed") || message.starts_with("plugin_not_found:") { } else if message.contains("is not installed") {
"plugin_not_found" "plugin_not_found"
} else if message.contains("plugin source") && message.contains("was not found") {
// #794: `plugins install /nonexistent/path` → "plugin source ... was not found"
"plugin_source_not_found"
} else if (message.contains("skill source") && message.contains("not found")) } else if (message.contains("skill source") && message.contains("not found"))
|| message.starts_with("skill '") || message.starts_with("skill '")
{ {
@@ -355,9 +349,6 @@ fn classify_error_kind(message: &str) -> &'static str {
} else if message.contains("has been removed.") { } else if message.contains("has been removed.") {
// #765: removed subcommands (login, logout) — hint contains migration guidance // #765: removed subcommands (login, logout) — hint contains migration guidance
"removed_subcommand" "removed_subcommand"
} else if message.starts_with("unknown subcommand:") {
// #785: typo/unknown top-level subcommand (e.g. `claw dump` → did you mean dump-manifests?)
"unknown_subcommand"
} else if message.starts_with("unexpected extra arguments") } else if message.starts_with("unexpected extra arguments")
|| message.starts_with("unexpected_extra_args:") || message.starts_with("unexpected_extra_args:")
{ {
@@ -407,28 +398,6 @@ fn fallback_hint_for_error_kind(kind: &str) -> Option<&'static str> {
"missing_credentials" => { "missing_credentials" => {
Some("Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN before running claw.") Some("Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN before running claw.")
} }
// #787: session load failures have no \n-delimited hint from the OS error path
"session_load_failed" => Some(
"Pass a path to a .jsonl session file, not a directory. Managed sessions live in .claw/sessions/.",
),
"session_path_is_directory" => Some(
"--resume expects a .jsonl session file path, not a directory. Run `claw --output-format json /session list` to list managed sessions.",
),
// #793: plugins uninstall/enable/disable of non-existing plugin propagates through
// the ? operator with no \n delimiter, so split_error_hint returns None.
"plugin_not_found" => Some("Run `claw plugins list` to see installed plugins."),
// #794: plugins install with a path that doesn't exist
"plugin_source_not_found" => Some(
"Check that the path or URL is correct. Use a local directory or a valid registry id.",
),
// #795: skills install/show of a non-existing skill path or name
"skill_not_found" => Some(
"Run `claw skills list` to see available skills, or `claw skills install <path>` to install a new one.",
),
// #795: unsupported action on skills (e.g. /skills uninstall) with no \n hint
"unsupported_skills_action" => Some(
"Supported: list, install <path>, show <name>, help. Run `claw skills help` for details.",
),
_ => None, _ => None,
} }
} }
@@ -1185,9 +1154,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let action = tail.first().cloned(); let action = tail.first().cloned();
let target = tail.get(1).cloned(); let target = tail.get(1).cloned();
if tail.len() > 2 { if tail.len() > 2 {
// #797: append \n usage hint so split_error_hint extracts it (parity with #791 config fix)
return Err(format!( return Err(format!(
"unexpected extra arguments after `claw {} {}`: {}\nUsage: claw plugins [list|show <id>|install <id>|enable <id>|disable <id>|uninstall <id>|update <id>|help]", "unexpected extra arguments after `claw {} {}`: {}",
rest[0], rest[0],
tail[..2].join(" "), tail[..2].join(" "),
tail[2..].join(" ") tail[2..].join(" ")
@@ -1209,9 +1177,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let tail = &rest[1..]; let tail = &rest[1..];
let section = tail.first().cloned(); let section = tail.first().cloned();
if tail.len() > 1 { if tail.len() > 1 {
// #791: append \n hint so split_error_hint extracts it and hint is non-null
return Err(format!( return Err(format!(
"unexpected extra arguments after `claw config {}`: {}\nUsage: claw config [env|hooks|model|plugins|mcp|settings]", "unexpected extra arguments after `claw config {}`: {}",
tail[0], tail[0],
tail[1..].join(" ") tail[1..].join(" ")
)); ));
@@ -1225,10 +1192,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// `git diff`). No session needed to inspect the working tree. // `git diff`). No session needed to inspect the working tree.
"diff" => { "diff" => {
if rest.len() > 1 { if rest.len() > 1 {
// #3129: keep malformed `diff ... --output-format json` on the return Err(format!(
// parser/error path, not the prompt/TUI fallback. The newline "unexpected extra arguments after `claw diff`: {}\nUsage: claw diff",
// before Usage is part of the JSON hint contract. rest[1..].join(" ")
return Err(unexpected_diff_args_error(&rest[1..])); ));
} }
Ok(CliAction::Diff { output_format }) Ok(CliAction::Diff { output_format })
} }
@@ -1378,9 +1345,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// an empty prompt when credentials are present). // an empty prompt when credentials are present).
let joined = rest.join(" "); let joined = rest.join(" ");
if joined.trim().is_empty() { if joined.trim().is_empty() {
// #798: add \n hint so split_error_hint extracts it (was empty_prompt + null)
return Err( return Err(
"empty prompt: provide a subcommand or a non-empty prompt string.\nUsage: claw <subcommand> or claw -p <prompt>. Run `claw --help` for the full list." "empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string"
.to_string(), .to_string(),
); );
} }
@@ -1625,13 +1591,6 @@ fn removed_auth_surface_error(command_name: &str) -> String {
) )
} }
fn unexpected_diff_args_error(extra: &[String]) -> String {
format!(
"unexpected extra arguments after `claw diff`: {}\nUsage: claw diff",
extra.join(" ")
)
}
fn parse_acp_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> { fn parse_acp_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
match args { match args {
[] => Ok(CliAction::Acp { output_format }), [] => Ok(CliAction::Acp { output_format }),
@@ -2119,16 +2078,11 @@ fn parse_system_prompt_args(
} }
other => { other => {
// #152: hint `--output-format json` when user types `--json`. // #152: hint `--output-format json` when user types `--json`.
// #790: use unknown_option: prefix + \n hint so classify_error_kind returns let mut msg = format!("unknown system-prompt option: {other}");
// unknown_option and split_error_hint extracts the remediation text. if other == "--json" {
let hint = if other == "--json" { msg.push_str("\nDid you mean `--output-format json`?");
"Did you mean `--output-format json`? Usage: claw system-prompt [--cwd <dir>] [--date <YYYY-MM-DD>] [--output-format text|json]".to_string() }
} else { return Err(msg);
"Usage: claw system-prompt [--cwd <dir>] [--date <YYYY-MM-DD>] [--output-format text|json]".to_string()
};
return Err(format!(
"unknown_option: unknown system-prompt option: {other}.\n{hint}"
));
} }
} }
} }
@@ -2202,15 +2156,14 @@ fn parse_dump_manifests_args(
if arg == "--manifests-dir" { if arg == "--manifests-dir" {
let value = args let value = args
.get(index + 1) .get(index + 1)
.ok_or_else(|| String::from("missing_flag_value: --manifests-dir requires a path.\nUsage: claw dump-manifests --manifests-dir <path> [--output-format json]"))?; .ok_or_else(|| String::from("--manifests-dir requires a path"))?;
manifests_dir = Some(PathBuf::from(value)); manifests_dir = Some(PathBuf::from(value));
index += 2; index += 2;
continue; continue;
} }
if let Some(value) = arg.strip_prefix("--manifests-dir=") { if let Some(value) = arg.strip_prefix("--manifests-dir=") {
if value.is_empty() { if value.is_empty() {
// #786: empty --manifests-dir= is also a missing value return Err(String::from("--manifests-dir requires a path"));
return Err(String::from("missing_flag_value: --manifests-dir requires a path.\nUsage: claw dump-manifests --manifests-dir <path> [--output-format json]"));
} }
manifests_dir = Some(PathBuf::from(value)); manifests_dir = Some(PathBuf::from(value));
index += 1; index += 1;
@@ -3370,10 +3323,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
// #77: classify session load errors for downstream consumers // #77: classify session load errors for downstream consumers
let full_message = format!("failed to restore session: {error}"); let full_message = format!("failed to restore session: {error}");
let kind = classify_error_kind(&full_message); let kind = classify_error_kind(&full_message);
let (short_reason, inline_hint) = split_error_hint(&full_message); let (short_reason, hint) = split_error_hint(&full_message);
// #787: fall back to kind-derived hint when message has no \n delimiter
let hint =
inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
eprintln!( eprintln!(
"{}", "{}",
serde_json::json!({ serde_json::json!({
@@ -3440,7 +3390,6 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
"status": "error", "status": "error",
"error_kind": "unsupported_command", "error_kind": "unsupported_command",
"error": format!("/{cmd_root} is not yet implemented in this build"), "error": format!("/{cmd_root} is not yet implemented in this build"),
"hint": "This command is not available in the current build. Update claw or use a different command.",
"exit_code": 2, "exit_code": 2,
"command": raw_command, "command": raw_command,
}) })
@@ -3463,7 +3412,6 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
"status": "error", "status": "error",
"error_kind": "unsupported_resumed_command", "error_kind": "unsupported_resumed_command",
"error": format!("unsupported resumed command: {raw_command}"), "error": format!("unsupported resumed command: {raw_command}"),
"hint": "This command cannot be used with --resume. Use it in an interactive REPL session instead.",
"exit_code": 2, "exit_code": 2,
"command": raw_command, "command": raw_command,
}) })
@@ -3483,7 +3431,6 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
"status": "error", "status": "error",
"error_kind": "cli_parse", "error_kind": "cli_parse",
"error": error.to_string(), "error": error.to_string(),
"hint": "Run `claw --help` for usage.",
"exit_code": 2, "exit_code": 2,
"command": raw_command, "command": raw_command,
}) })
@@ -3521,10 +3468,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
// hardcoded "resume_command_error" + prose in the error field // hardcoded "resume_command_error" + prose in the error field
let full_error = error.to_string(); let full_error = error.to_string();
let error_kind = classify_error_kind(&full_error); let error_kind = classify_error_kind(&full_error);
let (short_reason, inline_hint) = split_error_hint(&full_error); let (short_reason, hint) = split_error_hint(&full_error);
// #787: fall back to kind-derived hint when error has no \n delimiter
let hint = inline_hint
.or_else(|| fallback_hint_for_error_kind(error_kind).map(String::from));
eprintln!( eprintln!(
"{}", "{}",
serde_json::json!({ serde_json::json!({
@@ -4848,7 +4792,6 @@ fn enforce_broad_cwd_policy(
"status": "error", "status": "error",
"error_kind": "broad_cwd", "error_kind": "broad_cwd",
"error": message, "error": message,
"hint": "Change to a more specific project directory, or use --cwd to set the workspace root.",
"exit_code": 1, "exit_code": 1,
}) })
); );
@@ -6318,17 +6261,10 @@ impl LiveCli {
let cwd = env::current_dir()?; let cwd = env::current_dir()?;
match output_format { match output_format {
CliOutputFormat::Text => println!("{}", handle_agents_slash_command(args, &cwd)?), CliOutputFormat::Text => println!("{}", handle_agents_slash_command(args, &cwd)?),
CliOutputFormat::Json => { CliOutputFormat::Json => println!(
let value = handle_agents_slash_command_json(args, &cwd)?; "{}",
// #789: parity with print_mcp/#788 print_skills — exit 1 when envelope serde_json::to_string_pretty(&handle_agents_slash_command_json(args, &cwd)?)?
// reports an error so automation can rely on exit code instead of ),
// parsing the JSON status field.
let is_error = value.get("status").and_then(|v| v.as_str()) == Some("error");
println!("{}", serde_json::to_string_pretty(&value)?);
if is_error {
std::process::exit(1);
}
}
} }
Ok(()) Ok(())
} }
@@ -6377,10 +6313,12 @@ impl LiveCli {
let is_help_action = result.get("action").and_then(|v| v.as_str()) == Some("help"); let is_help_action = result.get("action").and_then(|v| v.as_str()) == Some("help");
println!("{}", serde_json::to_string_pretty(&result)?); println!("{}", serde_json::to_string_pretty(&result)?);
if is_error && !is_help_action { if is_error && !is_help_action {
// #788: the error JSON is already emitted above; returning Err here return Err(result
// would cause the top-level handler to emit a second error envelope. .get("message")
// Exit directly to signal failure without a duplicate envelope. .and_then(|v| v.as_str())
std::process::exit(1); .unwrap_or("skills command failed")
.to_string()
.into());
} }
} }
} }
@@ -6393,41 +6331,9 @@ impl LiveCli {
output_format: CliOutputFormat, output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?; let cwd = env::current_dir()?;
// #803: reject flag-shaped tokens in list filter for BOTH text and JSON modes.
// Previously the guard was JSON-only (#793); text mode silently returned empty success.
if action.as_deref() == Some("list") {
if let Some(filter) = target.as_deref() {
if filter.starts_with('-') {
return Err(format!(
"unknown option for `claw plugins list`: {filter}\nUsage: claw plugins list [<filter>]\nFilters are id substrings, not flags."
).into());
}
}
}
let payload = plugins_command_payload_for(&cwd, action, target)?; let payload = plugins_command_payload_for(&cwd, action, target)?;
match output_format { match output_format {
CliOutputFormat::Text => { CliOutputFormat::Text => println!("{}", payload.message),
// #806: text-mode show must return error when plugin not found (parity with JSON)
let action_str = action.unwrap_or("list");
if matches!(action_str, "show" | "info" | "describe") {
if let Some(name) = target {
let needle = name.to_lowercase();
let found = payload.plugins.iter().any(|p| {
p.get("id")
.and_then(|v| v.as_str())
.map(|id| id.to_lowercase() == needle)
.unwrap_or(false)
});
if !found {
return Err(format!(
"plugin_not_found: plugin '{}' not found\nRun `claw plugins list` to see available plugins.",
name
).into());
}
}
}
println!("{}", payload.message);
}
CliOutputFormat::Json => { CliOutputFormat::Json => {
let action_str = action.unwrap_or("list"); let action_str = action.unwrap_or("list");
// #743/#420: plugins help must return a usage envelope matching agents/mcp/skills help shape. // #743/#420: plugins help must return a usage envelope matching agents/mcp/skills help shape.
@@ -6470,20 +6376,6 @@ impl LiveCli {
} }
} else if is_list_action { } else if is_list_action {
if let Some(filter) = target { if let Some(filter) = target {
// #793: flag-shaped tokens silently became substring filters on
// plugins list, returning empty success instead of an error.
if filter.starts_with('-') {
let obj = json!({
"kind": "plugin",
"action": "list",
"status": "error",
"error_kind": "unknown_option",
"unexpected": filter,
"hint": "Usage: claw plugins list [<filter>]\nFilters are id substrings, not flags.",
});
println!("{}", serde_json::to_string_pretty(&obj)?);
std::process::exit(1);
}
let needle = filter.to_lowercase(); let needle = filter.to_lowercase();
payload payload
.plugins .plugins
@@ -6518,8 +6410,7 @@ impl LiveCli {
"hint": "Run `claw plugins list` to see available plugins.", "hint": "Run `claw plugins list` to see available plugins.",
}); });
println!("{}", serde_json::to_string_pretty(&obj)?); println!("{}", serde_json::to_string_pretty(&obj)?);
// #789: exit 1 on not-found so automation can rely on exit code return Ok(());
std::process::exit(1);
} }
} }
} }
@@ -8374,15 +8265,11 @@ fn render_diff_json_for(cwd: &Path) -> Result<serde_json::Value, Box<dyn std::er
.map(|o| o.status.success()) .map(|o| o.status.success())
.unwrap_or(false); .unwrap_or(false);
if !in_git_repo { if !in_git_repo {
// #801: add error_kind, hint, message fields for envelope parity with other error paths
return Ok(serde_json::json!({ return Ok(serde_json::json!({
"kind": "diff", "kind": "diff",
"action": "diff", "action": "diff",
"status": "error", "status": "error",
"error_kind": "no_git_repo",
"result": "no_git_repo", "result": "no_git_repo",
"message": format!("{} is not inside a git project", cwd.display()),
"hint": "Run `git init` to create a repository, or change to a directory that is inside a git project.",
"working_directory": cwd.display().to_string(), "working_directory": cwd.display().to_string(),
"detail": format!("{} is not inside a git project", cwd.display()), "detail": format!("{} is not inside a git project", cwd.display()),
})); }));
@@ -13118,20 +13005,10 @@ mod tests {
classify_error_kind("failed to restore session: file not found"), classify_error_kind("failed to restore session: file not found"),
"session_load_failed" "session_load_failed"
); );
// #787: directory-as-session-path gets its own kind (precedes generic session_load_failed)
assert_eq!(
classify_error_kind("failed to restore session: Is a directory (os error 21)"),
"session_path_is_directory"
);
assert_eq!( assert_eq!(
classify_error_kind("unrecognized argument `--foo` for subcommand `doctor`"), classify_error_kind("unrecognized argument `--foo` for subcommand `doctor`"),
"cli_parse" "cli_parse"
); );
// #785: unknown top-level subcommand (typo or unrecognised command)
assert_eq!(
classify_error_kind("unknown subcommand: dump.\nDid you mean dump-manifests"),
"unknown_subcommand"
);
assert_eq!( assert_eq!(
classify_error_kind("unsupported ACP invocation. Use `claw acp`."), classify_error_kind("unsupported ACP invocation. Use `claw acp`."),
"unsupported_acp_invocation" "unsupported_acp_invocation"
@@ -13242,11 +13119,6 @@ mod tests {
classify_error_kind("my-plugin is not installed"), classify_error_kind("my-plugin is not installed"),
"plugin_not_found" "plugin_not_found"
); );
// #794: plugins install with missing source path
assert_eq!(
classify_error_kind("plugin source `/nonexistent/path` was not found"),
"plugin_source_not_found"
);
assert_eq!( assert_eq!(
classify_error_kind("skill source /path/to/skill not found"), classify_error_kind("skill source /path/to/skill not found"),
"skill_not_found" "skill_not_found"
@@ -13306,20 +13178,6 @@ mod tests {
), ),
"invalid_resume_argument" "invalid_resume_argument"
); );
// coverage: invalid_history_count arm
assert_eq!(
classify_error_kind("invalid_history_count: abc is not a valid count"),
"invalid_history_count"
);
assert_eq!(
classify_error_kind("something invalid count something"),
"invalid_history_count"
);
// coverage: unknown_option arm (#790)
assert_eq!(
classify_error_kind("unknown_option: unknown system-prompt option: --foo."),
"unknown_option"
);
} }
#[test] #[test]

File diff suppressed because it is too large Load Diff