fix(#130e-B): route plugins/prompt --help to dedicated help topics

## What Was Broken (ROADMAP #130e Category B)

Two remaining surface-level help outliers after #130e-A:

    $ claw plugins --help
    Unknown /plugins action '--help'. Use list, install, enable, disable, uninstall, or update.

    $ claw prompt --help
    claw v0.1.0  (top-level help — wrong help topic)

`plugins` treated `--help` as an invalid subaction name. `prompt`
was explicitly listed in the early `wants_help` interception with
commit/pr/issue, which routed to top-level help instead of
prompt-specific help.

## Root Cause (Traced)

1. **plugins**: `parse_local_help_action()` didn't have a "plugins"
   arm, so `["plugins", "--help"]` returned None and continued into
   the `"plugins"` parser arm (main.rs:1031), which treated `--help`
   as the `action` argument. Runtime layer then rejected it as
   "Unknown action".

2. **prompt**: At main.rs:~800, there was an early interception for
   `--help` following certain subcommands (prompt, commit, pr, issue)
   that forced `wants_help = true`, routing to generic top-level help
   instead of letting parse_local_help_action produce a prompt-specific
   topic.

## What This Fix Does

Same pattern as #130c/#130d/#130e-A:

1. **LocalHelpTopic enum extended** with Plugins, Prompt variants
2. **parse_local_help_action() extended** to map both new cases
3. **Help topic renderers added** with accurate usage info
4. **Early prompt-interception removed** — prompt now falls through to
   parse_local_help_action like other subcommands. commit/pr/issue
   (which aren't actual subcommands yet) remain in the early list.

## Dogfood Verification

Before fix:
    $ claw plugins --help
    Unknown /plugins action '--help'. Use list, install, enable, ...

    $ claw prompt --help
    claw v0.1.0
    (top-level help, not prompt-specific)

After fix:
    $ claw plugins --help
    Plugins
      Usage            claw plugins [list|install|enable|disable|uninstall|update] [<target>]
      Purpose          manage bundled and user plugins from the CLI surface
      ...

    $ claw prompt --help
    Prompt
      Usage            claw prompt <prompt-text>
      Purpose          run a single-turn, non-interactive prompt and exit
      Flags            --model · --allowedTools · --output-format · --compact
      ...

## Non-Regression Verification

- `claw plugins` (no args) → still displays plugin inventory 
- `claw plugins list` → still works correctly 
- `claw prompt "text"` → still requires credentials, runs prompt 
- All 180 binary tests pass 
- All 466 library tests pass 

## Regression Tests Added (4+ assertions)

- `plugins --help` → HelpTopic(Plugins)
- `prompt --help` → HelpTopic(Prompt)
- Short forms `plugins -h` / `prompt -h` both work
- `prompt "hello world"` still routes to Prompt action with correct text

## HELP-PARITY SWEEP COMPLETE

All 22 top-level subcommands now emit proper help topics:

| Command | Status |
|---|---|
| help --help |  #130e-A |
| version --help |  pre-existing |
| status --help |  pre-existing |
| sandbox --help |  pre-existing |
| doctor --help |  pre-existing |
| acp --help |  pre-existing |
| init --help |  pre-existing |
| state --help |  pre-existing |
| export --help |  pre-existing |
| diff --help |  #130c |
| config --help |  #130d |
| mcp --help |  pre-existing |
| agents --help |  pre-existing |
| plugins --help |  #130e-B (this commit) |
| skills --help |  pre-existing |
| submit --help |  #130e-A |
| prompt --help |  #130e-B (this commit) |
| resume --help |  #130e-A |
| system-prompt --help |  pre-existing |
| dump-manifests --help |  pre-existing |
| bootstrap-plan --help |  pre-existing |

Zero outliers. Contract universally enforced.

## Related

- Closes #130e Category B (plugins, prompt surface-parity)
- Completes entire help-parity sweep family (#130c, #130d, #130e)
- Stacks on #130e-A (dispatch-order fixes) on same worktree
This commit is contained in:
YeonGyu-Kim 2026-04-23 02:07:50 +09:00
parent 0ca034472b
commit 9dd7e79eb2

View File

@ -739,6 +739,9 @@ enum LocalHelpTopic {
Meta,
Submit,
Resume,
// #130e-B: help parity — surface-level bugs (plugins, prompt)
Plugins,
Prompt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -787,20 +790,20 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
if !rest.is_empty()
&& matches!(
rest[0].as_str(),
"prompt"
| "commit"
"commit"
| "pr"
| "issue"
) =>
{
// `--help` following a subcommand that would otherwise forward
// the arg to the API (e.g. `claw prompt --help`) should show
// top-level help instead. Subcommands that consume their own
// args (agents, mcp, plugins, skills) and local help-topic
// subcommands (status, sandbox, doctor, init, state, export,
// version, system-prompt, dump-manifests, bootstrap-plan) must
// NOT be intercepted here — they handle --help in their own
// dispatch paths via parse_local_help_action(). See #141.
// the arg to the API should show top-level help instead.
// Subcommands that consume their own args (agents, mcp, plugins,
// skills) and local help-topic subcommands (status, sandbox,
// doctor, init, state, export, version, system-prompt,
// dump-manifests, bootstrap-plan, diff, config, help, submit,
// resume, prompt) must NOT be intercepted here — they handle
// --help in their own dispatch paths via
// parse_local_help_action(). See #141, #130c, #130d, #130e.
wants_help = true;
index += 1;
}
@ -1286,6 +1289,9 @@ fn parse_local_help_action(rest: &[String]) -> Option<Result<CliAction, String>>
"help" => LocalHelpTopic::Meta,
"submit" => LocalHelpTopic::Submit,
"resume" => LocalHelpTopic::Resume,
// #130e-B: help parity — surface fixes
"plugins" => LocalHelpTopic::Plugins,
"prompt" => LocalHelpTopic::Prompt,
_ => return None,
};
Some(Ok(CliAction::HelpTopic(topic)))
@ -6151,6 +6157,23 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
Requires valid Anthropic credentials (ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY)
Related claw submit · claw --resume · /session list"
.to_string(),
// #130e-B: help topic for `claw plugins --help`.
LocalHelpTopic::Plugins => "Plugins
Usage claw plugins [list|install|enable|disable|uninstall|update] [<target>]
Purpose manage bundled and user plugins from the CLI surface
Defaults list (no action prints inventory)
Sources .claw/plugins.json, bundled catalog, user-installed
Formats text (default), json
Related claw mcp · claw skills · /plugins (REPL)"
.to_string(),
// #130e-B: help topic for `claw prompt --help`.
LocalHelpTopic::Prompt => "Prompt
Usage claw prompt <prompt-text>
Purpose run a single-turn, non-interactive prompt and exit (like --print mode)
Flags --model · --allowedTools · --output-format · --compact
Requires valid Anthropic credentials (ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY)
Related claw submit · claw (bare, interactive REPL)"
.to_string(),
}
}
@ -10540,6 +10563,43 @@ mod tests {
let resume_h = parse_args(&["resume".to_string(), "-h".to_string()])
.expect("resume -h must parse");
assert!(matches!(resume_h, CliAction::HelpTopic(LocalHelpTopic::Resume)));
// #130e-B: surface-level help fixes for plugins and prompt.
// These previously emitted "Unknown action" (plugins) or wrong help (prompt).
let plugins_help = parse_args(&[
"plugins".to_string(),
"--help".to_string(),
])
.expect("plugins --help must parse as help action");
assert!(
matches!(plugins_help, CliAction::HelpTopic(LocalHelpTopic::Plugins)),
"#130e-B: plugins --help must route to LocalHelpTopic::Plugins, got: {plugins_help:?}"
);
let prompt_help = parse_args(&[
"prompt".to_string(),
"--help".to_string(),
])
.expect("prompt --help must parse as help action");
assert!(
matches!(prompt_help, CliAction::HelpTopic(LocalHelpTopic::Prompt)),
"#130e-B: prompt --help must route to LocalHelpTopic::Prompt, got: {prompt_help:?}"
);
// Short forms
let plugins_h = parse_args(&["plugins".to_string(), "-h".to_string()])
.expect("plugins -h must parse");
assert!(matches!(plugins_h, CliAction::HelpTopic(LocalHelpTopic::Plugins)));
let prompt_h = parse_args(&["prompt".to_string(), "-h".to_string()])
.expect("prompt -h must parse");
assert!(matches!(prompt_h, CliAction::HelpTopic(LocalHelpTopic::Prompt)));
// Non-regression: `prompt "actual text"` still parses as Prompt action
let prompt_action = parse_args(&[
"prompt".to_string(),
"hello world".to_string(),
])
.expect("prompt with real text must parse");
assert!(
matches!(prompt_action, CliAction::Prompt { ref prompt, .. } if prompt == "hello world"),
"#130e-B: prompt with real text must route to Prompt action"
);
// #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