Inline prompts

A prompt is an input, and painted's CLI grammar already has an input channel: declared flags. --force and 'Are you sure? [y/N]' are the same declaration at different fidelities — one resolves from argv, one resolves interactively at a TTY. A declared prompt is the parser's fourth reflection, after parse, help, and completion: one declaration generates a flag, a rendered question, an honest refusal, and completion of its answer values.

Confirm(name, question, default=, danger=)
A yes/no question — the two-element domain.
Generates --name/--no-name; danger=HARD swaps the pair for a value-carrying --name <challenge> and a bare --no-name.
Select(name, question, values=|vocabulary=, default=)
A choice over an enumerable domain.
values= is an open tuple; vocabulary= is a declared Vocabulary whose members are the legal values — the mark channel styles them wherever the answer renders.
Input(name, question, parse=, completer=, default=)
A free-text question over an open domain.
parse raises to reject; its return value becomes the answer. completer= rides the third reflection; without one the flag falls back to file/dir completion.
ctx.ask(name_or_prompt)
The single door an answer comes through — memoized, fires at most once per run.
A Tag's answer lives in ctx.args; a Prompt's answer lives behind ctx.ask — never both, so nothing silently bypasses the resolution ladder.
--no-input
One framework flag: every prompt resolves as if stdin were not a terminal.
CI scripts declare their nature instead of relying on TTY detection.

What you get for free

Declare a prompt beside your tags and it generates its own flag, its own -h entry, and completion of its answer values — with zero prompt-specific code in any of the three. At a TTY, the same declaration renders and reads an answer; everywhere else, it resolves from the flag or the declared default and leaves one line of proof.

✓ force: no (default)   
✓ scope: local (default)
Real record lines from a non-interactive run: a declared default resolving for Confirm and Select, each marked (default) — the transcript's proof that nobody was asked.

The resolution ladder

Every prompt resolves the same four-step ladder, declared or asked at runtime: the argv flag first (it's already visible in the invocation), then an interactive prompt at a TTY, then the declared default, then an honest refusal. A script without a flag or a default never hangs and never invents an answer — it gets a ContractError naming the exact flag that would resolve it.

stdin is not a terminal and no answer was provided for 'overwrite' — pass `--overwrite` / `--no-overwrite`
The real refusal text: no flag, no default, stdin not a terminal — the error names the flag, because the flag provably exists.

Danger tiers

Danger.NONE
y/N — Enter accepts the default.
The only tier that may carry default=.
Danger.SOFT
y/N — no Enter-default, an explicit key.
"Did you mean to proceed?" — accidental Enter, muscle memory.
Danger.HARD
Type the declared challenge= to proceed.
"Do you know what you're aiming at?" — Confirm-only; anything but an exact match resolves False, fail-closed.

Why this matters

The prompt UI draws on stderr, never stdout, so `tool --json | jq` stays parseable even when the tool asked a question mid-run. And a prompt never forces an environment rewrite: it renders at whatever rung the terminal supports — a raw-mode cursor at a real TTY, a cooked-mode y/n on a dumb terminal or screen reader, or no interaction at all in a script — and every rung answers the same question the same way.

Design note

clig.dev names 'conversation as the norm' as a tenet no standalone prompt library can fully honor, because an honest refusal must name the flag that answers the question — and the flag lives in the application's parser. painted is a prompt library and a CLI framework in one package, so it holds both ends.

default= fires on absence of a terminal, not on EOF — a deliberate break from the ecosystem, where 'the value on bare Enter' and 'the value when nobody answers' are conflated. EOF (Ctrl-D) and Ctrl-C take the identical abort path at every rung: never an answer, never a silent fall-through to the default.

Full design: docs/PROMPTS_DESIGN.md.