feat(cli): the command contract — help is the contract, and it is enforced (v0.20.0)#293
Merged
Conversation
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
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 clauderan 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
CliParamon each command'sInput: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 documentmodular_apigenerates from its registered use cases.Fixed
iq doctorcould 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-failurecatchnever fired.iq doctorwas blind to the skills that matter. It expected only the asset-tree skills, reportingagent + 3 skills deployedon a host carrying 7 — the four generatediq-*phase skills went unverified, and a host missingiq-planread as healthy.specificationandissueshipped 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.specification_readygate passed specs with a whole user story unimplemented. AC ids were bareAC-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
cleanrooms/working area is git-ignored — only the state file was, sodiagnosis.md/plan.mdaccumulated. A cleanroom is a per-cycle working area; the durable artifacts are the published issue, the code and the tests.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):
iqprinted the help instead of its banner, because the SDK assumed no route could claim the empty invocation.params: const []was the same value as the default — so zero-option commands could not be enforced. That is the original bug.--help: you had to supply the very argument you were asking about.Verification
578 tests green,
dart analyzeclean (one pre-existing doc-comment lint). Every behaviour above was driven against the compiled binary, not just asserted in tests.