Skip to content

feat(cli): the command contract — help is the contract, and it is enforced (v0.20.0)#293

Merged
ccisnedev merged 10 commits into
mainfrom
review/cli-ux
Jul 14, 2026
Merged

feat(cli): the command contract — help is the contract, and it is enforced (v0.20.0)#293
ccisnedev merged 10 commits into
mainfrom
review/cli-ux

Conversation

@ccisnedev

Copy link
Copy Markdown
Owner

Eight changes, each found by using the tool rather than reading it. Every one is reproduced by execution before it is fixed, and verified by execution after.

The through-line

iq init --host claude ran and exited 0. The flag did nothing — but nothing said so, so you believed it had. Chasing that led to the CLI's help, then to the SDK underneath it, and back. What comes out is a CLI where one declaration is the help, the machine catalog, and the parser — they cannot drift, because they are the same thing.

Added

Every command declares its contract. Parameters are declared as CliParam on each command's Input:

  • iq <command> --help → that command's full contract (type, required, default, allowed values), positionals included, without having to supply them. iq <module> --help → every command under it.
  • iq help --json → the whole catalog as JSON — the machine twin of the text help, and the CLI analogue of the OpenAPI document modular_api generates from its registered use cases.
  • An undeclared option is refused, not ignored. The 8 commands that take no options declare an empty contract — a statement, not an absence:
$ iq init --host claude
Error: unknown option --host [VALIDATION_FAILED]
  parameter: host
Usage: init ...
exit=7

Fixed

  • iq doctor could hang forever. Only the TCP handshake was bounded; a connection accepted but never answered waited indefinitely (observed: a 1m40s hang, killed by hand). Nothing threw, so the silent-on-failure catch never fired.
  • iq doctor was blind to the skills that matter. It expected only the asset-tree skills, reporting agent + 3 skills deployed on a host carrying 7 — the four generated iq-* phase skills went unverified, and a host missing iq-plan read as healthy.
  • The help had drifted and could not stop. A hand-written string: specification and issue shipped and were never listed in it, for two releases. It is now rendered from the catalog every registration feeds — a registered command cannot be absent from it.
  • The specification_ready gate passed specs with a whole user story unimplemented. AC ids were bare AC-N, but numbering restarts per story, so US-1/AC-1 and US-2/AC-1 collapsed onto one id. Reproduced end-to-end: a login issue green-lit a spec whose CSV-export story had no issue at all. Ids are now qualified by their story (US2-AC3), read from the heading (US-2: / HU-2:) so the token is the same in both languages. Spec tables are unchanged.

Changed

  • The whole cleanrooms/ working area is git-ignored — only the state file was, so diagnosis.md / plan.md accumulated. A cleanroom is a per-cycle working area; the durable artifacts are the published issue, the code and the tests.
  • Requires modular_cli_sdk 0.3.3 + cli_router 0.1.0.

What this found in the SDK

Integrating the SDK surfaced three defects in it — none observable from inside the SDK, because its example had no CLI shaped like a real one. Each was fixed with TDD and released upstream (0.3.1, 0.3.2, 0.3.3):

  1. The bare invocation was hijacked: iq printed the help instead of its banner, because the SDK assumed no route could claim the empty invocation.
  2. An empty parameter contract was inexpressible — params: const [] was the same value as the default — so zero-option commands could not be enforced. That is the original bug.
  3. A command with positionals could not answer --help: you had to supply the very argument you were asking about.

Verification

578 tests green, dart analyze clean (one pre-existing doc-comment lint). Every behaviour above was driven against the compiled binary, not just asserted in tests.

ccisnedev added 10 commits July 13, 2026 18:38
checkLatestVersion set only HttpClient.connectionTimeout, which bounds
connection establishment. Neither request.close() nor the body read had a
deadline, so a connection that is accepted but never answered left iq doctor
waiting forever (observed: a 1m40s hang, killed by hand). Nothing threw, so
the catch never fired.

Apply a deadline to the entire exchange and force-close the client, since a
plain close() waits for the in-flight connection a stalled request never
finishes.
The deployer installs the asset-tree skills plus the ones it generates from
the FSM/APE contracts (iq-analyze, iq-plan, iq-execute, iq-specification),
but doctor discovered its expectations from the asset tree alone. It reported
'agent + 3 skills deployed' on a host carrying 7, and a host missing iq-plan
read as healthy.

Expect the generated set as a fixed contract rather than mirroring the
deployer's best-effort skip: deriving expectations from what happens to build
would quietly lower the bar again, which is the blindness being removed.
Only cleanrooms/**/.iq.state.yaml was ignored, so diagnosis.md and plan.md
accumulated in the repo. The cleanroom is a per-cycle working area: the
durable artifacts of a cycle are the published issue, the code and the tests
— the same reasoning that already makes docs/requisitions/ local.
…ten string

inquiryHelpText was maintained by hand and had drifted: the specification and
issue modules shipped and were never listed, so half the methodology was
invisible to iq help. The stale 'specification new' description still pointed
at the pre-0.19.0 requisitions/<slug>/ layout.

Render the listing from the router's own registry, so a registered command
cannot be absent from the help, and drop the orphan dash the bare 'iq' route
produced. runInquiry now accepts stdout/stderr sinks so the help a user
actually sees can be asserted end-to-end.
…lse-green gate

AC numbering restarts in every story (the templates say the id cell holds only
the number), but the gate collapsed them into a flat Set of bare AC-N ids. So
US-1/AC-1 and US-2/AC-1 became the same id: an issue declaring covers: [AC-1]
traced both, and specification_ready passed a spec whose second story had no
issue at all. Reproduced end-to-end — a login issue green-lit a spec whose CSV
export story was entirely unimplemented.

The id is now composed from the story that contains it, read from the heading
(US-2: in English, HU-2: in Spanish) so it is the same token in both languages.
The spec tables are unchanged — the cell still holds the bare number, which is
what keeps the PDF column narrow.
The bump alone regressed the bare `iq`: 0.3.0 rewrote the empty invocation
into `help` unconditionally, silently replacing the TUI (banner + FSM
diagram). No test caught it — nothing asserted what `iq` with no arguments
does. Add that test, and take the SDK fix (0.3.1) that lets a registered root
route own the empty invocation.
…alog

Inquiry maintained its own help: first a hand-written string (which drifted —
the specification and issue modules were absent from it for two releases), then
a shim I wrote to render the listing from the router. modular_cli_sdk 0.3.x
does this natively, from the catalog every registration feeds, so a command
cannot ship without appearing in the help.

Delete help.dart, stop registering a help command (registering one would
override the SDK's), and stop normalizing --help/-h — the SDK routes every help
request, including the focused `iq <command> --help` this could not.
Each command now declares its parameters as CliParam on its Input, so one
declaration feeds three things that used to be maintained by hand and drift
apart: the help, the JSON catalog, and the parser.

- 10 commands declare their parameters; the 8 that take none declare an EMPTY
  contract, which is a statement, not an absence. That is what kills the bug
  this whole thread started from: 'iq init --host claude' used to run and exit
  0, letting you believe a flag did something. It now exits 7 and hands back the
  contract it violated.
- 'iq <command> --help' renders that command's contract, positionals included,
  without having to supply them; 'iq help --json' emits the whole catalog with
  each parameter's type, default and allowed values.
- Descriptions no longer restate their own flags ('--lang <en|es> (default:
  en)'): the contract renders that, and a description that repeats it is one
  more thing to drift.

Requires modular_cli_sdk 0.3.3, whose three defects this integration surfaced:
the empty invocation hijack, the inexpressible empty contract, and a command
with positionals being unable to answer --help.

Co-authored-by: ccisnedev <ccisndev@gmail.com>
Co-authored-by: ccisnedev <ccisndev@gmail.com>
Three defects in an already-published SDK with 102 green tests, surfaced
only by integrating it into a real consumer and each shipped upstream
(0.3.1, 0.3.2, 0.3.3). Contract errors at the integration boundary are
unreachable by tests written against the package's own example — use is
the measuring instrument, which is evidence-over-inference in practice.
@ccisnedev
ccisnedev merged commit 1e0cdbd into main Jul 14, 2026
2 checks passed
@ccisnedev
ccisnedev deleted the review/cli-ux branch July 14, 2026 07:16
ccisnedev added a commit that referenced this pull request Jul 14, 2026
feat(cli): the command contract — help is the contract, and it is enforced (v0.20.0)
ccisnedev added a commit that referenced this pull request Jul 15, 2026
feat(cli): the command contract — help is the contract, and it is enforced (v0.20.0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant