AFDATA protocol v1 decision record

Status: accepted.

AFDATA protocol v1 uses one discriminator field, kind, and one payload field whose name is identical to kind.

{"kind":"result","result":{"rows":12}}
{"kind":"error","error":{"code":"file_not_found","message":"missing input","hint":"check --input"}}
{"kind":"progress","progress":{"percent":50}}
{"kind":"log","log":{"event":"startup"}}

Top-level fields are closed:

trace, when present, must be a JSON object. Its internal fields are owned by the emitting tool. Normal recursive AFDATA redaction still applies when the event is formatted.

result, progress, and log may carry any valid JSON value. AFDATA does not define business payload structure for these event kinds.

error must be a JSON object with:

Tools may add extension fields directly to the error object. There is no required details wrapper.

Finite structured CLI event streams follow this lifecycle:

(log | progress)* -> exactly one (result | error) -> end

Canonical CLIs do not need --stream or --result-only mode switches. The default finite execution emits exactly one terminal result or error event. Diagnostic log or progress events are opt-in, for example through an explicit --log ... filter or --verbose shorthand. TTY detection, redirection, and pipe targets must not change this event policy.

Finite commands route by kind: result goes to stdout, while error, progress, and log go to stderr. Ordered event streams keep every kind on one selected destination so interleaving is preserved. --output-to selects between those policies as described in docs/transport-mappings.md.

Routing and event meaning are independent from process status. result means the command intent was fulfilled and always uses the result channel, even when a tool-defined condition uses a non-zero exit code. error always uses the error channel. AFDATA reserves exit 2 for closed-world CLI usage failures but does not define a global mapping from every result/error code to process exit status.

Cancellation is represented as an ordinary tool-defined error event when the tool can observe the cancellation and still write a terminal event. For example, a tool may use error.code: "cancelled", but AFDATA does not reserve that code. If the selected event destination or transport is already closed and the terminal event cannot be written, the outcome is a transport interruption with unknown business result.

A CLI resolution failure names itself in code, the same way a document failure does: cli_unknown_argument, cli_unknown_command, cli_missing_argument_value, cli_invalid_argument_value, cli_duplicate_argument, cli_unexpected_positional, cli_unregistered_combination, cli_invalid_utf8. A CLI that is not compiled from a registry — anything built with build_cli_error — reports the generic cli_error.

There is one place to look. An earlier draft put the classification in a separate rule field beside a generic cli_error, alongside command_path and argument_names; that asked an agent to learn a second branching key for one family of errors while document_* already spelled its classification into the code, and it cost a closed enum kept in step across four files and four mirrored schemas. message identifies the offending argument name or failure category, hint gives the command to run next, and neither ever quotes a raw value. Every such error is retryable: false and exits 2.

The machine-readable event schema is:

The shared fixtures proving constructor, validation, and lifecycle behavior are: