Agent surfaces
One declaration projected into every form a caller reads — help, the --json envelope, --schema, an MCP server, completions — and a program that stops and says what to run instead of prompting an agent.
What it is
A burgee command is declared once, as data. Every surface a caller reads is projected from that declaration rather than written beside it:
| Surface | For | What it is |
|---|---|---|
--help | a person | the rendered manifest; --help --json is the same as a document |
--json | a script, an agent | one envelope on stdout: { ok, data, meta } or { ok: false, error } |
--schema | an agent, before it calls | the program as data: commands, options, effects, the exit-code table, a JSON Schema per command |
--mcp | an MCP client | the same commands as MCP tools over stdio |
completion <shell> | a shell | a static script for bash, zsh, fish, pwsh or fig |
--explain <option> | anyone asking "why this value?" | the configuration record for one option |
Your CLI is an agent tool has each surface in full: --schema and its
character budget, what becomes an MCP tool, how tool arguments map back to flags, the client
configuration, and completions. This page is the model behind them.
Why it exists
An agent driving a CLI through a shell pays in calls, tokens and misreads: it scrapes help text, guesses which output line is the result, and waits on a prompt nobody will answer. When each surface is written by hand beside the command, they drift — the help names a flag the schema does not, the MCP tool takes an argument the command refuses. Projecting all of them from one declaration leaves them nothing to drift from.
The rules
The envelope is one document on stdout. Success is
{"ok":true,"data":<what run returned>,"meta":{"provenance":{…}}}, with meta.changed for an
idempotent command. Failure is {"ok":false,"error":{"code","message","hint"?,"fix"?}}, also on
stdout, with stderr empty. --json counts only before a --. Exit codes and
errors has the failure side.
effects is required on burgee's own API. defineCommand refuses a runnable command without
read_only, idempotent, non_idempotent or withheld. It becomes MCP's readOnlyHint,
idempotentHint and destructiveHint, and withheld keeps a command out of tools/list. On
burgee/commander and burgee/yargs, a command that declared nothing is listed as
effects: 'undeclared' with no hints (maps effects to the hints an agent reads, never defaulting destructive to silence (N6) in
mcp.test.ts).
An MCP result is the --json envelope, byte for byte. A tool call runs the command as a
--json caller would, so an agent on MCP and one shelling out see one payload (returns the --json envelope as the tool result, byte for byte (N4)).
An agent is detected, not assumed to be a terminal. detectAgent reads AI_AGENT,
CLAUDECODE, CURSOR_AGENT, CODEX_THREAD_ID and GEMINI_CLI; the first one set names
ctx.agent. ctx.interactive is FORCE_TTY=1, or a terminal on stdout with no agent
(agent detection, not just isTTY (N12) in
agent.test.ts).
caique's prompts ask roundel's interactive(), which reads the same five variables and also
treats CI, and a closed stdin, as nobody there (the output policy
has both rules).
Never prompt an agent. burgee draws no prompt itself. A handler that would ask reads
ctx.interactive and, when nobody can answer, calls ctx.actionRequired({ reason, message, next }):
the run ends with exit 4 and an envelope carrying status: "action_required" and next[] —
runnable commands with the program's name in front and the caller's --json carried. A prompt
drawn with caique refuses instead of waiting and names the flag to pass (exit 2). Either way the
caller gets a next step, not a hang (under --json: status, reason, message, runnable next[] carrying the caller's --json, hint; exit CANCELLED (4) in agent.test.ts).
Run it
import { defineCommand, defineProgram, run } from 'burgee';
const program = defineProgram({
name: 'tool',
commands: [
defineCommand({
name: 'deploy',
description: 'Deploy a build',
effects: 'non_idempotent',
options: {
target: { type: 'string', required: true, description: 'where to deploy' },
yes: { type: 'boolean', description: 'skip the confirmation' },
},
run: (ctx) => {
if (!ctx.options.yes && !ctx.interactive) {
ctx.actionRequired({
reason: 'confirm',
message: `deploying to ${ctx.options.target} needs confirmation`,
next: [{ command: `deploy --target ${ctx.options.target} --yes`, when: 'once you have reviewed the plan' }],
});
}
return { target: ctx.options.target, agent: ctx.agent ?? null };
},
}),
],
});
await run(program);Nobody to confirm, so the program stops and says what to run:
{"ok":false,"status":"action_required","reason":"confirm","message":"deploying to prod needs confirmation","next":[{"command":"tool deploy --target prod --yes --json","when":"once you have reviewed the plan"}],"error":{"code":4,"message":"deploying to prod needs confirmation"}}action required (confirm): deploying to prod needs confirmation
next:
tool deploy --target prod --yes once you have reviewed the planThe next command, run by the agent:
{"ok":true,"data":{"target":"prod","agent":"claude-code"},"meta":{"provenance":{"target":{"source":"flag","location":"--target"},"yes":{"source":"flag","location":"--yes"}}}}What an agent reads before it calls, from the same declaration:
{"type":"object","properties":{"target":{"type":"string","flag":"--target","description":"where to deploy"},"yes":{"type":"boolean","flag":"--yes","description":"skip the confirmation"}},"required":["target"],"additionalProperties":false}"non_idempotent"Where the rules live
- Your CLI is an agent tool — every surface in full.
- Getting started —
burgee dev, which serves the program over MCP while you write it. - Specs: burgee N1–N14 and agent-surface-declared.
- The document
--schemaprints is described byburgee/program-schema.json.
The plugin system
One plain object, one published schema, one set of refusal codes, and a check command in every package: how a plugin is written, validated and published across the family.
Exit codes and errors
Seven typed exit codes a caller can branch on, one error envelope under --json, and a fixed rule for which stream a failure goes to.