Schema: ManifestResponse
File: manifest-response.json
Used by: REQ-O-041 · REQ-O-030 · REQ-O-013 · REQ-C-001 · REQ-C-002 · REQ-C-005 · REQ-C-008 · REQ-C-010 · REQ-C-011 · REQ-C-012 · REQ-C-015 · REQ-C-016 · REQ-C-018 · REQ-C-019 · REQ-C-020 · REQ-C-021 · REQ-C-022 · REQ-C-023 · REQ-C-024 · REQ-C-025 · REQ-C-026 · REQ-C-027 · REQ-C-029 · REQ-F-051 · REQ-F-073 · REQ-F-079 · REQ-O-004 · REQ-O-031 · REQ-O-042 · REQ-O-048 · REQ-O-049 · REQ-F-054 · REQ-C-031 · REQ-C-032 · REQ-F-038 · REQ-O-001 · REQ-C-033 · REQ-F-081 · REQ-C-036 Returned as the
datafield of aResponseEnvelope.
Purpose
tool manifest returns every command, flag, exit code, and declared contract in one response. It replaces the O(N) loop of --help calls (§52) and is the only place an agent learns, before calling, whether a command mutates state, needs credentials, prompts, opens an editor, forwards arguments verbatim, or runs asynchronously.
Two decisions shape the type:
- Flat map, not a tree.
commandsis keyed by dot-separated path so lookup is O(1);subcommandsarrays carry hierarchy - Closed entries.
CommandEntryandFlagEntryreject unknown fields. Every field a Command Contract requirement declares is named here, so a manifest that validates is a manifest an agent can fully read
Values
| Field | Type | Required | Description |
|---|---|---|---|
schema_version |
string 3.MINOR |
yes | Contract version; 3.x for this schema. From 3.0, global options live in the root flags |
framework_version |
string | yes | Version of the tool binary |
etag |
string | yes | Deterministic content hash. Changes only when registrations change |
commands |
Record<string, CommandEntry> |
yes | Flat map keyed by dot-separated path such as "deploy.rollback" |
flags |
Record<string, FlagEntry> |
no | Global options every command accepts in any position, keyed by name without --; only format may carry media_types (REQ-F-079) |
exit_codes |
Record<string, ExitCodeEntry> |
no | Shared exit-code table every command inherits |
dependencies |
DependencyEntry[] |
no | External runtime dependencies checked by tool doctor (REQ-O-031) |
env_vars |
EnvVarEntry[] |
no | Variables the tool reads that back no flag and supply no secret, such as TOOL_DEBUG or TOOL_AUDIT_LOG; each entry carries description. A name without the tool prefix immediately follows the entry for its setting's prefixed name, which wins. Universal names (NO_COLOR, HOME, ...) are not listed (REQ-F-073) |
secret_env_vars |
string[] | no | Variables that supply a secret every command reads, such as a tool-wide API key; names only, never a value or default. A name here appears in no command's secret_env_vars and no flag's env_vars (REQ-F-073, REQ-C-016) |
CommandEntry — core
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | yes | One-sentence summary |
danger_level |
"safe" | "mutating" | "destructive" |
yes | Mutation risk level (REQ-C-002) |
required_scopes |
string[] | yes | Minimal permission strings, most critical first; empty when no auth is needed (REQ-C-029) |
flags |
Record<string, FlagEntry> |
yes | Command-local flags keyed by name without --; never repeats a name or short alias from the root flags |
positionals |
PositionalEntry[] |
no | Positional arguments in call order; absent when there are none (REQ-C-015) |
exit_codes |
Record<string, ExitCodeEntry> |
yes | Keyed by integer code as string (REQ-C-001). With a root exit_codes table present, holds only the command's additions and overrides; the effective table is root overlaid with this map, and tool <cmd> --schema prints it in full |
aliases |
string[] | no | Alternative invocation names |
output_schema |
object | no | JSON Schema for data on success (REQ-C-015) |
output_formats |
string[] | no | Formats beyond the framework defaults (REQ-O-049) |
output_media_types |
MediaTypeMap |
no | Media type each --format value writes for this command, overriding the root media_types; required for an output_formats value neither the spec's table nor the root map covers (REQ-O-049) |
examples |
Example[] |
no | Verbatim invocations |
subcommands |
string[] | no | Dot-separated paths of direct children |
builtin |
boolean | no | true when the framework registers the command, not the application; also true on a built-in's subcommands, false on an application command that replaces a built-in's name. Absent means false (REQ-O-041) |
CommandEntry — declared contracts
Present only when the command declares them.
| Field | Type | Description |
|---|---|---|
output_file |
"formatted" | "binary" | "handler" | "envelope" |
Command registers --output <path>. formatted: the file gets the --format representation; binary: the file gets the raw bytes and data is {path, bytes, content_type, sha256}, and --output - exits 2; handler: the handler writes the file, described by the command's documentation; envelope: the file gets the final ResponseEnvelope as JSON whatever --format says (REQ-O-001) |
output_file_base |
"cwd" | "project_root" | "resource" |
Directory a relative --output path resolves against; only with output_file, absent means cwd. An absolute path is used as given (REQ-O-001) |
stdin |
StdinDeclaration |
Command declares stdin input; --input-file exists and reads the same way. mode is buffered (whole, up to max_bytes), lines (one line at a time, each up to max_line_bytes, no total cap), or records (lines checked against record_schema, ended by a _summary line) (REQ-F-054, REQ-O-004) |
idempotent |
boolean | A repeat with the same arguments converges on the same state, whatever a previous attempt left behind; a non-retryable exit with side_effects: "partial" is recovered by rerunning the identical command. Absent means false; redundant on a safe command. Not --idempotency-key, which deduplicates one request (REQ-C-002) |
option_placement |
"any" | "strict" |
strict: every option, global or local, precedes the first positional; absent means any (REQ-C-027) |
arguments |
"declared" | "passthrough" |
passthrough: every token after the command path goes verbatim to another tool, which owns stdout and the exit code; the envelope is the last line of stderr. Requires option_placement: "strict", empty flags, and no positionals, and excludes danger_level: "destructive"; absent means declared (REQ-C-031) |
help_argv |
string[] | Passthrough only: the argv forwarded in place of a lone --help or -h after the command path; absent means the token is forwarded unchanged (REQ-C-031) |
stdout |
"protocol" |
protocol: the command serves the protocol named in protocol over stdio; stdout is the protocol channel from the first byte and never carries an envelope. A failure before serving begins goes to stderr (in JSON mode, an envelope on its last line) with a declared exit code; exit 0 is a clean shutdown such as stdin end-of-file. Absent means stdout carries envelopes. Excludes arguments: "passthrough", stderr, stdin, interactive: true, streaming_default: true, output_file, output_schema, output_formats, output_media_types, async: true, the REQ-C-010 fields, interruption, confirm_flag, and safe_default: true (REQ-C-032) |
protocol |
string | Lowercase kebab-case name of the protocol served: mcp-stdio, lsp, dap, or another protocol's name in the same form. Present exactly when stdout is "protocol" (REQ-C-032) |
mcp |
false |
false: the tool's own MCP server never offers the command as a tool (an approval a person gives, a project-creating init, a watch loop that never returns); an agent runs it through the CLI or hands it to a person. Present only when false; true is rejected, and absent means the tool's MCP server may offer the command as a tool (REQ-C-032) |
stderr |
"child_log" |
child_log: stderr carries a wrapped program's output as plain text, line by line, whatever --format and verbosity say; --quiet silences it, and stdout carries only the envelope. Absent means stderr carries the framework's own diagnostics only. Never on a passthrough command (REQ-F-038) |
interactive |
boolean | Command may prompt in a TTY; --yes and --non-interactive exist (REQ-C-005) |
requires_person |
true |
A person, not the calling agent, confirms the command by typing back an expected string at a terminal; no flag answers the prompt, --yes included. Off a terminal or under --non-interactive it exits 4 with PERSON_REQUIRED; a wrong answer exits 4 with ATTESTATION_MISMATCH. Requires interactive: true and mcp: false; present only when true (REQ-C-036) |
has_network_io |
boolean | Command performs network or long blocking I/O; --timeout exists (REQ-C-012) |
steps |
string[] | Ordered step names of a multi-step command (REQ-C-008) |
spawns_background_process |
boolean | Command starts a child that outlives it (REQ-C-010) |
cleanup_command |
string | Exact command that stops that child (REQ-C-010) |
max_lifetime_seconds |
integer | Upper bound on the child's lifetime (REQ-C-010) |
filesystem_side_effects |
FilesystemSideEffect[] |
Paths the command may write, with category and TTL (REQ-C-011) |
secret_env_vars |
string[] | Environment variables that supply this command's secrets beyond the root secret_env_vars (REQ-C-016); a secret is never a flag value, so these names never appear in a flag's env_vars |
platform |
string[] | Supported OS names; absent means all (REQ-C-018) |
required_tools |
Record<string, string> |
External binaries and minimum versions; "*" means any version, presence on PATH only (REQ-C-018) |
subprocess |
SubprocessDeclaration |
Child binary and which flags reach its argv (REQ-C-019) |
headless_supported |
boolean | Auth command works without a TTY (REQ-C-021) |
token_env_vars |
string[] | Pre-acquired token variables; required when headless_supported is false (REQ-C-021). A name here is not repeated in this command's secret_env_vars (REQ-F-073) |
async |
boolean | Command returns a job descriptor (REQ-C-022) |
job_descriptor_schema |
object | JSON Schema of that job descriptor (REQ-C-022) |
interruption |
{detach, resume, idle_timeout_ms?, max_lifetime_ms?} |
How the work survives the caller's sync call budget (REQ-F-080): detach hands it to a background job, resume saves checkpoints so the identical invocation continues it; at least one is true. Requires an INCOMPLETE (14) entry in exit_codes; excludes async: true, streaming_default: true, and stdout. Absent means neither, and the wall-clock timeout of REQ-F-011 applies (REQ-C-033) |
requires_editor |
boolean | Command opens $EDITOR unless an alternative is given (REQ-C-023) |
non_interactive_alternatives |
string[] | Flags that bypass the editor; required with requires_editor (REQ-C-023) |
gui_operations |
string[] | Display operations performed (REQ-C-024) |
headless_behavior |
"emit_in_output" | "skip" | "error" |
Headless handling of gui_operations; required with them (REQ-C-024) |
config_write_scope |
"local" | "global" | "session" |
Scope of config files written (REQ-C-025) |
requires |
ConditionalRule[] |
Conditional argument dependencies (REQ-C-026) |
streaming_default |
boolean | Command streams JSONL unless --no-stream (REQ-O-004) |
safe_default |
boolean | Command dry-runs unless --live (REQ-O-048) |
confirm_flag |
string | Boolean flag, without --, of the command or root that runs the command; without it the command previews (would_* effect, meta.dry_run: true, exit 0), and --dry-run wins over it. Only on mutating or destructive commands, never with safe_default: true or arguments: "passthrough" (REQ-O-048) |
FlagEntry
| Field | Type | Required | Description |
|---|---|---|---|
type |
"string" | "integer" | "number" | "boolean" | "array" | "enum" | "object" |
yes | Value type; an object value is one argv token of JSON text (REQ-C-015) |
required |
boolean | yes | Flag must be present |
description |
string | yes | What the flag controls, including range or format |
default |
any | no | Omit (do not set null) when no default exists |
enum_values |
string[] | integer[] | no | Every value a caller may pass, and nothing else: strings when type is "enum", integers when type is "integer"; only on those two types (REQ-C-015) |
short |
string (1 char) | no | Single-character shorthand |
pattern |
string | no | Anchored regex the value must match; exclusive with pattern_type (REQ-C-020) |
pattern_type |
"alphanumeric_id" | "uuid" | "semver" | "filepath" | "url" |
no | Built-in validation preset (REQ-C-020) |
env_vars |
EnvVarEntry[] |
no | Variables the flag reads when not passed, in precedence order (first set wins); the tool-prefixed name comes first whenever a name without the prefix is listed. Never a secret (REQ-F-073) |
schema |
JSON Schema object | when type is "object" |
Draft-07 schema of one value: the object itself for object, one item for array; never on other types (REQ-C-015) |
media_types |
MediaTypeMap |
when the root format flag lists a value outside the spec's table |
Root format flag only, with type: "enum": media type of each enum_values value. Every key is in enum_values; a spec value listed here maps to the spec's media type (REQ-O-001) |
PositionalEntry
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Name shown in help and errors; the caller never types it |
type |
"string" | "integer" | "number" | "enum" |
yes | Value type |
required |
boolean | yes | Must be given; an optional positional never precedes a required one |
description |
string | yes | What the argument selects, including range or format |
enum_values |
string[] | integer[] | when type is "enum" |
Every value a caller may pass, and nothing else: strings when type is "enum", integers (optional) when type is "integer"; only on those two types (REQ-C-015) |
variadic |
boolean | no | Takes every remaining value; last entry only |
Supporting types
| Type | Fields |
|---|---|
Example |
description, command (both required) |
FilesystemSideEffect |
path, type (cache | log | temp | credential | config | output) required; ttl_seconds, clearable_with optional, and never on output (the command's product, which cleanup never removes) |
SubprocessDeclaration |
binary required; user_controlled_args, hardcoded_args optional |
ConditionalRule |
One of { if_flag, if_value, then_required }, { if_flag, prohibited }, { if_flag, target_flag, default }, { any_of } (at least one listed flag present), { one_of } (exactly one listed flag present); any_of and one_of list at least two distinct flags. A declared default and a boolean flag given as false (--no-exact) are not present; if_value compares values, so if_value: false matches an explicit false (REQ-C-026) |
DependencyEntry |
name, check_command, min_version required; version_regex, fix_command optional. check_command verifies the dependency is present; when min_version is a version, its output carries the installed version for version_regex. min_version is a minimum version or "*", meaning any version, presence only: doctor passes it when check_command exits 0 and needs no version_regex (REQ-O-031) |
EnvVarEntry |
name required; deprecated optional (boolean, absent means false); description optional in a flag's env_vars, required in root env_vars |
MediaTypeMap |
Map from a --format value to a lowercase type/subtype media type without parameters, such as {"html": "text/html"}; at least one entry |
StdinDeclaration |
mode (buffered | lines | records) required; max_bytes (buffered only, absent means 65536), max_line_bytes (lines and records only, absent means 1048576), record_schema (records only, required there) |
Examples
Valid — two commands with declared contracts
{
"schema_version": "3.0",
"framework_version": "2.1.0",
"etag": "sha256:a3f9c1",
"flags": {
"format": { "type": "enum", "required": false, "enum_values": ["json", "jsonl", "tsv", "plain"], "description": "Output representation; json when stdout is not a terminal, plain in a terminal" }
},
"commands": {
"deploy": {
"description": "Deploy a build to a target environment",
"danger_level": "mutating",
"required_scopes": ["deploy:write"],
"has_network_io": true,
"flags": {
"target": { "type": "enum", "required": true, "enum_values": ["staging", "prod"], "description": "Target environment" },
"dry-run": { "type": "boolean", "required": false, "default": false, "short": "n", "description": "Validate without executing" },
"build-id": { "type": "string", "required": true, "pattern_type": "alphanumeric_id", "description": "Build to deploy" }
},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "Deployment completed", "retryable": false, "side_effects": "complete" },
"2": { "name": "ARG_ERROR", "description": "Invalid target or build id", "retryable": false, "side_effects": "none" },
"10": { "name": "TIMEOUT", "description": "Deployment timed out; partial writes may have occurred", "retryable": false, "side_effects": "partial" }
},
"requires": [{ "if_flag": "target", "if_value": "prod", "then_required": ["build-id"] }],
"examples": [{ "description": "Preview a staging deploy", "command": "tool deploy --target staging --build-id b42 --dry-run" }],
"subcommands": ["deploy.rollback"]
},
"deploy.rollback": {
"description": "Roll back the most recent deployment",
"danger_level": "destructive",
"required_scopes": ["deploy:write"],
"safe_default": true,
"flags": {},
"positionals": [{ "name": "target", "type": "enum", "required": true, "enum_values": ["staging", "prod"], "description": "Environment to roll back" }],
"exit_codes": { "0": { "name": "SUCCESS", "description": "Rollback completed or previewed", "retryable": false, "side_effects": "complete" } }
}
}
}
Valid — application command next to a built-in
{
"schema_version": "3.1",
"framework_version": "2.1.0",
"etag": "sha256:c71e08",
"commands": {
"deploy": {
"description": "Deploy a build to a target environment",
"danger_level": "mutating",
"required_scopes": ["deploy:write"],
"flags": {},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Deployment completed", "retryable": false, "side_effects": "complete" } }
},
"manifest": {
"description": "Print the full command tree as JSON",
"danger_level": "safe",
"required_scopes": [],
"builtin": true,
"flags": {
"etag": { "type": "string", "required": false, "description": "Etag of a cached manifest; returns meta.not_modified when unchanged" }
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Manifest returned or unchanged", "retryable": false, "side_effects": "none" } }
}
}
}
deploy omits builtin, so it is an application command; an agent building a task list keeps it and drops manifest.
Valid — command that needs exactly one identifier flag
{
"schema_version": "3.2",
"framework_version": "2.1.0",
"etag": "sha256:5d20b4",
"commands": {
"quote": {
"description": "Fetch the latest quote for one instrument",
"danger_level": "safe",
"required_scopes": [],
"has_network_io": true,
"flags": {
"isin": { "type": "string", "required": false, "description": "Instrument ISIN" },
"figi": { "type": "string", "required": false, "description": "Instrument FIGI" },
"symbol": { "type": "string", "required": false, "description": "Instrument ticker symbol" }
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Quote returned", "retryable": false, "side_effects": "none" } },
"requires": [{ "one_of": ["isin", "figi", "symbol"] }]
}
}
}
Each flag is optional on its own; the one_of rule makes exactly one of them mandatory. tool quote --symbol AAPL passes, while tool quote and tool quote --isin US0378331005 --symbol AAPL exit 2.
Valid — command that writes a binary result to a file
{
"schema_version": "3.3",
"framework_version": "2.2.0",
"etag": "sha256:4be9a1",
"commands": {
"download": {
"description": "Download a Flex report",
"danger_level": "safe",
"required_scopes": [],
"output_file": "binary",
"has_network_io": true,
"flags": {
"output": { "type": "string", "required": false, "description": "Path that receives the report's raw bytes" }
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Report written", "retryable": false, "side_effects": "complete" } }
}
}
}
tool download --output report.xml writes the XML itself to report.xml, and data carries path, bytes, content_type, and sha256.
Valid — flags that fall back to environment variables
{
"schema_version": "3.4",
"framework_version": "2.3.0",
"etag": "sha256:9d02f7",
"flags": {
"format": {
"type": "enum",
"required": false,
"enum_values": ["json", "jsonl", "tsv", "plain"],
"description": "Output representation; json when stdout is not a terminal, plain in a terminal",
"env_vars": [{ "name": "TOOL_FORMAT" }]
}
},
"commands": {
"deploy": {
"description": "Deploy a build to a project",
"danger_level": "mutating",
"required_scopes": ["deploy:write"],
"secret_env_vars": ["TOOL_TOKEN"],
"flags": {
"project": {
"type": "string",
"required": true,
"description": "Project to deploy to",
"env_vars": [{ "name": "TOOL_PROJECT" }, { "name": "CLOUDFALL_PROJECT" }, { "name": "TOOL_PROJECT_ID", "deprecated": true }]
}
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Deployment completed", "retryable": false, "side_effects": "complete" } }
}
}
}
--project wins over every variable; without it the tool reads TOOL_PROJECT, then CLOUDFALL_PROJECT (shared across the tool family), then the deprecated TOOL_PROJECT_ID. The token stays in secret_env_vars, never in a flag's env_vars. TOOL_FORMAT is the --format default from REQ-O-042.
Valid — variables that back no flag
{
"schema_version": "3.5",
"framework_version": "2.4.0",
"etag": "sha256:e7a412",
"env_vars": [
{ "name": "TOOL_DEBUG", "description": "1 turns on debug output on stderr, with secrets redacted" },
{ "name": "TOOL_AUDIT_LOG", "description": "1 turns the audit log on, 0 turns it off, an absolute path turns it on at that path" },
{ "name": "TOOL_SESSION_ID", "description": "Agent session id recorded as session_id in each audit log entry" },
{ "name": "TOOL_TRACE_ID", "description": "Trace id propagated to meta.trace_id, logs, and child processes" }
],
"flags": {
"format": {
"type": "enum",
"required": false,
"enum_values": ["json", "jsonl", "tsv", "plain"],
"description": "Output representation; json when stdout is not a terminal, plain in a terminal",
"env_vars": [{ "name": "TOOL_FORMAT" }]
}
},
"commands": {
"deploy": {
"description": "Deploy a build to a project",
"danger_level": "mutating",
"required_scopes": ["deploy:write"],
"secret_env_vars": ["TOOL_TOKEN"],
"flags": {},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Deployment completed", "retryable": false, "side_effects": "complete" } }
}
}
}
Every variable the tool reads has one home: TOOL_FORMAT backs --format, TOOL_TOKEN is a secret, and the four that back no flag sit in root env_vars, each described because no flag's description covers it.
Valid — a tool's own format values with their media types
{
"schema_version": "3.12",
"framework_version": "2.6.0",
"etag": "sha256:0b7e19",
"flags": {
"format": {
"type": "enum",
"required": false,
"enum_values": ["json", "jsonl", "plain", "html"],
"description": "Output representation; json when stdout is not a terminal, plain in a terminal",
"media_types": { "html": "text/html" }
}
},
"commands": {
"report": {
"description": "Summarize the ledger for a period",
"danger_level": "safe",
"required_scopes": [],
"output_formats": ["toon", "csv"],
"output_media_types": { "toon": "text/plain", "csv": "text/csv" },
"flags": {},
"exit_codes": {}
}
}
}
--format html writes a page a person reads, so an agent treats it as an opaque artifact; json, jsonl, and plain take the spec's media types without listing them. report adds two formats of its own and declares what each writes.
Invalid — media types on a command-local flag
{
"schema_version": "3.12",
"framework_version": "2.6.0",
"etag": "sha256:0b7e19",
"commands": {
"export": {
"description": "Export the ledger",
"danger_level": "safe",
"required_scopes": [],
"flags": {
"style": { "type": "enum", "required": false, "enum_values": ["html", "pdf"], "description": "Rendering of the export", "media_types": { "html": "text/html", "pdf": "application/pdf" } }
},
"exit_codes": {}
}
}
}
Violation: only the root format flag carries media_types; a command-specific --format value declares its media type in the command's output_media_types.
Valid — a tool-wide secret and a setting with an ecosystem name
{
"schema_version": "3.13",
"framework_version": "2.5.0",
"etag": "sha256:0b93e2",
"env_vars": [
{ "name": "TOOL_LEDGER", "description": "Path of the ledger file every command reads" },
{ "name": "LEDGER_FILE", "description": "Path of the ledger file, the ecosystem's established name; TOOL_LEDGER overrides it" }
],
"secret_env_vars": ["TOOL_API_KEY"],
"commands": {
"login": {
"description": "Store credentials for the price service",
"danger_level": "mutating",
"required_scopes": [],
"headless_supported": false,
"token_env_vars": ["TOOL_PRICE_TOKEN"],
"flags": {},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Credentials stored", "retryable": false, "side_effects": "complete" } }
},
"sync": {
"description": "Sync prices into the ledger",
"danger_level": "mutating",
"required_scopes": [],
"secret_env_vars": ["TOOL_PRICE_TOKEN"],
"flags": {},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Prices synced", "retryable": false, "side_effects": "complete" } }
}
}
}
Every command reads TOOL_API_KEY, so it sits once in root secret_env_vars. login accepts TOOL_PRICE_TOKEN as a pre-acquired token, so it names it in token_env_vars and not in its own secret_env_vars; sync reads the same token as a plain secret. LEDGER_FILE follows TOOL_LEDGER, which the tool reads first.
Valid — commands whose --output is not a --format rendering
{
"schema_version": "3.6",
"framework_version": "2.4.0",
"etag": "sha256:51c8e0",
"commands": {
"build": {
"description": "Compile the project into a single executable written to --output",
"danger_level": "mutating",
"required_scopes": [],
"output_file": "handler",
"flags": {
"output": { "type": "string", "required": true, "description": "Path of the compiled executable" }
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Executable written", "retryable": false, "side_effects": "complete" } }
},
"test": {
"description": "Run the test suite, streaming the runner's own output",
"danger_level": "safe",
"required_scopes": [],
"output_file": "envelope",
"flags": {
"output": { "type": "string", "required": false, "description": "Path that receives the final JSON envelope" }
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "All tests passed", "retryable": false, "side_effects": "none" } }
}
}
}
tool build --output app writes the executable the command describes; --format does not change it. tool test --output result.json --format plain streams the runner's output to stdout and writes the final envelope to result.json as JSON.
Valid — object flag and project-relative output
{
"schema_version": "3.7",
"framework_version": "2.5.0",
"etag": "sha256:7a03be",
"commands": {
"report": {
"description": "Render a report for the matching orders into the project's reports directory",
"danger_level": "mutating",
"required_scopes": ["orders:read"],
"output_file": "formatted",
"output_file_base": "project_root",
"flags": {
"filter": {
"type": "object",
"required": false,
"description": "Order filter as JSON text",
"schema": {
"type": "object",
"properties": { "status": { "type": "string" }, "min_total": { "type": "number" } },
"additionalProperties": false
}
},
"line": {
"type": "array",
"required": false,
"description": "Extra line item as JSON text; repeat for more",
"schema": {
"type": "object",
"required": ["sku", "qty"],
"properties": { "sku": { "type": "string" }, "qty": { "type": "integer", "minimum": 1 } }
}
},
"output": { "type": "string", "required": true, "description": "Report path, relative to the project root" }
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Report written", "retryable": false, "side_effects": "complete" } }
}
}
}
tool report --filter '{"status":"open"}' --output reports/open.json writes <project root>/reports/open.json from any subdirectory. Each --line value is one JSON object matching schema.
Invalid — object flag without its schema
{
"schema_version": "3.7",
"framework_version": "2.5.0",
"etag": "sha256:7a03be",
"commands": {
"search": {
"description": "Search orders",
"danger_level": "safe",
"required_scopes": [],
"flags": {
"filter": { "type": "object", "required": false, "description": "Order filter as JSON text" }
},
"exit_codes": {}
}
}
}
Violation: an object flag requires schema; without it an agent cannot build a value the command accepts.
Valid — a producer piped into a records consumer
{
"schema_version": "3.8",
"framework_version": "2.4.0",
"etag": "sha256:61c0de",
"commands": {
"import": {
"description": "Import a JSON payload as one batch",
"danger_level": "mutating",
"required_scopes": ["data:write"],
"stdin": { "mode": "buffered" },
"flags": {
"input-file": { "type": "string", "required": false, "description": "Read the payload from this path instead of stdin; - is stdin" }
},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Batch imported", "retryable": false, "side_effects": "complete" } }
},
"tag": {
"description": "Tag each security read from stdin, one record per line",
"danger_level": "mutating",
"required_scopes": ["data:write"],
"stdin": {
"mode": "records",
"max_line_bytes": 65536,
"record_schema": {
"type": "object",
"required": ["id", "symbol"],
"properties": { "id": { "type": "string" }, "symbol": { "type": "string" } }
}
},
"flags": {
"input-file": { "type": "string", "required": false, "description": "Read the records from this path instead of stdin; - is stdin" }
},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "Every record tagged", "retryable": false, "side_effects": "complete" },
"1": { "name": "GENERAL_ERROR", "description": "A line was too large, a record was invalid, or the upstream stream failed or ended early", "retryable": false, "side_effects": "partial" }
}
}
}
}
import refuses more than 64 KiB on stdin; a bigger payload goes through --input-file. tool list-securities --stream | tool tag streams any number of records into tag, each line at most 64 KiB, and tag stops at the producer's _summary line.
Invalid — records mode without a record schema
{
"schema_version": "3.8",
"framework_version": "2.4.0",
"etag": "sha256:61c0de",
"commands": {
"tag": { "description": "Tag securities", "danger_level": "mutating", "required_scopes": [], "stdin": { "mode": "records" }, "flags": {}, "exit_codes": {} }
}
}
Violation: records mode requires record_schema; without it an agent cannot tell which producers fit, and a command that checks nothing per line is lines mode.
Valid — passthrough command that wraps another tool's parser
{
"schema_version": "3.9",
"framework_version": "1.4.0",
"etag": "sha256:c41d07",
"commands": {
"ingest": {
"description": "Run beangulp on bank statements; every argument goes to beangulp",
"danger_level": "mutating",
"required_scopes": [],
"arguments": "passthrough",
"help_argv": ["extract", "--help"],
"option_placement": "strict",
"flags": {},
"exit_codes": {
"2": { "name": "ARG_ERROR", "description": "A framework option before the command path is invalid; the tool did not start", "retryable": false, "side_effects": "none" },
"10": { "name": "TIMEOUT", "description": "The delegated tool ran past the timeout; partial writes may have occurred", "retryable": false, "side_effects": "partial" }
}
}
}
}
ledger --format json ingest extract a.csv hands extract a.csv to beangulp; beangulp writes stdout and chooses the exit code, and the envelope is the last line of stderr. ledger ingest --help runs beangulp with extract --help.
Valid — safe command that writes its product to a declared output path
{
"schema_version": "3.10",
"framework_version": "2.5.0",
"etag": "sha256:0d7e5a",
"commands": {
"dashboard.build": {
"description": "Render the project dashboard to tmp/dashboard",
"danger_level": "safe",
"required_scopes": [],
"filesystem_side_effects": [{ "path": "{project_root}/tmp/dashboard/", "type": "output" }],
"flags": {},
"exit_codes": { "0": { "name": "SUCCESS", "description": "Dashboard rendered", "retryable": false, "side_effects": "complete" } }
}
}
}
tool status --show-side-effects lists tmp/dashboard/; tool cleanup leaves it in place. Writing its own product does not make the command mutating.
Invalid — output path with a TTL and a clear command
{
"schema_version": "3.10",
"framework_version": "2.5.0",
"etag": "sha256:0d7e5a",
"commands": {
"dashboard.build": {
"description": "Render the project dashboard",
"danger_level": "safe",
"required_scopes": [],
"filesystem_side_effects": [{ "path": "{project_root}/tmp/dashboard/", "type": "output", "ttl_seconds": 3600, "clearable_with": "tool cleanup" }],
"flags": {},
"exit_codes": {}
}
}
}
Violation: an output path is the command's product; it does not expire and no clear command owns it. A path the framework may delete is cache or temp.
Invalid — passthrough command with interspersed options
{
"schema_version": "3.9",
"framework_version": "1.4.0",
"etag": "sha256:c41d07",
"commands": {
"ingest": { "description": "Run beangulp", "danger_level": "mutating", "required_scopes": [], "arguments": "passthrough", "option_placement": "any", "flags": {}, "exit_codes": {} }
}
}
Violation: arguments: "passthrough" requires option_placement: "strict"; every token after the command path belongs to the delegated tool, so no option can follow it.
Valid — command that streams a wrapped program's log to stderr
{
"schema_version": "3.11",
"framework_version": "2.1.0",
"etag": "sha256:5be0a3",
"commands": {
"provision": {
"description": "Run the site playbook against the inventory; ansible-playbook's log streams to stderr",
"danger_level": "mutating",
"required_scopes": [],
"stderr": "child_log",
"flags": {
"limit": { "type": "string", "required": false, "description": "Host pattern passed to ansible-playbook as --limit" }
},
"exit_codes": {}
}
}
}
infra --format json provision writes ansible-playbook's log to stderr, line by line, as it runs, even without a terminal; stdout holds only the final envelope. infra --quiet provision writes nothing to stderr.
Invalid — child log on a passthrough command
{
"schema_version": "3.11",
"framework_version": "1.4.0",
"etag": "sha256:c41d07",
"commands": {
"ingest": { "description": "Run beangulp", "danger_level": "mutating", "required_scopes": [], "arguments": "passthrough", "option_placement": "strict", "stderr": "child_log", "flags": {}, "exit_codes": {} }
}
}
Violation: stderr: "child_log" promises that stdout carries only the envelope, while a passthrough command gives stdout to the delegated tool and puts the envelope on the last line of stderr. A passthrough command's stderr already carries the delegated tool's own stderr ahead of that line.
Valid — mutating command that previews unless --yes is given
{
"schema_version": "3.14",
"framework_version": "1.5.0",
"etag": "sha256:5e9a12",
"commands": {
"migrate": {
"description": "Apply pending schema migrations",
"danger_level": "mutating",
"required_scopes": [],
"confirm_flag": "yes",
"flags": {
"yes": { "type": "boolean", "required": false, "default": false, "description": "Apply the migrations; omit to preview them" },
"dry-run": { "type": "boolean", "required": false, "default": false, "description": "Preview the migrations even when --yes is given" }
},
"exit_codes": {}
}
}
}
db migrate previews with meta.dry_run: true; db migrate --yes applies the migrations; db migrate --dry-run --yes previews.
Invalid — confirm flag next to safe_default
{
"schema_version": "3.14",
"framework_version": "1.5.0",
"etag": "sha256:5e9a12",
"commands": {
"rollback": { "description": "Roll back the last deployment", "danger_level": "destructive", "required_scopes": [], "safe_default": true, "confirm_flag": "yes", "flags": { "yes": { "type": "boolean", "required": false, "description": "Run the rollback" } }, "exit_codes": {} }
}
}
Violation: confirm_flag excludes safe_default: true; a command has one confirmation mechanism, either the injected --live or its own flag.
Valid — MCP server over stdio
{
"schema_version": "3.16",
"framework_version": "2.0.0",
"etag": "sha256:7d1f40",
"commands": {
"mcp.serve": {
"description": "Serve the tool's commands as MCP tools over stdio until stdin closes",
"danger_level": "mutating",
"required_scopes": [],
"stdout": "protocol",
"protocol": "mcp-stdio",
"flags": {},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "The client closed stdin and the server shut down cleanly", "retryable": false, "side_effects": "complete" }
}
}
}
}
An agent registers tool mcp serve as an MCP server and never parses its stdout as an envelope. When the server fails before serving begins, stdout stays empty and the envelope is the last line of stderr.
Invalid — protocol server that streams JSONL by default
{
"schema_version": "3.16",
"framework_version": "2.0.0",
"etag": "sha256:7d1f40",
"commands": {
"lsp": { "description": "Run the language server", "danger_level": "safe", "required_scopes": [], "stdout": "protocol", "protocol": "lsp", "streaming_default": true, "flags": {}, "exit_codes": {} }
}
}
Violation: streaming_default: true promises JSONL events on stdout, while stdout: "protocol" gives stdout to LSP messages. A protocol command has no result to stream, format, or write to an --output file.
Valid — commands kept off the MCP server
{
"schema_version": "3.17",
"framework_version": "2.1.0",
"etag": "sha256:91c3e7",
"commands": {
"release.approve": {
"description": "Record a person's approval of a pending release",
"danger_level": "mutating",
"required_scopes": ["releases:approve"],
"mcp": false,
"flags": {},
"positionals": [{ "name": "release", "type": "string", "required": true, "description": "Release ID to approve" }],
"exit_codes": {
"0": { "name": "SUCCESS", "description": "The approval is recorded", "retryable": false, "side_effects": "complete" }
}
},
"release.list": {
"description": "List pending releases",
"danger_level": "safe",
"required_scopes": [],
"flags": {},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "The releases are listed", "retryable": false, "side_effects": "none" }
}
}
}
}
The tool's MCP server offers release.list as a tool and never lists release.approve. An agent that needs an approval hands tool release approve <id> to a person rather than calling it from an MCP session.
Invalid — mcp: true
{
"schema_version": "3.17",
"framework_version": "2.1.0",
"etag": "sha256:91c3e7",
"commands": {
"release.list": { "description": "List pending releases", "danger_level": "safe", "required_scopes": [], "mcp": true, "flags": {}, "exit_codes": {} }
}
}
Violation: mcp is present only when false. A command the MCP server may offer omits the field, so two producers never spell the default two ways.
Valid — a command only a person confirms
{
"schema_version": "3.21",
"framework_version": "2.5.0",
"etag": "sha256:3e8a51",
"commands": {
"decisions.approve": {
"description": "Record a person's approval of a decision an agent proposed",
"danger_level": "mutating",
"required_scopes": ["decisions:approve"],
"interactive": true,
"mcp": false,
"requires_person": true,
"flags": {},
"positionals": [{ "name": "id", "type": "string", "required": true, "description": "ID of the decision to approve" }],
"exit_codes": {
"0": { "name": "SUCCESS", "description": "The approval is recorded", "retryable": false, "side_effects": "complete" },
"4": { "name": "PRECONDITION", "description": "No person confirmed the approval at a terminal; nothing is recorded", "retryable": false, "side_effects": "none", "error_codes": ["PERSON_REQUIRED", "ATTESTATION_MISMATCH"] }
}
}
}
}
Before the first call, an agent reads that --yes does not confirm decisions.approve and that the tool's MCP server never offers it. It hands tool decisions approve <id> to a person instead of calling it.
Invalid — person-only command an MCP server may offer
{
"schema_version": "3.21",
"framework_version": "2.5.0",
"etag": "sha256:3e8a51",
"commands": {
"decisions.approve": { "description": "Record a person's approval of a decision", "danger_level": "mutating", "required_scopes": [], "interactive": true, "requires_person": true, "flags": {}, "exit_codes": {} }
}
}
Violation: requires_person: true requires mcp: false. An MCP call has no terminal and no person behind it, so a server that offered the command could only ever fail it.
Valid — integer flag with a fixed set of values
{
"schema_version": "3.18",
"framework_version": "2.2.0",
"etag": "sha256:4a07d2",
"commands": {
"order.sign": {
"description": "Sign a pending order",
"danger_level": "mutating",
"required_scopes": [],
"flags": {
"sig-type": { "type": "integer", "required": false, "default": 0, "enum_values": [0, 1, 2], "description": "Signature scheme: 0 EOA, 1 proxy, 2 safe" }
},
"exit_codes": {}
}
}
}
--sig-type takes 0, 1, or 2 on the command line, and a JSON route such as a payload or an MCP call takes the numbers. Shell completion offers the three values, and --sig-type 3 exits 2.
Invalid — integer flag listing its values as strings
{
"schema_version": "3.18",
"framework_version": "2.2.0",
"etag": "sha256:4a07d2",
"commands": {
"order.sign": { "description": "Sign a pending order", "danger_level": "mutating", "required_scopes": [], "flags": { "sig-type": { "type": "integer", "required": false, "enum_values": ["0", "1", "2"], "description": "Signature scheme" } }, "exit_codes": {} }
}
}
Violation: on a type: "integer" entry, enum_values holds integers. Strings belong to a type: "enum" entry, and an integer list on one of those is rejected the same way.
Valid — exit codes that name their error.code values
{
"schema_version": "3.19",
"framework_version": "2.3.0",
"etag": "sha256:0c6e5b",
"commands": {
"resource.create": {
"description": "Create a named resource",
"danger_level": "mutating",
"required_scopes": ["resources:write"],
"flags": {
"name": { "type": "string", "required": true, "description": "Name of the resource to create" }
},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "The resource is created", "retryable": false, "side_effects": "complete", "error_codes": [] },
"2": { "name": "ARG_ERROR", "description": "The resource name is invalid", "retryable": false, "side_effects": "none", "error_codes": ["INVALID_NAME"] },
"6": { "name": "CONFLICT", "description": "A resource with this name already exists; data holds it", "retryable": false, "side_effects": "none", "error_codes": ["ALREADY_EXISTS"] }
}
}
}
}
Before the first call, an agent reads that exit 6 carries error.code: "ALREADY_EXISTS" and plans to take the existing resource from data (REQ-C-028). The empty list on 0 says the success exit carries no error.code.
Valid — idempotent command whose partial failure is rerun
{
"schema_version": "3.15",
"framework_version": "0.9.0",
"etag": "sha256:5be2a0",
"commands": {
"observe": {
"description": "Rewrite one snapshot file per server from its live state",
"danger_level": "mutating",
"idempotent": true,
"required_scopes": ["servers:read"],
"has_network_io": true,
"flags": {},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "Every snapshot file is rewritten", "retryable": false, "side_effects": "complete" },
"3": { "name": "PARTIAL_FAILURE", "description": "Some snapshot files are rewritten and others are not", "retryable": false, "side_effects": "partial" }
}
}
}
}
Exit 3 stays retryable: false because files were written. idempotent: true tells the agent that rerunning tool observe unchanged converges on the state a clean run leaves, so the rerun is the recovery and no state inspection comes first.
Valid — long read-only command that outlives the call budget
{
"schema_version": "3.20",
"framework_version": "2.4.0",
"etag": "sha256:91d4c7",
"commands": {
"analyze": {
"description": "Compute statistics over a data file",
"danger_level": "safe",
"required_scopes": [],
"interruption": { "detach": true, "resume": true, "idle_timeout_ms": 300000 },
"flags": {},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "The statistics are computed", "retryable": false, "side_effects": "complete" },
"14": { "name": "INCOMPLETE", "description": "The work continues; run data.continue_command", "retryable": true, "side_effects": "none", "error_codes": ["INCOMPLETE"] }
}
}
}
}
Before the first call, an agent reads that tool analyze returns within the call budget whatever the file size: with the result, or with exit 14 and a continue_command. It plans a loop over continue_command instead of raising its own timeout, and knows that rerunning the identical command after a lost response continues from the last checkpoint.
Invalid — command entry without required contract fields
{
"schema_version": "3.0",
"framework_version": "2.1.0",
"etag": "sha256:a3f9c1",
"commands": {
"deploy": { "description": "Deploy a build", "flags": {}, "exit_codes": {} }
}
}
Violation: danger_level and required_scopes are required on every command; an agent cannot tell whether the call is safe.
Invalid — editor command without alternatives
{
"schema_version": "3.0",
"framework_version": "2.1.0",
"etag": "sha256:a3f9c1",
"commands": {
"commit": { "description": "Record a change", "danger_level": "mutating", "required_scopes": [], "requires_editor": true, "flags": {}, "exit_codes": {} }
}
}
Violation: requires_editor: true requires non_interactive_alternatives; otherwise the agent has no path that avoids the editor trap (§62).
Invalid — root variable without a description
{
"schema_version": "3.5",
"framework_version": "2.4.0",
"etag": "sha256:e7a412",
"env_vars": [{ "name": "TOOL_DEBUG" }],
"commands": {}
}
Violation: a root env_vars entry requires description; no flag's description explains what TOOL_DEBUG accepts or changes.
Common mistakes
- Emitting
commandsas an array. The map keyed by dot path is what enables O(1) lookup; arrays force a scan and invite duplicate names - Listing a path in
subcommandswithout a matchingcommandsentry. Every registered command, including children and built-ins, appears in the flat map - Adding ad-hoc fields to
CommandEntry. Entries are closed; a new contract field belongs in this schema with a sourcing requirement, not as an undocumented extension - Declaring a static
defaultfor a flag whose default depends on the environment.--formatresolves tojsonwithout a terminal andplainin one (REQ-F-003); omitdefaultand state the rule indescription, so an agent passes the value it needs - Setting
default: nullfor flags without a default. Omit the key;nullreads as "the default value is null" - Declaring
retryable: truewith partial side effects inexit_codes. TheExitCodeEntryinvariant rejects it; timeouts that may have written areretryable: false - Declaring
retryable: trueon a partial exit because the command is idempotent.retryable: truestill promises that nothing was written; keep the exitretryable: false,side_effects: "partial", and declareidempotent: trueon the command - Declaring
interruptionwithout the code paths behind it.detach: trueneeds nothing from the handler but progress reports (REQ-C-034);resume: truepromises checkpoints (REQ-C-035). A command that declaresresumeand starts over on every call sends the agent into the same budget again - Declaring
idempotent: trueon a command that only deduplicates. A command that skips a repeat by its--idempotency-key(REQ-C-007) but appends, increments, or sends on every new key does not converge;idempotentmeans the same arguments reach the same state with or without a key - Repeating global options in every
CommandEntry.flags. A global option appears once, in the rootflags; a copy inside a command reads as a local flag that happens to share the name, and hides whether it is accepted before the command path - Reusing a global short alias for a local flag.
-fmeaning--formatat the root and--forceon one command changes meaning with position; the framework rejects it at registration - Generating the manifest from a static file. It must be computed from live registrations or the
etaglies - Separating built-ins by a hard-coded name list. Frameworks differ in which built-ins they register (
doctor,manifest,audit-log, ...), and an application may replace one with its own command; readbuiltininstead - Marking an application command that replaces a built-in's name as
builtin: true. The flag follows who registered the command, not its name; the replacement isfalse - Marking every flag of an "exactly one of" group
required: true. No call can then satisfy the command; keep each flagrequired: falseand declare the group as aone_ofrule - Emitting pairwise
prohibitedrules next to aone_ofgroup.one_ofalready forbids combining its members; the extra rules repeat the constraint and let the two drift apart - Declaring
output_file: "formatted"on a command that returns a binary result. The file would then hold a JSON or plain wrapper around base64, not the file the caller asked for; a binary result is"binary" - Omitting
output_filebecause the handler writes the file itself. Absence tells the agent the command has no--output; declare"handler" - Declaring a command's product as
cache.tool cleanupthen deletes the report the user asked for; declare itoutput, whichcleanupnever removes - Declaring an
outputside effect for the--outputpath. The caller picks that path per call, andoutput_filealready declares it;type: "output"is for a location the command chooses itself - Leaving
output_file_baseout when--outputdoes not resolve against the working directory. Absence meanscwd, so the agent looks for the file in the wrong place; declareproject_rootorresource - Declaring a JSON-valued flag as
type: "string". The agent then has no shape to build and no signal that the value is parsed as JSON; declaretype: "object"withschema - Declaring an integer choice as
type: "enum"with["0", "1", "2"]. A JSON route then sees strings where the command takes numbers; declaretype: "integer"withenum_values: [0, 1, 2] - Moving an integer flag's allowed values into its
description. Agents and shell completion cannot read prose as data; list them inenum_values - Naming a flag's environment variables only in its
description. "(read from$Aor$Bwhen not passed)" is prose an agent must parse; list the names inenv_vars, in precedence order - Listing a secret in
env_vars. A token or password is not a flag value (REQ-C-016); declare its variable insecret_env_vars - Declaring
stdin: {mode: "buffered"}on a command that consumes a record stream. The 64 KiB total cap then rejects any real pipeline; a command that handles one line at a time declareslinesorrecords - Declaring
max_byteson alinesorrecordscommand. Line mode has no total cap, so the field is rejected; the per-line cap ismax_line_bytes - Marking a passthrough command only through
option_placement: "strict"and itsdescription.strictalso fits a command that parses its own options and forwards the rest; onlyarguments: "passthrough"tells an agent that stdout, the exit code, and every token after the path belong to another tool - Stating "previews unless
--yes" only in a flag'sdescription. An agent that reads the manifest then expects a bare call to run; declareconfirm_flagso the preview default is machine-readable - Declaring
confirm_flagwith a name the command does not accept. The name must be a boolean flag in the command'sflagsor the rootflags; the framework refuses any other at registration - Declaring
help_argvon a declared command. The framework answers--helpitself there;help_argvexists only where a lone--helpwould otherwise reach the delegated tool - Saying only in
descriptionthat a command is an MCP or language server. An agent that calls it as an ordinary command parses MCP messages as an envelope or waits for an exit that comes only when stdin closes; declarestdout: "protocol"andprotocol - Writing a startup error to stdout on a protocol command. The client reads stdout as protocol messages from the first byte; a failure before serving begins goes to stderr, and stdout stays empty
- Saying only in
descriptionthat a command is not an MCP tool. "(not an MCP tool)" is prose a server generator and an agent must parse; declaremcp: false - Emitting
mcp: true. The schema rejects it; a command the MCP server may offer omitsmcp - Letting
--yesanswer a person-only confirmation. An agent passes--yesas readily as any flag, so it approves its own proposal; declarerequires_person: true, which no flag answers (REQ-C-036) - Declaring
requires_person: truewithoutinteractive: trueandmcp: false. The schema rejects it: the attestation is a prompt, and an MCP call has no person behind it - Saying only in
descriptionthat a command streams a wrapped program's log. An agent cannot match prose before the call; declarestderr: "child_log"so it knows stderr will be busy and carries no failure signal - Letting auto-quiet or
--verbosegate a declared child log.child_logstreams whatever the verbosity; only--quietsilences it, so a log that appears only under--verboseis a framework diagnostic, not a child log - Listing a borrowed name before the tool-prefixed one.
CLOUDFALL_PROJECTahead ofTOOL_PROJECTlets a variable set for another tool override the one set for this tool (REQ-F-073) - Reading a variable the manifest never names.
TOOL_DEBUGorTOOL_AUDIT_LOGbacks no flag, so it belongs in rootenv_vars; documenting it only in a README leaves an agent unable to see that it changes the tool's behavior - Listing one variable in two places. A name in root
env_varsappears nowhere else in the manifest; a variable that supplies a flag's value is declared on that flag only; a root secret appears in no command'ssecret_env_vars; an auth command's token sits in itstoken_env_vars, not also in itssecret_env_vars - Repeating a tool-wide secret on every command. A secret every command reads goes in root
secret_env_varsonce - Listing a secret in root
env_vars. A rootdescriptioninvites a default or an example value; a secret goes in asecret_env_vars, which holds names only - Listing an ecosystem name in root
env_varsalone.LEDGER_FILEwithoutTOOL_LEDGERright before it lets a variable set for another tool configure this one; the prefixed name comes first and wins (REQ-F-073) - Naming a format's media type only in the
--formatdescription. "html writes text/html" is prose an agent must parse; declare it in the rootformatflag'smedia_types - Leaving a tool's own format value out of
media_types. An agent can then assume nothing about it and must treat its output as opaque; every value outside the spec's table has an entry - Mapping a spec value to another media type.
jsonis alwaysapplication/json; a variant with another shape is a new format value with its own name - Listing universal names in root
env_vars.NO_COLOR,CI,HOME,COLUMNS, the proxy and CA bundle names, and the other REQ-F-073 exceptions are read by every conforming tool; listing them adds noise without telling the agent anything
Agent interpretation
Rules for agents consuming ManifestResponse to plan and execute command calls.
Fetching the manifest
- Read schema_version first. A 3.x manifest lists global options once in root flags; treat any other value as pre-3.0, where global options may repeat inside each CommandEntry.flags and root flags is absent. Pre-3.0 producers commonly emit 1.0, the value every 2.x example showed, so never test for a 2. prefix
- Fetch once per session, not per call — the manifest is expensive to generate and stable between command registrations
- Cache using etag: on subsequent fetches pass the previous etag; if meta.not_modified: true, reuse the cached manifest
- If tool manifest itself is unavailable (exit code 5 or 12) — fall back to per-command --help calls; this is O(N) but safe
Looking up a command
- Key format is dot-separated (e.g. "deploy.rollback"); split the user-intended subcommand path on spaces and join with . to construct the key
- If the key is not in commands — do not guess; emit REDIRECTED behavior: try tool manifest again in case it was stale, then escalate
- Check aliases before concluding a command does not exist — the agent may be using an alias that maps to a different primary key
Separating application commands from built-ins
- When building a task list, a skill set, or a summary of what the tool does, drop entries with builtin: true; they are framework plumbing (manifest, doctor, audit-log) present in every conforming CLI
- Treat an absent builtin as false. A pre-3.1 manifest never sets it, so every command there reads as an application command
- Keep built-ins in the lookup table: they are still callable, and doctor or audit-log is the right call when diagnosing a failure
Writing a result to a file
- output_file: "binary": pass --output <path> to get the file itself; --format then shapes only the envelope on stdout. Verify the write with data.sha256 or data.bytes instead of reading the file back. Never pass --output -; it exits 2
- output_file: "formatted": the file holds the --format representation of data, so choose --format for the file's consumer
- output_file: "handler": the command's handler writes the file; read the command's description for what it holds, and do not expect --format to change it
- output_file: "envelope": the file holds the final ResponseEnvelope as JSON whatever --format says; read ok, data, and error from the file, not from stdout
- output_file_base other than cwd: a relative --output lands under the project root (project_root) or the target resource's directory (resource), not the working directory. Pass an absolute path when the file must land in a known place; it is used as given
- output_file absent: the command takes no --output; a binary result arrives base64-encoded in data (REQ-F-017). Treat a pre-3.3 manifest the same way and read output_schema for a binary wrapper
Feeding stdin from stdin
- mode: "buffered": pipe at most max_bytes (absent means 65536); write anything bigger to a file and pass --input-file <path>, or the call exits 2 with STDIN_TOO_LARGE
- mode: "lines" or "records": pipe a stream of any length, each line at most max_line_bytes (absent means 1048576); a longer line exits 1 with LINE_TOO_LARGE and context.line. When you write stdin and read stdout from the same thread, write the input to a file and pass --input-file instead; only a separate writer, such as the producer in a shell pipeline, keeps both pipes draining
- mode: "records": before piping a producer into the command, check the producer's item type against record_schema. Feed it a REQ-O-004 stream that ends with a _summary line; a stream without one exits 1 with UPSTREAM_INCOMPLETE, and an upstream error line exits 1 with UPSTREAM_FAILED
- stdin absent on a 3.8 or later manifest: the command reads no stdin payload. On a pre-3.8 manifest, absent means unknown; treat a command that accepts --input-file as buffered with the 65536-byte cap
Building a call from FlagEntry
- A command's accepted flags are the root flags plus its own flags; a name in neither produces ARG_ERROR (2)
- Emit tokens in the canonical order tool <global options> <command path> <local options> [--] <positionals>; every parser mode and both option_placement values accept it
- Give positionals in array order; a required entry must be present, and only a variadic last entry takes more than one value
- Put -- before any positional that starts with -
- Pass each option once; a scalar option repeated with a different value produces ARG_ERROR (2)
- required: true flags must always be present; absence will produce ARG_ERROR (2)
- type: "enum" — only values in enum_values are accepted; sending any other value produces ARG_ERROR (2)
- type: "integer" with enum_values: the list is data, not prose; pass one of its integers (as a number on a JSON route, as its decimal text on argv), and any other value produces ARG_ERROR (2). Shell completion offers the same list. An integer entry without enum_values takes any integer its description allows, including on a pre-3.18 manifest, where the allowed values can appear only in the description
- type: "object": pass one argv token of compact JSON text that validates against schema (--filter '{"status":"open"}'); text that is not a JSON object, or does not match, produces ARG_ERROR (2). An array flag with schema takes each item as such a token
- default absent — the flag is optional but has no fallback; omitting it changes behavior; include explicitly if the outcome matters
- short present — both --flag-name value and -f value are valid; prefer long form for clarity in agent-constructed calls
Supplying a flag through the environment from env_vars
- A passed flag beats every variable; pass the flag when the value matters for this call alone
- To set a value for the whole session, export the first name whose deprecated is absent or false; never export a deprecated name
- A name earlier in the list overrides the one you export, since the first one set wins; check that every earlier name is unset
- env_vars absent on a 3.4 or later manifest: the flag reads no environment variable. On a pre-3.4 manifest, absent means unknown, not none; read the flag's description and keep that environment clean of guesses
- A required flag with env_vars still counts as present when one of its variables is set
Reading root env_vars
- Root env_vars lists the variables that change the tool's behavior without backing a flag; read each description before exporting one, and unset any you did not set on purpose, since a leftover TOOL_DEBUG or TOOL_AUDIT_LOG changes every call
- Root env_vars absent on a 3.5 or later manifest: the tool reads no such variable. On a pre-3.5 manifest, absent means unknown, not none
- An entry without the tool prefix, such as LEDGER_FILE, follows the prefixed entry for the same setting; set the prefixed one, since it wins, and unset the other if you inherited it
Supplying secrets
- A command's secrets are the root secret_env_vars plus its own secret_env_vars; an auth command also accepts the names in its token_env_vars. Export the value from your secret store; never pass it on the command line
- Root secret_env_vars absent on a 3.13 or later manifest: no secret is read by every command. On a pre-3.13 manifest, absent means unknown; a tool-wide secret may appear only in a README or in each command's secret_env_vars
Selecting output format from output_formats
- If output_formats is absent, treat json as the only guaranteed format — do not attempt non-standard values
- Look up a value's media type in the command's output_media_types, then the root format flag's media_types, then the spec's table in REQ-O-001 (json → application/json, jsonl → application/x-ndjson, and tsv, plain, table, id as text)
- Parse output as JSON only when its media type is application/json, application/x-ndjson (one value per line), or ends in +json; treat any other format's output, such as text/html, as an opaque artifact to store or hand to a person
- A value with no media type from any of the three sources (always the case for a non-spec value on a pre-3.12 manifest) is opaque too
- If output_formats is present, select the most appropriate format for your consumer: json for programmatic parsing, an LLM-optimized value (e.g. toon) when the language model is the final reader and token cost matters
- Never assume a format value is valid unless it appears in output_formats or is one of the framework defaults
Pre-planning retries from exit_codes
- Before the first call, read the command's exit_codes map and identify which codes are retryable
- Build the retry/rollback plan before calling, not reactively — this avoids ambiguity about whether a retry is safe after a partial failure
- idempotent: true — a non-retryable exit whose entry declares side_effects: "partial" (such as PARTIAL_FAILURE (3), a TIMEOUT (10) that may have written, or 130/143 after a signal) is recovered by rerunning the identical command once, without inspecting state. A second failure with the same error.code is deterministic: stop and escalate
- The rerun rule never covers ARG_ERROR (2) or any error carrying fix_required or fix_command: the identical call fails until the input changes, so apply the fix first. Exits with side_effects: "none" follow retryable as usual
- An absent idempotent means false, including on a pre-3.15 manifest: verify what was committed before rerunning a mutating command
- error_codes on an entry lists the error.code values that exit carries; plan a branch for each, then branch on the received error.code. An absent error_codes means not declared, never "none", including on a pre-3.19 manifest, where an entry cannot carry the field; [] means the exit carries no error.code
Reading declared contracts before calling
- danger_level other than safe — prefer --dry-run first; safe_default: true means the command previews until --live is passed
- filesystem_side_effects with type: "output" — the command writes its product there, even when it is safe; read the result from that path, and do not run tool cleanup expecting it to go. A pre-3.10 manifest cannot mark it and may declare it cache
- confirm_flag present: a call without --<confirm_flag> only previews and exits 0 with meta.dry_run: true. Read the would_* preview, then repeat the call with the flag to run it; check meta.dry_run rather than the exit code to know whether it ran. --dry-run wins, so drop it from the confirmed call. On a pre-3.14 manifest an absent confirm_flag means unknown; read the flag descriptions
- option_placement: "strict" — place every option, global or local, before the first positional argument; anything after it is forwarded to the child process
- requires — evaluate each rule against the flags you plan to send before calling; a violated rule produces ARG_ERROR (2)
- any_of and one_of groups — send at least one flag of an any_of group and exactly one flag of a one_of group, choosing the one whose value you already hold; a declared default never satisfies either rule, and neither does a boolean flag given as false (--no-exact leaves exact absent)
- interactive: true or requires_editor: true — always pass --yes / --non-interactive or one of non_interactive_alternatives
- async: true — the response is a job descriptor; poll with its status_command instead of waiting on the call
- interruption present — every call returns within the sync call budget (meta.budget_ms); exit 14 hands back data.continue_command, which the agent runs until the result arrives. With resume: true, the identical invocation continues the work after a lost response or a killed process. Absent on a pre-3.20 manifest, where a long command's only bound is its wall-clock timeout
- required_scopes not covered by the active credential — expect AUTH_REQUIRED (8); do not call until credentials change
Calling a passthrough command (arguments: "passthrough")
- Put every global option and framework flag (--format, --output, --timeout) before the command path; every token after it goes to the delegated tool, --help and -- included
- Read stdout as the delegated tool's output, not as an envelope. The envelope is the last line of stderr; when that line is not an envelope, the framework rejected its own options before the tool started and the envelope is on stdout
- Classify by error.code: DELEGATED_EXIT means the tool chose the exit code, and data.exit_code repeats it. A delegated 2 is the tool's usage error and does not rule out side effects; decide from retryable, which is false, not from the code
- For the tool's own help, call tool <cmd> --help; the framework forwards help_argv when the command declares it
- An absent arguments means declared, including on a pre-3.9 manifest; such a manifest cannot mark a passthrough command, so treat a strict command whose description says its arguments go to another tool as one
Starting a protocol server (stdout: "protocol")
- Never parse stdout as an envelope. Start the command only as a client of the protocol named in protocol (mcp-stdio, lsp, dap), keep its stdin open for the session, and expect no exit until you close stdin or send a signal; do not start it when you do not speak that protocol
- An exit before the first protocol message is a failure before serving: stdout is empty, and in JSON mode the last line of stderr is the envelope. Classify it by error.code and the declared exit_codes; 2 means a flag was invalid and the server never ran
- Exit 0 after you close stdin is a clean shutdown and carries no envelope. 143 after you sent SIGTERM is the expected end of the session, not a failure to report
- Stderr while serving holds the framework's plain-text diagnostics; never read it as protocol messages
- An absent stdout means stdout carries envelopes, including on a pre-3.16 manifest; such a manifest cannot mark a protocol command, so treat a command whose description says it serves MCP, LSP, or another protocol over stdio as one
Commands only a person confirms (requires_person: true)
- requires_person: true: a person must type the confirmation at a terminal. Hand the command to a person with its exact arguments; never add --yes or another flag to get past it
- Exit 4 with error.code: "PERSON_REQUIRED": the command ran off a terminal and did nothing. Do not retry it, with or without flags, and do not start it under a pseudo-terminal; hand it to a person
- Exit 4 with error.code: "ATTESTATION_MISMATCH": the person typed the wrong confirmation and nothing ran; leave the retry to them
- An absent requires_person means an agent may confirm the command, including on a pre-3.21 manifest, which cannot mark a person-only command; read its description, and treat mcp: false with interactive: true as a hint that a person may be needed
Commands kept off the MCP server (mcp: false)
- mcp: false: the tool's own MCP server never offers the command as a tool. Run it through the CLI, or hand it to a person when it needs one (an approval, a project-creating init); never expect to find it among the server's tools
- When building an MCP tool list from the manifest, leave out every entry with mcp: false
- An absent mcp means the tool's MCP server may offer the command as a tool, including on a pre-3.17 manifest; such a manifest cannot mark a command kept off the server, so read its description for a note such as "not an MCP tool"
Reading stderr from stderr
- stderr: "child_log": expect a wrapped program's log on stderr, as plain text, whatever --format and verbosity say. Discard it or keep only its tail for a person; pass --quiet to silence it when no one will read it
- Never treat stderr text from such a command as a failure signal, even when it contains words like ERROR or failed; the exit code and the envelope on stdout stay authoritative
- stderr absent: stderr carries only the framework's own diagnostics (REQ-F-006, REQ-F-038). On a pre-3.11 manifest absent means unknown; a command whose description says it streams another program's log may still do so
Manifest staleness
- If a command call returns REDIRECTED (13) for a path that exists in the manifest — the manifest is stale; re-fetch unconditionally and update the cache
- If a call returns ARG_ERROR (2) for a flag shown as valid in the manifest — possible version skew; re-fetch manifest before retrying
Coding agent notes
Generation strategy
- Generate the manifest by reflecting over registered commands at startup — do not generate it by hand or from a separate config file
- The etag must be a deterministic hash of the serialized command registrations — generate it as sha256(canonical_json(commands)) where canonical JSON sorts keys
Type representation
- Generate ManifestResponse as a read-only snapshot type — it reflects the registration state at the moment enable_manifest() is called and does not update dynamically
- commands keys must be dot-separated strings matching the command's full invocation path — generate the key from the command's registration name, not from its handler function name
Validation to generate
- Assert every command in the registry appears in commands — missing commands are a silent discovery failure
- Assert every command's effective exit_codes table (root table overlaid with the entry's own map) matches its ExitCodeEntry declarations exactly — no additions, no omissions
- Assert an entry's own exit_codes map never repeats a root-table entry unchanged
- Assert no CommandEntry.flags key or short value equals a root flags key or short value
- Assert positionals lists every positional the parser accepts, in order, with no required entry after an optional one and variadic only on the last
- Assert every command the framework registers, and each of its subcommands, carries builtin: true, and no application command does
- Assert output_file is present on exactly the commands that register --output <path>, and is "binary" exactly when the command's result is a binary value, "handler" exactly when the handler writes the file, and "envelope" exactly when the framework writes the final envelope to it
- Assert output_file_base appears only with output_file, and that a relative --output path lands under the declared base from a working directory other than that base
- Assert every type: "object" flag carries a schema the parser enforces, and prefer type: "object" for every flag whose value the parser reads as a JSON object
- Assert enum_values lists exactly the values the parser accepts, as strings on type: "enum" entries and as integers on type: "integer" entries, and that it appears on no other type; declare an integer choice as type: "integer" with integer enum_values, not as type: "enum" with numeric strings
- Assert every arguments: "passthrough" command has option_placement: "strict", empty flags, no positionals, and a danger_level other than destructive, and that help_argv appears only on such commands
- Assert every confirm_flag names a boolean flag in the command's flags or the root flags, appears only on mutating or destructive commands without safe_default: true or arguments: "passthrough", and that a call without it previews with meta.dry_run: true while --dry-run plus the flag still previews
- Assert idempotent: true only on commands whose tests show that a rerun after each declared partial exit leaves the state a clean run leaves; accept it on a safe command without warning
- Assert every flag's env_vars lists exactly the variables its parser reads, in the order it reads them, with the tool-prefixed name first whenever a name without the prefix is listed, and no name from secret_env_vars
- Assert root env_vars lists every other variable the tool reads outside the universal exceptions, each with a description and either the tool prefix or a declared name that immediately follows its setting's prefixed entry, and no name that also appears in a flag's env_vars, a secret_env_vars, or a token_env_vars
- Assert root secret_env_vars lists exactly the secrets every command reads, and that none of them repeats in a command's secret_env_vars or a flag's env_vars; assert no name in an auth command's token_env_vars repeats in that command's secret_env_vars
- Assert stdout: "protocol" and protocol appear together on exactly the commands that serve a protocol over stdio, that such a command writes nothing to stdout before serving begins, ends stderr with the envelope on any non-zero exit in JSON mode, exits 0 when stdin closes, and declares none of the fields REQ-C-032 excludes
- Assert mcp: false appears on exactly the commands the tool's MCP server leaves out, that the server's tool list holds every other command it serves and none marked mcp: false, and that no entry carries mcp: true
- Assert requires_person: true appears on exactly the commands that ask for an attestation, always with interactive: true and mcp: false, and that each one exits 4 with PERSON_REQUIRED under --yes with stdin from /dev/null
- Assert stderr: "child_log" appears on exactly the commands that stream a wrapped program's output to stderr, never on a passthrough command, and that --quiet leaves stderr empty for them
- Assert the root format flag's media_types keys are all in its enum_values, cover every value outside the spec's media type table, and map each spec value they list to the table's media type; assert no other flag carries media_types
- Assert every output_formats value covered by neither the spec's table nor the root media_types has an output_media_types entry, and every output_media_types key is a value the command accepts
- Assert stdin is present on exactly the commands that declare stdin input, with the mode the handler reads in, record_schema equal to the registered record type, and no max_bytes outside buffered mode
- Assert etag changes when any command registration changes, and is stable across identical registrations (determinism test)
Tests to generate
- A test that tool manifest returns all registered commands including built-ins
- A test that tool manifest --etag <current> returns meta.not_modified: true and data: null
- A test that adding a new command changes the etag
- A test that an agent can construct a valid call to every command using only the manifest — no --help calls
Anti-patterns
- Do not generate exit_codes in the manifest independently of ExitCodeEntry declarations — they must be the same source
- Do not generate the manifest as a static file checked into the repo — it must be computed from live registrations
- Do not use function names or file paths as command keys — use the registered command name
Implementation notes
commandsis a flat map, not a tree. Usesubcommandsarrays for hierarchy — O(1) lookup without recursionetagmust be computed from registrations (names, flags, exit codes, descriptions), not runtime state. Same registrations → same etagexit_codeskeys are strings ("0","2") because JSON object keys are always stringsFlagEntry.defaultmust be omitted (notnull) when no default exists, to distinguish "optional without fallback" from "default is null"output_formatsmust list only values the command actually accepts; do not list formats that resolve to an errorFlagEntry.schemais a plain draft-07 schema, so a generated MCPinputSchemauses it as the property's schema for anobjectflag and asitemsfor anarrayflag; the CLI side still takes JSON text on argv
Related
| Document | Relationship |
|---|---|
| REQ-O-041 | Consumes: the command that returns this schema |
| REQ-O-049 | Sources: output_formats and output_media_types: command-specific formats and what each writes |
| REQ-O-001 | Sources: the root format flag's media_types and the spec's media type table |
| REQ-C-001 | Sources: exit_codes per command |
| REQ-C-015 | Sources: flags and positionals per command |
| REQ-C-002 | Sources: danger_level and idempotent per command |
| REQ-C-029 | Sources: required_scopes per command |
| REQ-F-079 | Sources: top-level flags (global options) |
| REQ-F-073 | Sources: FlagEntry.env_vars, root env_vars, root secret_env_vars, the four homes of a variable, and the precedence rule for names without the tool prefix |
| REQ-F-051 | Sources: TOOL_DEBUG listed in root env_vars when no flag backs it |
| REQ-O-030 | Sources: TOOL_AUDIT_LOG and the session variable listed in root env_vars |
| REQ-O-042 | Sources: TOOL_FORMAT listed in the root format flag's env_vars |
| REQ-C-027 | Sources: option_placement per command |
| REQ-F-054 | Sources: stdin mode and the max_bytes and max_line_bytes caps |
| REQ-O-004 | Sources: streaming_default, and the records mode a stream consumer reads in with its record_schema |
| REQ-C-026 | Sources: requires conditional rules |
| REQ-C-031 | Sources: arguments and help_argv per command |
| REQ-C-032 | Sources: stdout, protocol, and mcp per command |
| REQ-C-036 | Sources: requires_person per command |
| REQ-F-038 | Sources: stderr per command, the child log that auto-quiet leaves alone |
| REQ-O-048 | Sources: safe_default and confirm_flag per command |
| REQ-O-031 | Sources: top-level dependencies |
| schemas/exit-code-entry.md | Provides: ExitCodeEntry type used in exit_codes map |
| schemas/response-envelope.md | Wraps: manifest is returned as the data field of a ResponseEnvelope |