The floor
The 74 requirements every CLI on the layer meets, and who holds each — runtime, lint, or both.
Each requirement is held by the runtime (the commander-* / yargs-* extensions), by the lint plugin
(eslint-plugin-cli-floor), or both. A rule with neither is a suggestion, and suggestions are
not a floor. Evidence for every line is an open issue in a leading CLI library — see
Research.
| ID | Requirement | Holds |
|---|
| F1 | --schema prints the full command tree — commands, options, types, defaults, env bindings, examples, deprecations, exit codes — as JSON | runtime |
| F2 | --help --json prints help as data; text help is rendered from that data | runtime |
| F3 | Every command declares a description and at least one single-line example | lint |
| F4 | Commands may be grouped and hidden; groups are stable in the schema | runtime |
| ID | Requirement | Holds |
|---|
| O1 | --json on every command, one envelope { ok, data, error?, meta } | both |
| O2 | No ANSI, spinners, redraws or prompts when stdout is not a TTY or NO_COLOR is set; FORCE_COLOR overrides | both |
| O3 | Command code writes through the output layer, never console.* | lint |
| O4 | Colour via util.styleText; no chalk dependency | both |
| O5 | stdout is flushed before every exit path | runtime |
| ID | Requirement | Holds |
|---|
| E1 | Exit codes are a contract: 0 ok, 1 runtime, 2 usage, 3 config, 4 cancelled, 130 SIGINT | both |
| E2 | A runtime failure never prints help; a usage error never prints a stack | both |
| E3 | Every error carries code, message, hint and, where possible, fix | runtime |
| E4 | Explicit lifecycle: parse → config → validate → run → render → exit; validation failure stops the handler; async handlers are awaited | runtime |
| E5 | SIGINT restores the terminal and exits 130 | runtime |
| ID | Requirement | Holds |
|---|
| V1 | Flags > env > config file > default, per command, without leaking env into unrelated commands | runtime |
| V2 | Any option may declare an env name; env-bound options appear in help and schema | both |
| V3 | --explain <option> reports where each value came from | runtime |
| V4 | name, version, description come from the owning package.json, not the monorepo root | runtime |
| V5 | Reserved option names cannot be redefined | lint |
| ID | Requirement | Holds |
|---|
| S1 | Options and positionals are declared once as a schema; types and help are derived | runtime |
| S2 | Relationships are first-class: exactly-one-of, at-least-one-of, implies, conflicts | runtime |
| S3 | Invalid numbers fail validation, never NaN; invalid types fail at definition time | runtime |
| S4 | - means stdin for file positionals; -- pass-through reaches child processes intact | runtime |
| ID | Requirement | Holds |
|---|
| P1 | Every prompt is backed by a flag; a flag value skips the prompt | lint |
| P2 | In a non-TTY the prompt becomes an error naming the flag, exit 2 | runtime |
| ID | Requirement | Holds |
|---|
| D1 | Deprecating anything requires a replacement, shown in help, schema and the warning | both |
| D2 | Completions for bash, zsh, fish and PowerShell are generated statically from the schema | runtime |
| T1 | A CLI runs in-process with injected argv, env, stdin, cwd and TTY-ness, returning { code, stdout, stderr, json } | runtime |
| ID | Requirement | Holds |
|---|
| S5 | Every option has one declared type and one canonical camelCase key; kebab-case is derived | runtime |
| S6 | Relations are validated before choices and before the handler | runtime |
| S7 | A flag type never consumes a value | runtime |
| S8 | multiple options accept repetition and a declared separator | runtime |
| ID | Requirement | Holds |
|---|
| V6 | Config discovery order is fixed, documented, and shown by --explain | runtime |
| V7 | extends merges deeply and resolves from the extending file's node_modules | runtime |
| ID | Requirement | Holds |
|---|
| H1 | Help is rendered from the manifest only | runtime |
| H2 | Examples are single-line and copy-pasteable | both |
| H3 | Width comes from the runtime, default 100 in non-TTY | runtime |
| H4 | Command options render before global options | runtime |
| H5 | Deprecations and env names render inline | runtime |
| H6 | Type hints are off by default | runtime |
| ID | Requirement | Holds |
|---|
| D3 | Completions never execute the CLI unless an option is marked dynamic | runtime |
| D4 | Every shell script is snapshot-pinned and exercised by that shell in CI | runtime |
| D5 | A Fig spec is exported from the same node | runtime |
| ID | Requirement | Holds |
|---|
| P3 | Cancellation exits CANCELLED (4), never RUNTIME | runtime |
| ID | Requirement | Holds |
|---|
| M1 | Every command carries a group | runtime |
| M2 | The manifest is complete before any handler module loads | runtime |
| M3 | Every plugin's contributions are attributed in the manifest | runtime |
| M4 | Shared options are declared once and copied per command | runtime |
| M5 | A deprecated command names its replacement in help, schema and warning | both |
| M6 | resolveCommand and runCommand are public | runtime |
| ID | Requirement | Holds |
|---|
| K1 | Zero runtime dependencies in every layer package; hosts and UI libraries are peers | lock |
| K2 | ESM only, Node 24 or newer | lock |
| K3 | Node natives over packages | both |
| K4 | An artifact gate runs on the built output before publish | release |
| K5 | Per-package size budget, ratcheted | lock |
| K6 | Weight is paid per import: compatibility and host quirks live behind their own specifiers, never behind a runtime flag | lock |
Every claim about a host is graded by that host's own test suite, so "compatible" is a
number rather than an adjective. The rate is published on every release.
| ID | Requirement | Holds |
|---|
| C1 | Every package declares a supported host range; the host's own suite runs against it at every supported major | CI |
| C2 | A compatibility front-end is graded by the host's own suite through a one-line shim; the rate is published per release | CI |
| C3 | Every package is tested on every Node LTS in its engines range, across Linux, macOS and Windows | CI |
| C4 | Every intentional divergence has an id, a written reason, and a test asserting it — an unlisted failure is a bug | lock |
| C5 | Pass rates ratchet; lowering one requires editing the baseline with a reason | CI |
| C6 | Vendored suites record their upstream commit; a scheduled job opens a pull request when the count changes | workflow |
Four axes, because there are four kinds of public claim. Every number the project cites
anywhere links back to the generated benchmarks page.
| ID | Requirement | Holds |
|---|
| B1 | Agent cost: tokens, turns and success per task, layer on versus off | band |
| B2 | Performance: cold start p50/p95 over 30 or more spawns, always including a bare-node floor row | band |
| B3 | Compatibility: per-host pass rate, read from the oracle, never recomputed | band |
| B4 | Weight: bundled size per entry point against a published target; a core-only import pulls zero front-end bytes | band |
| B5 | Every axis emits one JSON shape, so one collector reads all of them | lock |
| B6 | Performance, compatibility and weight gate every pull request; agent cost runs weekly | CI |
| B7 | Every public number links to the generated benchmarks page | lock |
The manifest already carries everything an MCP tool definition needs. These requirements
turn it into a served interface rather than a document.
| ID | Requirement | Holds |
|---|
| N1 | --mcp serves the CLI over MCP stdio; tool definitions are generated from the manifest, never hand-written | runtime |
| N2 | A command appears as a tool only if it opts in; destructive commands default to absent | both |
| N3 | Zero runtime dependencies: JSON-RPC over stdio against node:readline | lock |
| N4 | Tool results are the standard envelope, so MCP and --json callers see identical payloads | runtime |
| N5 | --mcp implies non-TTY: no prompts, no colour, structured errors | runtime |