Skip to content

Commit 0e8a449

Browse files
committed
fix+test(#755): -p consumes exactly one token; flags after prompt text now parse normally
1 parent c70312b commit 0e8a449

3 files changed

Lines changed: 116 additions & 17 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7675,3 +7675,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
76757675
753. **`claw --output-format json -p` (no prompt arg) returned `error_kind:"unknown"` and `hint: null`** — parity gap with #750/#751 which fixed the explicit `prompt` verb. Identified by gaebal-gajae on `ddc71b56`. Fix: same `missing_prompt:` prefix + newline usage hint as #750. Regression guard: `short_p_flag_no_arg_json_error_kind_753` asserting nonzero exit, `error_kind:"missing_prompt"`, non-empty hint mentioning `claw -p` or `claw prompt`. Source: gaebal-gajae dogfood on `ddc71b56`, 2026-05-26.
76767676

76777677
754. **`missing_credentials` JSON envelope always had `hint: null` even when a contextual hint was available** — dogfooded 2026-05-26 on `e9327135`. `ApiError::Display` for `MissingCredentials` appended the hint via ` — hint: {hint}` (inline, no `\n`), so `split_error_hint()` could not extract it and left the JSON `hint` field null. Fix: change delimiter from ` — hint: ` to `\n` in `api/src/error.rs` Display impl; update two tests in `api/src/error.rs` and `api/src/providers/mod.rs` to assert newline separator. Source: Jobdori dogfood on `e9327135`, 2026-05-26.
7678+
7679+
755. **`claw -p hello --model sonnet` swallowed `--model sonnet` into the prompt string** — gaebal-gajae pinpoint on `e9327135` (#117 revival). `-p` used `args[index+1..].join(" ")`, consuming all remaining tokens as prompt. Fix: capture exactly one token via `args.get(index+1)`, reject flag-like tokens (`starts_with('-')`) as `missing_prompt`, support `--` sentinel for literal flag-text, then `continue` the flag loop so `--model`/`--output-format`/etc. parse normally. Dispatch via `short_p_prompt` after full flag scan. Regression guard: `short_p_flag_swallows_no_flags_755` asserts `--output-format json` is parsed (not swallowed) and `--model` as prompt-arg is rejected. Source: gaebal-gajae dogfood on `e9327135`, 2026-05-26.

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,9 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
742742
let mut base_commit: Option<String> = None;
743743
let mut reasoning_effort: Option<String> = None;
744744
let mut allow_broad_cwd = false;
745+
// #755: -p prompt text captured as single token; remaining args continue
746+
// flag parsing. None until `-p <text>` is seen.
747+
let mut short_p_prompt: Option<String> = None;
745748
let mut rest: Vec<String> = Vec::new();
746749
let mut index = 0;
747750

@@ -858,24 +861,40 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
858861
index += 1;
859862
}
860863
"-p" => {
861-
// Claw Code compat: -p "prompt" = one-shot prompt
862-
let prompt = args[index + 1..].join(" ");
863-
if prompt.trim().is_empty() {
864-
// #753: same missing_prompt shape as claw prompt (no arg) fix in #750
865-
return Err("missing_prompt: -p requires a prompt string.\nUsage: claw -p <text> or claw prompt <text>".to_string());
864+
// Claw Code compat: -p "prompt" = one-shot prompt.
865+
// #755: consume exactly one token so subsequent flags like
866+
// --model/--output-format are parsed normally instead of
867+
// being swallowed into the prompt string (#117).
868+
let next = args.get(index + 1).map(|s| s.as_str());
869+
match next {
870+
None | Some("") => {
871+
return Err("missing_prompt: -p requires a prompt string.\nUsage: claw -p <text> or claw prompt <text>".to_string());
872+
}
873+
Some(tok) if tok.starts_with('-') && tok != "--" => {
874+
// Looks like a flag, not a prompt. Reject so the user
875+
// knows to quote the literal text or use `--`.
876+
return Err(format!(
877+
"missing_prompt: -p requires a prompt string before flags; got `{tok}`.\nUsage: claw -p <text> --model sonnet or claw -p -- {tok} (literal)"
878+
));
879+
}
880+
Some(tok) => {
881+
// `--` sentinel: skip it and take the token after as literal
882+
let (prompt_text, skip) = if tok == "--" {
883+
match args.get(index + 2) {
884+
Some(t) => (t.as_str(), 3usize),
885+
None => return Err("missing_prompt: -p -- requires a prompt string after `--`.\nUsage: claw -p -- <text>".to_string()),
886+
}
887+
} else {
888+
(tok, 2usize)
889+
};
890+
if prompt_text.trim().is_empty() {
891+
return Err("missing_prompt: -p requires a non-empty prompt string.\nUsage: claw -p <text> or claw prompt <text>".to_string());
892+
}
893+
short_p_prompt = Some(prompt_text.to_string());
894+
index += skip;
895+
continue;
896+
}
866897
}
867-
return Ok(CliAction::Prompt {
868-
prompt,
869-
model: resolve_model_alias_with_config(&model),
870-
output_format,
871-
allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
872-
permission_mode: permission_mode_override
873-
.unwrap_or_else(default_permission_mode),
874-
compact,
875-
base_commit: base_commit.clone(),
876-
reasoning_effort: reasoning_effort.clone(),
877-
allow_broad_cwd,
878-
});
879898
}
880899
"--print" => {
881900
// Claw Code compat: --print makes output non-interactive
@@ -965,6 +984,21 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
965984

966985
let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
967986

987+
// #755: -p consumed exactly one token; dispatch now that all flags are parsed
988+
if let Some(prompt) = short_p_prompt {
989+
return Ok(CliAction::Prompt {
990+
prompt,
991+
model: resolve_model_alias_with_config(&model),
992+
output_format,
993+
allowed_tools,
994+
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
995+
compact,
996+
base_commit,
997+
reasoning_effort,
998+
allow_broad_cwd,
999+
});
1000+
}
1001+
9681002
if rest.is_empty() {
9691003
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
9701004
// When stdin is not a terminal (pipe/redirect) and no prompt is given on the

rust/crates/rusty-claude-cli/tests/output_format_contract.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,6 +1504,69 @@ fn prompt_no_arg_json_error_kind_750() {
15041504
);
15051505
}
15061506

1507+
#[test]
1508+
fn short_p_flag_swallows_no_flags_755() {
1509+
// #755: `claw -p hello --output-format json` must parse --output-format json
1510+
// as a flag rather than swallowing it as part of the prompt. Before #755,
1511+
// args[index+1..].join(" ") consumed all remaining tokens into the prompt.
1512+
// After #755, -p consumes exactly one token and remaining flags are parsed.
1513+
// We verify by checking that the envelope IS JSON (meaning --output-format json
1514+
// was interpreted as a flag, not literal prompt text).
1515+
use std::process::Command;
1516+
let root = unique_temp_dir("short-p-flags");
1517+
fs::create_dir_all(&root).expect("temp dir");
1518+
let bin = env!("CARGO_BIN_EXE_claw");
1519+
1520+
// -p hello --output-format json: with no credentials, should fail with
1521+
// missing_credentials (not missing_prompt), proving --output-format json was parsed.
1522+
let output = Command::new(bin)
1523+
.current_dir(&root)
1524+
.args(["-p", "hello", "--output-format", "json"])
1525+
.env_remove("ANTHROPIC_API_KEY")
1526+
.env_remove("ANTHROPIC_AUTH_TOKEN")
1527+
.output()
1528+
.expect("claw -p should run");
1529+
assert!(
1530+
!output.status.success(),
1531+
"claw -p hello --output-format json must exit non-zero (no credentials)"
1532+
);
1533+
let raw = String::from_utf8_lossy(&output.stderr)
1534+
.lines()
1535+
.filter(|l| l.starts_with('{'))
1536+
.collect::<Vec<_>>()
1537+
.join("");
1538+
// Must be valid JSON (i.e. --output-format json was parsed, not swallowed)
1539+
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap_or_else(|_| {
1540+
panic!("--output-format json must be parsed as a flag, not prompt text; stderr: {raw}")
1541+
});
1542+
assert_eq!(
1543+
parsed["error_kind"], "missing_credentials",
1544+
"flags after -p prompt text must be parsed normally (#755); got: {parsed}"
1545+
);
1546+
1547+
// Also verify -p --model bogus is rejected as missing_prompt (flag-as-prompt guard)
1548+
let output2 = Command::new(bin)
1549+
.current_dir(&root)
1550+
.args(["--output-format", "json", "-p", "--model", "sonnet"])
1551+
.output()
1552+
.expect("claw -p flag-as-prompt should run");
1553+
let raw2 = String::from_utf8_lossy(&output2.stderr)
1554+
.lines()
1555+
.filter(|l| l.starts_with('{'))
1556+
.collect::<Vec<_>>()
1557+
.join("");
1558+
let parsed2: serde_json::Value = serde_json::from_str(&raw2)
1559+
.unwrap_or_else(|_| panic!("claw -p --model must emit JSON; got: {raw2}"));
1560+
assert_eq!(
1561+
parsed2["error_kind"], "missing_prompt",
1562+
"flag-like token after -p must be rejected as missing_prompt (#755): {parsed2}"
1563+
);
1564+
assert!(
1565+
parsed2["hint"].as_str().map_or(false, |h| !h.is_empty()),
1566+
"missing_prompt hint must be non-empty (#755)"
1567+
);
1568+
}
1569+
15071570
#[test]
15081571
fn short_p_flag_no_arg_json_error_kind_753() {
15091572
// #753: `claw --output-format json -p` (no prompt) must emit error_kind:"missing_prompt"

0 commit comments

Comments
 (0)