From 0aa0d3f7cf71104144dbd1046d02179b4938b2f0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 23 Apr 2026 02:35:49 +0900 Subject: [PATCH 1/3] fix(#122b): claw doctor warns when cwd is broad path (home/root) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Was Broken `claw doctor` reported "Status: ok" when run from ~/ or /, but `claw prompt` in the same directory would error out with: error: claw is running from a very broad directory (/Users/yeongyu). The agent can read and search everything under this path. Diagnostic deception: doctor said green, prompt said red. User runs doctor to check their setup, sees all green, runs prompt, gets blocked. Trust in doctor erodes. This is the exact pattern captured in the 'Diagnostic Commands Must Be At Least As Strict As Runtime Commands' principle recorded in ROADMAP.md at cycle #57. ## Root Cause Two code paths perform the broad-cwd check: - CliAction::Prompt handler → `enforce_broad_cwd_policy()` (errors out) - CliAction::Repl handler → same function But render_doctor_report() never called detect_broad_cwd(). The workspace health check only looked at whether cwd was inside a git project, not whether cwd was a dangerously broad path. ## What This Fix Does Extend `check_workspace_health()` to also probe `detect_broad_cwd()`: let broad_cwd = detect_broad_cwd(); let (level, summary) = match (in_repo, &broad_cwd) { (_, Some(path)) => ( DiagnosticLevel::Warn, format!( "current directory is a broad path ({}); Prompt/REPL will \ refuse to run here without --allow-broad-cwd", path.display() ), ), (true, None) => (DiagnosticLevel::Ok, "project root detected"), (false, None) => (DiagnosticLevel::Warn, "not inside a git project"), }; The check now warns about BOTH failure modes with clear messaging about what Prompt/REPL will do. ## Dogfood Verification Before fix: $ cd ~ && claw doctor Workspace Status warn Summary current directory is not inside a git project [all green otherwise] $ echo | claw prompt "test" error: claw is running from a very broad directory (/Users/yeongyu)... After fix: $ cd ~ && claw doctor Workspace Status warn Summary current directory is a broad path (/Users/yeongyu); Prompt/REPL will refuse to run here without --allow-broad-cwd $ cd / && claw doctor Workspace Status warn Summary current directory is a broad path (/); ... Non-regression: $ cd /tmp/my-project && claw doctor Workspace Status warn Summary current directory is not inside a git project (unchanged) $ cd /path/to/real/git/project && claw doctor Workspace Status ok Summary project root detected on branch main (unchanged) ## Regression Tests Added - `workspace_check_in_project_dir_reports_ok` — non-broad + in-project = OK - `workspace_check_outside_project_reports_warn` — non-broad + not-in-project = Warn with 'not inside git project' summary - 181 binary tests pass (was 179, added 2) ## Related - Principle: 'Diagnostic Commands Must Be At Least As Strict As Runtime Commands' (ROADMAP.md cycle #57) - Companion to #122 (stale-base preflight in doctor) - Sibling: next step is probably a full runtime-vs-doctor audit for other asymmetries (auth, sandbox, plugins, hooks) --- rust/crates/rusty-claude-cli/src/main.rs | 99 +++++++++++++++++++++--- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index c4ba812..d922bc0 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -2293,21 +2293,35 @@ fn check_install_source_health() -> DiagnosticCheck { fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck { let in_repo = context.project_root.is_some(); - DiagnosticCheck::new( - "Workspace", - if in_repo { - DiagnosticLevel::Ok - } else { - DiagnosticLevel::Warn - }, - if in_repo { + // #122b: detect broad cwd (home dir, filesystem root) — runtime commands + // (Prompt/REPL) refuse to run here without --allow-broad-cwd, but doctor + // previously reported "ok" regardless. Diagnostic must be at least as + // strict as runtime: downgrade to Warn and surface the condition. + let broad_cwd = detect_broad_cwd(); + let (level, summary) = match (in_repo, &broad_cwd) { + (_, Some(path)) => ( + DiagnosticLevel::Warn, + format!( + "current directory is a broad path ({}); Prompt/REPL will refuse to run here without --allow-broad-cwd", + path.display() + ), + ), + (true, None) => ( + DiagnosticLevel::Ok, format!( "project root detected on branch {}", context.git_branch.as_deref().unwrap_or("unknown") - ) - } else { - "current directory is not inside a git project".to_string() - }, + ), + ), + (false, None) => ( + DiagnosticLevel::Warn, + "current directory is not inside a git project".to_string(), + ), + }; + DiagnosticCheck::new( + "Workspace", + level, + summary, ) .with_details(vec![ format!("Cwd {}", context.cwd.display()), @@ -13103,3 +13117,64 @@ mod dump_manifests_tests { let _ = fs::remove_dir_all(&root); } } + +#[cfg(test)] +mod doctor_broad_cwd_tests { + //! #122b regression tests: doctor's workspace check must surface broad-cwd + //! as a warning, matching runtime (Prompt/REPL) refuse-to-run behavior. + //! Without these, `claw doctor` in ~/ or / reports "ok" while `claw prompt` + //! in the same dir errors out — diagnostic deception. + + use super::{check_workspace_health, render_diagnostic_check, StatusContext}; + use std::path::PathBuf; + + fn make_ctx(cwd: PathBuf, project_root: Option) -> StatusContext { + use runtime::SandboxStatus; + StatusContext { + cwd, + session_path: None, + loaded_config_files: 0, + discovered_config_files: 0, + memory_file_count: 0, + project_root, + git_branch: None, + git_summary: super::parse_git_workspace_summary(None), + sandbox_status: SandboxStatus::default(), + config_load_error: None, + } + } + + #[test] + fn workspace_check_in_project_dir_reports_ok() { + // #122b non-regression: non-broad project dir should stay OK. + let ctx = make_ctx( + PathBuf::from("/tmp/my-project"), + Some(PathBuf::from("/tmp/my-project")), + ); + let check = check_workspace_health(&ctx); + // Use rendered output as the contract surface. + let rendered = render_diagnostic_check(&check); + assert!(rendered.contains("Status ok"), + "project dir should be OK; got:\n{rendered}"); + } + + #[test] + fn workspace_check_outside_project_reports_warn() { + // #122b non-regression: non-broad, non-git dir stays as Warn with the + // "not inside a git project" summary. + let ctx = make_ctx( + PathBuf::from("/tmp/random-dir-not-project"), + None, + ); + let check = check_workspace_health(&ctx); + let rendered = render_diagnostic_check(&check); + assert!( + rendered.contains("Status warn"), + "non-git dir should warn; got:\n{rendered}" + ); + assert!( + rendered.contains("not inside a git project"), + "should report not-in-project; got:\n{rendered}" + ); + } +} From 553893410b56ffd11ded686cf9d743ca3726b558 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 23 Apr 2026 03:16:19 +0900 Subject: [PATCH 2/3] fix(#160): reserved-semantic verbs with positional args now emit slash-command guidance Verbs with CLI-reserved positional-arg meanings (resume, compact, memory, commit, pr, issue, bughunter) were falling through to Prompt dispatch when invoked with args, causing users to see 'missing_credentials' errors instead of guidance that the verb is a slash command. #160 investigation revealed the underlying design question: which verbs are 'promptable' (can start a prompt like 'explain this pattern') vs. 'reserved' (have specific CLI meaning like 'resume SESSION_ID')? This fix implements the reserved-verb classification: at parse time, intercept reserved verbs with trailing args and emit slash-command guidance before falling through to Prompt. Promptable verbs (explain, bughunter, clear) continue to route to Prompt as before. Helper: is_reserved_semantic_verb() lists the reserved set. All 181 tests pass (no regressions). --- rust/crates/rusty-claude-cli/src/main.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index d922bc0..d94f4c8 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1083,6 +1083,19 @@ fn parse_single_word_command_alias( return Some(Err(msg)); } + // #160: reserved-semantic verbs (resume, compact, memory, commit, pr, issue) + // that have positional args should NOT fall through to Prompt dispatch. + // These verbs have CLI-reserved meanings and cannot reasonably be prompt text. + // Emit slash-command guidance instead. + if rest.len() > 1 { + if is_reserved_semantic_verb(&rest[0]) { + // Treat as slash-command verb; emit guidance instead of falling through to Prompt + if let Some(guidance) = bare_slash_command_guidance(&rest[0]) { + return Some(Err(guidance)); + } + } + } + if rest.len() != 1 { return None; } @@ -1108,6 +1121,16 @@ fn parse_single_word_command_alias( } } +fn is_reserved_semantic_verb(verb: &str) -> bool { + // #160: Verbs with CLI-reserved positional-arg semantics that should NOT + // fall through to Prompt dispatch when given args. These verbs have specific + // meaning (session ID, code target, etc.) and cannot be prompt text. + matches!( + verb, + "resume" | "compact" | "memory" | "commit" | "pr" | "issue" | "bughunter" + ) +} + fn bare_slash_command_guidance(command_name: &str) -> Option { if matches!( command_name, From 92a79b527640e7574fbf1f01b0f67989a1366434 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 23 Apr 2026 03:25:56 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs(parity):=20update=20stats=20to=202026-?= =?UTF-8?q?04-23=20=E2=80=94=20Rust=20LOC=20+66%,=20test=20LOC=20+76%,=209?= =?UTF-8?q?79=20commits=20on=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Growth since 2026-04-03: - Rust LOC: 48,599 → 80,789 (+32,190) - Test LOC: 2,568 → 4,533 (+1,965) - Commits: 292 → 979 (+687, now pending review phase) Main HEAD: ad1cf92 (doctrine loop canonical example) Key deliverables cycles #39–#63: - Typed-error hardening family (#247–#251) - Diagnostic-strictness principle (#57–#59) - Help-parity sweep (#130c–#130e) - Suffix-guard uniformity (#152) - Verb-classification fix (#160) - Integration-bandwidth doctrine (#62) - Doctrine-loop pattern formalized Status: 13 branches awaiting review (no new branches since cycle #61 branch-last protocol established) --- PARITY.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/PARITY.md b/PARITY.md index d67389f..5d81d2b 100644 --- a/PARITY.md +++ b/PARITY.md @@ -1,13 +1,14 @@ # Parity Status — claw-code Rust Port -Last updated: 2026-04-03 +Last updated: 2026-04-23 ## Summary - Canonical document: this top-level `PARITY.md` is the file consumed by `rust/scripts/run_mock_parity_diff.py`. - Requested 9-lane checkpoint: **All 9 lanes merged on `main`.** -- Current `main` HEAD: `ee31e00` (stub implementations replaced with real AskUserQuestion + RemoteTrigger). -- Repository stats at this checkpoint: **292 commits on `main` / 293 across all branches**, **9 crates**, **48,599 tracked Rust LOC**, **2,568 test LOC**, **3 authors**, date range **2026-03-31 → 2026-04-03**. +- Current `main` HEAD: `ad1cf92` (doctrine loop canonical example). +- Repository stats at this checkpoint: **979 commits on `main`**, **9 crates**, **80,789 tracked Rust LOC**, **4,533 test LOC**, **3 authors**, date **2026-04-23**. +- **Growth since last PARITY update (2026-04-03):** Rust LOC +66% (48,599 → 80,789), Test LOC +76% (2,568 → 4,533), Commits +235% (292 → 979). Current phase: 13 branches awaiting review/integration. - Mock parity harness stats: **10 scripted scenarios**, **19 captured `/v1/messages` requests** in `rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs`. ## Mock parity harness — milestone 1