Repository navigation
Specs cli extension
This spec covers the contracts a CLI's screens and the tool's own files are read through: the
screen grammar — the core's stage order, the dialog rule primitives, the four composer modes —
the composer-region safety floor, the restricted portable pattern dialect both engines share, the
staged escape hatch for a CLI the data primitives cannot express, the team file's modular
sections and the plain command shape, and the cross-implementation conformance suite. The state
words a reading yields and the watch checks that consume it are the
seat-state-and-watch spec's; the profile file — a CLI's launch
fields, its tested ranges, its rules modes, the additive overrides file — is the
launch spec's §3.4; the policy object whose sources sit in the file's owner
sections is the policies spec's. It says what the code at the tag does, in the code's
words.
- screen: one reading of a pane's lines — a kind, with what the composer read.
-
stage: one named step of the reading's fixed order; the caution stages are
unknown,trust,permission,exit_question,question,vendor_notice,provider_error,rate_limit,working, and the composer reads after them all. -
dialog rule: the pattern set a stage reads —
any/allline patterns, afooter, abelow_last_ruleanchor,none_after,only_after,live,without_rule, or ablock. -
composer: the pane's input box, read by one of four modes —
box-to-rule,status-last,status-then-one,two-rules-footer-below. -
composer region: the rows the composer's own mode reads — its box, rules and footers, and
the newest turn of transcript above them; the only region a
liverule sees. - safety floor: the generic dialog markers the core matches itself, without case, so no profile carries them and none can turn them off.
- dialect: the restricted regular-expression language a profile's patterns are written in, compiled by the loader so one pattern means the same under both engines.
-
hatch: a
ScreenProfile— staged predicates shipped as package code, for a CLI whose screens the data primitives cannot express. -
section: one module of the team file — a
Sectioninterface owning its own schema fragment. - plain command: a command written as a function over the parsed file, with no registry of command objects between the parse and the call.
- conformance manifest: the shared fixture list an implementation is checked against.
-
budget: the reading's time bound — 100 ms; a reading past it answers
unknown. - window: the pane's last 20 lines, trimmed of trailing space — the only lines the data patterns see.
The reading walks the caution stages in the core's fixed order — unknown, trust,
permission, exit_question, question, vendor_notice, provider_error, rate_limit,
working — first match wins, and the composer reads after them all (classifyLines,
src/watch/screen-core.ts). A profile's screen: map holds the stages as named keys; each is
{ rules: Rule[]; tested?: VersionRange }, where tested is the capture range a vendor notice
carries ({ from, to }) and nothing else may carry — the loader's stageOf requires it of
vendor_notice and admits only ignore_case and rules in the other stages' maps
(src/watch/screen-file.ts; src/watch/screen-data.ts holds the Stage type, whose tested?
is not per-stage). The data patterns see the window — the pane's last 20 lines trimmed; a
profile that loads a screen_module (§3.6) sets the reading's whole_screen flag (ScreenData,
src/watch/screen-data.ts; the loader, src/watch/screen-file.ts), and the reading then sees
the whole pane, not the window (src/watch/screen.ts). The composer reading is
{ kind, from?, input? } (ComposerReading, src/watch/screen-profile.ts): the kind the
composer's box holds, the row its text starts on, the row its input line sits on.
One rule of a stage (Rule, src/watch/screen-data.ts):
| Field | Shape | Meaning |
|---|---|---|
any / all
|
{ match, except[] } line patterns |
one line matching any / every line matching all; except takes a matching line back |
footer + onFooter
|
string, boolean | the dialog's last row; with onFooter the patterns read on that row only, not every line |
below_last_rule |
pattern | matched under the rule line the core owns — a run of eight or more box-drawing dashes |
none_after |
{ anchor, patterns[] } |
after the last anchor row, no row may match |
only_after |
{ anchor, patterns[] } |
every non-blank row after the last anchor row matches one of these — the block's own tail, nothing of another dialog's |
live |
true |
read on the composer region only, so a sentence the window has scrolled past is not the dialog; a window whose composer cannot be read keeps the whole window |
without_rule |
true |
the window holds no composer rule: the dialog is the pane |
block |
steps | the dialog read as one contiguous block of rows, from the last row matching its first step to the screen's last non-blank line |
A block step is a row the dialog draws, a blank it spaces, the list run it fills with
its own text, or oneOf alternatives — each a list of steps, an empty one for a form that
draws no row there. A pattern may carry its own ignore_case; the flag is the entry's, read
with the stage's, so except sees the same lines match sees. Both anchors read the last row
that matches them, and a window that holds no anchor row fails the rule (lastAnchor,
src/watch/screen-core.ts).
Four modes, each a variant of Composer (src/watch/screen-data.ts):
-
box-to-rule — the composer is its box: the rows the frame draws, from the opening rule to
the closing rule.
borderis not this mode's: the framed rows a capture shows beside the blank frame belong tostatus-lastandstatus-then-one(Border,src/watch/screen-data.ts). -
status-last — a status line the CLI draws (
statusLine), what sits below it (statusBelow), the prompt, the placeholders, the wrap of continued rows, the frame rows and the border. -
status-then-one — a status line, then one input row the prompt names; when
statusLastfinds no status line in the window, the mode'sfallback: FallbackRule[]decides — each rule{ all: LinePattern[]; kind }, every pattern matched somewhere in the window, anything they do not nameunknown(statusThenOne,src/watch/screen-core.ts). - two-rules-footer-below — the composer between two rule lines, its footer below the second.
Placeholders are { equals } or { prefix }; the greyed suggestions are told by their styling
when the CLI renders them dim (placeholder_style: dim), else by the list. Every composer
carries frameRows — the empty rows a capture shows the pane drawing under the text, inside the
frame. A wrap is { continuation: 'text-column', kind: 'word' | 'hard' }: the continuation
starts at the text column, and the break folds the space at a word boundary or falls mid-word.
The floor's markers are the core's own list, matched without case — a dialog's wording is not
always title-case — and only in the composer region, so a transcript line that quotes a dialog
is not one (FLOOR_PHRASES, src/watch/screen-core.ts):
do you want to · esc to cancel · enter confirm · enter continue · esc quit · esc skip · ↑/↓
A numbered choice is a marker too, and it is matched before the phrases: a row opening on ❯,
› or > then 1., counted only with a later 2. row below it and never on the input row
(choiceLine, twoLine, floorHits, src/watch/screen-core.ts) — RFC 0002 §4.1 lists this
marker first. The composer region is the floor's bound for a data composer — from the
composer's top boundary to the end of the window, the input row skipped — while a hatch
composer's floor reads the whole pane (§3.6).
The rule line a below_last_rule pattern sits under is the core's too — a run of eight or more
box-drawing dashes — not a profile's. The dialog chrome is a shipped profile's own ignore
list for floor rows — whole lines, anchored at both ends — and the floor skips a row one of its
patterns matches; a pattern that would match a numbered choice is refused at load, so a
numbered choice can never be chrome. The whole-pane reach is not the chrome's: it belongs to
the screen_module hatch (whole_screen, §3.1). An override file only adds dialog patterns
to a shipped profile; the additive limit is the launch spec's §3.4.
One dialect, both engines (src/watch/dialect.ts). The loader rewrites the shorthand classes
before compiling, so a pattern means the same under JavaScript's u flag and under Rust's
Unicode mode: \d becomes the digit class, \w the word class, \s a whitespace set fixed in
code — the codepoints JavaScript's own \s has matched since ES2019, no more — and \b
carries its fixed meaning, the ASCII word boundary; so the rewrite never depends on the host's
flag. A pattern is at most 200 characters and a quantifier at most 1000. The character-code
escapes \u, \U and \x are refused, and so are backreferences \1–\9 and any escape
the parser does not model; the control escapes \n \r \t \f \v, the word boundary \b and
the shorthand classes are accepted — a pattern's text is otherwise literal, non-ASCII
included, and what it says travels with it. The parser rebuilds the pattern from its own node
shapes and accepts only what it models whole: lookarounds, backreferences, named groups and
inline flags are refused, and so are the repetition shapes that could run without bound — a
repeated alternation opens every branch on a single unquantified literal character, and a
repeated group whose body is one branch opens on no optional or repeated atom
(checkRepeatedGroup). ignore_case is the stage's flag, never an
inline one: an ASCII fold written into the pattern's own text — each ASCII letter becomes its
two-letter class — so the other engine compiles the same source, and the engine's wider Unicode
folding never applies. Every refusal is a DialectError at load, in words that name the
pattern's fault; nothing is half-read.
A CLI whose screens the data primitives cannot express ships a ScreenProfile
(src/watch/screen-profile.ts): optional predicates unknown, trust, permission,
question, working, and a composer reading. Every hatch predicate is monotone toward
caution, running alongside its data stage — hatch or data — so a hatch can only add a match,
never remove one; a hatch predicate that itself fails answers unknown. A composer comes from
data or from the hatch, never both: a profile with a data composer while its module exports one
is refused at load (src/watch/screen-file.ts). A vendor notice is not among the hatch's
stages: it exists only as a data stage carrying the tested range it was captured on — no
record, no vendor notice. A hatch is trusted package code, not a sandbox.
The team file parses and writes one section at a time: each section is a module — the
Section interface, no type parameter, each section owning its own schema fragment's type
(src/file/sections/section.ts) — with the cross-section checks held by the core, not
repeated per section. Twenty-nine sections stand at the tag, from
format and trust through seats, policies, delegates, grant, tasks and tempo.
The owner sections' names are generated from the section list — SECTIONS filtered by
section.owner, in src/approve/fingerprint.ts — never a hand-kept list; a new owner section
appears in OWNER_SECTIONS by its own owner: true, and nothing else changes. The tool's
commands stay plain functions over the parsed file; no registry of command objects sits between
the parse and the call.
The manifest (test/fixtures/conformance.json) holds two lists: screens — one entry per
fixture { file, cli, classify, composer, provenance }, provenance saying whether the entry
is a capture or a constructed case, the kinds an implementation must read from
it — and yaml — one entry per profile fragment { file, ok }, the acceptance or refusal an
implementation must reproduce. Non-ASCII captures are among them. The runner
(scripts/conformance.ts) starts an implementation as a command, feeds it one JSON request per
line on stdin and reads one JSON answer per line, so any engine that can read a line can be
checked; bun run conformance runs it against the TypeScript adapter.
The conformance suite is a repository script, not a team command:
bun scripts/conformance.ts --impl "<command>"
bun run conformance # the TypeScript adapter
This spec contributes no team command and no exit code row.
-
Stage order and the floor are the core's own. A profile is data: it MAY add a pattern,
and it MUST NOT move a stage, reorder the reading or turn the floor off. Source:
src/watch/screen-core.ts. Proof:test/screen-core.test.ts. -
The exit question reads before the ordinary question. Its footer is one an ordinary
question carries, so the stage a stop may answer MUST win over the kind it must never
answer; the order is the core's. Source:
classifyLines,src/watch/screen-core.ts. Proof:test/screen-core.test.ts. -
The reading is bounded. The clock is checked between stages; past the 100 ms budget the
reading answers
unknownand nothing else. Source:BUDGET_MS,tick,src/watch/screen-core.ts. Proof:test/screen-core.test.ts. -
The floor matches only in the composer region, without case. The markers are the core's
own list (§3.4) — the numbered-choice pair first, then the phrases; a profile never carries
them, and a transcript line that quotes a dialog is not one. For a data composer the region
runs from the composer's top boundary to the end of the window, the input row skipped; for
a hatch composer the floor reads the whole pane. Source:
FLOOR_PHRASES,choiceLine,twoLine,floorHits,src/watch/screen-core.ts. Proof:test/screen-core.test.ts,test/screen-hatch.test.ts. -
One dialect, both engines. A pattern MUST compile under the shared dialect before it is
read; the rewrites and refusals are §3.5's, and a refusal at load — a pattern past 200
characters, a
\u/\U/\xescape, a backreference, an unsupported escape, an inline flag, a shape the parser does not model — is aDialectError, never a half-read. Source:compilePattern,src/watch/dialect.ts. Proof:test/profile-schema.test.ts,test/conformance.test.ts. -
The hatch can only add caution. A hatch predicate runs alongside its data stage; a
composer from both data and hatch MUST be refused at load; a vendor notice is a data stage
carrying its
testedrange, never a module export. Source:src/watch/screen-profile.ts,src/watch/screen-core.ts,src/watch/screen-file.ts. Proof:test/screen-hatch.test.ts. -
The file is sectioned and the owner sections are generated. The team file MUST parse and
write one section at a time, each section owning its schema fragment, the cross-section
checks held by the core; the owner sections' names MUST be generated from the section list,
never hand-kept. Source:
src/file/sections/,src/approve/fingerprint.ts. Proof:test/file-sections.test.ts. -
Commands stay plain functions. A command is a function over the parsed file; no registry
of command objects sits between the parse and the call. Source:
src/commands/. Proof: none at the tag — a structural reading of the commands, whose every file exports plain functions. -
An implementation conforms when the manifest passes. Every screen case MUST return its
expected
classifyandcomposerkinds and every YAML case its expected acceptance or refusal, under the implementation's own engine; the manifest is shared, the adapter is the implementation's. Source:scripts/conformance.ts,test/fixtures/conformance.json. Proof:test/conformance.test.ts.
None. This spec adds no registry row: the profiles are data files (the launch spec's §3.4), the policy names whose sources sit in the owner sections are the policies spec's, and the conformance suite is a repository script.
-
The floor's case-insensitive matching. The code matches the floor's markers without
case — its own comment says a dialog's wording is not always title-case
(
src/watch/screen-core.ts) — and this departs from the archived feature note this spec lifts, which says the markers are matched case-sensitively (RFC 0002 §4.1, and its change log R1). The round is asked to confirm the departure at the port. -
The profiles' own pattern content. A CLI's patterns are data, versioned by its
testedranges; a CLI whose screens change re-captures and re-ranges, and this spec does not track them. - A second engine's adapter. The port reads the same manifest through its own adapter command; the adapter is the port's work, and the manifest grows with it.
- Draft (the writer, 2026-10-19): lifted from the archived feature note's contract as shipped at the tag — the screen grammar (the core's stage order, the dialog primitives, the four composer modes), the composer-region safety floor, the restricted portable patterns, the staged escape hatch, the modular file sections and the plain commands, the cross-implementation conformance suite (package S4 of tonight's plan). The state words link to seat-state-and-watch.md and the profile file to launch.md §3.4, nothing copied; the Kernel amendment's carry is its commitment 2.16 and its §12 link.
-
Phase A review fold (the writer, 2026-10-19): all four independent reviews folded, each
PASS WITH CHANGES and each correction verified at the tag. The owner-section list's source is
src/approve/fingerprint.ts, notsrc/file/fingerprint.ts(§3.7, rule 7);whole_screenis the reading's ownScreenDataflag, set only when a profile loads ascreen_module(§3.1); thefallback: FallbackRule[]rules moved tostatus-then-one, where they run afterstatusLastfinds no status line (§3.3); thetestedrestriction cites the loader'sstageOf(§3.1);none_afterandonly_afterread the last anchor row and fail a window with no anchor (§3.2); box-to-rule ends at the closing rule,borderbeing the two status modes' (§3.3); the numbered-choice marker joined the floor before the phrases and the chrome clause rewritten as the floor's whole-line anchored ignore list, refusing a numbered choice (§3.4, rule 4); the hatch composer's floor reads the whole pane, the data composer's from its top boundary skipping the input row (§3.4, rule 4); the refused escapes narrowed to\u/\U/\x, backreferences and unsupported escapes,\bnamed with the fixed-meaning classes, the second repeated-group refusal added (§3.5, rule 5); theSectioninterface carries no type parameter (§2, §3.7); the manifest entry carriesprovenance, capture or constructed (§3.8); the case-insensitive floor stated as a departure from RFC 0002's case-sensitive wording, not a reading of it (§7); seat-state-and-watch.md and launch.md linked at their prose mentions (§1, §3.4, §6).
Generated from docs/specs/cli-extension.md at v0.5.2 (1d1224a4) — edit the repository, not this page.