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)
This commit is contained in:
YeonGyu-Kim 2026-04-23 01:55:25 +09:00
parent 83f744adf0
commit 19638a015e

View File

@ -733,6 +733,8 @@ enum LocalHelpTopic {
BootstrapPlan,
// #130c: help parity for `claw diff --help`
Diff,
// #130d: help parity for `claw config --help`
Config,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -1046,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 {
@ -1270,6 +1276,8 @@ fn parse_local_help_action(rest: &[String]) -> Option<Result<CliAction, String>>
"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)))
@ -6102,6 +6110,15 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
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(),
}
}
@ -10412,6 +10429,44 @@ mod tests {
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