Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -7888,3 +7888,9 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
**Required fix shape.** Introduce a typed `error_kind` for unrecognized slash commands (e.g. `unknown_slash_command` or `command_not_found`). Update the JSON emit in the `--resume` unknown-command handler to use the typed kind. Add regression coverage asserting the typed kind.

**Acceptance.** `claw --resume latest --output-format json /bogus` exits rc=2, stdout `error_kind:"unknown_slash_command"` (or similar typed constant), stderr empty. [SCOPE: claw-code]

828. **`/approve` and `/deny` outside REPL emit `unknown_slash_command` instead of `interactive_only`** — dogfooded 2026-05-29 16:05 on `main` `9d05573f`. `claw --output-format json /approve` exited rc=1 with `error_kind:"unknown_slash_command"` — these are valid REPL-only slash commands but are not `SlashCommand` enum variants, so they fell through to `format_unknown_direct_slash_command`. Machine consumers saw the wrong error class.

**Fix applied.** `SlashCommand::Unknown` arm now special-cases `approve | yes | y | deny | no | n` and emits `interactive_only:` prefix before falling through to `format_unknown_direct_slash_command`. Both `error_kind` and hint are correct.

**Acceptance.** `claw --output-format json /approve` exits rc=1, stdout `error_kind:"interactive_only"`, stderr empty. [SCOPE: claw-code]
15 changes: 14 additions & 1 deletion rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1739,7 +1739,20 @@ fn parse_direct_slash_cli_action(
}),
}
}
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
Ok(Some(SlashCommand::Unknown(name))) => {
// #828: /approve and /deny are valid REPL-only slash commands that
// are not SlashCommand enum variants (they require an active tool
// call in the REPL to be meaningful). Emit interactive_only so
// machine consumers see the correct error_kind instead of
// unknown_slash_command.
if matches!(name.as_str(), "approve" | "yes" | "y" | "deny" | "no" | "n") {
Err(format!(
"interactive_only: /{name} requires an active tool call in the REPL.\nStart `claw` and use /{name} to approve or deny a pending tool execution."
))
} else {
Err(format_unknown_direct_slash_command(&name))
}
}
Ok(Some(command)) => Err({
let _ = command;
format!(
Expand Down
22 changes: 22 additions & 0 deletions rust/crates/rusty-claude-cli/tests/output_format_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3993,3 +3993,25 @@ fn direct_unknown_slash_command_emits_typed_error_kind() {
"direct unknown slash JSON must have empty stderr (#827)"
);
}

// #828: /approve and /deny outside REPL must emit interactive_only, not unknown_slash_command
#[test]
fn approve_deny_outside_repl_emits_interactive_only() {
let root = unique_temp_dir("approve-deny-828");
std::fs::create_dir_all(&root).expect("create temp dir");
for cmd in &["/approve", "/yes", "/deny", "/no"] {
let output = run_claw(&root, &["--output-format", "json", cmd], &[]);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let j: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|_| panic!("{cmd} must emit JSON (#828), got: {stdout:?}"));
assert_eq!(
j["error_kind"], "interactive_only",
"{cmd} outside REPL must emit interactive_only (#828): {j}"
);
assert!(
stderr.is_empty(),
"{cmd} JSON must have empty stderr (#828): {stderr:?}"
);
}
}
Loading