Help must be able to guide an agent: an overview index, and a --help on every command that prints instead of executing #463

Closed
opened 2026-08-27 08:05:50 +00:00 by mph · 1 comment
Owner

iw help cannot guide anyone today — not a person at a shell, not an agent driving the CLI. Four things are wrong, and they have to be fixed together: the first makes the next two unreachable, and the fourth is what stops all three from rotting again.

Supersedes #439 (a --help flag must never execute a command; reject unknown flags across subcommands) and #202 (review-state update should support --help). Both are carried into this issue in full. Close them in favour of this one.

1. The help flag never reaches a help printer

Only 4 of 34 commands handle it: dashboard, feedback, issue, review-state. Everything else either runs, or fails on an unrelated error:

$ ./iw worktrees --help
=== Worktrees ===
IW-437  Phase CI gate: phase-pr restarts CI o...  open  PR: Merged ...
$ ./iw start --help
Error: Invalid issue ID format: --help (expected: TEAM-123).

The worst case is recorded in #439: ./iw phase-merge --help merged pull request 434. The command that performed the merge was a request for documentation. phase-merge has no required argument, so an unrecognised flag falls through to the action. phase-pr looks correct only by accident — its required-argument check happens to run first.

The other half of #439 matters beyond help: unknown flags are silently discarded on every subcommand, so a typo'd flag becomes "run with defaults" instead of an error. A related case from the same family: ./iw issue create --title whose value begins with two dashes is swallowed by the arg parser, and the API rejects the request with a bare 422.

Required:

  • Handle the help flags before dispatch, once, for every subcommand — not per command.
  • Reject unknown flags rather than ignore them.
  • A help request must never perform the command's action, and must exit 0.

2. There is no overview worth reading

iw --help prints six lines and sends the reader elsewhere:

iw-cli - Issue Worktree CLI

Usage: iw <command> [args...]
       iw --list              List all commands
       iw --describe <cmd>    Describe a command
       iw --bootstrap         Pre-compile for offline use

iw --list then prints 34 flat Purpose:/Usage: pairs with no grouping and no sense of which command to reach for when. Both are scraped out of two comment lines in each command script by iw-run (parse_command_header, iw-run:261-265).

Required: an overview that is an index of the tool. Every command listed, grouped by what it is for (worktree lifecycle, phase lifecycle, review state, server and dashboard, project setup, diagnostics), one line each that says what the command does — enough for a reader who has never used iw to pick the right one without running three of them to find out.

3. The text says nothing that can be acted on

This is the part neither #439 nor #202 covers, and it is the reason help is useless even where it exists. Take the most-used command:

Command: phase-pr
Purpose: Pushes the phase sub-branch, opens the PR/MR, and waits for its CI to prove green
Usage:   iw phase-pr --title TITLE [--body BODY] [--batch] [--issue-id ID]
         [--phase-number N] [--timeout DUR] [--poll-interval DUR] [--poll-window DUR]

That is the whole of it. It never says:

  • What a flag means. --timeout, --poll-interval and --poll-window are all DUR. The difference between the total budget, the cadence of one poll and how long one invocation blocks is the entire model of the gate, and none of it is here.
  • Any default. --timeout is 30m, --poll-window 8m, --poll-interval 30s. All invisible.
  • The exit codes. phase-pr and phase-merge return 3 for "the wait is unfinished, run me again"; 1 for "CI refused the phase". That contract is the single most important thing a caller must know, and it exists only in the kanon skill text. A person at a shell, or an agent that does not have that skill loaded, cannot find it.
  • Any example.

An agent reading this help cannot drive the command correctly. That is the bar: the help text must be instructive enough that an agent with no other documentation can use the command correctly on the first try.

Required, per command:

  • Intent — what the command is for, and when to reach for it, in a sentence or two.
  • Exact syntax — the full form, with required and optional parts distinguished.
  • Every flag — what it means, what it takes, and its default.
  • Exit codes — every code the command returns and what the caller must do about each.
  • At least one worked example for a command with any real shape to it.

4. The help must be inherent to the command, not built beside it

This is a requirement on the design, not a nicety. Any arrangement where the help text lives somewhere other than the command will drift, because every added command and every changed flag then needs a second edit that nothing forces. We have that today in four independent copies of the same facts — the // USAGE: comment headers, the ad-hoc printHelp bodies in four commands, docs/commands/review-state.md, and the kanon skill text — and they already disagree.

Each command must declare its own help, in code, and that declaration must be load-bearing.

Load-bearing is the part that makes it stick. If the declaration is only a doc string, an author can add a flag and not document it. So derive the argument parsing from the declaration: an undeclared flag cannot be read, and a declared flag cannot be undocumented. Then adding a flag without describing it does not compile or does not work, and changing a flag changes its help in the same edit. Nothing else has to be remembered. This also delivers requirement 1 for free — a parser that knows its own flags rejects the ones it does not know, and a help request is dispatched from the same declaration before any action runs.

Make the declaration's shape put the author in the caller's position. The type should ask for what a caller needs, in fields the author cannot leave blank:

  • Not description — what this command does, and when to reach for it.
  • Per flag: what it controls and its default, not just its name and arity.
  • Per exit code: what the caller does next, not just what the code means.
  • At least one worked example for any command with real shape.

Required fields with no defaults, so the compiler refuses a command that declares no intent or no exit codes. An author writing a new command should be answering "how does the caller understand this?" while writing it, not filling in a form afterwards.

The // USAGE: header scrape in iw-run (parse_command_header, iw-run:261-265, used by list_commands at :337) is the external mechanism this replaces. It must be deleted, not kept alongside, or it becomes a fifth copy. (The // PURPOSE: file-header convention stays — it applies to every source file in the project and is not the help.)

docs/commands/review-state.md and the published iw-cli-ops skill must both be generated from the same declaration rather than maintained beside it. The build-iw-cli-skills skill already regenerates the skill from command source and is the natural consumer.

The one real constraint

iw --list is a bash loop that greps two comment lines per file and never starts a JVM (iw-run:337-360). It also lists plugin commands from directories iw-cli does not own, by the same scrape. Moving the source of truth into compiled Scala has to answer both:

  • Built-in commands. A single help entry point in core can hold the registry of every command's declaration and print the whole index in one JVM start. iw --list becomes as slow as any other iw command, which is the price and is probably right — but decide it deliberately rather than discover it.
  • Plugin commands. A plugin is a separate scala-cli script outside this repo. Either each plugin command answers a "describe yourself" invocation (one JVM start per plugin command, too slow for a listing), or the launcher reads a generated index file that the plugin ships. Pick one and state it, because it also settles dev-docs #161.
  • dev-docs #161 — pre-commit validator checking ./iw invocations in commands and skills against iw --list. Reads the same source.
  • #440 — regenerate docs/api and llms.txt for the undocumented core modules.
  • The project-local build-iw-cli-skills skill regenerates the published iw-cli-ops skill from command source. It must consume the same declaration (see section 4).

#439 recorded during IW-423; see project-management/issues/IW-423/LESSONS.md entry 11.

`iw` help cannot guide anyone today — not a person at a shell, not an agent driving the CLI. Four things are wrong, and they have to be fixed together: the first makes the next two unreachable, and the fourth is what stops all three from rotting again. **Supersedes #439** (a `--help` flag must never execute a command; reject unknown flags across subcommands) and **#202** (`review-state update` should support `--help`). Both are carried into this issue in full. Close them in favour of this one. ## 1. The help flag never reaches a help printer Only 4 of 34 commands handle it: `dashboard`, `feedback`, `issue`, `review-state`. Everything else either runs, or fails on an unrelated error: ``` $ ./iw worktrees --help === Worktrees === IW-437 Phase CI gate: phase-pr restarts CI o... open PR: Merged ... ``` ``` $ ./iw start --help Error: Invalid issue ID format: --help (expected: TEAM-123). ``` The worst case is recorded in #439: `./iw phase-merge --help` **merged pull request 434**. The command that performed the merge was a request for documentation. `phase-merge` has no required argument, so an unrecognised flag falls through to the action. `phase-pr` looks correct only by accident — its required-argument check happens to run first. The other half of #439 matters beyond help: **unknown flags are silently discarded on every subcommand**, so a typo'd flag becomes "run with defaults" instead of an error. A related case from the same family: `./iw issue create --title` whose value begins with two dashes is swallowed by the arg parser, and the API rejects the request with a bare 422. **Required:** - Handle the help flags before dispatch, once, for every subcommand — not per command. - Reject unknown flags rather than ignore them. - A help request must never perform the command's action, and must exit 0. ## 2. There is no overview worth reading `iw --help` prints six lines and sends the reader elsewhere: ``` iw-cli - Issue Worktree CLI Usage: iw <command> [args...] iw --list List all commands iw --describe <cmd> Describe a command iw --bootstrap Pre-compile for offline use ``` `iw --list` then prints 34 flat `Purpose:`/`Usage:` pairs with no grouping and no sense of which command to reach for when. Both are scraped out of two comment lines in each command script by `iw-run` (`parse_command_header`, `iw-run:261-265`). **Required:** an overview that is an index of the tool. Every command listed, grouped by what it is for (worktree lifecycle, phase lifecycle, review state, server and dashboard, project setup, diagnostics), one line each that says what the command does — enough for a reader who has never used `iw` to pick the right one without running three of them to find out. ## 3. The text says nothing that can be acted on This is the part neither #439 nor #202 covers, and it is the reason help is useless even where it exists. Take the most-used command: ``` Command: phase-pr Purpose: Pushes the phase sub-branch, opens the PR/MR, and waits for its CI to prove green Usage: iw phase-pr --title TITLE [--body BODY] [--batch] [--issue-id ID] [--phase-number N] [--timeout DUR] [--poll-interval DUR] [--poll-window DUR] ``` That is the whole of it. It never says: - **What a flag means.** `--timeout`, `--poll-interval` and `--poll-window` are all `DUR`. The difference between the total budget, the cadence of one poll and how long one invocation blocks *is the entire model of the gate*, and none of it is here. - **Any default.** `--timeout` is 30m, `--poll-window` 8m, `--poll-interval` 30s. All invisible. - **The exit codes.** `phase-pr` and `phase-merge` return **3** for "the wait is unfinished, run me again"; 1 for "CI refused the phase". That contract is the single most important thing a caller must know, and it exists only in the kanon skill text. A person at a shell, or an agent that does not have that skill loaded, cannot find it. - **Any example.** An agent reading this help cannot drive the command correctly. That is the bar: **the help text must be instructive enough that an agent with no other documentation can use the command correctly on the first try.** **Required, per command:** - **Intent** — what the command is for, and when to reach for it, in a sentence or two. - **Exact syntax** — the full form, with required and optional parts distinguished. - **Every flag** — what it means, what it takes, and its default. - **Exit codes** — every code the command returns and what the caller must do about each. - **At least one worked example** for a command with any real shape to it. ## 4. The help must be inherent to the command, not built beside it This is a requirement on the design, not a nicety. Any arrangement where the help text lives somewhere other than the command will drift, because every added command and every changed flag then needs a second edit that nothing forces. We have that today in four independent copies of the same facts — the `// USAGE:` comment headers, the ad-hoc `printHelp` bodies in four commands, `docs/commands/review-state.md`, and the kanon skill text — and they already disagree. **Each command must declare its own help, in code, and that declaration must be load-bearing.** Load-bearing is the part that makes it stick. If the declaration is only a doc string, an author can add a flag and not document it. So **derive the argument parsing from the declaration**: an undeclared flag cannot be read, and a declared flag cannot be undocumented. Then adding a flag without describing it does not compile or does not work, and changing a flag changes its help in the same edit. Nothing else has to be remembered. This also delivers requirement 1 for free — a parser that knows its own flags rejects the ones it does not know, and a help request is dispatched from the same declaration before any action runs. Make the declaration's shape put the author in the caller's position. The type should ask for what a caller needs, in fields the author cannot leave blank: - Not `description` — **what this command does**, and **when to reach for it**. - Per flag: **what it controls** and **its default**, not just its name and arity. - Per exit code: **what the caller does next**, not just what the code means. - At least one **worked example** for any command with real shape. Required fields with no defaults, so the compiler refuses a command that declares no intent or no exit codes. An author writing a new command should be answering "how does the caller understand this?" while writing it, not filling in a form afterwards. The `// USAGE:` header scrape in `iw-run` (`parse_command_header`, `iw-run:261-265`, used by `list_commands` at `:337`) is the external mechanism this replaces. It must be deleted, not kept alongside, or it becomes a fifth copy. (The `// PURPOSE:` file-header convention stays — it applies to every source file in the project and is not the help.) `docs/commands/review-state.md` and the published `iw-cli-ops` skill must both be generated from the same declaration rather than maintained beside it. The `build-iw-cli-skills` skill already regenerates the skill from command source and is the natural consumer. ### The one real constraint `iw --list` is a bash loop that greps two comment lines per file and never starts a JVM (`iw-run:337-360`). It also lists **plugin** commands from directories iw-cli does not own, by the same scrape. Moving the source of truth into compiled Scala has to answer both: - **Built-in commands.** A single `help` entry point in `core` can hold the registry of every command's declaration and print the whole index in one JVM start. `iw --list` becomes as slow as any other `iw` command, which is the price and is probably right — but decide it deliberately rather than discover it. - **Plugin commands.** A plugin is a separate scala-cli script outside this repo. Either each plugin command answers a "describe yourself" invocation (one JVM start per plugin command, too slow for a listing), or the launcher reads a generated index file that the plugin ships. Pick one and state it, because it also settles dev-docs #161. ## Related, not superseded - dev-docs #161 — pre-commit validator checking `./iw` invocations in commands and skills against `iw --list`. Reads the same source. - #440 — regenerate `docs/api` and `llms.txt` for the undocumented core modules. - The project-local `build-iw-cli-skills` skill regenerates the published `iw-cli-ops` skill from command source. It must consume the same declaration (see section 4). --- #439 recorded during IW-423; see `project-management/issues/IW-423/LESSONS.md` entry 11.
Author
Owner

Concrete site for the "reject unknown flags" half, found during IW-436.

ReviewStateCliParser.extractFlag (core/model/ReviewStateCliParser.scala:19-22) returns Some(args(idx + 1)) without checking whether that token starts with --. Its sibling extractRepeatedFlag (:24-30) does check:

case List(`flag`, value) if !value.startsWith("--") => value

So ./iw review-state update --status --commit silently sets status to the string "--commit" and drops the --commit behaviour. The card then holds an invalid status.

Note for whoever fixes this: this is a second mechanism, not the one this issue's remedy covers. --commit is a known flag, so an unknown-flag check does not catch it — the guard needed is "a flag's value must not itself be flag-shaped", the same guard extractRepeatedFlag already has and extractFlag lacks. It is the same shape as the ./iw issue create --title case already noted above. A full fix needs an inventory of which flags take values, including the flags consumed in core/commands/ReviewState.scala that the parser never sees.

Recorded during IW-436; see project-management/issues/IW-436/LESSONS.md entry 1.

Concrete site for the "reject unknown flags" half, found during IW-436. `ReviewStateCliParser.extractFlag` (`core/model/ReviewStateCliParser.scala:19-22`) returns `Some(args(idx + 1))` without checking whether that token starts with `--`. Its sibling `extractRepeatedFlag` (`:24-30`) does check: ```scala case List(`flag`, value) if !value.startsWith("--") => value ``` So `./iw review-state update --status --commit` silently sets `status` to the string `"--commit"` and drops the `--commit` behaviour. The card then holds an invalid status. Note for whoever fixes this: **this is a second mechanism, not the one this issue's remedy covers.** `--commit` is a *known* flag, so an unknown-flag check does not catch it — the guard needed is "a flag's value must not itself be flag-shaped", the same guard `extractRepeatedFlag` already has and `extractFlag` lacks. It is the same shape as the `./iw issue create --title` case already noted above. A full fix needs an inventory of which flags take values, including the flags consumed in `core/commands/ReviewState.scala` that the parser never sees. Recorded during IW-436; see `project-management/issues/IW-436/LESSONS.md` entry 1.
mph closed this issue 2026-09-09 07:57:16 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/iw-cli#463
No description provided.