Skip to content

Serve humans, agents, and scripts from one command

State: open/draft

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

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.

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.

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.

  • 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: --json where 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 --json describing 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 --json parseable and the line form pipeable.
  • Exit codes are contract. 0 answered; 1 well-formed but failed against the state it found, including a search that matched nothing; 2 cannot run as typed. A script composes on those, so they are documented and tested.
  • Prose is an output form too. A guide that 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.
  • waypost --json (the whole catalog), --list (one command per line), and way query <terms> (the terminal UI’s own ranked search, from a shell, with --json for the tasks themselves).
  • The withheld-runners note and the --cached staleness line going to stderr in every non-interactive mode, so --list stays pipeable and --json stays parseable.
  • way query’s exit 1 on no match, so way query dev && … composes the way grep does.
  • 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|jsonl on every registry-generated sdlc verb, beside the human text form.
  • 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.

← Back to Principles