mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-27 02:44:56 +08:00
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing
Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed
at fbcbe9d). Closes the two typed-error contract drifts surfaced in that
pinpoint against the Rust `claw` binary.
## What was wrong
1. `classify_error_kind()` (main.rs:~251) used substring matching but did
NOT match two common prompt-related parse errors:
- "prompt subcommand requires a prompt string"
- "empty prompt: provide a subcommand..."
Both fell through to `"unknown"`. §4.44 typed-error contract specifies
`parse | usage | unknown` as distinct classes, so claws dispatching on
`error.kind == "cli_parse"` missed those paths entirely.
2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode
appends it at stderr-print time (main.rs:~234) AFTER split_error_hint()
has already serialized the envelope, so JSON consumers never saw it.
Text-mode humans got an actionable pointer; machine consumers did not.
## Fix
Two small, targeted edits:
1. `classify_error_kind()`: add explicit branches for "prompt subcommand
requires" and "empty prompt:" (the latter anchored with `starts_with`
so it never hijacks unrelated error messages containing the word).
Both route to `cli_parse`.
2. JSON error render path in `main()`: after calling split_error_hint(),
if the message carried no embedded hint AND kind is `cli_parse` AND
the short-reason does not already embed a `claw --help` pointer,
synthesize the same `Run `claw --help` for usage.` trailer that
text-mode stderr appends. The embedded-pointer check prevents
duplication on the `empty prompt: ... (run `claw --help`)` message
which already carries inline guidance.
## Verification
Direct repro on the compiled binary:
$ claw --output-format json prompt
{"error":"prompt subcommand requires a prompt string",
"hint":"Run `claw --help` for usage.",
"kind":"cli_parse","type":"error"}
$ claw --output-format json ""
{"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string",
"hint":null,"kind":"cli_parse","type":"error"}
$ claw --output-format json doctor --foo # regression guard
{"error":"unrecognized argument `--foo` for subcommand `doctor`",
"hint":"Run `claw --help` for usage.",
"kind":"cli_parse","type":"error"}
Text mode unchanged in shape; `[error-kind: ...]` prefix now reads
`cli_parse` for the two previously-misclassified paths.
## Regression coverage
- Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks
both patterns route to `cli_parse` AND that generic "prompt"-containing
messages still fall through to `unknown`.
- Integration tests in `tests/output_format_contract.rs`:
* prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247
* empty_positional_arg_emits_cli_parse_envelope_247
* whitespace_only_positional_arg_emits_cli_parse_envelope_247
* unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard
- Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15
output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli).
## Family / related
Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181,
and now **#247**. All four quartet items now have real fixes landed on
the canonical binary surface rather than only the Python harness.
ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
This commit is contained in:
@@ -213,7 +213,16 @@ fn main() {
|
||||
// #77: classify error by prefix so downstream claws can route without
|
||||
// regex-scraping the prose. Split short-reason from hint-runbook.
|
||||
let kind = classify_error_kind(&message);
|
||||
let (short_reason, hint) = split_error_hint(&message);
|
||||
let (short_reason, mut hint) = split_error_hint(&message);
|
||||
// #247: JSON envelope was losing the `Run claw --help for usage.`
|
||||
// trailer that text-mode stderr includes. When the error is a
|
||||
// cli_parse and the message itself carried no embedded hint,
|
||||
// synthesize the trailer so typed-error consumers get the same
|
||||
// actionable pointer that text-mode users see. Cross-channel
|
||||
// consistency is a §4.44 typed-envelope contract requirement.
|
||||
if hint.is_none() && kind == "cli_parse" && !short_reason.contains("`claw --help`") {
|
||||
hint = Some("Run `claw --help` for usage.".to_string());
|
||||
}
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
@@ -264,6 +273,12 @@ fn classify_error_kind(message: &str) -> &'static str {
|
||||
"no_managed_sessions"
|
||||
} else if message.contains("unrecognized argument") || message.contains("unknown option") {
|
||||
"cli_parse"
|
||||
} else if message.contains("prompt subcommand requires") {
|
||||
// #247: `claw prompt` with no argument — a parse error, not `unknown`.
|
||||
"cli_parse"
|
||||
} else if message.starts_with("empty prompt:") {
|
||||
// #247: `claw ""` or `claw " "` — a parse error, not `unknown`.
|
||||
"cli_parse"
|
||||
} else if message.contains("invalid model syntax") {
|
||||
"invalid_model_syntax"
|
||||
} else if message.contains("is not yet implemented") {
|
||||
@@ -10434,6 +10449,32 @@ mod tests {
|
||||
assert_eq!(classify_error_kind("something completely unknown"), "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_error_kind_covers_prompt_parse_errors_247() {
|
||||
// #247: prompt-related parse errors must classify as `cli_parse`,
|
||||
// not fall through to `unknown`. Regression guard for ROADMAP #247
|
||||
// (typed-error contract drift found in cycle #33 dogfood).
|
||||
assert_eq!(
|
||||
classify_error_kind("prompt subcommand requires a prompt string"),
|
||||
"cli_parse",
|
||||
"bare `claw prompt` must surface as cli_parse so typed-error consumers can dispatch"
|
||||
);
|
||||
assert_eq!(
|
||||
classify_error_kind(
|
||||
"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string"
|
||||
),
|
||||
"cli_parse",
|
||||
"`claw \"\"` must surface as cli_parse, not unknown"
|
||||
);
|
||||
// Sanity: the new patterns must be specific enough not to hijack
|
||||
// genuinely unknown errors that happen to contain the word `prompt`.
|
||||
assert_eq!(
|
||||
classify_error_kind("some random prompt-adjacent failure we don't recognize"),
|
||||
"unknown",
|
||||
"generic prompt-containing text should still fall through to unknown"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_error_hint_separates_reason_from_runbook() {
|
||||
// #77: short reason / hint separation for JSON error payloads
|
||||
|
||||
Reference in New Issue
Block a user