mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-05-30 03:35:20 +08:00
Keep doctor help machine-discoverable locally (#3184)
Doctor help was already on the local help path in current source, but the exact #702 dogfood surface lacked a focused guard and the JSON help envelope was still too prose-oriented for wrappers. Strengthen the JSON contract while preserving text help.\n\nConstraint: Preserve unrelated dirty rust/Cargo.lock from prior #701 work.\nRejected: Starting runtime/provider/session to inspect doctor semantics | help must be local and credential-free.\nConfidence: high\nScope-risk: narrow\nDirective: Keep doctor help routed through parse_local_help_action and print_help_topic; do not call run_doctor for --help.\nTested: cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract doctor_help -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract help -- --nocapture; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p rusty-claude-cli; timeout 5s cargo run -q --bin claw -- --output-format json doctor --help; timeout 5s cargo run -q --bin claw -- doctor --help.\nNot-tested: full workspace test suite.
This commit is contained in:
parent
9ac66cbeb3
commit
73d8d6e638
11
ROADMAP.md
11
ROADMAP.md
@ -7783,3 +7783,14 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||||||
809. **Top-level help/version/MCP/plugin JSON spellings hang with zero stdout in trailing `--output-format json` form instead of returning bounded JSON/help or typed unsupported envelopes** — dogfooded 2026-05-27 on rebuilt main `db81598` (`cargo build --manifest-path rust/Cargo.toml -p rusty-claude-cli`; `claw --version` Git SHA `db81598`). `help --output-format json`, `version --output-format json`, `mcp --output-format json`, `mcp help --output-format json`, `plugins --output-format json`, and `plugins help --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Leading global-style probes (`--help --output-format json`, `--version --output-format json`) fail immediately as `[error-kind: cli_parse] unknown option`, so the hang is again in the trailing subcommand-style routing/startup path. **Required fix shape:** treat help/version/MCP/plugin discovery surfaces as bounded non-interactive control-plane commands; either return JSON help/list/version payloads or standard typed JSON unsupported envelopes with `error_kind`, non-null `hint`, and `message`; add timeout/nonzero-stdout regression coverage for the six trailing repro commands and parser-envelope coverage for leading global-style spellings. **Why this matters:** claws need safe scriptable help/version/plugin/MCP discovery before provider/session startup; silent hangs hide whether a command is unsupported, misparsed, or initializing runtime state. Source: gaebal-gajae 19:00 dogfood probe. [SCOPE: claw-code]
|
809. **Top-level help/version/MCP/plugin JSON spellings hang with zero stdout in trailing `--output-format json` form instead of returning bounded JSON/help or typed unsupported envelopes** — dogfooded 2026-05-27 on rebuilt main `db81598` (`cargo build --manifest-path rust/Cargo.toml -p rusty-claude-cli`; `claw --version` Git SHA `db81598`). `help --output-format json`, `version --output-format json`, `mcp --output-format json`, `mcp help --output-format json`, `plugins --output-format json`, and `plugins help --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Leading global-style probes (`--help --output-format json`, `--version --output-format json`) fail immediately as `[error-kind: cli_parse] unknown option`, so the hang is again in the trailing subcommand-style routing/startup path. **Required fix shape:** treat help/version/MCP/plugin discovery surfaces as bounded non-interactive control-plane commands; either return JSON help/list/version payloads or standard typed JSON unsupported envelopes with `error_kind`, non-null `hint`, and `message`; add timeout/nonzero-stdout regression coverage for the six trailing repro commands and parser-envelope coverage for leading global-style spellings. **Why this matters:** claws need safe scriptable help/version/plugin/MCP discovery before provider/session startup; silent hangs hide whether a command is unsupported, misparsed, or initializing runtime state. Source: gaebal-gajae 19:00 dogfood probe. [SCOPE: claw-code]
|
||||||
810. **TTY JSON success for `config`/`plugins --output-format json` contaminates stdout with deprecated-settings warnings before the JSON object** — dogfooded 2026-05-27 on rebuilt main `db81598` after #809. Under pseudo-TTY (`script -q -c "./rust/target/debug/claw config --output-format json"` and `plugins --output-format json`), the commands return rc `0` and bounded JSON, but stdout begins with `warning: /home/bellman/.claw/settings.json: field "enabledPlugins" is deprecated ...` before the JSON object (`first_json_index=121`). Parsing succeeds only after manually stripping the warning/prefix; raw stdout is not valid JSON. **Required fix shape:** in JSON mode, keep diagnostics/warnings on stderr or include structured warning fields inside the JSON envelope, but never prepend human warnings to stdout; add regression coverage that raw stdout from JSON commands parses from byte 0 under TTY and non-TTY modes. **Why this matters:** even when the TTY path avoids the hang from #807/#808/#809, claws and scripts still cannot safely `json.loads(stdout)` if configuration warnings are mixed into stdout. Source: gaebal-gajae 20:00 pseudo-TTY dogfood probe. [SCOPE: claw-code]
|
810. **TTY JSON success for `config`/`plugins --output-format json` contaminates stdout with deprecated-settings warnings before the JSON object** — dogfooded 2026-05-27 on rebuilt main `db81598` after #809. Under pseudo-TTY (`script -q -c "./rust/target/debug/claw config --output-format json"` and `plugins --output-format json`), the commands return rc `0` and bounded JSON, but stdout begins with `warning: /home/bellman/.claw/settings.json: field "enabledPlugins" is deprecated ...` before the JSON object (`first_json_index=121`). Parsing succeeds only after manually stripping the warning/prefix; raw stdout is not valid JSON. **Required fix shape:** in JSON mode, keep diagnostics/warnings on stderr or include structured warning fields inside the JSON envelope, but never prepend human warnings to stdout; add regression coverage that raw stdout from JSON commands parses from byte 0 under TTY and non-TTY modes. **Why this matters:** even when the TTY path avoids the hang from #807/#808/#809, claws and scripts still cannot safely `json.loads(stdout)` if configuration warnings are mixed into stdout. Source: gaebal-gajae 20:00 pseudo-TTY dogfood probe. [SCOPE: claw-code]
|
||||||
811. **Previously typed JSON error/list surfaces hang in plain non-TTY trailing `--output-format json` form instead of emitting their JSON envelopes** — dogfooded 2026-05-27 on rebuilt main `b0e94c9` after #810. In plain non-TTY automation, `agents list --bogus --output-format json`, `skills show does-not-exist --output-format json`, `plugins show does-not-exist --output-format json`, `diff --output-format json`, `sessions show does-not-exist --output-format json`, and `resume bogus --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Several of these surfaces had prior roadmap fixes for typed JSON/text envelopes, so this is a regression-class scriptability gap: the command-specific envelope may exist, but plain non-TTY trailing JSON invocation routes into interactive startup before reaching it. **Required fix shape:** ensure trailing `--output-format json` is honored before any interactive/provider/session startup for error/list surfaces; add plain non-TTY timeout regression coverage that asserts raw stdout is a parseable typed JSON envelope for the six repro commands, including `error_kind`, non-null `hint`, and `message` where applicable. **Why this matters:** claws primarily invoke CLI checks from non-TTY automation; a fix that only works in manual/TTY mode still leaves JSON error handling unusable for agents. Source: gaebal-gajae 20:30 dogfood probe. [SCOPE: claw-code]
|
811. **Previously typed JSON error/list surfaces hang in plain non-TTY trailing `--output-format json` form instead of emitting their JSON envelopes** — dogfooded 2026-05-27 on rebuilt main `b0e94c9` after #810. In plain non-TTY automation, `agents list --bogus --output-format json`, `skills show does-not-exist --output-format json`, `plugins show does-not-exist --output-format json`, `diff --output-format json`, `sessions show does-not-exist --output-format json`, and `resume bogus --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Several of these surfaces had prior roadmap fixes for typed JSON/text envelopes, so this is a regression-class scriptability gap: the command-specific envelope may exist, but plain non-TTY trailing JSON invocation routes into interactive startup before reaching it. **Required fix shape:** ensure trailing `--output-format json` is honored before any interactive/provider/session startup for error/list surfaces; add plain non-TTY timeout regression coverage that asserts raw stdout is a parseable typed JSON envelope for the six repro commands, including `error_kind`, non-null `hint`, and `message` where applicable. **Why this matters:** claws primarily invoke CLI checks from non-TTY automation; a fix that only works in manual/TTY mode still leaves JSON error handling unusable for agents. Source: gaebal-gajae 20:30 dogfood probe. [SCOPE: claw-code]
|
||||||
|
|
||||||
|
|
||||||
|
812. **`claw --output-format json doctor --help` must stay a local help fast path and never fall through into runtime/provider startup** — dogfooded 2026-05-28 04:01 UTC after #701 worktree drift. The reported repro was `cargo run -q --bin claw -- --output-format json doctor --help`, which did not produce local help promptly and had to be killed, while the positive control `cargo run -q --bin claw -- --output-format json --help` emitted valid JSON help. Fresh bounded repro on branch `fix/doctor-help-json-local` did not reproduce the hang on current code (`timeout 5s cargo run -q --bin claw -- --output-format json doctor --help` exited 0 with a `kind:"help"`/`status:"ok"` doctor help envelope), which means the parser fast path is present but under-tested for this exact dogfood surface.
|
||||||
|
|
||||||
|
**Pinpoint.** The guarded path is `rust/crates/rusty-claude-cli/src/main.rs`: global `--output-format json` is parsed before `rest`, `parse_local_help_action()` maps `doctor --help` to `CliAction::HelpTopic { topic: Doctor }`, and `print_help_topic()` must return without calling `run_doctor()`, `LiveCli`, provider setup, session resume, or runtime startup. The previous risk class is help fallthrough: treating `doctor --help` as prompt text or as `doctor` diagnostics would either hit provider/session startup or run checks instead of local help.
|
||||||
|
|
||||||
|
**Fix.** Keep the local help interception and strengthen the doctor JSON help contract with structured, machine-readable metadata: `usage`, `formats`, `local_only:true`, `requires_credentials:false`, `requires_provider_request:false`, `requires_session_resume:false`, `mutates_workspace:false`, `output_fields`, `check_names`, and `status_values`. Preserve prose-only text help for `claw doctor --help`.
|
||||||
|
|
||||||
|
**Acceptance.** `timeout 5s cargo run -q --bin claw -- --output-format json doctor --help` exits 0 and parses as JSON with `.kind=="help"`, `.command=="doctor"`, `.local_only==true`, `.requires_provider_request==false`, and `output_fields` containing `checks`. `timeout 5s cargo run -q --bin claw -- doctor --help` exits 0 with plaintext beginning `Doctor` and no JSON parsing requirement. Neither command starts a provider request or session resume.
|
||||||
|
|
||||||
|
**Verification.** Regression tests: `doctor_help_json_is_local_structured_and_bounded_702` and `doctor_help_text_stays_plaintext_and_local_702` in `rust/crates/rusty-claude-cli/tests/output_format_contract.rs`; focused command repros recorded in `/tmp/claw_doctor_help_json.out` and `/tmp/claw_doctor_help_json.err` during the doctor-help fix branch.
|
||||||
|
|||||||
@ -7885,10 +7885,51 @@ fn render_export_help_json() -> serde_json::Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_doctor_help_json() -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"kind": "help",
|
||||||
|
"action": "help",
|
||||||
|
"status": "ok",
|
||||||
|
"topic": "doctor",
|
||||||
|
"command": "doctor",
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"usage": "claw doctor [--output-format <format>]",
|
||||||
|
"purpose": "diagnose local auth, config, workspace, sandbox, boot preflight, and build metadata",
|
||||||
|
"formats": ["text", "json"],
|
||||||
|
"local_only": true,
|
||||||
|
"requires_credentials": false,
|
||||||
|
"requires_provider_request": false,
|
||||||
|
"requires_session_resume": false,
|
||||||
|
"mutates_workspace": false,
|
||||||
|
"output_fields": ["kind", "action", "status", "message", "report", "has_failures", "summary", "checks"],
|
||||||
|
"check_names": ["auth", "config", "install source", "workspace", "boot preflight", "sandbox", "system"],
|
||||||
|
"status_values": ["ok", "warn", "fail"],
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"name": "--output-format",
|
||||||
|
"value": "<format>",
|
||||||
|
"values": ["text", "json"],
|
||||||
|
"default": "text",
|
||||||
|
"description": "format for the doctor report or help envelope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "--help",
|
||||||
|
"aliases": ["-h"],
|
||||||
|
"description": "show help for the doctor command without running diagnostics"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"related": ["/doctor", "claw --resume latest /doctor"],
|
||||||
|
"message": render_help_topic(LocalHelpTopic::Doctor),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn render_help_topic_json(topic: LocalHelpTopic) -> serde_json::Value {
|
fn render_help_topic_json(topic: LocalHelpTopic) -> serde_json::Value {
|
||||||
if topic == LocalHelpTopic::Export {
|
if topic == LocalHelpTopic::Export {
|
||||||
return render_export_help_json();
|
return render_export_help_json();
|
||||||
}
|
}
|
||||||
|
if topic == LocalHelpTopic::Doctor {
|
||||||
|
return render_doctor_help_json();
|
||||||
|
}
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"kind": "help",
|
"kind": "help",
|
||||||
|
|||||||
@ -66,6 +66,52 @@ fn export_help_preserves_plaintext_in_text_mode_384() {
|
|||||||
serde_json::from_str::<Value>(&stdout).expect_err("text help should remain plaintext");
|
serde_json::from_str::<Value>(&stdout).expect_err("text help should remain plaintext");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doctor_help_json_is_local_structured_and_bounded_702() {
|
||||||
|
let root = unique_temp_dir("doctor-help-json-702");
|
||||||
|
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||||
|
|
||||||
|
let parsed = assert_json_command(&root, &["--output-format", "json", "doctor", "--help"]);
|
||||||
|
assert_eq!(parsed["kind"], "help");
|
||||||
|
assert_eq!(parsed["action"], "help");
|
||||||
|
assert_eq!(parsed["status"], "ok");
|
||||||
|
assert_eq!(parsed["topic"], "doctor");
|
||||||
|
assert_eq!(parsed["command"], "doctor");
|
||||||
|
assert_eq!(parsed["usage"], "claw doctor [--output-format <format>]");
|
||||||
|
assert_eq!(parsed["local_only"], true);
|
||||||
|
assert_eq!(parsed["requires_credentials"], false);
|
||||||
|
assert_eq!(parsed["requires_provider_request"], false);
|
||||||
|
assert_eq!(parsed["requires_session_resume"], false);
|
||||||
|
assert_eq!(parsed["mutates_workspace"], false);
|
||||||
|
|
||||||
|
let fields = parsed["output_fields"].as_array().expect("output_fields");
|
||||||
|
assert!(fields.iter().any(|field| field == "checks"));
|
||||||
|
let statuses = parsed["status_values"].as_array().expect("status_values");
|
||||||
|
assert!(statuses.iter().any(|status| status == "warn"));
|
||||||
|
let checks = parsed["check_names"].as_array().expect("check_names");
|
||||||
|
assert!(checks.iter().any(|check| check == "auth"));
|
||||||
|
assert!(checks.iter().any(|check| check == "boot preflight"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doctor_help_text_stays_plaintext_and_local_702() {
|
||||||
|
let root = unique_temp_dir("doctor-help-text-702");
|
||||||
|
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||||
|
|
||||||
|
let output = run_claw(&root, &["doctor", "--help"], &[]);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stdout:\n{}\n\nstderr:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8(output.stdout).expect("stdout utf8");
|
||||||
|
assert!(stdout.starts_with("Doctor\n"));
|
||||||
|
assert!(stdout.contains("Usage claw doctor"));
|
||||||
|
assert!(stdout.contains("no provider request or session resume required"));
|
||||||
|
serde_json::from_str::<Value>(&stdout).expect_err("text help should remain plaintext");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn version_emits_json_when_requested() {
|
fn version_emits_json_when_requested() {
|
||||||
let root = unique_temp_dir("version-json");
|
let root = unique_temp_dir("version-json");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user