machinemode.io
/
aoi‑cli · v1.0‑alpha

AOI‑CLI#

The Agent-Operable Interface · Command‑Line Profile

The first profile of the Machine Mode standard. A typed, streamable, discoverable, bounded, safe contract for command‑line tools used by autonomous systems.


Executive summary#

AOI‑CLI is the command‑line profile of the Machine Mode standard. A conforming tool exposes:

A stable machine mode that emits UTF‑8 newline‑delimited JSON event objects to stdout, never mixes human prose into that stream, provides a credential‑free schema and capability discovery command, uses a standard event envelope and error taxonomy, has explicit completion and failure semantics, and makes side effects safe, bounded, and retry‑aware.

The analogy is not "POSIX for agents." POSIX standardizes operating‑system interfaces. AOI is closer to OpenAPI, AsyncAPI, or gRPC/protobuf for command‑line tools: a typed interface contract for local process execution.

Roles in a pipeline#

An AOI‑CLI tool plays one of three roles depending on whether it consumes JSONL input, produces JSONL output, or both:

RoleReads JSONL on stdin?Emits JSONL on stdout?Example
Sourcenoyesoutline search … produces hits
Transformeryesyessummarize --input-jsonl - reads hits, emits abstracts
Sinkyesyes (audit + summary only)outline import --input-jsonl - performs side effects

Every conforming tool — source, transformer, or sink — still emits aoi:meta first and a terminal aoi:summary last, and follows every other factor of the standard. The role only describes the tool's position in a pipeline; it is not a separate conformance category.

The spec uses these terms throughout where the producer/consumer distinction matters: § 12 Input contract applies to transformers and sinks; § 13 Pipeline composition describes how the three roles compose.

Relationship to Machine Mode#

Machine Mode is the public concept and the foundations document: the framework for thinking about agent‑operable tools, naming the ten factors that describe a well‑shaped one (Typed, Discoverable, Streamable, Bounded, Safe, Idempotent, Auditable, Verifiable, Composable, Versioned). AOI is the formal specification of that framework. AOI‑CLI, defined in this document, is the first formal profile — it specifies how each of those ten factors applies at the command‑line process boundary.

Forthcoming profiles for other interface surfaces, alternate modes beyond batch invocation (such as a JSON‑RPC session mode), alternative wire formats, and the relationships to adjacent standards (OpenAPI, MCP) are described in Direction — beyond v1.0‑alpha. Nothing in that document is normative for v1.0‑alpha.

Design goals#

A conforming AOI‑CLI tool is:

  • Stable — explicit compatibility contract for machine output.
  • Discoverable — schema and capabilities can be fetched without credentials, network, or side effects.
  • Parseable — machine mode stdout is only JSONL event objects.
  • Streamable — consumers can process records incrementally.
  • Verifiable — successful completion, partial failure, truncation, and crashes have defined semantics.
  • Composable — pipes and process exit status behave predictably.
  • Safe — read‑only by default, destructive work explicit, secrets redacted.
  • Bounded — large reads and searches have limits, cursors, and truncation signals.
  • Retry‑aware — errors say whether retry may help; writes can be idempotent.
  • Language‑neutral — no Python or Nushell‑specific behavior is normative.

Non‑goals#

AOI‑CLI does not standardize:

  • operating‑system process APIs,
  • package‑manager layouts,
  • programming‑language frameworks,
  • human CLI UX,
  • transport protocols beyond local process stdin/stdout/stderr,
  • a universal schema‑registry requirement.

Normative keywords#

The words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are used in the RFC 2119 sense.

Relationship to existing standards#

AOI‑CLI builds on existing conventions:

AOI‑CLI is a typed CLI interface convention, not an OS standard.


Part I — Normative core#

Everything in Part I is required to claim AOI-CLI conformance. It is deliberately small: an envelope, a discriminator, a declared schema version, defined completion, a portable error taxonomy, and predictable composition. A maintainer should be able to implement all of it in an afternoon.

These are interoperability invariants — they only pay off if every tool spells them identically. One tool implementing them alone gains little; a hundred tools implementing them differently gains nothing. That is what makes them a standard rather than advice, and it is why they are stated strictly.


1. Machine mode#

A conforming tool MUST expose at least one stable machine mode that emits AOI events.

Recommended invocation pattern:

outline --output jsonl ...

or equivalently:

outline --format jsonl ...

A compatibility wrapper such as outline-jsonl MAY exist, but the suffix is not the normative contract.

1.1 Machine‑mode stability#

If a tool advertises AOI conformance, its machine mode MUST be a stable, schema‑governed API. It is not "best effort JSON." The project MUST document compatibility rules for event types, field names, field types, and versioning.

1.2 Human mode#

The default unsuffixed command MAY remain human‑readable. Human output MAY include tables, color, progress bars, and prose. Machine mode MUST NOT.

1.3 Interactive flows#

Machine mode is non‑interactive by contract. A conforming tool MUST NOT prompt for input, open a browser, or otherwise require human attention while running in machine mode. This is what makes the contract callable from an agent or a non‑terminal automation context.

Interactive flows that a CLI legitimately needs — OAuth authorization, first‑run config, MFA approval, interactive credential prompts — belong in the unsuffixed human‑mode invocation. The human runs tool auth login once, completes the flow, and the resulting credential lands in the user's existing credential ecosystem (keychain, config file, environment, kubeconfig, ssh agent, etc.). After that, subsequent machine‑mode calls inherit the credential by inheriting the process environment.

If a machine‑mode invocation reaches a point where it would otherwise prompt, it MUST instead emit a structured error:

{
  "type": "aoi:error",
  "aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
  "category": "authn",
  "code": "INTERACTIVE_REQUIRED",
  "message": "Interactive authentication required. Run `outline auth login` to complete the flow.",
  "retryable": false,
  "hint": "outline auth login"
}

The hint field is informational only; consumers MUST NOT parse it as a command to execute automatically. A reasonable agent surfaces the hint to a human operator.

This carve‑out is what lets AOI work with the full breadth of real auth ecosystems (browser‑bound OAuth, hardware MFA, enterprise SSO) without dragging interactive‑flow handling into the wire protocol.

2. JSONL event stream#

Machine‑mode stdout MUST be UTF‑8 newline‑delimited JSON.

Each line MUST be one complete JSON object.

Every event, including the terminal aoi:summary, MUST be followed by a line feed. A consumer MUST NOT treat a final line lacking a line terminator as a complete event; it MUST discard it and treat the stream as truncated. NDJSON does not settle this, and naive line iterators will happily yield {"type":"aoi:sum as a record — which would let a half‑written summary be read as a whole one, defeating § 4 entirely.

Every event object MUST include:

{"type":"..."}

Machine‑mode stdout MUST NOT include:

  • banners,
  • markdown headings,
  • color or ANSI escape codes,
  • progress bars,
  • human prose,
  • stack traces,
  • mixed plain‑text warnings.

Recoverable diagnostics belong in structured aoi:warning events. Fatal diagnostics should be structured aoi:error events when possible; usage errors may use stderr before stream startup.

3. Discovery without credentials or network#

A conforming tool MUST provide a credential‑free, network‑free way to discover its schema and capabilities.

Recommended subcommands:

outline schema --output json
outline capabilities --output json

Equivalent flags MAY exist:

outline --schema
outline --capabilities

If both flags and subcommands exist, they MUST return equivalent information.

3.1 Schema sidecars#

Sidecar schema files MAY be provided but MUST NOT be required for conformance.

A sidecar is a packaging convenience, not the wire contract. Real installation layouts include npm/pnpm shims, Homebrew Cellar paths, immutable Nix store paths, single‑file Go/Rust binaries, container images with stripped filesystems, FHS share/<package>/ resources, and embedded schemas.

Consumers MUST be able to rely on the schema and capability command. They MAY opportunistically read local sidecars when available.

3.2 Schema command requirements#

schema MUST NOT require:

  • authentication,
  • config files beyond local install metadata,
  • network access,
  • environment secrets,
  • a valid business query.

Startup cost is acceptable.

4. Completion semantics#

A normal finite operation SHOULD emit aoi:meta first and MUST emit a terminal aoi:summary event before exiting 0.

ok means the tool accomplished what was asked, not that the process avoided crashing. It has no default: a producer MUST emit it explicitly. SARIF's equivalent field had an ambiguous default and GCC consequently shipped executionSuccessful: true while failing with an error.

Once aoi:meta has been emitted, a process that exits under its own control MUST emit aoi:summary regardless of outcome. That is what makes the absence of a summary mean exactly one thing — crash, kill, or truncation — which is the whole basis of the contract below.

A consumer MUST treat EOF without a terminal aoi:summary as an incomplete stream unless the operation is explicitly documented as unbounded.

A producer that crashes cannot guarantee a terminal sentinel. Therefore the absence of aoi:summary on EOF is the cross‑language crash and truncation signal.

4.1 Finite streams#

For finite commands such as list, search, get, create, and doctor:

  • exit 0 + terminal aoi:summary.ok=true means success,
  • exit 0 without terminal aoi:summary MUST be treated as protocol failure,
  • non‑zero exit with partial events MUST be treated as failure unless the caller explicitly requested partial tolerance,
  • aoi:summary.partial=true MAY indicate successful partial results when partial is a valid outcome.

4.2 Unbounded streams#

For commands like watch, tail, or event subscriptions:

  • the command MUST document that the stream is unbounded,
  • it MAY emit heartbeat or checkpoint events,
  • on graceful cancellation it SHOULD emit aoi:summary.ok=false with reason:"cancelled" when possible,
  • consumers cannot require a terminal aoi:summary until EOF.

5. Versioning#

AOI versioning has three separate concepts and they MUST NOT be conflated:

  1. Tool version — implementation release, e.g. outline 1.8.2.
  2. AOI version — the version of this interface convention, e.g. aoi_version: "1.0-alpha".
  3. Schema version — the version of a specific event/input schema, e.g. schema_version: "1.0.0".

The aoi:meta event and the terminal aoi:summary SHOULD each carry all three, and MUST to claim the Versioned factor (§ 18). Requiring the identity triple at both ends means a consumer holding only tail -1 still knows what it read, and one holding only the first line does too.

The field name aoi_version is frozen permanently. CloudEvents failed the one version transition it existed for because the version field itself was renamed between 0.1 and 0.2, leaving old parsers unable to locate the version at all.

schema_name + schema_version are the normative identity. A schema $id URL is advisory: consumers MUST NOT be required to dereference it and MUST NOT use it as the version identity. SARIF made this mistake — its schema URL became the de-facto identity and then churned across four host variants, breaking GCC, Trivy, tflint, and both Microsoft SARIF viewers.

The cheap incompatibility check: if major(schema_version) differs from what the consumer was built against, it MUST NOT interpret domain events — but MAY still interpret aoi:* framework events if major(aoi_version) matches. That is the payoff for carrying two independent version numbers.

{
  "type": "aoi:meta",
  "aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
  "tool": "outline",
  "tool_version": "1.8.2",
  "aoi_version": "1.0-alpha",
  "schema_name": "com.example.outline.events",
  "schema_version": "1.0.0"
}

5.1 Breaking-change policy#

A breaking machine‑output change MUST change schema_version major version.

A tool MAY expose multiple schema versions through one binary:

outline --output jsonl --schema-version 1 ...
outline --output jsonl --schema-version 2 ...

or through content negotiation:

outline --output jsonl --accept-schema com.example.outline.events@2

5.2 Breaking changes in v1.0-alpha#

v1.0-alpha is not backward compatible with v0.2. A v0.2 consumer will silently mis-handle a v1.0-alpha stream, which is precisely the failure this spec exists to prevent — so the break is stated explicitly rather than left to discovery.

Changev0.2v1.0-alphaMigration
Framework envelopeflat, top levelnested under aoiRead aoi.seq / aoi.run; two top-level names are reserved, type and aoi.
category:"temporary"presentrenamed unavailableA consumer branching on temporary stops matching silently. Update the branch.
category:"partial"presentremovedEmit the item's real category with scope:"item"; express partiality on aoi:summary.partial.
retryableoptionalrequired, authoritativeEmit it on every aoi:error.
retry_after_msundefineddefinedReplaces prose "include retry hint if known". MUST NOT appear with retryable:false.
aoi:error.upstream{protocol,status}renamed wrappedupstream now means provenance on aoi:summary — a different shape.
Trailing newlineunstatedMUSTA final line without a terminator is truncated, not complete.
reasonundefinedrequired when ok:falseClosed enum: failed, cancelled, timeout, denied.

5.3 Rollout rule#

Consumers SHOULD branch on schema_name and schema_version, not executable name.

6. Exit status#

AOI does not invent low‑number exit meanings that conflict with existing conventions.

Required baseline:

  • 0 — process completed successfully at the process layer. For finite streams, a terminal aoi:summary.ok=true is still required for application‑level success.
  • Non‑zero — process failed or was interrupted. Inspect structured events if present.

Recommended mapping using sysexits‑compatible codes:

CodeConstantMeaning
64EX_USAGEusage error
65EX_DATAERRinput or data validation error
69EX_UNAVAILABLEservice or dependency unavailable
70EX_SOFTWAREinternal software error
73EX_CANTCREATcannot create output
74EX_IOERRI/O error
75EX_TEMPFAILtemporary failure, retry may succeed
77EX_NOPERMpermission or authz failure
78EX_CONFIGconfiguration error
124—timeout (when produced by timeout wrappers or documented)
130—interrupted by SIGINT
141—SIGPIPE / downstream closed pipe

Tools on platforms without sysexits MAY use local conventions but MUST document them. Consumers MUST NOT rely on exit code alone for application semantics when structured aoi:summary or aoi:error events are available.

7. Signals and cancellation#

All AOI implementations SHOULD exit cleanly on SIGPIPE/downstream pipe close without printing stack traces to stderr. This requirement is language‑neutral.

On SIGINT/SIGTERM, a tool SHOULD emit, if safe and possible:

{"type":"aoi:summary","aoi":{"seq":41,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"cancelled","event_count":42}

then exit with the conventional signal code. If emitting a final summary would corrupt state or hang, exiting promptly is more important.


8. Event model#

8.1 Common envelope#

Every event MUST include type. Every framework field that may appear on a domain event lives inside a reserved object named aoi. A framework (aoi:*) event additionally carries its own spec-defined fields at the top level — see Envelope fields below.

{"type":"hit","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_123","title":"Agent-operable tools"}

Exactly two top-level names are reserved, permanently: type and aoi. Everything else at the top level of a domain event belongs to the tool's schema and AOI will never claim it.

Why the envelope is nested when the type prefix is not#

This is the one place AOI nests, and the asymmetry is deliberate.

A colon is safe in a JSON value and hostile in a JSON key: jq 'select(.type=="aoi:summary")' works, while .aoi:seq is a syntax error requiring .["aoi:seq"]. So the framework namespace is expressed as a prefix in the type value and as a nested object in the field space. One namespace, two syntactic positions, each chosen for what that position tolerates.

The field space is the one that actually needed protecting. Domain payloads are flattened into the event, and they are owned by third parties. § 10.3 tells tools to allow unknown fields for forward compatibility, so tools will add their own. Without a reserved container, every field AOI adds in a later version can collide with one a deployed tool already emits — and the collision is live today: the hit shape in § 8.3 carries id, a name any envelope would want. Flattening the framework into that space guarantees the collision the type prefix was introduced to prevent.

Nesting costs roughly sixteen bytes per line and .aoi.seq instead of .seq. It buys unbounded framework growth with zero collision risk, and it lets a generic filter, router, or tee strip the envelope without knowing any domain schema.

Envelope fields#

FieldStatusMeaning
typeMUSTEvent discriminator. Top level, never inside aoi.
aoi.seqSHOULD; MUST to claim VerifiableMonotonic counter from 0, no gaps, one per emitted line.
aoi.runSHOULD; MUST alongside seqOpaque identifier stable for this invocation. ULID RECOMMENDED.
aoi.tMAYMilliseconds elapsed since aoi:meta. Absolute time is recoverable from the meta event; a monotonic offset is always determinable where a wall-clock timestamp is not.
aoi.trace_idMAYCorrelates this run with an external trace.

A framework event (aoi:*) carries its own spec-defined fields at the top level — they are framework-owned by definition, so there is no third party to collide with. Tool-specific additions to a framework event MUST go in a properties object rather than at the top level:

{"type":"aoi:summary","aoi":{"seq":9,"run":"01JZQ7X4TQ9"},"ok":true,"event_count":10,"count":8,
 "properties":{"vendor_cache_hits":41}}

properties is the one part of SARIF that has drawn no criticism in six years: a single closed container for vendor data, with no prefixes and no registry to administer.

Why seq and run matter#

These are SHOULD, not MUST, because a tool that omits them is still useful and the cost of retrofitting them is real. But a tool cannot claim the Verifiable factor without them, and aoi-lint reports their absence. seq closes a failure mode specific to AOI's actual consumers. The terminal-summary contract (§ 4) detects a stream cut at the end. It cannot detect a stream cut in the middle — and agent harnesses routinely elide the middle of long tool output, keeping the head and tail with a [… N lines omitted …] marker. The summary survives that elision; the data does not. The consumer then reports success while having silently lost records. A contiguous aoi.seq, reconciled against aoi:summary.event_count, makes that undetectable loss detectable. This is TAP's plan line (1..N) applied to a stream that can be truncated from the middle rather than only the end.

aoi.run makes every line attributable on its own. aoi:meta is a sibling line, not a structural wrapper, so any grep, head, split, sort, sampling step, or parallel fan-in severs a line from its metadata. An orchestrator running five tools concurrently and merging their stdout otherwise has no way to say which line came from which tool.

8.2 Type namespace#

Event type values live in one of three namespaces, and the namespace is visible on the wire by prefix convention.

LayerConventionExamples
Framework events (defined by this spec)aoi: prefixaoi:meta, aoi:summary, aoi:warning, aoi:error, aoi:heartbeat, aoi:plan, aoi:check, aoi:progress
Recommended domain patterns (common shapes for common operations; each tool's schema owns them)unprefixedentry, hit, match
Tool‑specific domain eventsunprefixed; scoped by aoi:meta.schema_namewhatever the tool defines, e.g. created, deleted, abstract, compiled, transcoded

Rationale:

  • The aoi: prefix on framework events prevents wire‑level collision with domain events that share generic words. A tool that emits a textual document summary ({"type":"summary","text":"…"}) does not conflict with the framework's terminal {"type":"aoi:summary","ok":true,…} — they coexist in the same stream without ambiguity.
  • Domain types stay unprefixed because they are already scoped by the schema declared in aoi:meta.schema_name. The full coordinate for a domain event is schema_name#type (e.g. com.example.outline.events#hit).
  • The visual asymmetry is a feature, not an inconsistency. A consumer reading a stream knows at a glance whether a line is framework control‑plane (parser cares) or domain data‑plane (domain code cares). grep aoi: extracts control events from a tee'd log.
  • No aliases. A framework type is always written with the aoi: prefix. Aliases would re‑introduce the ambiguity the prefix exists to prevent.

The namespace invariant. The aoi: type namespace is closed and owned by this specification. Every unprefixed type name is owned by the tool's schema, permanently. AOI will never claim an unprefixed type name.

Nothing in the bare namespace is reserved. A domain schema MAY define an event named summary, meta, or error — that is precisely the point of the prefix, and forbidding those names would make the collision argument above circular while growing a reserved-word list with every framework type ever added.

This resolves what would otherwise be a contradiction in § 8.3: entry, hit, and match are defined by this specification and yet carry no prefix. They are recommended shapes, not framework events — common field layouts for common operations, which each tool's own schema owns. They are not reserved, and per the invariant above they will never be promoted into the aoi: namespace. Prefixing them would also destroy the property that makes the prefix practically useful: grep aoi: extracts the control plane, and data-plane events must stay out of it.

Prose convention. Throughout this document, framework event types are always written with their prefix (aoi:summary), including in field paths (aoi:summary.ok). Domain event types are written bare (hit). A bare framework name in a normative sentence about the framework is an error — except where this document deliberately quotes a domain spelling to contrast it, as in the {"type":"summary","text":"…"} example above.

Verbosity cost: 4 bytes per framework event. A typical AOI stream emits ~5 framework events per invocation, so total overhead is ~20 bytes per stream — negligible against the clarity benefit.

8.3 Standard event types#

aoi:meta#

First successful event for finite commands. Describes schema, command, version, and source context. MUST NOT echo raw argv or environment wholesale.

{
  "type": "aoi:meta",
  "aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
  "tool": "outline",
  "tool_version": "1.8.2",
  "aoi_version": "1.0-alpha",
  "schema_name": "com.example.outline.events",
  "schema_version": "1.0.0",
  "command": "search"
}

entry#

One resource in a list or inventory.

hit#

One search result. Should include source, stable ID/path/URL, rank/score where meaningful, and snippet/text.

match#

One exact match inside a source, such as a file line match. Recommended fields: path, line_number, column, text, submatches.

aoi:check#

One readiness or doctor check.

{
  "type": "aoi:check",
  "aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
  "name": "config_file",
  "ok": true,
  "severity": "info",
  "detail": "found"
}

aoi:plan#

One planned side effect, usually emitted during --dry-run or before apply.

aoi:progress#

Long‑running operation progress. Use sparingly. Progress events MUST NOT be required for correctness.

aoi:heartbeat#

Optional for unbounded streams. Lets consumers distinguish a quiet but alive stream from an idle transport when process supervision is not enough.

aoi:warning#

Recoverable issue. The operation continues.

aoi:error#

Structured failure. May be item‑level or process‑level.

aoi:summary#

Terminal event for finite commands. Required for process‑level success.

{
  "type": "aoi:summary",
  "aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
  "ok": true,
  "count": 42,
  "input_count": 50,
  "warning_count": 1,
  "error_count": 0,
  "partial": false,
  "truncated": false,
  "next_cursor": null,
  "elapsed_ms": 384
}

Counter semantics, which MUST NOT be conflated:

FieldCounts
countdomain events this tool emitted. Never framework events.
input_countdomain events this tool consumed from stdin. Never framework events. A transformer with count == input_count dropped nothing.
warning_countaoi:warning events this tool emitted plus those it consumed from upstream.
error_countaoi:error events this tool emitted plus those it consumed from upstream.
partialtrue when the run produced usable output and at least one item failed.
event_countRecommended; required to claim the Verifiable factor (§ 18). Total lines emitted in this stream, including this one. The reconciliation counter: a consumer asserts lines received == event_count and seq contiguous over 0…event_count-1. Without it, middle-of-stream elision is undetectable.
reasonrequired when ok is false. Closed enum: failed, cancelled, timeout, denied. Also valid with ok:true: skipped, meaning the operation was not applicable and did not run — distinct from count:0, which means it ran and found nothing.
caused_bypresent when ok is false due to an upstream error (§ 13.1).
upstreamthe direct upstream's identity (§ 13.1). Omitted when unknown.

9. Error taxonomy#

AOI needs portable retry and handling logic. Tools MAY define domain‑specific codes, but MUST map them to a standard category. A code without a category is unroutable by a consumer that has never seen the tool before, which is the entire premise of the taxonomy.

{
  "type": "aoi:error",
  "aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
  "scope": "item",
  "category": "rate_limited",
  "code": "UPSTREAM_RATE_LIMIT",
  "domain": "outline",
  "message": "Upstream API rate limit exceeded.",
  "retryable": true,
  "retry_after_ms": 4500,
  "target": "/documents/abc123",
  "item_index": 17
}

9.1 Fields#

FieldStatusTypeSemantics
typeMUST"aoi:error"—
categoryMUSTclosed enum (§ 9.2)Semantic class. A consumer that meets an unrecognized value MUST NOT fail (§ 9.4).
codeMUST[A-Z][A-Z0-9_]*Stable, tool‑defined identifier. MUST remain stable once published.
messageMUSTstring, single lineHuman‑readable. No ANSI, no stack trace, no secrets. Not machine‑parsed.
retryableMUSTbooleanAuthoritative retry disposition for this occurrence (§ 9.3).
retry_after_msMUST when known and retryable:true; MUST NOT appear when retryable:falseinteger ≥ 0Minimum delay before retry, in milliseconds, relative to receipt.
domainSHOULDstringNamespace for code. Defaults to aoi:meta.tool.
originMUST when forwardedstringNames the tool that raised this error, set when a producer forwards an upstream error rather than raising it locally (§ 13.1). Absent means the error is local.
scopeSHOULDprocess | item (default process)process means the run is over; item means the stream continues.
targetSHOULDstringThe resource the error concerns — path, URI, or id.
item_index / line_numberSHOULD when scope:"item"integerCorrelation back to the input record.
fieldMAYstringInput path that failed, for validation.
wrappedMAY{protocol, status?, code?}Raw status from a wrapped service, where protocol is http, grpc, or exit. Named wrapped, not upstream, because aoi:summary.upstream is a different shape — one key, two meanings, is the collision this spec exists to prevent.
hintMAYstringInformational. Consumers MUST NOT execute it (§ 1.3).
docsMAYURLDocumentation for this condition.
detailsMAYobjectExtension bag. Consumers MUST ignore unrecognized keys.

9.2 Standard categories#

The category enum is closed and owned by this specification. Adding a value is a minor version bump.

CategoryDefault retryableMeaning
usagefalseInvalid CLI usage or missing required operand.
validationfalseInput parsed but failed schema/domain validation.
configfalseMissing or invalid configuration.
authnfalseMissing or expired credentials. Waiting does not refresh a token.
authzfalseCredentials valid but action not allowed.
not_foundfalseTarget resource does not exist.
conflictfalseConcurrent modification, version/ETag mismatch, or destination exists. Retry requires re‑reading state first, so the identical invocation is not retryable.
precondition_failedfalseSystem state must be explicitly fixed before this can succeed (e.g. rmdir on a non‑empty directory).
unsupportedfalseThe invocation is well‑formed but the capability does not exist. The version‑skew signal between an agent and a tool.
rate_limitedtrueTime‑based throttling or quota. SHOULD carry retry_after_ms.
unavailabletrueTransient dependency, network, or service failure.
timeouttrueOperation exceeded a deadline.
cancelledfalseInterrupted by caller or system. Retry is the caller's decision.
iofalseFilesystem or stdio failure. Includes BROKEN_PIPE. Transient I/O (EAGAIN, EINTR) MAY override to true.
internalfalseTool bug or unexpected exception. A bug does not fix itself.

rate_limited is deliberately distinct from unavailable: a consumer's base backoff for throttling should be roughly an order of magnitude longer than for a transient failure, so collapsing them loses actionable information.

There is no partial error category. Partiality is a property of the run, not of a failure. Item‑level failures MUST use the specific category of that failure with scope:"item" and an index; aggregate partiality is expressed only by aoi:summary.partial and aoi:summary.error_count.

9.3 retryable is authoritative#

retryable is the retry disposition for this occurrence. Consumers MUST honor it over the default listed in § 9.2. The Default column applies only when retryable is absent, which producers MUST NOT allow.

category:"rate_limited" with retryable:false is not a contradiction — it is the most important case in the taxonomy. It means throttling was hit and retrying will not help: a hard quota, a depleted retry budget, or an explicit upstream instruction not to retry.

A producer MUST set retryable:false when it cannot determine whether a side effect was applied. Retry safety is a joint property of the error and the operation's idempotency. A tool invoked with --idempotency-key MAY report retryable:true for a mutating operation that would otherwise be false, and MUST document that it does so.

Consumers MUST NOT automatically retry a failed automatic retry without applying their own attempt limit, and SHOULD treat retry_after_ms as a minimum with jitter applied, never as a schedule.

Branch on retryable for disposition; branch on category for routing.

9.4 Forward compatibility#

A consumer that encounters an unrecognized category MUST NOT fail. It MUST use retryable for disposition and SHOULD route the error as internal. This lets the enum grow without breaking deployed consumers.

Fine‑grained identification is the open, namespaced domain + code pair — there is deliberately no central registry of AOI error codes. A closed semantic enum plus a tool‑owned code namespace is what gRPC, Kubernetes, and RFC 9457 all converge on in practice.

9.5 Mapping from wrapped protocols#

A tool wrapping an HTTP or gRPC service SHOULD use this mapping so that agents get identical retry behavior across wrappers.

gRPCHTTPAOI categoryretryable
INVALID_ARGUMENT400, 422validationfalse
UNAUTHENTICATED401authnfalse
PERMISSION_DENIED403authzfalse
NOT_FOUND404, 410not_foundfalse
UNIMPLEMENTED405, 501unsupportedfalse
DEADLINE_EXCEEDED408, 504timeouttrue
ABORTED, ALREADY_EXISTS409conflictfalse
FAILED_PRECONDITION412, 428precondition_failedfalse
RESOURCE_EXHAUSTED429rate_limitedtrue
UNAVAILABLE502, 503unavailabletrue
INTERNAL, UNKNOWN, DATA_LOSS500internalfalse
CANCELLED499cancelledfalse

Unlisted statuses: unknown 4xx → validation/false, unknown 5xx → unavailable/true. This is a mechanical heuristic, not a semantic claim — RFC 9110 assigns no retry semantics to the 4xx/5xx classes, and 408 is a retryable 4xx in the same document. An implementer with real knowledge of the upstream MUST override it.

Retry timing maps as: Retry-After: 120 → 120000; Retry-After: <HTTP-date> → max(0, date − now) in ms; grpc-retry-pushback-ms: 4500 → 4500; a negative pushback → omit and set retryable:false.

A UNAVAILABLE on a non‑idempotent operation invoked without --idempotency-key MUST be downgraded to retryable:false.


10. Schema rules#

10.1 Discovery#

The schema command SHOULD emit JSON Schema to stdout:

outline schema --output json

It MAY support:

outline schema --output json --schema-version 1
outline schema --output json --name com.example.outline.events

10.2 $id#

Schema $id MUST NOT be a machine‑local file URI unless the schema is explicitly local‑only.

Preferred:

{
  "$id": "https://schemas.example.com/outline/events/1.0.0/schema.json"
}

10.3 Strictness#

Recommended policy:

  • Event/output schemas SHOULD be permissive for forward‑compatible extension: allow unknown fields unless a specific event class has a reason to forbid them.
  • Input schemas SHOULD be strict: reject unknown fields to catch agent typos and hallucinated parameters.

10.4 Event discriminators#

Prefer discriminator‑style schemas using type with if/then or $defs keyed by event type. Avoid examples that produce opaque oneOf matched zero schemas errors.


11. Confidentiality and argument redaction#

aoi:meta.args is optional and dangerous. A conforming framework SHOULD NOT echo raw argv by default.

If arguments are included, the tool MUST redact sensitive values.

Sensitive patterns include at least:

  • flags containing token, secret, password, passwd, key, credential, cookie, authorization, auth, bearer, private, client-secret,
  • URLs with credentials, tokens, signatures, SAS parameters, or query keys such as sig, token, access_token, code, key,
  • custom headers,
  • environment variable values,
  • config file contents.

Preferred aoi:meta pattern:

{
  "type": "aoi:meta",
  "aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
  "command": "search",
  "input_fingerprint": "sha256:...optional...",
  "args_redacted": true
}

The caller already knows what it passed. Do not leak secrets for convenience.


12. Input contract#

Output typing is not enough. Tools acting as transformers or sinks (those that consume JSONL on stdin) need equal rigor on the input side.

12.1 Input modes#

A tool MAY accept:

  • ordinary CLI operands and options,
  • stdin text via -,
  • JSON input via --input-json -,
  • JSONL input via --input-jsonl -,
  • file paths.

If accepting structured stdin, the tool SHOULD provide an input schema:

outline input-schema --output json --command import

12.2 Malformed JSONL input#

For JSONL input streams, tools MUST document whether failures are:

  • whole‑input fail‑fast — any malformed line aborts the whole command,
  • per‑line tolerant — malformed lines emit item‑level errors and processing continues,
  • configurable — --fail-fast / --continue-on-error.

Recommended default:

  • Mutating imports — fail fast before side effects where possible.
  • Read‑only transforms — item‑level errors may continue if --continue-on-error is set.

13. Pipeline composition#

An AOI pipeline composes sources → transformers → sinks. A source produces events. A transformer reads upstream events and produces its own. A sink reads events and produces side effects (with a corresponding audit trail in its own output). Any role may be a conforming AOI‑CLI tool; standard shell pipes are the canonical transport.

A transformer or sink that accepts AOI JSONL input SHOULD:

  • accept and parse upstream aoi:meta, aoi:warning, aoi:error, and aoi:summary events,
  • ignore unknown unprefixed event types, which MUST always be safe to ignore,
  • treat an unknown aoi: event type asymmetrically: it MAY be ignored, but a consumer MUST NOT report success on a stream containing an unrecognized aoi: event it did not interpret, because framework events can carry failure semantics,
  • decide whether upstream errors are data or control signals.

Recommended default:

  • Upstream entry, hit, match, and domain events are data.
  • Upstream aoi:error events cause downstream aoi:summary.ok=false unless --continue-on-error is set.
  • Upstream missing terminal aoi:summary causes downstream failure if EOF occurs.

13.1 Framework events across a pipe#

Scope. This section binds an AOI producer — a tool that emits framework events of its own. A transparent relay — a tool that emits no framework events and passes the stream through unchanged (tee, grep, head, jq -c ., stdbuf, pv) — is out of scope and MAY forward everything byte for byte. Relays are not transformers in the § 13 sense even though they read and write JSONL; the distinction is whether the tool speaks the framework itself.

An AOI producer MUST NOT forward upstream aoi:meta or aoi:summary to its own stdout. These two are cardinality‑critical: a consumer must be able to read aoi:meta as "the description of the stream I am holding" and a terminal aoi:summary as "the outcome of the process that produced it." A stream therefore carries at most one aoi:meta, which when present is its first event, and at most one aoi:summary, which when present is its last.

An AOI producer MAY forward upstream aoi:error and aoi:warning, and when it does it MUST tag them with an origin naming the emitting tool, so a consumer can tell a local failure from an inherited one:

{"type":"aoi:error","aoi":{"seq":7,"run":"01JZQ7X4TQ9"},"origin":"outline","category":"rate_limited","code":"UPSTREAM_RATE_LIMIT","message":"…","retryable":true,"retry_after_ms":7200}

Whether or not it forwards them, it MUST account for upstream outcome in its own terminal event:

  • Upstream aoi:error sets aoi:summary.ok=false unless --continue-on-error is set.
  • Upstream EOF without a terminal aoi:summary sets aoi:summary.ok=false and SHOULD emit an aoi:error with category:"io" first.
  • Upstream aoi:warning events MUST be counted into aoi:summary.warning_count.

Retry detail MUST survive the pipe#

Collapsing an upstream failure to ok:false destroys the retry semantics of § 9. A rate_limited error carrying retry_after_ms:7200 is actionable; {"ok":false,"error_count":1} is not. When aoi:summary.ok is false because of an upstream error, the summary MUST carry caused_by reproducing the retry‑critical fields of the most severe upstream error:

{
  "type": "aoi:summary",
  "aoi": { "seq": 14, "run": "01JZQ7X4TQ9" },
  "ok": false,
  "reason": "failed",
  "event_count": 15,
  "count": 12,
  "warning_count": 0,
  "error_count": 1,
  "partial": true,
  "caused_by": {
    "origin": "outline",
    "category": "rate_limited",
    "code": "UPSTREAM_RATE_LIMIT",
    "retryable": true,
    "retry_after_ms": 7200
  },
  "upstream": {
    "tool": "outline",
    "tool_version": "1.8.2",
    "schema_name": "com.example.outline.events",
    "schema_version": "1.0.0"
  }
}

Provenance lives on the summary, not the meta#

upstream records the direct upstream only — one object, not an array. It is not transitive: a three‑stage pipeline does not accumulate a chain. Each stage names the stage that fed it, and a caller reconstructs the chain by reading the stages it launched.

It is carried on aoi:summary rather than aoi:meta for a structural reason: aoi:meta must be the first event, but upstream identity is not known until the upstream's own aoi:meta has been read — which is after the first byte of stdin. Requiring it on aoi:meta would force a transformer to block its own first event on a possibly‑hung upstream, making "starting" indistinguishable from "wedged." By the time the summary is emitted, upstream identity is known or definitively absent.

If upstream identity is unavailable (an empty stream, or a non‑conforming producer that emitted no aoi:meta), the producer MUST omit the upstream key rather than emit nulls.

Fan‑in is not defined in v1.0‑alpha. § 12.1 specifies a single stdin, so a conforming pipeline is linear. Merging two AOI streams into one tool — and how two upstream outcomes combine into one ok — is deferred; see Direction.

13.2 Pipefail#

Shell pipelines should use process exit status and terminal aoi:summary checks together:

set -o pipefail
producer --output jsonl ... \
  | consumer --input-jsonl - --output jsonl ... \
  | tee output.jsonl \
  | jq -e 'select(.type=="aoi:summary") | .ok == true'

A library wrapper should validate:

  • every stdout line is valid JSON,
  • EOF includes terminal aoi:summary for finite commands,
  • process exit is acceptable,
  • aoi:summary.ok is true unless partial success was explicitly accepted.

Part II — Recommended profile#

Nothing in Part II is required for conformance. These are properties that pay off for a single tool immediately, with no coordination: safe defaults, dry-run plans, confirmation guards, idempotency keys, bounded reads, a predictable flag vocabulary.

They are unilateral quality properties, not interoperability invariants. --dry-run is a good idea whether or not any other tool has it, and a read-only tool that omits --confirm is fully conforming because it has nothing to confirm. Conformance is asserted per (factor × command) — see § 18.1 — so a tool adopts from Part II what its surface actually warrants.

The distinction matters because the two halves have opposite adoption dynamics. Part I is worthless until it is universal. Part II is valuable to the first tool that adopts it. Conflating them is what makes specifications feel too large to start.


14. Safety and side effects#

14.1 Read‑only by default#

Commands SHOULD default to read‑only behavior. Mutating operations must be obvious in the command or options.

14.2 Dry‑run#

Non‑trivial mutating operations SHOULD support --dry-run and emit aoi:plan events.

14.3 Confirmation#

Destructive operations MUST require explicit confirmation such as --confirm.

Bulk destructive operations SHOULD require --confirm-count N or an equivalent guard to prevent accidental broad deletes.

14.4 Idempotency#

Retryable writes SHOULD support:

--idempotency-key KEY

or a documented natural key.

14.5 Auditability#

Mutating operations SHOULD emit stable IDs/paths/URLs for resources created, updated, or deleted.


15. Bounds, pagination, and truncation#

List, search, and read operations SHOULD be bounded by default. Recommended options:

--limit N
--cursor TOKEN
--timeout SECONDS
--max-bytes N
--max-lines N

If output is truncated, aoi:summary.truncated MUST be true and aoi:summary.next_cursor SHOULD be set when more data can be fetched.

{"type":"aoi:summary","aoi":{"seq":101,"run":"01JZQ7X4TQ9"},"ok":true,"event_count":102,"count":100,"truncated":true,"next_cursor":"opaque-token"}

16. Standard CLI surface#

16.1 Global options#

OptionStatusMeaning
--help, -hMUSTHuman‑readable help.
--versionSHOULDTool version.
--output jsonl / --format jsonlMUSTEnter AOI JSONL mode unless the tool is AOI‑only.
--schema-version VERSIONSHOULDSelect supported schema version where applicable.
--timeout SECONDSSHOULDBound external calls.
--limit NSHOULDBound result count.
--cursor TOKENSHOULDContinue from prior cursor.
--dry-runSHOULDPlan without side effects.
--idempotency-key KEYSHOULDSafe retry key.
--confirmMUST¹Required for destructive operations.
--confirm-count NSHOULDGuard expected affected count.
--no-colorSHOULDDisable color. Machine mode never uses color.
--debugMAYExtra diagnostics, redacted, not corrupting JSONL stdout.

¹ --confirm is MUST for any tool that exposes a destructive operation, and not applicable to a tool that exposes none. A read-only tool is fully conforming without it. Per § 18.1, conformance is asserted per (factor × command); a requirement that cannot apply to a command is not a requirement that command fails.

16.2 Discovery subcommands#

SubcommandMeaning
schemaEmit event schema JSON. Credential‑free and network‑free.
input-schemaEmit input schema JSON for a command or input mode.
capabilitiesEmit supported commands, schema versions, event types, input modes, auth requirements.
doctorCheck credentials, dependencies, connectivity. Emits AOI events in machine mode.

16.3 Resource subcommands#

Use boring verbs:

SubcommandMeaning
listBounded collection.
getOne resource by stable key.
searchRanked or filtered results.
createCreate resource.
updateUpdate resource.
deleteDelete resource. Confirmation required.
moveMove or rename resource.
copyCopy resource.
sendSend message, email, notification.
syncReconcile state. Must state direction and planned changes.
watchUnbounded stream. Must document completion semantics.
statusCurrent state.

16.4 Flag names are conventions, not prefixed reservations#

The flag and subcommand names this section lists — --output, --format, --limit, --cursor, --dry-run, --confirm, --idempotency-key, schema, capabilities, doctor, and so on — are conventions intended to make a conforming tool feel the same to an agent across every implementation. They are deliberately not namespaced (e.g. --aoi-output, aoi-schema).

This is a different design call from the one made for event types (§ 8.2). Event types appear inline in the output stream where parser ambiguity is real; the aoi: prefix prevents collision with domain payloads. Flag names appear in the input the operator or agent writes; collisions there are configuration questions, not parse questions, and the per‑invocation byte cost of a prefix would land on every call.

Where a tool already uses a flag name AOI also defines (for example, an existing --output FILE for an output destination), the tool MAY:

  • Use the listed equivalent — e.g. --format jsonl instead of --output jsonl (already a recommended alternate in § 16.1).
  • Document a tool‑specific alias and advertise both in capabilities.
  • Resolve the conflict locally however it sees fit; AOI does not mandate a global rename.

The standard's leverage comes from consistent flag names where possible, not from prefixed reservations. Tools whose existing surface conflicts with AOI's recommended names should use the listed alternates rather than ship --aoi-output, which would defeat the convention‑is‑the‑value‑proposition that motivates AOI's flag vocabulary in the first place.

The same logic applies to subcommand names. A tool whose domain already includes a schema subcommand (a database CLI managing literal database schemas) may rename its conformance discovery to aoi-schema and document the alias; AOI does not prescribe a global subcommand prefix.


17. Capability manifest#

capabilities --output json SHOULD return a single JSON object, not JSONL, because it is discovery metadata.

{
  "tool": "outline",
  "tool_version": "1.8.2",
  "aoi_versions": ["1.0-alpha"],
  "outputs": ["jsonl"],
  "schemas": [
    {
      "name": "com.example.outline.events",
      "versions": ["1.0.0", "2.0.0"],
      "default": "2.0.0"
    }
  ],
  "commands": [
    {
      "name": "search",
      "read_only": true,
      "bounded": true,
      "supports_cursor": true,
      "event_types": ["aoi:meta", "hit", "aoi:warning", "aoi:error", "aoi:summary"]
    },
    {
      "name": "delete",
      "read_only": false,
      "destructive": true,
      "requires_confirm": true,
      "supports_dry_run": true,
      "supports_idempotency_key": true
    }
  ]
}

Part III — Conformance and reference#

Part III is neither normative requirement nor optional profile: it defines how conformance to Part I is asserted and checked, and collects examples, implementation notes, and open questions. The Part II statement that nothing is required for conformance does not extend here.


18. Conformance — aoi-lint#

18.1 Per‑factor, not per‑level#

Conformance in AOI is asserted per factor and per command, not as an aggregate level. A tool is Typed for search, or Safe for delete, or not yet Auditable. There is no "AOI Level 2"; there is the set of (factor × command) pairs the tool passes.

This is intentional. A read‑only tool that lacks Idempotent is still a legitimate, useful, conforming tool — there is nothing to be idempotent about. Aggregating factors into levels would either hide that nuance or force every tool to implement requirements it doesn't need. The granularity is the truth; aoi-lint reports it.

18.2 Lint checks#

aoi-lint runs the following minimum checks against a command invocation. Each one maps to one or more factors:

#CheckFactor(s)
1schema --output json succeeds without credentials or network. Schema is valid JSON Schema. capabilities --output json succeeds if advertised.Discoverable
2Machine mode emits only valid JSONL objects to stdout. Every event has type. No ANSI/control/prose pollution.Typed
3Finite success exits 0 with terminal aoi:summary.ok=true. EOF without aoi:summary is detected as failure.Verifiable
4aoi:meta, aoi:summary, aoi:warning, aoi:error, and aoi:check events validate against the declared schema when emitted.Typed · Verifiable
5Malformed usage exits non‑zero. Every aoi:error carries category, code, message, and retryable. retry_after_ms is absent whenever retryable is false. No error uses the removed partial category.Verifiable
6Destructive commands refuse to run without confirmation. Dry‑run emits aoi:plan events and performs no side effects when advertised.Safe
7Mutations accept an --idempotency-key and replay returns the prior result without doubling the effect.Idempotent
8Mutating events carry stable IDs for created/updated/deleted resources.Auditable
9Known secret‑looking args are not echoed raw in output.Safe
10Early pipe close does not produce stack traces. SIGINT/SIGTERM produce a reason:"cancelled" summary when safe.Composable
11Commands advertising --limit report truncated and cursor behavior consistently.Bounded · Streamable
12Malformed input JSONL line handling matches declared mode.Composable
13schema_version is reported in aoi:meta and is honored when negotiated.Versioned
14A stream carries at most one aoi:meta (first if present) and at most one aoi:summary (last if present).Composable · Verifiable

| 15 | When aoi.seq is emitted it is contiguous from 0, aoi.run accompanies it, and both reconcile with aoi:summary.event_count; when absent, the Verifiable factor is not claimed. | Verifiable | | 16 | An AOI producer fed a known upstream stream forwards no upstream aoi:meta or aoi:summary, tags any forwarded aoi:error/aoi:warning with origin, and reproduces upstream retry fields in aoi:summary.caused_by when failing. | Composable |

A tool is conforming for the (factor × command) pairs it passes. It claims conformance via the Machine Mode Ready badge and the entries in its capabilities manifest. Verification is aoi-lint's job — the badge is the claim, not the proof.


19. Examples#

outline search "agent operable" --output jsonl --limit 2
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","tool_version":"1.8.2","aoi_version":"1.0-alpha","schema_name":"com.example.outline.events","schema_version":"1.0.0","command":"search"}
{"type":"hit","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_123","title":"Agent-Operable Tools","snippet":"A conforming tool exposes a stable interface..."}
{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":true,"count":1,"warning_count":0,"error_count":0,"partial":false,"truncated":false}

Consumer rule: if EOF arrives before the aoi:summary, the stream failed.

19.2 Schema discovery#

outline schema --output json --schema-version 1

Returns JSON Schema to stdout. No credentials. No network. No business query.

19.3 Doctor#

outline doctor --output jsonl
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","aoi_version":"1.0-alpha","schema_name":"com.example.outline.events","schema_version":"1.0.0","command":"doctor"}
{"type":"aoi:check","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"name":"config_file","ok":true,"severity":"info","detail":"found"}
{"type":"aoi:check","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"name":"api_token","ok":false,"severity":"error","detail":"missing"}
{"type":"aoi:summary","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"failed","event_count":4,"count":0,"error_count":0,"properties":{"checks_failed":1}}

19.4 Input JSONL error#

outline import --input-jsonl - --output jsonl --continue-on-error
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","command":"import","input_mode":"jsonl","continue_on_error":true}
{"type":"aoi:error","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"scope":"item","category":"validation","code":"INPUT_JSONL_PARSE_ERROR","line_number":17,"message":"Invalid JSON on input line 17","retryable":false}
{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"failed","event_count":32,"count":29,"error_count":1,"partial":true}

20. Implementation guidance (non‑normative)#

Python#

  • Compact JSON per line.
  • Handle BrokenPipeError/SIGPIPE cleanly.
  • Do not dump raw argparse.Namespace into aoi:meta.
  • Usage errors can exit before aoi:meta.

Node / Bun#

  • Handle EPIPE on stdout and stderr.
  • Avoid console logging in machine mode except through the event emitter.
  • Ensure async streams flush before exit when emitting terminal aoi:summary.

Go#

  • Treat broken pipe as normal termination.
  • Keep event structs versioned.
  • Avoid logging to stdout in machine mode.

Rust#

  • Handle BrokenPipe without panic output.
  • Serialize event enums with type discriminator.
  • Keep stderr diagnostics redacted.

Shell wrappers#

  • Prefer wrapping upstream JSON APIs over parsing human tables.
  • Use jq -c for emitted objects.
  • Keep stderr separate.
  • Be careful with set -o pipefail and partial output.

21. Anti‑patterns#

  • --json sometimes emits prose, warnings, or pretty tables.
  • Machine output is a giant JSON array, blocking all results until completion.
  • Schema discovery requires login, network, or a real query.
  • Schema $id is file:///opt/data/bin/... in a public spec.
  • Raw argv or env is echoed into aoi:meta.
  • Consumers treat EOF without aoi:summary as success.
  • Exit code alone is used as application semantics.
  • Every tool invents error codes with no category mapping.
  • An error carries category and retryable that contradict each other with no stated precedence.
  • Partial-run status is expressed as an error rather than on the terminal summary.
  • Destructive bulk operations run without preview or count confirmation.
  • Language‑specific stack traces appear on pipe close.
  • Wrapper names become the only versioning mechanism.

22. Adoption path#

  1. Tighten this into a public specification. Done — this document, at v1.0-alpha, served at /spec, /spec.md, and /llms-full.txt.
  2. Build aoi-lint around testable conformance checks.
  3. Provide reference implementations in Python, Node, Go, and Rust.
  4. Local *-jsonl wrappers remain a useful convenience profile, not the universal rule.
  5. Publish a small example repo with schema discovery, finite search, doctor checks, input JSONL import, cancellation/pipe tests, and redaction tests.


23. Open questions#

  • Should --output jsonl or --format jsonl be the preferred flag name?
  • Should aoi:summary be mandatory for every finite non‑zero failure once aoi:meta has emitted, or only best effort?
  • How strict should the default capabilities schema be?
  • Should schema emit all event schemas by default or require a schema name?
  • Should the protocol define a standard watch heartbeat interval hint?

Open questions concerning forthcoming modes, profiles, and adjacent‑standard relationships are tracked separately in Direction § 06.


Cite as#

Hunt, K. (2026). AOI‑CLI: Agent-Operable Interface,
command-line profile. Machine Mode v1.0‑alpha (draft).
https://machinemode.io/spec