From e628b4bb683f2b5425625b17791abcd8f7059589 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 27 May 2026 08:07:32 +0900 Subject: [PATCH] fix(#784): export --output missing-value and extra-positional errors now return typed error_kind + non-null hint --- ROADMAP.md | 2 + rust/crates/rusty-claude-cli/src/main.rs | 10 ++- .../tests/output_format_contract.rs | 66 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index dbb89406..ace72b24 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7733,3 +7733,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 782. **`claw acp start` returned `error_kind:"unsupported_acp_invocation"` + `hint:null` — remediation text was on same line** — dogfooded 2026-05-27 on `16c1117a` (pinpoint by Gaebal-gajae). The error message `"unsupported ACP invocation. Use `claw acp`, `claw acp serve`, `claw --acp`, or `claw -acp`."` had no `\n` delimiter, so `split_error_hint` returned `hint:null`. Automation could tell ACP was unsupported but could not read the remediation structurally. Fix: inserted a `\n` before the remediation text: `"unsupported ACP invocation. Use ... claw -acp.\nACP/Zed editor integration is currently a discoverability alias only; ..."`. Integration test `acp_unsupported_invocation_has_hint_782` added. 41 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `16c1117a`, 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. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 118f42c6..b7eac2b6 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -349,8 +349,11 @@ 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("unexpected extra arguments") { + } else if message.starts_with("unexpected extra arguments") + || message.starts_with("unexpected_extra_args:") + { // #766: extra positionals after commands that take no arguments (e.g. claw diff) + // #784: export extra-positional errors use the typed prefix form "unexpected_extra_args" } else if message.starts_with("invalid_resume_argument:") { // #768: --resume trailing arg is not a slash command @@ -2113,7 +2116,7 @@ fn parse_export_args(args: &[String], output_format: CliOutputFormat) -> Result< "--output" | "-o" => { let value = args .get(index + 1) - .ok_or_else(|| format!("missing value for {}", args[index]))?; + .ok_or_else(|| format!("missing_flag_value: missing value for {}.\nUsage: claw export [PATH] [--session SESSION] [--output PATH]", args[index]))?; output_path = Some(PathBuf::from(value)); index += 2; } @@ -2129,7 +2132,8 @@ fn parse_export_args(args: &[String], output_format: CliOutputFormat) -> Result< index += 1; } other => { - return Err(format!("unexpected export argument: {other}")); + // #784: use typed prefix so classify_error_kind returns unexpected_extra_args + return Err(format!("unexpected_extra_args: unexpected export argument: {other}.\nUsage: claw export [PATH] [--session SESSION] [--output PATH]")); } } } diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 85a6b0bc..d8a79123 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -2436,3 +2436,69 @@ fn init_json_envelope_has_hint_and_already_initialized_783() { "re-init hint should acknowledge workspace exists, got: {hint2:?}" ); } + +#[test] +fn export_arg_errors_have_typed_kind_and_hint_784() { + // #784: `claw export --output` (missing flag value) returned error_kind:"unknown" + hint:null. + // `claw export a.md b.md` (extra positional) also returned unknown+null. + // Both export arg errors now use typed prefixes + usage hint. + let root = unique_temp_dir("export-arg-errors-784"); + fs::create_dir_all(&root).expect("temp dir"); + std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(&root) + .output() + .ok(); + + // Missing --output value + let out1 = run_claw( + &root, + &["--output-format", "json", "export", "--output"], + &[], + ); + assert!(!out1.status.success(), "--output with no value should fail"); + 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 --output should emit JSON error"); + assert_eq!( + j1["error_kind"], "missing_flag_value", + "missing --output value should be missing_flag_value, got {:?}", + j1["error_kind"] + ); + let h1 = j1["hint"] + .as_str() + .expect("missing_flag_value must have hint (#784)"); + assert!( + !h1.is_empty() && h1.contains("export"), + "hint must reference export usage, got: {h1:?}" + ); + + // Extra positional argument + let out2 = run_claw( + &root, + &["--output-format", "json", "export", "first.md", "second.md"], + &[], + ); + assert!(!out2.status.success(), "extra positional should fail"); + 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("extra positional should emit JSON error"); + assert_eq!( + j2["error_kind"], "unexpected_extra_args", + "extra positional should be unexpected_extra_args, got {:?}", + j2["error_kind"] + ); + let h2 = j2["hint"] + .as_str() + .expect("unexpected_extra_args must have hint (#784)"); + assert!( + !h2.is_empty() && h2.contains("export"), + "hint must reference export usage, got: {h2:?}" + ); +}