diff --git a/ROADMAP.md b/ROADMAP.md index 1ff7b1f1..9ce64300 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7721,3 +7721,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 776. **Resume-mode JSON errors had opaque `error_kind:"resume_command_error"` + `hint:null`** — dogfooded 2026-05-27 on `028998d0` (pinpoint identified by Gaebal-gajae). `run_resume_command` returned errors (e.g. from `parse_history_count`) with hardcoded `error_kind:"resume_command_error"` and the full error string in `error` with no hint extraction. Wrappers had to regex prose instead of switching on typed fields. Three co-located gaps fixed: (1) `resume_session` JSON error path now applies `classify_error_kind` + `split_error_hint` so errors get specific `error_kind` (e.g. `invalid_history_count`) and non-null `hint`; (2) `parse_history_count` errors now use `invalid_history_count:` prefix + `\n` usage hint; (3) `/session exists|delete|switch|fork` missing-arg and unsupported-action errors now use `\n`-delimited format with `unsupported_resumed_command:` prefix. Existing test updated to match new error message format. 38 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `028998d0`, 2026-05-27. 777. **Resumed `/plugins install|enable|disable|uninstall|update` returned opaque error_kind instead of interactive_only** — dogfooded 2026-05-27 on `2684737d` (pinpoint by Gaebal-gajae). The mutation arm in `run_resume_command` returned a bare single-line error; after #776 it was classified/split by the caller but fell to `error_kind:"unknown"` + `hint:null` because there was no `interactive_only:` prefix. Orchestrators had no stable signal to distinguish "command rejected — switch to REPL" from a transient error. Fix: each mutation verb now returns `interactive_only: /plugins {action} requires a live session...\n...hint...` so the caller emits `error_kind:"interactive_only"` + non-null hint pointing at REPL or direct CLI. Integration test `resume_plugin_mutations_are_typed_interactive_only_777` covers all 5 mutation verbs. 39 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `2684737d`, 2026-05-27. + +778. **`claw doctor --output-format json` check objects had no `hint` field — all warn/fail remediation was buried in `details_prose`** — dogfooded 2026-05-27 on `e0203036`. Automation had to parse prose strings to find remediation text instead of reading a stable `hint` field. `DiagnosticCheck.json_value()` never emitted a `hint` field. Fix: added `hint: Option` field to `DiagnosticCheck`, added `with_hint()` builder, populated for all warn/fail cases (auth: set env var; config: fix JSON syntax; workspace: git init; boot_preflight: install missing binaries; sandbox: expected on non-Linux). Empty hint string collapses to `null` (ok checks). 39 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori doctor-envelope probe on `e0203036`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 0a8e063f..b967d744 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -2212,6 +2212,9 @@ struct DiagnosticCheck { summary: String, details: Vec, data: Map, + /// #778: stable remediation hint for warn/fail checks so automation can read + /// a structured field instead of parsing details_prose. + hint: Option, } impl DiagnosticCheck { @@ -2222,6 +2225,7 @@ impl DiagnosticCheck { summary: summary.into(), details: Vec::new(), data: Map::new(), + hint: None, } } @@ -2235,6 +2239,14 @@ impl DiagnosticCheck { self } + fn with_hint(mut self, hint: impl Into) -> Self { + let h = hint.into(); + if !h.is_empty() { + self.hint = Some(h); + } + self + } + fn json_value(&self) -> Value { // Derive a stable snake_case id from the check name for machine-readable keying (#704). let id = self @@ -2297,6 +2309,14 @@ impl DiagnosticCheck { ), ), ]); + // #778: include hint field so automation can read remediation without parsing prose + value.insert( + "hint".to_string(), + self.hint + .as_deref() + .map(|h| Value::String(h.to_string())) + .unwrap_or(Value::Null), + ); value.extend(self.data.clone()); Value::Object(value) } @@ -2596,6 +2616,7 @@ fn check_auth_health() -> DiagnosticCheck { "Suggested action set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN; `claw login` is removed" .to_string(), ]) + .with_hint("Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN env var. The saved OAuth token is no longer accepted.") .with_data(Map::from_iter([ ("api_key_present".to_string(), json!(api_key_present)), ("auth_token_present".to_string(), json!(auth_token_present)), @@ -2624,6 +2645,7 @@ fn check_auth_health() -> DiagnosticCheck { }, ) .with_details(vec![env_details]) + .with_hint(if !any_auth_present { "Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN to authenticate." } else { "" }) .with_data(Map::from_iter([ ("api_key_present".to_string(), json!(api_key_present)), ("auth_token_present".to_string(), json!(auth_token_present)), @@ -2637,6 +2659,7 @@ fn check_auth_health() -> DiagnosticCheck { DiagnosticLevel::Fail, format!("failed to inspect legacy saved credentials: {error}"), ) + .with_hint("Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN env var to authenticate.") .with_data(Map::from_iter([ ("api_key_present".to_string(), json!(api_key_present)), ("auth_token_present".to_string(), json!(auth_token_present)), @@ -2728,6 +2751,7 @@ fn check_config_health( .map(|path| format!("Discovered file {path}")) .collect() }) + .with_hint("Fix the JSON syntax error in the listed config file, then rerun `claw doctor`.") .with_data(Map::from_iter([ ("discovered_files".to_string(), json!(discovered_paths)), ( @@ -2791,6 +2815,13 @@ fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck { "current directory is not inside a git project".to_string() }, ) + .with_hint(if !in_repo { + "Run `git init` to initialise a repository, or `cd` into a git project." + } else if stale_base_warning.is_some() { + "Rebase or merge to bring the branch up to date with its base." + } else { + "" + }) .with_details(vec![ format!("Cwd {}", context.cwd.display()), format!( @@ -2930,6 +2961,16 @@ fn check_boot_preflight_health(context: &StatusContext) -> DiagnosticCheck { preflight.summary(), ) .with_details(details) + .with_hint( + // #778: stable remediation hint for automation + if !preflight.repo_exists || !preflight.worktree_exists { + "Ensure you are inside a git worktree (`git init` or `git worktree add`)." + } else if !missing_binaries.is_empty() { + "Install the listed missing required binaries." + } else { + "" + }, + ) .with_data(Map::from_iter([( "boot_preflight".to_string(), preflight.json_value(), @@ -2964,6 +3005,16 @@ fn check_sandbox_health(status: &runtime::SandboxStatus) -> DiagnosticCheck { }, ) .with_details(details) + .with_hint( + // #778: stable remediation hint — sandbox degraded on non-Linux hosts is expected, not an error + if degraded && !status.supported { + "Sandbox namespace isolation requires Linux with `unshare`. On macOS/non-Linux hosts this warning is expected and can be ignored. Filesystem isolation is still active." + } else if degraded { + "Check that the `unshare` binary is available and the process has the required capabilities." + } else { + "" + }, + ) .with_data(Map::from_iter([ ("enabled".to_string(), json!(status.enabled)), ("active".to_string(), json!(status.active)),