feat: #141 unify claw <subcommand> --help contract across all 14 subcommands

Previously, `claw <subcommand> --help` had 5 different behaviors:
- 7 subcommands returned subcommand-specific help (correct)
- init/export/state/version silently fell back to global `claw --help`
- system-prompt/dump-manifests errored with `unknown <cmd> option: --help`
- bootstrap-plan printed its phase list instead of help text

Changes:
- Extend LocalHelpTopic enum with Init, State, Export, Version, SystemPrompt,
  DumpManifests, BootstrapPlan variants.
- Extend parse_local_help_action() to resolve those 7 subcommands to their
  local help topic instead of falling through to the main dispatch.
- Remove init/state/export/version from the explicit wants_help=true matcher
  so they reach parse_local_help_action() before being routed to global help.
- Add render_help_topic() entries for the 7 new topics with consistent
  Usage/Purpose/Output/Formats/Related structure.
- Add regression test subcommand_help_flag_has_one_contract_across_all_subcommands_141
  asserting every documented subcommand + both --help and -h variants resolve
  to a HelpTopic with non-empty text that contains a Usage line.

Verification:
- All 14 subcommands now return subcommand-specific help (live dogfood).
- Full workspace test green except pre-existing resume_latest flake.

Closes ROADMAP #141.
This commit is contained in:
YeonGyu-Kim 2026-04-21 17:36:48 +09:00
parent 2665ada94e
commit 7763ca3260

View File

@ -375,6 +375,15 @@ enum LocalHelpTopic {
Sandbox,
Doctor,
Acp,
// #141: extend the local-help pattern to every subcommand so
// `claw <subcommand> --help` has one consistent contract.
Init,
State,
Export,
Version,
SystemPrompt,
DumpManifests,
BootstrapPlan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -421,10 +430,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
&& matches!(
rest[0].as_str(),
"prompt"
| "version"
| "state"
| "init"
| "export"
| "commit"
| "pr"
| "issue"
@ -434,8 +439,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// 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) must NOT be intercepted
// here — they handle --help in their own dispatch paths.
// 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.
wants_help = true;
index += 1;
}
@ -746,6 +753,17 @@ fn parse_local_help_action(rest: &[String]) -> Option<Result<CliAction, String>>
"sandbox" => LocalHelpTopic::Sandbox,
"doctor" => LocalHelpTopic::Doctor,
"acp" => LocalHelpTopic::Acp,
// #141: add the subcommands that were previously falling back
// to global help (init/state/export/version) or erroring out
// (system-prompt/dump-manifests) or printing their primary
// output instead of help text (bootstrap-plan).
"init" => LocalHelpTopic::Init,
"state" => LocalHelpTopic::State,
"export" => LocalHelpTopic::Export,
"version" => LocalHelpTopic::Version,
"system-prompt" => LocalHelpTopic::SystemPrompt,
"dump-manifests" => LocalHelpTopic::DumpManifests,
"bootstrap-plan" => LocalHelpTopic::BootstrapPlan,
_ => return None,
};
Some(Ok(CliAction::HelpTopic(topic)))
@ -5369,6 +5387,56 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
Formats text (default), json
Related ROADMAP #64a (discoverability) · ROADMAP #76 (real ACP support) · claw --help"
.to_string(),
LocalHelpTopic::Init => "Init
Usage claw init [--output-format <format>]
Purpose create .claw/, .claw.json, .gitignore, and CLAUDE.md in the current project
Output list of created vs. skipped files (idempotent: safe to re-run)
Formats text (default), json
Related claw status · claw doctor"
.to_string(),
LocalHelpTopic::State => "State
Usage claw state [--output-format <format>]
Purpose read the worker state file written by the interactive REPL
Output worker id, model, permissions, session reference (text or json)
Formats text (default), json
Prerequisite run `claw` interactively or `claw prompt <text>` to produce worker state first
Related ROADMAP #139 (worker-concept discoverability) · claw status"
.to_string(),
LocalHelpTopic::Export => "Export
Usage claw export [--session <id|latest>] [--output <path>] [--output-format <format>]
Purpose serialize a managed session to JSON for review, transfer, or archival
Defaults --session latest (most recent managed session in .claw/sessions/)
Formats text (default), json
Related /session list · claw --resume latest"
.to_string(),
LocalHelpTopic::Version => "Version
Usage claw version [--output-format <format>]
Aliases claw --version · claw -V
Purpose print the claw CLI version and build metadata
Formats text (default), json
Related claw doctor (full build/auth/config diagnostic)"
.to_string(),
LocalHelpTopic::SystemPrompt => "System Prompt
Usage claw system-prompt [--cwd <path>] [--date YYYY-MM-DD] [--output-format <format>]
Purpose render the resolved system prompt that `claw` would send for the given cwd + date
Options --cwd overrides the workspace dir · --date injects a deterministic date stamp
Formats text (default), json
Related claw doctor · claw dump-manifests"
.to_string(),
LocalHelpTopic::DumpManifests => "Dump Manifests
Usage claw dump-manifests [--manifests-dir <path>] [--output-format <format>]
Purpose emit every skill/agent/tool manifest the resolver would load for the current cwd
Options --manifests-dir scopes discovery to a specific directory
Formats text (default), json
Related claw skills · claw agents · claw doctor"
.to_string(),
LocalHelpTopic::BootstrapPlan => "Bootstrap Plan
Usage claw bootstrap-plan [--output-format <format>]
Purpose list the ordered startup phases the CLI would execute before dispatch
Output phase names (text) or structured phase list (json) primary output is the plan itself
Formats text (default), json
Related claw doctor · claw status"
.to_string(),
}
}
@ -8519,7 +8587,7 @@ mod tests {
parse_git_status_branch, parse_git_status_metadata_for, parse_git_workspace_summary,
parse_history_count, permission_policy, print_help_to, push_output_block,
render_config_report, render_diff_report, render_diff_report_for, render_memory_report,
render_prompt_history_report, render_repl_help, render_resume_usage,
render_help_topic, render_prompt_history_report, render_repl_help, render_resume_usage,
render_session_markdown, resolve_model_alias, resolve_model_alias_with_config,
resolve_repl_model, resolve_session_reference, response_to_events,
resume_supported_slash_commands, run_resume_command, short_tool_id,
@ -9487,6 +9555,50 @@ mod tests {
);
}
#[test]
fn subcommand_help_flag_has_one_contract_across_all_subcommands_141() {
// #141: every documented subcommand must resolve `<subcommand> --help`
// to a subcommand-specific help topic, never to global help, never to
// an "unknown option" error, never to the subcommand's primary output.
let cases: &[(&str, LocalHelpTopic)] = &[
("status", LocalHelpTopic::Status),
("sandbox", LocalHelpTopic::Sandbox),
("doctor", LocalHelpTopic::Doctor),
("acp", LocalHelpTopic::Acp),
("init", LocalHelpTopic::Init),
("state", LocalHelpTopic::State),
("export", LocalHelpTopic::Export),
("version", LocalHelpTopic::Version),
("system-prompt", LocalHelpTopic::SystemPrompt),
("dump-manifests", LocalHelpTopic::DumpManifests),
("bootstrap-plan", LocalHelpTopic::BootstrapPlan),
];
for (subcommand, expected_topic) in cases {
for flag in ["--help", "-h"] {
let parsed = parse_args(&[subcommand.to_string(), flag.to_string()])
.unwrap_or_else(|error| {
panic!("`{subcommand} {flag}` should parse as help but errored: {error}")
});
assert_eq!(
parsed,
CliAction::HelpTopic(*expected_topic),
"`{subcommand} {flag}` should resolve to HelpTopic({expected_topic:?})"
);
}
// And the rendered help must actually mention the subcommand name
// (or its canonical title) so users know they got the right help.
let rendered = render_help_topic(*expected_topic);
assert!(
!rendered.is_empty(),
"{subcommand} help text should not be empty"
);
assert!(
rendered.contains("Usage"),
"{subcommand} help text should contain a Usage line"
);
}
}
#[test]
fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode() {
let _guard = env_lock();