Compare commits

..

2 Commits

Author SHA1 Message Date
YeonGyu-Kim
19638a015e fix(#130d): accept --help / -h in claw config arm, route to help topic
## What Was Broken (ROADMAP #130d, filed cycle #52)

`claw config --help` was silently ignored — the command executed and
displayed the config dump instead of showing help:

    $ claw config --help
    Config
      Working directory /private/tmp/dogfood-probe-47
      Loaded files      0
      Merged keys       0
      (displays full config, not help)

Expected: help for the config command. Actual: silent acceptance of
`--help`, runs config display anyway.

This is the opposite outlier from #130c (which rejected help with an
error). Together they form the help-parity anomaly:
- #130c `diff --help` → error (rejects help)
- #130d `config --help` → silent ignore (runs command, ignores help)
- Others (status, mcp, export) → proper help
- Expected behavior: all commands should show help on `--help`

## Root Cause (Traced)

At main.rs:1050, the `"config"` parser arm parsed arguments positionally:

    "config" => {
        let tail = &rest[1..];
        let section = tail.first().cloned();
        // ... ignores unrecognized args like --help silently
        Ok(CliAction::Config { section, ... })
    }

Unlike the `diff` arm (#130c), `config` had no explicit check for
extra args. It positionally parsed the first arg as an optional
`section` and silently accepted/ignored any trailing arg, including
`--help`.

## What This Fix Does

Same pattern as #130c (help-surface parity):

1. **LocalHelpTopic enum extended** with new `Config` variant
2. **parse_local_help_action() extended** to map `"config"` → `LocalHelpTopic::Config`
3. **config arm guard added**: check for help flag before parsing section
4. **Help topic renderer added**: human-readable help text for config

Fix locus at main.rs:1050:

    "config" => {
        // #130d: accept --help / -h and route to help topic
        if rest.len() >= 2 && is_help_flag(&rest[1]) {
            return Ok(CliAction::HelpTopic(LocalHelpTopic::Config));
        }
        let tail = &rest[1..];
        // ... existing parsing continues
    }

## Dogfood Verification

Before fix:
    $ claw config --help
    Config
      Working directory ...
      Loaded files      0
      (no help, runs config)

After fix:
    $ claw config --help
    Config
      Usage            claw config [--cwd <path>] [--output-format <format>]
      Purpose          merge and display the resolved configuration
      Options          --cwd overrides the workspace directory
      Output           loaded files and merged key-value pairs
      Formats          text (default), json
      Related          claw status · claw doctor · claw init

Short form `claw config -h` also works.

## Non-Regression Verification

- `claw config` (no args) → still displays config dump 
- `claw config permissions` (section arg) → still works 
- All 180 binary tests pass 
- All 466 library tests pass 

## Regression Tests Added (4 assertions)

- `config --help` → routes to `HelpTopic(LocalHelpTopic::Config)`
- `config -h` (short form) → routes to help topic
- bare `config` (no args) → still routes to `Config` action
- `config permissions` (with section) → still works correctly

## Pattern Note

#130c and #130d form a pair: two outlier failure modes in help
handling for local introspection commands:
- #130c `diff` rejected help (loud error) → fixed with guard + routing
- #130d `config` silently ignored help (silent accept) → fixed with same pattern

Both are now consistent with the rest of the CLI (status, mcp, export, etc.).

## Related

- Closes #130d (config help discoverability gap)
- Completes help-parity family (#130c, #130d)
- Stacks on #130c (diff help fix) on same worktree branch
- Part of help-consistency thread (#141 audit)
2026-04-23 01:55:25 +09:00
YeonGyu-Kim
83f744adf0 fix(#130c): accept --help / -h in claw diff arm
## What Was Broken (ROADMAP #130c, filed cycle #50)

`claw diff --help` was rejected with:

    [error-kind: unknown]
    error: unexpected extra arguments after `claw diff`: --help

Other local introspection commands accept --help fine:
- `claw status --help` → shows help 
- `claw mcp --help` → shows help 
- `claw export --help` → shows help 
- `claw diff --help` → error  (outlier)

This is a help-surface parity bug: `diff` is the only local command
that rejects --help as "extra arguments" before the help detector
gets a chance to run.

## Root Cause (Traced)

At main.rs:1063, the `"diff"` parser arm rejected ALL extra args:

    "diff" => {
        if rest.len() > 1 {
            return Err(format!("unexpected extra arguments after `claw diff`: {}", ...));
        }
        Ok(CliAction::Diff { output_format })
    }

When parsing `["diff", "--help"]`, `rest.len() > 1` was true (length
is 2) and `--help` was rejected as extra argument.

Other commands (status, sandbox, doctor, init, state, export, etc.)
routed through `parse_local_help_action()` which detected
`--help` / `-h` and routed to a LocalHelpTopic. The `diff` arm
lacked this guard.

## What This Fix Does

Three minimal changes:

1. **LocalHelpTopic enum extended** with new `Diff` variant
2. **parse_local_help_action() extended** to map `"diff"` → `LocalHelpTopic::Diff`
3. **diff arm guard added**: check for help flag before extra-args validation
4. **Help topic renderer added**: human-readable help text for diff command

Fix locus at main.rs:1063:

    "diff" => {
        // #130c: accept --help / -h as first argument and route to help topic
        if rest.len() == 2 && is_help_flag(&rest[1]) {
            return Ok(CliAction::HelpTopic(LocalHelpTopic::Diff));
        }
        if rest.len() > 1 { /* existing error */ }
        Ok(CliAction::Diff { output_format })
    }

## Dogfood Verification

Before fix:
    $ claw diff --help
    [error-kind: unknown]
    error: unexpected extra arguments after `claw diff`: --help

After fix:
    $ claw diff --help
    Diff
      Usage            claw diff [--output-format <format>]
      Purpose          show local git staged + unstaged changes
      Requires         workspace must be inside a git repository
      ...

And `claw diff -h` (short form) also works.

## Non-Regression Verification

- `claw diff` (no args) → still routes to Diff action correctly
- `claw diff foo` (unknown arg) → still rejected as "unexpected extra arguments"
- `claw diff --output-format json` (valid flag) → still works
- All 180 binary tests pass
- All 466 library tests pass

## Regression Tests Added (4 assertions)

- `diff --help` → routes to HelpTopic(LocalHelpTopic::Diff)
- `diff -h` (short form) → routes to HelpTopic(LocalHelpTopic::Diff)
- bare `diff` → still routes to Diff action
- `diff foo` (unknown arg) → still errors with "extra arguments"

## Pattern

Follows #141 help-consistency work (extending LocalHelpTopic to
cover more subcommands). Clean surface-parity fix: identify the
outlier, add the missing guard. Low-risk, high-clarity.

## Related

- Closes #130c (diff help discoverability gap)
- Stacks on #130b (filesystem context) and #251 (session dispatch)
- Part of help-consistency thread (#141 audit, #145 plugins wiring)
2026-04-23 01:48:40 +09:00

View File

@@ -731,6 +731,10 @@ enum LocalHelpTopic {
SystemPrompt,
DumpManifests,
BootstrapPlan,
// #130c: help parity for `claw diff --help`
Diff,
// #130d: help parity for `claw config --help`
Config,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1044,6 +1048,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// which is synthetic friction. Accepts an optional section name
// (env|hooks|model|plugins) matching the slash command shape.
"config" => {
// #130d: accept --help / -h and route to help topic instead of silently ignoring
if rest.len() >= 2 && is_help_flag(&rest[1]) {
return Ok(CliAction::HelpTopic(LocalHelpTopic::Config));
}
let tail = &rest[1..];
let section = tail.first().cloned();
if tail.len() > 1 {
@@ -1061,6 +1069,12 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// #146: `diff` is pure-local (shells out to `git diff --cached` +
// `git diff`). No session needed to inspect the working tree.
"diff" => {
// #130c: accept --help / -h as first argument and route to help topic,
// matching the behavior of status/sandbox/doctor/etc.
// Without this guard, `claw diff --help` was rejected as extra arguments.
if rest.len() == 2 && is_help_flag(&rest[1]) {
return Ok(CliAction::HelpTopic(LocalHelpTopic::Diff));
}
if rest.len() > 1 {
return Err(format!(
"unexpected extra arguments after `claw diff`: {}",
@@ -1260,6 +1274,10 @@ fn parse_local_help_action(rest: &[String]) -> Option<Result<CliAction, String>>
"system-prompt" => LocalHelpTopic::SystemPrompt,
"dump-manifests" => LocalHelpTopic::DumpManifests,
"bootstrap-plan" => LocalHelpTopic::BootstrapPlan,
// #130c: help parity for `claw diff --help`
"diff" => LocalHelpTopic::Diff,
// #130d: help parity for `claw config --help`
"config" => LocalHelpTopic::Config,
_ => return None,
};
Some(Ok(CliAction::HelpTopic(topic)))
@@ -6083,6 +6101,24 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
Formats text (default), json
Related claw doctor · claw status"
.to_string(),
// #130c: help topic for `claw diff --help`.
LocalHelpTopic::Diff => "Diff
Usage claw diff [--output-format <format>]
Purpose show local git staged + unstaged changes for the current workspace
Requires workspace must be inside a git repository
Output unified diff (text) or structured diff object (json)
Formats text (default), json
Related claw status · claw config"
.to_string(),
// #130d: help topic for `claw config --help`.
LocalHelpTopic::Config => "Config
Usage claw config [--cwd <path>] [--output-format <format>]
Purpose merge and display the resolved .claw.json / settings.json configuration
Options --cwd overrides the workspace directory for config lookup
Output loaded files and merged key-value pairs (text) or JSON object (json)
Formats text (default), json
Related claw status · claw doctor · claw init"
.to_string(),
}
}
@@ -10352,6 +10388,85 @@ mod tests {
"filesystem_io_error",
"#130b: enriched messages must be classifier-recognizable"
);
// #130c: `claw diff --help` must route to help topic, not reject as extra args.
// Regression: `diff` was the outlier among local introspection commands
// (status/config/mcp all accepted --help) because its parser arm rejected
// all extra args before help detection could run.
let diff_help_action = parse_args(&[
"diff".to_string(),
"--help".to_string(),
])
.expect("diff --help must parse as help action");
assert!(
matches!(diff_help_action, CliAction::HelpTopic(LocalHelpTopic::Diff)),
"#130c: diff --help must route to LocalHelpTopic::Diff, got: {diff_help_action:?}"
);
let diff_h_action = parse_args(&[
"diff".to_string(),
"-h".to_string(),
])
.expect("diff -h must parse as help action");
assert!(
matches!(diff_h_action, CliAction::HelpTopic(LocalHelpTopic::Diff)),
"#130c: diff -h (short form) must route to LocalHelpTopic::Diff"
);
// #130c: bare `claw diff` still routes to Diff action, not help.
let diff_action = parse_args(&[
"diff".to_string(),
])
.expect("bare diff must parse as diff action");
assert!(
matches!(diff_action, CliAction::Diff { .. }),
"#130c: bare diff must still route to Diff action, got: {diff_action:?}"
);
// #130c: unknown args still rejected (non-regression).
let diff_bad_arg = parse_args(&[
"diff".to_string(),
"foo".to_string(),
])
.expect_err("diff foo must still be rejected as extra args");
assert!(
diff_bad_arg.contains("unexpected extra arguments"),
"#130c: diff with unknown arg must still error, got: {diff_bad_arg}"
);
// #130d: `claw config --help` must route to help topic, not silently run config.
let config_help_action = parse_args(&[
"config".to_string(),
"--help".to_string(),
])
.expect("config --help must parse as help action");
assert!(
matches!(config_help_action, CliAction::HelpTopic(LocalHelpTopic::Config)),
"#130d: config --help must route to LocalHelpTopic::Config, got: {config_help_action:?}"
);
let config_h_action = parse_args(&[
"config".to_string(),
"-h".to_string(),
])
.expect("config -h must parse as help action");
assert!(
matches!(config_h_action, CliAction::HelpTopic(LocalHelpTopic::Config)),
"#130d: config -h (short form) must route to LocalHelpTopic::Config"
);
// #130d: bare `claw config` still routes to Config action with no section
let config_action = parse_args(&[
"config".to_string(),
])
.expect("bare config must parse as config action");
assert!(
matches!(config_action, CliAction::Config { section: None, .. }),
"#130d: bare config must still route to Config action with section=None"
);
// #130d: config with section still works (non-regression)
let config_section = parse_args(&[
"config".to_string(),
"permissions".to_string(),
])
.expect("config permissions must parse");
assert!(
matches!(config_section, CliAction::Config { section: Some(ref s), .. } if s == "permissions"),
"#130d: config with section must still work"
);
// #147: empty / whitespace-only positional args must be rejected
// with a specific error instead of falling through to the prompt
// path (where they surface a misleading "missing Anthropic