From 364e7909f4dcb9549fcab6ba046901cadb68c236 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 27 May 2026 05:09:07 +0900 Subject: [PATCH] fix(#779): resumed /skills invocation returns interactive_only error_kind + non-null hint --- ROADMAP.md | 2 + rust/crates/rusty-claude-cli/src/main.rs | 9 ++-- .../tests/output_format_contract.rs | 54 +++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9ce64300..36331c92 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7723,3 +7723,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 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. + +779. **Resumed `/skills ` invocation returned bare prose → `error_kind:"unknown"` + `hint:null` after #776** — dogfooded 2026-05-27 on `fded4f6b` (pinpoint by Gaebal-gajae). Sibling of #777: the `/skills` invoke-dispatch guard emitted a single-line prose error identical in structure to the pre-#777 plugins mutation guard. After #776's classify/split it fell to `unknown+null` because no `interactive_only:` prefix was present. Fix: replaced with `interactive_only: /skills {skill_name} invocation requires a live session.\n...hint...` format. Integration test `resume_skills_invocation_is_typed_interactive_only_779` added. 40 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `fded4f6b`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index b967d744..cdd3d944 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -4512,9 +4512,12 @@ fn run_resume_command( } SlashCommand::Skills { args } => { if let SkillSlashDispatch::Invoke(_) = classify_skills_slash_command(args.as_deref()) { - return Err( - "resumed /skills invocations are interactive-only; start `claw` and run `/skills ` in the REPL".into(), - ); + // #779: use interactive_only: prefix + \n hint so #776 classify/split emits + // error_kind:interactive_only + non-null hint instead of unknown+null. + let skill_name = args.as_deref().unwrap_or(""); + return Err(format!( + "interactive_only: /skills {skill_name} invocation requires a live session.\nStart `claw` and run `/skills {skill_name}` inside the REPL, or use `claw -p ` with skill context." + ).into()); } let cwd = env::current_dir()?; Ok(ResumeCommandOutcome { diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 02a0ec37..ea88b2cc 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -2275,3 +2275,57 @@ fn resume_plugin_mutations_are_typed_interactive_only_777() { ); } } + +#[test] +fn resume_skills_invocation_is_typed_interactive_only_779() { + // #779: `/skills ` invocation in resume mode returned bare prose; + // after #776 classify/split it fell to error_kind:"unknown" + hint:null. + // Fix: use interactive_only: prefix + \n hint so callers get typed fields. + let root = unique_temp_dir("resume-skills-invocation-779"); + fs::create_dir_all(&root).expect("temp dir should exist"); + std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(&root) + .output() + .ok(); + let session_file = write_session_fixture(&root, "resume-skills-779", None); + + // A non-empty skills arg that would classify as Invoke + let output = run_claw( + &root, + &[ + "--resume", + session_file.to_str().unwrap(), + "--output-format", + "json", + "/skills my-skill", + ], + &[], + ); + assert!( + !output.status.success(), + "/skills in resume mode should exit non-zero" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + let json_line = stderr + .lines() + .find(|l| l.trim_start().starts_with('{')) + .unwrap_or_else(|| { + panic!("/skills invocation should emit JSON error, got stderr: {stderr}") + }); + let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap(); + assert_eq!( + parsed["error_kind"], "interactive_only", + "resumed /skills invocation must return interactive_only, got {:?}", + parsed["error_kind"] + ); + let hint = parsed["hint"].as_str().unwrap_or(""); + assert!( + !hint.is_empty(), + "resumed /skills invocation must have non-null hint (#779)" + ); + assert!( + hint.contains("claw") || hint.contains("REPL") || hint.contains("skills"), + "hint must reference live session or CLI, got: {hint:?}" + ); +}