# The floor

> The requirements every CLI on burgee meets, and who holds each — runtime, lint, or both. 74 of the floor's 114 are tabled here.

Source: https://burgee.interlace.tools/docs/the-floor

The floor is **114 requirements**: the 109 rows of the requirement tables in
[`.sdlc/intents/burgee/spec.md`](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/intents/burgee/spec.md#requirements--the-cli-floor)
plus the five shape-lock rows Z1–Z5 above them, which is the count that spec states. This
page tables **74** of them — the families below. The spec is the complete list, and what is
built, requirement by requirement, is recorded there too.

Each requirement is held by the **runtime**, 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](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/research).

## Discoverability

| 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 |

## Output

| 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 |

## Errors and lifecycle

| 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 |

## Values and precedence

| 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 |

## Validation

| 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 |

## Prompts

| 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 |

## Deprecation and testing

| 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 |

## Validation, continued

| 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 |

## Values, continued

| 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 |

## Help

| 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 |

## Completions, continued

| 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 |

## Prompts, continued

| ID | Requirement | Holds |
| :-- | :-- | :-- |
| P3 | Cancellation exits `CANCELLED` (4), never `RUNTIME` | runtime |

## Modularity

| 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 |

## Packaging

| ID | Requirement | Holds |
| :-- | :-- | :-- |
| K1 | No dependency outside the burgee family in any published package; hosts and UI libraries are peers | lock |
| K2 | ESM only, Node 20.19+ or 22.13+ (restated 2026-09-23, D-132; was 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 |

## Compatibility

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 |

## Benchmarks

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 |

## Agent interface

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 is kept from agents only by a declaration: `effects: 'withheld'`. Every runnable command on burgee's own API declares its effects; a façade command that declared nothing is listed as `effects: 'undeclared'`, with no hints | both |
| N3 | No dependency outside the burgee family: 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 |
