Serve humans, agents, and scripts from one command
State: open/draft
Summary
Section titled “Summary”- Every command has three readers, a person at a terminal, an agent reading the output as context, and a script parsing it, and one implementation serves all three by varying the output, not by growing a second tool.
- The human form is the default; a machine form is always available, always on stdout, and never contaminated by the human form’s asides.
- An interactive surface is additive. A capability reachable only through a TUI is a capability an agent and a script do not have.
Statement
Section titled “Statement”One implementation serves all three readers, a person at a terminal, an agent taking the output as context, and a script parsing it, by varying its output, never by growing a second tool. The human form is the default. A machine form, structured for a parser or line-oriented for a pipe, is always available, always on stdout, and never mixed with the notes, warnings, and staleness admissions the human form carries. Exit codes are part of that contract, not decoration.
A tool usable by one audience gets wrapped by the others
Section titled “A tool usable by one audience gets wrapped by the others”A terminal UI an agent cannot drive gets screen-scraped. A JSON firehose with no human form gets a
hand-rolled jq wrapper in someone’s dotfiles. Both wrappers encode assumptions the tool never
promised, and both break silently on the next release.
Duplicating the tool per audience duplicates the semantics
Section titled “Duplicating the tool per audience duplicates the semantics”Two search implementations, one in the UI and one for the shell, eventually disagree about what a query means, and the disagreement is invisible until someone compares them side by side. Varying the output keeps one answer.
Agents are a first-class caller now
Section titled “Agents are a first-class caller now”An agent has the binary and not the README. It needs the data structured, the prose addressable,
and the failure legible: the same things a script needs, plus sentences. Designing for it costs a
--json flag and a guide; not designing for it costs every consumer a parser.
Mixed streams destroy both forms at once
Section titled “Mixed streams destroy both forms at once”A staleness note printed to stdout makes --json unparseable and the line-oriented form unpipeable
in the same stroke, while telling the human nothing they could not have been told on stderr.
How it applies
Section titled “How it applies”- The default output is for a person. Aligned, ranked, coloured where a TTY is present; never the machine form by default.
- Every data-bearing command has a machine form:
--jsonwhere the answer has structure, one record per line where it does not. A command whose whole answer is a single fact may print one stable documented line instead. A command whose job is an effect rather than an answer still reports through the same seam, with--jsondescribing what it changed or one stable documented line naming the result, so a script can confirm the effect without re-reading state. - stdout carries the answer, stderr carries everything else: warnings, cache staleness, “N
runners withheld”, the hint about what to do next. This is what keeps
--jsonparseable and the line form pipeable. - Exit codes are contract.
0answered;1well-formed but failed against the state it found, including a search that matched nothing;2cannot run as typed. A script composes on those, so they are documented and tested. - Prose is an output form too. A
guidethat prints Markdown on stdout is the agent’s manual page, and it pipes into a file, a pager, or a context window. - Interactive surfaces are additive, never the only route to a capability. If the terminal UI can search, the shell can search; if the page can trust a repository, so can a flag.
- Never require a TTY to get an answer. A command that only works interactively cannot be scripted, tested end to end, or driven by an agent.
Examples
Section titled “Examples”waypost --json(the whole catalog),--list(one command per line), andway query <terms>(the terminal UI’s own ranked search, from a shell, with--jsonfor the tasks themselves).- The withheld-runners note and the
--cachedstaleness line going to stderr in every non-interactive mode, so--liststays pipeable and--jsonstays parseable. way query’s exit 1 on no match, soway query dev && …composes the waygrepdoes.waypost guide, written for “a person, or a model, who has the binary and not the README”, printing Markdown through the same output seam.--output text|json|jsonlon every registry-generatedsdlcverb, beside the human text form.
Implications
Section titled “Implications”- A new interactive capability ships with its non-interactive form in the same change. Deferring the second form is the deferral P-0011-avoid-deferral-for-simple-tasks rules out, and it is how the two drift apart.
- The output seam is a design requirement. Commands write through an injected io object rather
than
process.stdout, which is also what makes the machine forms testable without spawning. - Pairs with P-0014-progressive-cli-disclosure. A short first tier is affordable because an agent can ask for the deep tier directly instead of needing it on screen.
- It does not say wrap every output in JSON. It says every answer must be reachable in a form the reader can consume, and that the human form is not the machine form’s only source.
- The working test: can a script get this answer without a TTY, an agent get it without a parser I did not document, and a person get it without reading JSON? Three yeses or the command is unfinished.
- “Three readers” is a design frame, not three code paths. Where it becomes three code paths, the semantics have already split and should be pulled back into one.