Compare commits

..

2 Commits

Author SHA1 Message Date
YeonGyu-Kim
3a533ceba0 fix(#152-follow-up-2): claw bootstrap-plan rejects trailing arguments
## What Was Broken

`claw bootstrap-plan garbage` silently accepted the `garbage` argument:

    $ claw bootstrap-plan garbage
    - CliEntry
    - FastPathVersion
    - StartupProfiler
    - ...

Same pattern as #152 init follow-up (previous cycle): a no-arg diagnostic
verb was missing its `rest.len()` guard.

## What This Fix Does

Add parse-time length check before constructing CliAction::BootstrapPlan:

    "bootstrap-plan" => {
        if rest.len() > 1 {
            return Err(format!(
                "unrecognized argument \`{}\` for subcommand \`bootstrap-plan\`",
                rest[1]
            ));
        }
        Ok(CliAction::BootstrapPlan { output_format })
    }

## Dogfood Verification

Before:
    $ claw bootstrap-plan garbage
    - CliEntry
    - FastPathVersion
    (continues listing...)

After:
    $ claw bootstrap-plan garbage
    [error-kind: cli_parse]
    error: unrecognized argument `garbage` for subcommand `bootstrap-plan`

    $ claw bootstrap-plan  (no args)
    - CliEntry
    - FastPathVersion
    (still works normally)

## Full No-Arg Verb Suffix-Guard Sweep (Post-Fix)

All no-arg verbs now uniformly reject trailing garbage:

| Verb | Status |
|---|---|
| help garbage |  rejects |
| version garbage |  rejects |
| status garbage |  rejects |
| sandbox garbage |  rejects |
| doctor garbage |  rejects |
| state garbage |  rejects |
| init garbage |  rejects (previous cycle #55) |
| diff garbage |  rejects |
| plugins garbage |  rejects |
| skills garbage |  rejects |
| system-prompt garbage |  rejects |
| dump-manifests garbage |  rejects |
| bootstrap-plan garbage |  rejects (this commit) |
| acp garbage |  rejects |

Legitimate positionals (not a bug):
- `export <file-path>` — file path is the intended arg
- `config <section>` — flexible section filter (design question, not bug)

## Non-Regression

- `claw bootstrap-plan` (no args) still works 
- All 180 binary tests pass 
- All 466 library tests pass 

## Related

- #152 follow-up (init suffix guard, previous cycle)
- Completes diagnostic-verb suffix-guard contract hygiene
2026-04-23 02:17:16 +09:00
YeonGyu-Kim
860f285f70 fix(#152-follow-up): claw init rejects trailing arguments
## What Was Broken

`claw init foo` silently accepted the `foo` argument instead of rejecting it at parse time:

    $ claw init foo
    Init
      Project          /path/to/project
      .claw/           skipped

Trailing args should be rejected like other no-arg verbs (status, sandbox, etc.):

    $ claw status bar
    [error-kind: cli_parse]
    error: unrecognized argument `bar` for subcommand `status`

## Root Cause

The `init` arm (main.rs:1191) was missing a `rest.len()` guard.
Compare to `status` (main.rs:1319 in parse_diagnostic_verb), which correctly
rejects extra args.

## What This Fix Does

Add a simple length guard before constructing the CliAction::Init:

    "init" => {
        if rest.len() > 1 {
            return Err(format!(
                "unrecognized argument \`{}\` for subcommand \`init\`",
                rest[1]
            ));
        }
        Ok(CliAction::Init { output_format })
    }

## Dogfood Verification

Before:
    $ claw init foo
    Init
      Project          /path/to/project

After:
    $ claw init foo
    [error-kind: cli_parse]
    error: unrecognized argument `foo` for subcommand `init`

    $ claw init
    Init
      Project          /path/to/project
      (continues normally)

## Non-Regression

- `claw init` (no args) still works
- All 180 binary tests pass
- All 466 library tests pass

## Related

- #152 (diagnostic verb suffix guard)
- Follows pattern from parse_diagnostic_verb() guard clauses
2026-04-23 02:14:48 +09:00

View File

@@ -1015,7 +1015,18 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
match rest[0].as_str() {
"dump-manifests" => parse_dump_manifests_args(&rest[1..], output_format),
"bootstrap-plan" => Ok(CliAction::BootstrapPlan { output_format }),
"bootstrap-plan" => {
// #152: bootstrap-plan is a no-arg verb. Reject unexpected suffixes
// like `claw bootstrap-plan garbage` that silently accept trailing
// args instead of rejecting at parse time.
if rest.len() > 1 {
return Err(format!(
"unrecognized argument `{}` for subcommand `bootstrap-plan`",
rest[1]
));
}
Ok(CliAction::BootstrapPlan { output_format })
}
"agents" => Ok(CliAction::Agents {
args: join_optional_args(&rest[1..]),
output_format,
@@ -1188,7 +1199,16 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
"acp" => parse_acp_args(&rest[1..], output_format),
"login" | "logout" => Err(removed_auth_surface_error(rest[0].as_str())),
"init" => Ok(CliAction::Init { output_format }),
"init" => {
// #152: init is a no-arg verb. Reject unexpected suffixes like `claw init foo`.
if rest.len() > 1 {
return Err(format!(
"unrecognized argument `{}` for subcommand `init`",
rest[1]
));
}
Ok(CliAction::Init { output_format })
}
"export" => parse_export_args(&rest[1..], output_format),
"prompt" => {
let prompt = rest[1..].join(" ");