From 9968a27e9202ab46c3f9e89d09b6e677ccbe4af5 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 27 May 2026 10:36:12 +0900 Subject: [PATCH] fix(#790): system-prompt unknown-option errors now return typed unknown_option kind + non-null hint --- ROADMAP.md | 2 + rust/crates/rusty-claude-cli/src/main.rs | 15 +++-- .../tests/output_format_contract.rs | 62 +++++++++++++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d37c5a11..0ebfd247 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7745,3 +7745,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 788. **`claw --output-format json skills show ` 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 ` and `plugins show ` 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 ` 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` 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. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index aae04f00..dd7c1aab 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -2091,11 +2091,16 @@ fn parse_system_prompt_args( } other => { // #152: hint `--output-format json` when user types `--json`. - let mut msg = format!("unknown system-prompt option: {other}"); - if other == "--json" { - msg.push_str("\nDid you mean `--output-format json`?"); - } - return Err(msg); + // #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 ] [--date ] [--output-format text|json]".to_string() + } else { + "Usage: claw system-prompt [--cwd ] [--date ] [--output-format text|json]".to_string() + }; + return Err(format!( + "unknown_option: unknown system-prompt option: {other}.\n{hint}" + )); } } } 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 868cdef3..774df458 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -2842,3 +2842,65 @@ fn plugins_show_not_found_exits_nonzero_789() { 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:?}" + ); +}