Skip to content

Commit f877aca

Browse files
committed
feat: ultraworkers#146 — wire claw config and claw diff as standalone subcommands
## Problem `claw config` and `claw diff` are pure-local read-only introspection commands (config merges .claw.json + .claw/settings.json from disk; diff shells out to `git diff --cached` + `git diff`). Neither needs a session context, yet both rejected direct CLI invocation: $ claw config error: `claw config` is a slash command. Use `claw --resume SESSION.jsonl /config` ... $ claw diff error: `claw diff` is a slash command. ... This forced clawing operators to spin up a full session just to inspect static disk state, and broke natural pipelines like `claw config --output-format json | jq`. ## Root cause Sibling of ultraworkers#145: `SlashCommand::Config { section }` and `SlashCommand::Diff` had working renderers (`render_config_report`, `render_config_json`, `render_diff_report`, `render_diff_json_for`) exposed for resume sessions, but the top-level CLI parser in `parse_subcommand()` had no arms for them. Zero-arg `config`/`diff` hit `parse_single_word_command_alias`'s fallback to `bare_slash_command_guidance`, producing the misleading guidance. ## Changes ### rust/crates/rusty-claude-cli/src/main.rs - Added `CliAction::Config { section, output_format }` and `CliAction::Diff { output_format }` variants. - Added `"config"` / `"diff"` arms to the top-level parser in `parse_subcommand()`. `config` accepts an optional section name (env|hooks|model|plugins) matching SlashCommand::Config semantics. `diff` takes no positional args. Both reject extra trailing args with a clear error. - Added `"config" | "diff" => None` to `parse_single_word_command_alias` so bare invocations fall through to the new parser arms instead of the slash-guidance error. - Added dispatch in run() that calls existing renderers: text mode uses `render_config_report` / `render_diff_report`; JSON mode uses `render_config_json` / `render_diff_json_for` with `serde_json::to_string_pretty`. - Added 5 regression assertions in parse_args test covering: parse_args(["config"]), parse_args(["config", "env"]), parse_args(["config", "--output-format", "json"]), parse_args(["diff"]), parse_args(["diff", "--output-format", "json"]). ### ROADMAP.md Added Pinpoint ultraworkers#146 documenting the gap, verification, root cause, fix shape, and acceptance. Explicitly notes which other slash commands (`hooks`, `usage`, `context`, etc.) are NOT candidates because they are session-state-modifying. ## Live verification $ claw config # no config files Config Working directory /private/tmp/cd-146-verify Loaded files 0 Merged keys 0 Discovered files user missing ... project missing ... local missing ... Exit 0. $ claw config --output-format json { "cwd": "...", "files": [...], ... } $ claw diff # no git Diff Result no git repository Detail ... Exit 0. $ claw diff --output-format json # inside claw-code { "kind": "diff", "result": "changes", "staged": "", "unstaged": "diff --git ..." } Exit 0. ## Tests - rusty-claude-cli bin: 177 tests pass (5 new assertions in parse_args) - Full workspace green except pre-existing resume_latest flake (unrelated) ## Not changed `hooks`, `usage`, `context`, `tasks`, `theme`, `voice`, `rename`, `copy`, `color`, `effort`, `branch`, `rewind`, `ide`, `tag`, `output-style`, `add-dir` — all session-mutating or interactive-only; correctly remain slash-only. Closes ROADMAP ultraworkers#146.
1 parent 7d63699 commit f877aca

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

ROADMAP.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5578,3 +5578,44 @@ MCP
55785578
**Blocker.** None. `CliAction::Plugins` already exists with a working dispatcher.
55795579

55805580
**Source.** Jobdori dogfood 2026-04-21 19:30 KST on main HEAD `faeaa1d` in response to Clawhip nudge. Joins **prompt misdelivery** cluster. Session tally: ROADMAP #145.
5581+
5582+
## Pinpoint #146. `claw config` and `claw diff` are pure-local introspection commands but require `--resume SESSION.jsonl` wrapping
5583+
5584+
**Gap.** Running `claw config` or `claw diff` directly exits with an error pointing to `claw --resume SESSION.jsonl /config` as the only path. Both commands are pure, read-only introspection: `config` reads files from disk and merges them; `diff` shells out to `git diff --cached` + `git diff`. Neither needs a session context to produce correct output.
5585+
5586+
**Verified on main HEAD `7d63699` (2026-04-21 20:03 KST):**
5587+
5588+
```
5589+
$ claw config
5590+
error: `claw config` is a slash command. Use `claw --resume SESSION.jsonl /config` or start `claw` and run `/config`.
5591+
5592+
$ claw config --output-format json
5593+
{"error":"`claw config` is a slash command. ...","type":"error"}
5594+
5595+
$ claw diff
5596+
error: `claw diff` is a slash command. Use `claw --resume SESSION.jsonl /diff` or start `claw` and run `/diff`.
5597+
```
5598+
5599+
Meanwhile `agents`, `mcp`, `skills`, `status`, `doctor`, `sandbox`, `plugins` (after #145) all work standalone.
5600+
5601+
**Why this is a clawability gap.**
5602+
1. **Synthetic friction**: requires a session file to inspect static disk state. A claw probing configuration has to spin up a session it doesn't need.
5603+
2. **Surface asymmetry**: all other read-only diagnostics are standalone. `config` and `diff` are the remaining holdouts.
5604+
3. **Pipeline-unfriendly**: `claw config --output-format json | jq` and `claw diff | less` are natural operator workflows; both are currently broken.
5605+
4. **Both already have working JSON renderers** (`render_config_json`, `render_diff_json_for`) — infrastructure for top-level wiring exists.
5606+
5607+
**Fix shape (~30 lines).** Add `"config"` and `"diff"` arms to the top-level parser in `main.rs` (mirroring #145's `plugins` wiring). Each dispatches to a new `CliAction` variant or to existing resume-supported renderers directly. Text mode uses `render_config_report` / `render_diff_report`; JSON mode uses `render_config_json` / `render_diff_json_for`. Remove `config` from `bare_slash_command_guidance`'s fallback allowlist only if explicitly gating (parser arm already short-circuits).
5608+
5609+
**Acceptance.**
5610+
- `claw config` exits 0 with discovered-file listing + merged-keys count.
5611+
- `claw config --output-format json` emits typed envelope with discovered files and merged JSON.
5612+
- `claw config env` / `claw config plugins` surface specific sections (matches `SlashCommand::Config { section }` semantics).
5613+
- `claw diff` exits 0 with clean-tree message or staged/unstaged summary.
5614+
- `claw diff --output-format json` emits typed envelope.
5615+
- Regression tests: `parse_args(["config"])` → `CliAction::Config`; `parse_args(["diff"])` → `CliAction::Diff`.
5616+
5617+
**Blocker.** None. Renderers exist and are resume-supported (proving they're pure-local).
5618+
5619+
**Not applying to.** `hooks` (session-state-modifying, explicitly flagged "unsupported resumed slash command" in main.rs), `usage`, `context`, `tasks`, `theme`, `voice`, `rename`, `copy`, `color`, `effort`, `branch`, `rewind`, `ide`, `tag`, `output-style`, `add-dir` — all session-mutating or interactive-only.
5620+
5621+
**Source.** Jobdori dogfood 2026-04-21 20:03 KST on main HEAD `7d63699` in response to Clawhip nudge. Joins **surface asymmetry** cluster (#145 sibling). Session tally: ROADMAP #146.

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,37 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
253253
CliAction::Acp { output_format } => print_acp_status(output_format)?,
254254
CliAction::State { output_format } => run_worker_state(output_format)?,
255255
CliAction::Init { output_format } => run_init(output_format)?,
256+
// #146: dispatch pure-local introspection. Text mode uses existing
257+
// render_config_report/render_diff_report; JSON mode uses the
258+
// corresponding _json helpers already exposed for resume sessions.
259+
CliAction::Config {
260+
section,
261+
output_format,
262+
} => {
263+
match output_format {
264+
CliOutputFormat::Text => {
265+
println!("{}", render_config_report(section.as_deref())?);
266+
}
267+
CliOutputFormat::Json => {
268+
println!(
269+
"{}",
270+
serde_json::to_string_pretty(&render_config_json(section.as_deref())?)?
271+
);
272+
}
273+
}
274+
}
275+
CliAction::Diff { output_format } => match output_format {
276+
CliOutputFormat::Text => {
277+
println!("{}", render_diff_report()?);
278+
}
279+
CliOutputFormat::Json => {
280+
let cwd = env::current_dir()?;
281+
println!(
282+
"{}",
283+
serde_json::to_string_pretty(&render_diff_json_for(&cwd)?)?
284+
);
285+
}
286+
},
256287
CliAction::Export {
257288
session_reference,
258289
output_path,
@@ -349,6 +380,15 @@ enum CliAction {
349380
Init {
350381
output_format: CliOutputFormat,
351382
},
383+
// #146: `claw config` and `claw diff` are pure-local read-only
384+
// introspection commands; wire them as standalone CLI subcommands.
385+
Config {
386+
section: Option<String>,
387+
output_format: CliOutputFormat,
388+
},
389+
Diff {
390+
output_format: CliOutputFormat,
391+
},
352392
Export {
353393
session_reference: String,
354394
output_path: Option<PathBuf>,
@@ -685,6 +725,38 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
685725
output_format,
686726
})
687727
}
728+
// #146: `config` is pure-local read-only introspection (merges
729+
// `.claw.json` + `.claw/settings.json` from disk, no network, no
730+
// state mutation). Previously callers had to spin up a session with
731+
// `claw --resume SESSION.jsonl /config` to see their own config,
732+
// which is synthetic friction. Accepts an optional section name
733+
// (env|hooks|model|plugins) matching the slash command shape.
734+
"config" => {
735+
let tail = &rest[1..];
736+
let section = tail.first().cloned();
737+
if tail.len() > 1 {
738+
return Err(format!(
739+
"unexpected extra arguments after `claw config {}`: {}",
740+
tail[0],
741+
tail[1..].join(" ")
742+
));
743+
}
744+
Ok(CliAction::Config {
745+
section,
746+
output_format,
747+
})
748+
}
749+
// #146: `diff` is pure-local (shells out to `git diff --cached` +
750+
// `git diff`). No session needed to inspect the working tree.
751+
"diff" => {
752+
if rest.len() > 1 {
753+
return Err(format!(
754+
"unexpected extra arguments after `claw diff`: {}",
755+
rest[1..].join(" ")
756+
));
757+
}
758+
Ok(CliAction::Diff { output_format })
759+
}
688760
"skills" => {
689761
let args = join_optional_args(&rest[1..]);
690762
match classify_skills_slash_command(args.as_deref()) {
@@ -843,6 +915,11 @@ fn parse_single_word_command_alias(
843915
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
844916
"doctor" => Some(Ok(CliAction::Doctor { output_format })),
845917
"state" => Some(Ok(CliAction::State { output_format })),
918+
// #146: let `config` and `diff` fall through to parse_subcommand
919+
// where they are wired as pure-local introspection, instead of
920+
// producing the "is a slash command" guidance. Zero-arg cases
921+
// reach parse_subcommand too via this None.
922+
"config" | "diff" => None,
846923
other => bare_slash_command_guidance(other).map(Err),
847924
}
848925
}
@@ -9619,6 +9696,53 @@ mod tests {
96199696
output_format: CliOutputFormat::Json,
96209697
}
96219698
);
9699+
// #146: `config` and `diff` must parse as standalone CLI actions,
9700+
// not fall through to the "is a slash command" error. Both are
9701+
// pure-local read-only introspection.
9702+
assert_eq!(
9703+
parse_args(&["config".to_string()]).expect("config should parse"),
9704+
CliAction::Config {
9705+
section: None,
9706+
output_format: CliOutputFormat::Text,
9707+
}
9708+
);
9709+
assert_eq!(
9710+
parse_args(&["config".to_string(), "env".to_string()])
9711+
.expect("config env should parse"),
9712+
CliAction::Config {
9713+
section: Some("env".to_string()),
9714+
output_format: CliOutputFormat::Text,
9715+
}
9716+
);
9717+
assert_eq!(
9718+
parse_args(&[
9719+
"config".to_string(),
9720+
"--output-format".to_string(),
9721+
"json".to_string(),
9722+
])
9723+
.expect("config --output-format json should parse"),
9724+
CliAction::Config {
9725+
section: None,
9726+
output_format: CliOutputFormat::Json,
9727+
}
9728+
);
9729+
assert_eq!(
9730+
parse_args(&["diff".to_string()]).expect("diff should parse"),
9731+
CliAction::Diff {
9732+
output_format: CliOutputFormat::Text,
9733+
}
9734+
);
9735+
assert_eq!(
9736+
parse_args(&[
9737+
"diff".to_string(),
9738+
"--output-format".to_string(),
9739+
"json".to_string(),
9740+
])
9741+
.expect("diff --output-format json should parse"),
9742+
CliAction::Diff {
9743+
output_format: CliOutputFormat::Json,
9744+
}
9745+
);
96229746
}
96239747

96249748
#[test]

0 commit comments

Comments
 (0)