mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-27 07:56:46 +00:00
Compare commits
1 Commits
main
...
fix/roadma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
667772e3b8 |
2
.github/hooks/pre-push
vendored
2
.github/hooks/pre-push
vendored
@@ -13,7 +13,7 @@ cd "$repo_root"
|
||||
|
||||
if [[ -x scripts/roadmap-check-ids.sh ]]; then
|
||||
echo "pre-push: scripts/roadmap-check-ids.sh" >&2
|
||||
scripts/roadmap-check-ids.sh
|
||||
scripts/roadmap-check-ids.sh >&2
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_CLAW_PRE_PUSH_BUILD:-}" == "1" ]]; then
|
||||
|
||||
24
ROADMAP.md
24
ROADMAP.md
@@ -7735,27 +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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -2429,18 +2429,6 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
||||
}
|
||||
Some(args) if args.starts_with("list ") => {
|
||||
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 agents = load_agents_from_roots(&roots)?;
|
||||
let filtered: Vec<_> = agents
|
||||
@@ -2459,29 +2447,12 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
||||
|| args.starts_with("info ")
|
||||
|| args.starts_with("describe ") =>
|
||||
{
|
||||
let name_raw = args
|
||||
let name = args
|
||||
.split_once(' ')
|
||||
.map(|(_, name)| name)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.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 agents = load_agents_from_roots(&roots)?;
|
||||
let matched: Vec<_> = agents
|
||||
@@ -2611,18 +2582,6 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
||||
}
|
||||
Some(args) if args.starts_with("list ") => {
|
||||
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 skills = load_skills_from_roots(&roots)?;
|
||||
let filtered: Vec<_> = skills
|
||||
@@ -2641,29 +2600,12 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
||||
|| args.starts_with("info ")
|
||||
|| args.starts_with("describe ") =>
|
||||
{
|
||||
let name_raw = args
|
||||
let name = args
|
||||
.split_once(' ')
|
||||
.map(|(_, name)| name)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.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 skills = load_skills_from_roots(&roots)?;
|
||||
let matched: Vec<_> = skills
|
||||
|
||||
@@ -283,9 +283,6 @@ fn classify_error_kind(message: &str) -> &'static str {
|
||||
// error message is "failed to restore session: legacy session is missing workspace
|
||||
// binding: ...", so the specific arm must be checked first.
|
||||
"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") {
|
||||
"session_load_failed"
|
||||
} else if message.contains("unsupported ACP invocation") {
|
||||
@@ -337,9 +334,6 @@ fn classify_error_kind(message: &str) -> &'static str {
|
||||
"agent_not_found"
|
||||
} else if message.contains("is not installed") {
|
||||
"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"))
|
||||
|| message.starts_with("skill '")
|
||||
{
|
||||
@@ -355,9 +349,6 @@ fn classify_error_kind(message: &str) -> &'static str {
|
||||
} else if message.contains("has been removed.") {
|
||||
// #765: removed subcommands (login, logout) — hint contains migration guidance
|
||||
"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")
|
||||
|| message.starts_with("unexpected_extra_args:")
|
||||
{
|
||||
@@ -407,28 +398,6 @@ fn fallback_hint_for_error_kind(kind: &str) -> Option<&'static str> {
|
||||
"missing_credentials" => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1185,9 +1154,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let action = tail.first().cloned();
|
||||
let target = tail.get(1).cloned();
|
||||
if tail.len() > 2 {
|
||||
// #797: append \n usage hint so split_error_hint extracts it (parity with #791 config fix)
|
||||
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],
|
||||
tail[..2].join(" "),
|
||||
tail[2..].join(" ")
|
||||
@@ -1209,9 +1177,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let tail = &rest[1..];
|
||||
let section = tail.first().cloned();
|
||||
if tail.len() > 1 {
|
||||
// #791: append \n hint so split_error_hint extracts it and hint is non-null
|
||||
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[1..].join(" ")
|
||||
));
|
||||
@@ -1378,9 +1345,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
// an empty prompt when credentials are present).
|
||||
let joined = rest.join(" ");
|
||||
if joined.trim().is_empty() {
|
||||
// #798: add \n hint so split_error_hint extracts it (was empty_prompt + null)
|
||||
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(),
|
||||
);
|
||||
}
|
||||
@@ -2112,16 +2078,11 @@ fn parse_system_prompt_args(
|
||||
}
|
||||
other => {
|
||||
// #152: hint `--output-format json` when user types `--json`.
|
||||
// #790: use unknown_option: prefix + \n hint so classify_error_kind returns
|
||||
// unknown_option and split_error_hint extracts the remediation text.
|
||||
let hint = if other == "--json" {
|
||||
"Did you mean `--output-format json`? Usage: claw system-prompt [--cwd <dir>] [--date <YYYY-MM-DD>] [--output-format text|json]".to_string()
|
||||
} else {
|
||||
"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}"
|
||||
));
|
||||
let mut msg = format!("unknown system-prompt option: {other}");
|
||||
if other == "--json" {
|
||||
msg.push_str("\nDid you mean `--output-format json`?");
|
||||
}
|
||||
return Err(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2195,15 +2156,14 @@ fn parse_dump_manifests_args(
|
||||
if arg == "--manifests-dir" {
|
||||
let value = args
|
||||
.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));
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = arg.strip_prefix("--manifests-dir=") {
|
||||
if value.is_empty() {
|
||||
// #786: empty --manifests-dir= is also a missing value
|
||||
return Err(String::from("missing_flag_value: --manifests-dir requires a path.\nUsage: claw dump-manifests --manifests-dir <path> [--output-format json]"));
|
||||
return Err(String::from("--manifests-dir requires a path"));
|
||||
}
|
||||
manifests_dir = Some(PathBuf::from(value));
|
||||
index += 1;
|
||||
@@ -3363,10 +3323,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
// #77: classify session load errors for downstream consumers
|
||||
let full_message = format!("failed to restore session: {error}");
|
||||
let kind = classify_error_kind(&full_message);
|
||||
let (short_reason, inline_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));
|
||||
let (short_reason, hint) = split_error_hint(&full_message);
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
@@ -3511,10 +3468,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
// hardcoded "resume_command_error" + prose in the error field
|
||||
let full_error = error.to_string();
|
||||
let error_kind = classify_error_kind(&full_error);
|
||||
let (short_reason, inline_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));
|
||||
let (short_reason, hint) = split_error_hint(&full_error);
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
@@ -6307,17 +6261,10 @@ impl LiveCli {
|
||||
let cwd = env::current_dir()?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("{}", handle_agents_slash_command(args, &cwd)?),
|
||||
CliOutputFormat::Json => {
|
||||
let value = handle_agents_slash_command_json(args, &cwd)?;
|
||||
// #789: parity with print_mcp/#788 print_skills — exit 1 when envelope
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&handle_agents_slash_command_json(args, &cwd)?)?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -6366,10 +6313,12 @@ impl LiveCli {
|
||||
let is_help_action = result.get("action").and_then(|v| v.as_str()) == Some("help");
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
if is_error && !is_help_action {
|
||||
// #788: the error JSON is already emitted above; returning Err here
|
||||
// would cause the top-level handler to emit a second error envelope.
|
||||
// Exit directly to signal failure without a duplicate envelope.
|
||||
std::process::exit(1);
|
||||
return Err(result
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("skills command failed")
|
||||
.to_string()
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6427,20 +6376,6 @@ impl LiveCli {
|
||||
}
|
||||
} else if is_list_action {
|
||||
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();
|
||||
payload
|
||||
.plugins
|
||||
@@ -6475,8 +6410,7 @@ impl LiveCli {
|
||||
"hint": "Run `claw plugins list` to see available plugins.",
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&obj)?);
|
||||
// #789: exit 1 on not-found so automation can rely on exit code
|
||||
std::process::exit(1);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13071,20 +13005,10 @@ mod tests {
|
||||
classify_error_kind("failed to restore session: file not found"),
|
||||
"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!(
|
||||
classify_error_kind("unrecognized argument `--foo` for subcommand `doctor`"),
|
||||
"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!(
|
||||
classify_error_kind("unsupported ACP invocation. Use `claw acp`."),
|
||||
"unsupported_acp_invocation"
|
||||
@@ -13195,11 +13119,6 @@ mod tests {
|
||||
classify_error_kind("my-plugin is not installed"),
|
||||
"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!(
|
||||
classify_error_kind("skill source /path/to/skill not found"),
|
||||
"skill_not_found"
|
||||
|
||||
@@ -203,9 +203,7 @@ fn inventory_commands_emit_structured_json_when_requested() {
|
||||
isolated_codex.to_str().expect("utf8 codex home"),
|
||||
),
|
||||
];
|
||||
// #789: agents show not-found now exits 1 (parity with skills #788);
|
||||
// use run_claw directly instead of assert_json_command_with_env which checks success.
|
||||
let agents_show_out = run_claw(
|
||||
let agents_show_missing = assert_json_command_with_env(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
@@ -216,12 +214,6 @@ fn inventory_commands_emit_structured_json_when_requested() {
|
||||
],
|
||||
&agents_show_env,
|
||||
);
|
||||
assert!(
|
||||
!agents_show_out.status.success(),
|
||||
"agents show not-found must exit non-zero"
|
||||
);
|
||||
let agents_show_missing: serde_json::Value =
|
||||
serde_json::from_slice(&agents_show_out.stdout).expect("agents show stdout should be json");
|
||||
assert_eq!(agents_show_missing["kind"], "agents", "agents show kind");
|
||||
assert_eq!(agents_show_missing["action"], "show", "agents show action");
|
||||
assert_eq!(
|
||||
@@ -2510,944 +2502,3 @@ fn export_arg_errors_have_typed_kind_and_hint_784() {
|
||||
"hint must reference export usage, got: {h2:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_subcommand_returns_typed_kind_785() {
|
||||
// #785: `claw dump` (a near-miss for dump-manifests) returned error_kind:"unknown"
|
||||
// because the classifier had no arm for "unknown subcommand:" prose prefix.
|
||||
// Fix: added "unknown_subcommand" arm in classify_error_kind.
|
||||
let root = unique_temp_dir("unknown-subcommand-785");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
// "dump" is close enough to "dump-manifests" to trigger the typo suggestion path
|
||||
let output = run_claw(&root, &["--output-format", "json", "dump"], &[]);
|
||||
assert!(!output.status.success(), "unknown subcommand should fail");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let j: serde_json::Value = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("unknown subcommand should emit JSON error");
|
||||
assert_eq!(
|
||||
j["error_kind"], "unknown_subcommand",
|
||||
"unknown subcommand should return unknown_subcommand kind, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
// hint should point at the suggestion and/or --help
|
||||
let hint = j["hint"].as_str().unwrap_or("");
|
||||
assert!(
|
||||
hint.contains("dump-manifests") || hint.contains("--help") || hint.contains("claw"),
|
||||
"hint should reference the suggested subcommand or help, got: {hint:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_missing_dir_has_typed_kind_and_hint_786() {
|
||||
// #786: `claw dump-manifests --manifests-dir` (no value) and `--manifests-dir=` (empty)
|
||||
// both emitted plain "--manifests-dir requires a path" with error_kind:"unknown" + hint:null.
|
||||
// Fix: use missing_flag_value: prefix + \n usage hint.
|
||||
let root = unique_temp_dir("dump-manifests-missing-dir-786");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
// Case 1: --manifests-dir with no following value (next arg is --output-format)
|
||||
let out1 = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"dump-manifests",
|
||||
"--manifests-dir",
|
||||
"--output-format",
|
||||
"json",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(!out1.status.success());
|
||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||
let j1: serde_json::Value = stderr1
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("missing --manifests-dir value should emit JSON error");
|
||||
assert_eq!(
|
||||
j1["error_kind"], "missing_flag_value",
|
||||
"missing --manifests-dir value should be missing_flag_value, got {:?}",
|
||||
j1["error_kind"]
|
||||
);
|
||||
let h1 = j1["hint"]
|
||||
.as_str()
|
||||
.expect("missing_flag_value must have hint (#786)");
|
||||
assert!(
|
||||
h1.contains("dump-manifests") || h1.contains("manifests-dir"),
|
||||
"hint should reference dump-manifests usage, got: {h1:?}"
|
||||
);
|
||||
|
||||
// Case 2: --manifests-dir= with empty value
|
||||
let out2 = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"dump-manifests",
|
||||
"--manifests-dir=",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(!out2.status.success());
|
||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||
let j2: serde_json::Value = stderr2
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("empty --manifests-dir= should emit JSON error");
|
||||
assert_eq!(
|
||||
j2["error_kind"], "missing_flag_value",
|
||||
"empty --manifests-dir= should be missing_flag_value, got {:?}",
|
||||
j2["error_kind"]
|
||||
);
|
||||
let h2 = j2["hint"]
|
||||
.as_str()
|
||||
.expect("missing_flag_value must have hint (#786)");
|
||||
assert!(!h2.is_empty(), "hint must not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_directory_path_returns_typed_kind_and_hint_787() {
|
||||
// #787: `claw --resume /tmp` (directory instead of .jsonl file) returned
|
||||
// error_kind:"session_load_failed" + hint:null. The OS error "Is a directory (os error 21)"
|
||||
// had no \n delimiter so split_error_hint returned None, and the resume error path
|
||||
// didn't call fallback_hint_for_error_kind.
|
||||
// Fix: (1) added session_path_is_directory classifier arm for os error 21;
|
||||
// (2) wired fallback_hint_for_error_kind into both resume error emission sites.
|
||||
let root = unique_temp_dir("resume-dir-787");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
// Pass the root directory itself as the session path
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"--resume",
|
||||
root.to_str().unwrap(),
|
||||
"/status",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"resume with directory should fail"
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let j: serde_json::Value = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("resume with directory should emit JSON error");
|
||||
assert_eq!(
|
||||
j["error_kind"], "session_path_is_directory",
|
||||
"directory resume path should return session_path_is_directory, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
let hint = j["hint"]
|
||||
.as_str()
|
||||
.expect("session_path_is_directory must have hint (#787)");
|
||||
assert!(
|
||||
hint.contains(".jsonl") || hint.contains("session") || hint.contains("file"),
|
||||
"hint should explain expected path format, got: {hint:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skills_show_not_found_emits_single_json_object_788() {
|
||||
// #788: `claw --output-format json skills show no-such-skill` emitted TWO JSON objects:
|
||||
// one from the skills handler (action:"show", status:"error") and a second from the
|
||||
// top-level error handler (action:"abort"). The skills handler returned Err() after
|
||||
// printing its JSON, which caused the ? propagation to trigger a duplicate envelope.
|
||||
// Fix: exit(1) directly after the skills JSON is emitted instead of returning Err.
|
||||
let root = unique_temp_dir("skills-show-double-emit-788");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"skills",
|
||||
"show",
|
||||
"no-such-skill-xyz",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(!output.status.success(), "skills show unknown should fail");
|
||||
// Skills handler emits JSON to stdout; the duplicate was on stderr from the main error path.
|
||||
// After fix: stdout has 1 JSON object, stderr has none (no duplicate).
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// Count JSON objects in stdout — must be exactly 1
|
||||
let json_objects: Vec<serde_json::Value> = {
|
||||
let mut objects = Vec::new();
|
||||
let mut remaining = stdout.trim();
|
||||
while !remaining.is_empty() {
|
||||
match serde_json::from_str::<serde_json::Value>(remaining) {
|
||||
Ok(v) => {
|
||||
objects.push(v);
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
// Try finding a complete JSON object
|
||||
if let Some(pos) = remaining.find('{') {
|
||||
remaining = &remaining[pos..];
|
||||
let mut depth = 0i32;
|
||||
let mut end = 0;
|
||||
for (i, c) in remaining.char_indices() {
|
||||
match c {
|
||||
'{' => depth += 1,
|
||||
'}' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if end > 0 {
|
||||
if let Ok(v) = serde_json::from_str(&remaining[..end]) {
|
||||
objects.push(v);
|
||||
remaining = remaining[end..].trim_start();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
objects
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
json_objects.len(),
|
||||
1,
|
||||
"skills show not-found must emit exactly 1 JSON object on stdout, got {}. stdout: {} stderr: {}",
|
||||
json_objects.len(),
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
// Verify stderr has no duplicate error JSON (the pre-#788 bug was a second abort envelope here)
|
||||
let stderr_has_json = stderr.lines().any(|l| l.trim_start().starts_with('{'));
|
||||
assert!(
|
||||
!stderr_has_json,
|
||||
"stderr must have no duplicate JSON error envelope, got: {stderr}"
|
||||
);
|
||||
assert_eq!(
|
||||
json_objects[0]["error_kind"], "skill_not_found",
|
||||
"single JSON object must have skill_not_found error_kind"
|
||||
);
|
||||
assert_eq!(json_objects[0]["status"], "error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_show_not_found_exits_nonzero_789() {
|
||||
// #789: `claw --output-format json agents show <not-found>` returned exit 0 despite
|
||||
// emitting status:"error". print_agents had no error check — just println + Ok(()).
|
||||
// Skills was fixed in #788 (exit 1 via process::exit); agents/plugins had the same gap.
|
||||
let root = unique_temp_dir("agents-show-exit-789");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"agents",
|
||||
"show",
|
||||
"no-such-agent-xyz-789",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"agents show not-found must exit non-zero (#789), got exit 0"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("agents show should emit valid JSON");
|
||||
assert_eq!(j["error_kind"], "agent_not_found");
|
||||
assert_eq!(j["status"], "error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugins_show_not_found_exits_nonzero_789() {
|
||||
// #789: same as agents — `claw --output-format json plugins show <not-found>` exited 0
|
||||
// despite status:"error". The not-found branch used `return Ok(())` instead of exit(1).
|
||||
let root = unique_temp_dir("plugins-show-exit-789");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"plugins",
|
||||
"show",
|
||||
"no-such-plugin-xyz-789",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"plugins show not-found must exit non-zero (#789), got exit 0"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("plugins show should emit valid JSON");
|
||||
assert_eq!(j["error_kind"], "plugin_not_found");
|
||||
assert_eq!(j["status"], "error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_prompt_unknown_option_returns_typed_kind_790() {
|
||||
// #790: `claw --output-format json system-prompt bogus` returned error_kind:"unknown" + hint:null.
|
||||
// The unknown-option branch emitted plain "unknown system-prompt option: bogus" with no typed
|
||||
// prefix. Fix: use unknown_option: prefix + \n usage hint.
|
||||
let root = unique_temp_dir("system-prompt-unknown-opt-790");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
// Generic unknown option
|
||||
let out1 = run_claw(
|
||||
&root,
|
||||
&["--output-format", "json", "system-prompt", "bogus"],
|
||||
&[],
|
||||
);
|
||||
assert!(!out1.status.success());
|
||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||
let j1: serde_json::Value = stderr1
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("unknown option should emit JSON error");
|
||||
assert_eq!(
|
||||
j1["error_kind"], "unknown_option",
|
||||
"system-prompt unknown option should be unknown_option, got {:?}",
|
||||
j1["error_kind"]
|
||||
);
|
||||
let h1 = j1["hint"]
|
||||
.as_str()
|
||||
.expect("unknown_option must have hint (#790)");
|
||||
assert!(
|
||||
h1.contains("system-prompt") || h1.contains("claw"),
|
||||
"hint should reference system-prompt usage, got: {h1:?}"
|
||||
);
|
||||
|
||||
// Special --json case: hint should mention --output-format json
|
||||
let out2 = run_claw(
|
||||
&root,
|
||||
&["--output-format", "json", "system-prompt", "--json"],
|
||||
&[],
|
||||
);
|
||||
assert!(!out2.status.success());
|
||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||
let j2: serde_json::Value = stderr2
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("--json flag should emit JSON error");
|
||||
assert_eq!(j2["error_kind"], "unknown_option");
|
||||
let h2 = j2["hint"]
|
||||
.as_str()
|
||||
.expect("--json case must have hint (#790)");
|
||||
assert!(
|
||||
h2.contains("output-format") || h2.contains("json"),
|
||||
"hint for --json should suggest --output-format json, got: {h2:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_extra_args_have_non_null_hint_791() {
|
||||
// #791: `claw config show bogus-key` and `claw config set a b` returned
|
||||
// error_kind:"unexpected_extra_args" + hint:null because the error message
|
||||
// "unexpected extra arguments after `claw config ...`: ..." had no \n delimiter.
|
||||
// Fix: appended \n + usage hint to the format string.
|
||||
let root = unique_temp_dir("config-extra-args-791");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
// config show with extra positional arg
|
||||
let out1 = run_claw(
|
||||
&root,
|
||||
&["--output-format", "json", "config", "show", "bogus-key"],
|
||||
&[],
|
||||
);
|
||||
assert!(!out1.status.success());
|
||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||
let j1: serde_json::Value = stderr1
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("config show extra arg should emit JSON error");
|
||||
assert_eq!(
|
||||
j1["error_kind"], "unexpected_extra_args",
|
||||
"config show extra arg should be unexpected_extra_args, got {:?}",
|
||||
j1["error_kind"]
|
||||
);
|
||||
let h1 = j1["hint"]
|
||||
.as_str()
|
||||
.expect("unexpected_extra_args must have hint (#791)");
|
||||
assert!(
|
||||
h1.contains("config") || h1.contains("claw"),
|
||||
"hint should reference config usage, got: {h1:?}"
|
||||
);
|
||||
|
||||
// config set with extra positionals
|
||||
let out2 = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"config",
|
||||
"set",
|
||||
"bogus-section.key",
|
||||
"value",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(!out2.status.success());
|
||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||
let j2: serde_json::Value = stderr2
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("config set extra arg should emit JSON error");
|
||||
assert_eq!(j2["error_kind"], "unexpected_extra_args");
|
||||
assert!(
|
||||
j2["hint"].as_str().is_some_and(|h| !h.is_empty()),
|
||||
"config set extra arg must have non-null hint (#791)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_list_flag_shaped_filter_returns_unknown_option_792() {
|
||||
// #792: `claw --output-format json agents list --bogus-flag` silently returned
|
||||
// status:"ok" count:0 instead of an error. The list filter arm in
|
||||
// handle_agents_slash_command_json treated "--bogus-flag" as a name substring
|
||||
// filter (no agents match), producing a false-positive empty success result.
|
||||
// Fix: detect filter tokens starting with "-" and return unknown_option + hint.
|
||||
let root = unique_temp_dir("agents-list-flag-792");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"agents",
|
||||
"list",
|
||||
"--unknown-flag",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"agents list --unknown-flag must exit non-zero (#792)"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value = serde_json::from_str(stdout.trim())
|
||||
.expect("agents list flag-filter should emit valid JSON");
|
||||
assert_eq!(
|
||||
j["error_kind"], "unknown_option",
|
||||
"agents list flag-shaped filter must return unknown_option, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
assert_eq!(j["status"], "error");
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("unknown_option must have hint (#792)");
|
||||
assert!(
|
||||
h.contains("claw agents list") || h.contains("filter"),
|
||||
"hint should reference correct usage, got: {h:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skills_list_flag_shaped_filter_returns_unknown_option_792() {
|
||||
// #792: same gap as agents — `claw skills list --bogus-flag` returned success
|
||||
// with empty list instead of unknown_option error.
|
||||
let root = unique_temp_dir("skills-list-flag-792");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"skills",
|
||||
"list",
|
||||
"--unknown-flag",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"skills list --unknown-flag must exit non-zero (#792)"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value = serde_json::from_str(stdout.trim())
|
||||
.expect("skills list flag-filter should emit valid JSON");
|
||||
assert_eq!(
|
||||
j["error_kind"], "unknown_option",
|
||||
"skills list flag-shaped filter must return unknown_option, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
assert_eq!(j["status"], "error");
|
||||
assert!(
|
||||
j["hint"]
|
||||
.as_str()
|
||||
.is_some_and(|h| h.contains("claw skills list") || h.contains("filter")),
|
||||
"hint should reference correct usage (#792)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugins_list_flag_shaped_filter_returns_unknown_option_793() {
|
||||
// #793: `claw plugins list --bogus-flag` silently returned status:"ok" with empty
|
||||
// plugins list instead of an error. The list filter branch in print_plugins treated
|
||||
// "--bogus-flag" as an id substring filter and found no matches, producing a false-positive.
|
||||
// Fix: added flag-prefix guard; filter tokens starting with "-" now return unknown_option.
|
||||
let root = unique_temp_dir("plugins-list-flag-793");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"plugins",
|
||||
"list",
|
||||
"--unknown-flag",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"plugins list --unknown-flag must exit non-zero (#793)"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value = serde_json::from_str(stdout.trim())
|
||||
.expect("plugins list flag-filter should emit valid JSON");
|
||||
assert_eq!(
|
||||
j["error_kind"], "unknown_option",
|
||||
"plugins list flag-shaped filter must return unknown_option, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
assert_eq!(j["status"], "error");
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("unknown_option must have hint (#793)");
|
||||
assert!(
|
||||
h.contains("plugins list") || h.contains("filter"),
|
||||
"hint should reference plugins list usage, got: {h:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugins_uninstall_not_found_has_hint_793() {
|
||||
// #793: `claw plugins uninstall no-such-plugin` returned plugin_not_found + hint:null.
|
||||
// The error propagated from plugins_command_payload_for via ? with no \n delimiter;
|
||||
// split_error_hint returned None and plugin_not_found wasn't in the fallback table.
|
||||
// Fix: added "plugin_not_found" entry to fallback_hint_for_error_kind().
|
||||
let root = unique_temp_dir("plugins-uninstall-793");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"plugins",
|
||||
"uninstall",
|
||||
"no-such-xyz-793",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"plugins uninstall not-found must exit non-zero (#793)"
|
||||
);
|
||||
// Error envelope goes to stderr (propagated via ? to main error handler)
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let j: serde_json::Value = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("plugins uninstall not-found should emit JSON error envelope");
|
||||
assert_eq!(j["error_kind"], "plugin_not_found");
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("plugin_not_found must have non-null hint (#793)");
|
||||
assert!(
|
||||
h.contains("plugins list") || h.contains("claw plugins"),
|
||||
"hint should reference plugins list, got: {h:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugins_install_not_found_path_returns_typed_kind_794() {
|
||||
// #794: `claw plugins install /nonexistent/path` returned error_kind:"unknown" + hint:null.
|
||||
// The message "plugin source ... was not found" had no classifier arm; fell to "unknown".
|
||||
// Fix: added "plugin_source_not_found" classifier arm + fallback hint table entry.
|
||||
let root = unique_temp_dir("plugins-install-794");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"plugins",
|
||||
"install",
|
||||
"/nonexistent-path-xyz-794",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"plugins install not-found-path must exit non-zero (#794)"
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let j: serde_json::Value = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("plugins install not-found should emit JSON error envelope");
|
||||
assert_eq!(
|
||||
j["error_kind"], "plugin_source_not_found",
|
||||
"plugins install not-found should be plugin_source_not_found, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("plugin_source_not_found must have non-null hint (#794)");
|
||||
assert!(!h.is_empty(), "hint must be non-empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skills_install_not_found_and_unsupported_action_have_hints_795() {
|
||||
// #795: `claw skills install /nonexistent` returned skill_not_found + hint:null, and
|
||||
// `claw skills uninstall x` returned unsupported_skills_action + hint:null. Both error
|
||||
// kinds were missing from fallback_hint_for_error_kind table. Fix: added both entries.
|
||||
let root = unique_temp_dir("skills-install-795");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
// skills install with nonexistent local path
|
||||
let out1 = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"skills",
|
||||
"install",
|
||||
"/nonexistent-xyz-795",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!out1.status.success(),
|
||||
"skills install not-found must exit non-zero (#795)"
|
||||
);
|
||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||
let j1: serde_json::Value = stderr1
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("skills install not-found should emit JSON error");
|
||||
assert_eq!(
|
||||
j1["error_kind"], "skill_not_found",
|
||||
"skills install not-found should be skill_not_found, got {:?}",
|
||||
j1["error_kind"]
|
||||
);
|
||||
let h1 = j1["hint"]
|
||||
.as_str()
|
||||
.expect("skill_not_found must have non-null hint (#795)");
|
||||
assert!(
|
||||
h1.contains("skills list") || h1.contains("skills install"),
|
||||
"hint should reference skills commands, got: {h1:?}"
|
||||
);
|
||||
|
||||
// skills uninstall (unsupported action)
|
||||
let out2 = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"skills",
|
||||
"uninstall",
|
||||
"some-skill",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!out2.status.success(),
|
||||
"skills uninstall must exit non-zero (#795)"
|
||||
);
|
||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||
let j2: serde_json::Value = stderr2
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("skills uninstall should emit JSON error");
|
||||
assert_eq!(
|
||||
j2["error_kind"], "unsupported_skills_action",
|
||||
"skills uninstall should be unsupported_skills_action, got {:?}",
|
||||
j2["error_kind"]
|
||||
);
|
||||
let h2 = j2["hint"]
|
||||
.as_str()
|
||||
.expect("unsupported_skills_action must have non-null hint (#795)");
|
||||
assert!(!h2.is_empty(), "hint must be non-empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_show_extra_positional_arg_returns_unexpected_extra_796() {
|
||||
// #796: `claw agents show <name> <extra>` treated the full "name extra" as a single
|
||||
// agent name, producing agent_not_found for "name extra" instead of flagging the
|
||||
// unexpected extra argument. Fix: detect space-containing "name" and return
|
||||
// unexpected_extra_args with usage hint.
|
||||
let root = unique_temp_dir("agents-show-extra-796");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"agents",
|
||||
"show",
|
||||
"some-agent",
|
||||
"--extra-flag",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"agents show with extra arg must exit non-zero (#796)"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("agents show extra arg should emit valid JSON");
|
||||
assert_eq!(
|
||||
j["error_kind"], "unexpected_extra_args",
|
||||
"agents show extra arg should return unexpected_extra_args, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("unexpected_extra_args must have hint (#796)");
|
||||
assert!(
|
||||
h.contains("claw agents show") || h.contains("Usage"),
|
||||
"hint should reference usage, got: {h:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skills_show_extra_positional_arg_returns_unexpected_extra_796() {
|
||||
// #796: same gap as agents — `claw skills show <name> <extra>` treated "name extra"
|
||||
// as a single skill name → skill_not_found. Fix: detect space-containing name.
|
||||
let root = unique_temp_dir("skills-show-extra-796");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"skills",
|
||||
"show",
|
||||
"some-skill",
|
||||
"--extra-flag",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"skills show with extra arg must exit non-zero (#796)"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("skills show extra arg should emit valid JSON");
|
||||
assert_eq!(
|
||||
j["error_kind"], "unexpected_extra_args",
|
||||
"skills show extra arg should return unexpected_extra_args, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
assert!(
|
||||
j["hint"]
|
||||
.as_str()
|
||||
.is_some_and(|h| h.contains("claw skills show") || h.contains("Usage")),
|
||||
"hint should reference usage (#796)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugins_extra_args_have_non_null_hint_797() {
|
||||
// #797: `claw plugins show <name> <extra>` returned unexpected_extra_args + hint:null.
|
||||
// The plugins arg parser at the top level emitted "unexpected extra arguments after
|
||||
// `claw plugins show ...`: ..." with no \n delimiter. Parity with #791 config fix.
|
||||
let root = unique_temp_dir("plugins-extra-args-797");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(
|
||||
&root,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"plugins",
|
||||
"show",
|
||||
"some-plugin",
|
||||
"extra-arg",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"plugins show with extra arg must exit non-zero (#797)"
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let j: serde_json::Value = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("plugins extra arg should emit JSON error");
|
||||
assert_eq!(j["error_kind"], "unexpected_extra_args");
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("unexpected_extra_args must have non-null hint (#797)");
|
||||
assert!(
|
||||
h.contains("plugins") || h.contains("Usage"),
|
||||
"hint should reference plugins usage, got: {h:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_prompt_has_non_null_hint_798() {
|
||||
// #798: `claw --output-format json ""` returned empty_prompt + hint:null.
|
||||
// The error message "empty prompt: provide a subcommand..." had no \n delimiter.
|
||||
let root = unique_temp_dir("empty-prompt-798");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let output = run_claw(&root, &["--output-format", "json", ""], &[]);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"empty prompt must exit non-zero (#798)"
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let j: serde_json::Value = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("empty prompt should emit JSON error envelope");
|
||||
assert_eq!(j["error_kind"], "empty_prompt");
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("empty_prompt must have non-null hint (#798)");
|
||||
assert!(
|
||||
h.contains("claw") || h.contains("Usage"),
|
||||
"hint should reference usage, got: {h:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user