diff --git a/ROADMAP.md b/ROADMAP.md index 9394630b..8d5b09f9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7703,3 +7703,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 767. **`claw session bogus --output-format json` ignores JSON flag and falls through to credential check** — dogfooded 2026-05-27 on `d29a8e21`. `claw --output-format json session bogus` dispatches to the full interactive REPL runtime instead of rejecting `bogus` as an unknown session subcommand. Output is `error_kind:"missing_credentials"` rather than `error_kind:"unknown_session_subcommand"`. Root cause: `session` arg parser has no unknown-subcommand guard before dispatch; `bogus` is silently accepted as a session ID / switch target and reaches the credential-check gate. Fix needed: validate known session subcommands (`list`, `exists`, `switch`, `fork`, `delete`) before dispatch, return structured `unknown_session_subcommand` error for unrecognized tokens. [SCOPE: claw-code] Source: Jobdori probe on `d29a8e21`, 2026-05-27. 768. **`claw --resume latest compact` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `89735dbd` (gaebal-gajae pinpoint against `d29a8e21`, revised ID after #766/#767 landed). Resume trailing-arg validator emitted single-line `"--resume trailing arguments must be slash commands"` with no typed prefix and no `\n` hint. Fix: (1) changed error to `"invalid_resume_argument: \`{token}\` is not a slash command.\nUsage: claw --resume /"` so `split_error_hint()` extracts the hint; (2) added `invalid_resume_argument` classifier arm; (3) unit test assertion + integration test `resume_non_slash_trailing_arg_has_typed_error_kind_and_hint_768` added. 34 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae + Jobdori probe on `89735dbd`, 2026-05-27. + +769. **`claw session bogus` fell through to credential check instead of interactive-only guidance** — dogfooded 2026-05-27 on `b778d4e3` (tracked as #767). `claw session ` with more than one token bypassed `parse_single_word_command_alias` (which only fires for `rest.len()==1`) and had no match arm in `parse_args`, so `rest.join(" ")` became a prompt literal dispatched to `CliAction::Prompt`, hitting `missing_credentials` at the gate. Fix: added `"session"` match arm that emits `interactive_only:` error with `\n`-delimited hint referencing `--resume SESSION.jsonl /session` and REPL usage. Integration test `session_with_unknown_subcommand_returns_interactive_only_not_credentials_767` asserts `error_kind:interactive_only` + non-null hint for `bogus`, `nuke`, `delete-all`. 35 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori probe on `b778d4e3`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index fffa5eae..30fd1ab1 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1166,6 +1166,15 @@ fn parse_args(args: &[String]) -> Result { "`claw permissions` is a slash command. Start `claw` and run `/permissions` inside the REPL.\n Usage /permissions [read-only|workspace-write|danger-full-access]" .to_string(), ), + // #767: `claw session bogus` bypassed parse_single_word_command_alias (rest.len()>1), + // had no match arm, and fell to CliAction::Prompt — reaching the credential gate + // instead of a structured error. Mirror the guard on `permissions`. + "session" => { + let action_hint = rest.get(1).map_or(String::new(), |a| format!(" (got: `{a}`)" )); + Err(format!( + "interactive_only: `claw session` is a slash command{action_hint}.\nUse `claw --resume SESSION.jsonl /session ` or start `claw` and run `/session [list|exists|switch|fork|delete]`." + )) + } "skills" => { let args = join_optional_args(&rest[1..]); if let Some(action) = args.as_deref() { 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 d2d19699..bc4a1598 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -1980,3 +1980,42 @@ fn resume_non_slash_trailing_arg_has_typed_error_kind_and_hint_768() { "hint must reference slash-command usage, got: {hint:?}" ); } + +#[test] +fn session_with_unknown_subcommand_returns_interactive_only_not_credentials_767() { + // #767: `claw session bogus` bypassed all guards and fell through to + // CliAction::Prompt, reaching the credential-check gate and returning + // error_kind:"missing_credentials" instead of a structured routing error. + // Fix: explicit "session" match arm returns interactive_only guidance. + let root = unique_temp_dir("session-unknown-767"); + fs::create_dir_all(&root).expect("temp dir should exist"); + + for sub in &["bogus", "nuke", "delete-all"] { + let output = run_claw(&root, &["--output-format", "json", "session", sub], &[]); + assert!( + !output.status.success(), + "claw session {sub} should exit non-zero" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + let json_line = stderr + .lines() + .find(|l| l.trim_start().starts_with('{')) + .unwrap_or_else(|| panic!("claw session {sub} stderr should contain JSON")); + let parsed: serde_json::Value = + serde_json::from_str(json_line).expect("error envelope should be valid JSON"); + + assert_eq!( + parsed["error_kind"], "interactive_only", + "claw session {sub} must return error_kind:interactive_only (#767), not missing_credentials" + ); + let hint = parsed["hint"].as_str().unwrap_or(""); + assert!( + !hint.is_empty(), + "claw session {sub} must return non-null hint (#767)" + ); + assert!( + hint.contains("/session") || hint.contains("--resume"), + "hint must reference /session usage, got: {hint:?}" + ); + } +}