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

- Document: **AOI‑CLI v1.0‑alpha**
- Standard: **Machine Mode**
- Status: Alpha. Breaking change from v0.2 (see § 5.2).
- Date: 2026‑07‑30
- Editor: Kevin Hunt
- Related: [Machine Mode foundations](/) · [Why a standard](/why) · [In practice](/examples)

---

## 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:

| Role | Reads JSONL on stdin? | Emits JSONL on stdout? | Example |
|---|:---:|:---:|---|
| **Source** | no | yes | `outline search …` produces hits |
| **Transformer** | yes | yes | `summarize --input-jsonl -` reads hits, emits abstracts |
| **Sink** | yes | yes (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](/direction). 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:

- POSIX utility conventions for process, arguments, stdin/stdout/stderr, and exit status: <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html>
- NDJSON / JSON Lines for newline‑delimited JSON streams: <https://github.com/ndjson/ndjson-spec> and <https://jsonlines.org/>
- JSON Schema 2020‑12 for event and input schemas: <https://json-schema.org/draft/2020-12/json-schema-core>
- Modern CLI UX guidance: <https://clig.dev/>
- sysexits‑style exit categories where available: `sysexits.h`

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:

```bash
outline --output jsonl ...
```

or equivalently:

```bash
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:

```json
{
  "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:

```json
{"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:

```bash
outline schema --output json
outline capabilities --output json
```

Equivalent flags **MAY** exist:

```bash
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.

```json
{
  "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:

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

or through content negotiation:

```bash
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.

| Change | v0.2 | v1.0-alpha | Migration |
|---|---|---|---|
| Framework envelope | flat, top level | nested under `aoi` | Read `aoi.seq` / `aoi.run`; two top-level names are reserved, `type` and `aoi`. |
| `category:"temporary"` | present | **renamed** `unavailable` | A consumer branching on `temporary` stops matching silently. Update the branch. |
| `category:"partial"` | present | **removed** | Emit the item's real category with `scope:"item"`; express partiality on `aoi:summary.partial`. |
| `retryable` | optional | **required**, authoritative | Emit it on every `aoi:error`. |
| `retry_after_ms` | undefined | defined | Replaces prose "include retry hint if known". MUST NOT appear with `retryable:false`. |
| `aoi:error.upstream` | `{protocol,status}` | **renamed** `wrapped` | `upstream` now means provenance on `aoi:summary`, which has a different shape. |
| Trailing newline | unstated | **MUST** | A final line without a terminator is truncated, not complete. |
| `reason` | undefined | **required** when `ok:false` | Closed 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:

| Code | Constant | Meaning |
|---:|---|---|
| 64 | `EX_USAGE` | usage error |
| 65 | `EX_DATAERR` | input or data validation error |
| 69 | `EX_UNAVAILABLE` | service or dependency unavailable |
| 70 | `EX_SOFTWARE` | internal software error |
| 73 | `EX_CANTCREAT` | cannot create output |
| 74 | `EX_IOERR` | I/O error |
| 75 | `EX_TEMPFAIL` | temporary failure, retry may succeed |
| 77 | `EX_NOPERM` | permission or authz failure |
| 78 | `EX_CONFIG` | configuration 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:

```json
{"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 also carries its own spec-defined fields at the top level (see *Envelope fields* below).

```json
{"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

| Field | Status | Meaning |
|---|---|---|
| `type` | **MUST** | Event discriminator. Top level, never inside `aoi`. |
| `aoi.seq` | SHOULD; **MUST** to claim Verifiable | Monotonic counter from 0, no gaps, one per emitted line. |
| `aoi.run` | SHOULD; **MUST** alongside `seq` | Opaque identifier stable for this invocation. ULID **RECOMMENDED**. |
| `aoi.t` | MAY | Milliseconds 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_id` | MAY | Correlates 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:

```json
{"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.

| Layer | Convention | Examples |
|---|---|---|
| **Framework events** (defined by this spec) | `aoi:` prefix | `aoi: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) | unprefixed | `entry`, `hit`, `match` |
| **Tool‑specific domain events** | unprefixed; scoped by `aoi:meta.schema_name` | whatever 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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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:

| Field | Counts |
|---|---|
| `count` | **domain** events this tool emitted. Never framework events. |
| `input_count` | **domain** events this tool consumed from stdin. Never framework events. A transformer with `count == input_count` dropped nothing. |
| `warning_count` | `aoi:warning` events this tool emitted **plus** those it consumed from upstream. |
| `error_count` | `aoi:error` events this tool emitted **plus** those it consumed from upstream. |
| `partial` | true when the run produced usable output *and* at least one item failed. |
| `event_count` | **Recommended; 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. |
| `reason` | **required 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, as distinct from `count:0`, which means it ran and found nothing. |
| `caused_by` | present when `ok` is false due to an upstream error (§ 13.1). |
| `upstream` | the 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.

```json
{
  "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

| Field | Status | Type | Semantics |
|---|---|---|---|
| `type` | MUST | `"aoi:error"` | — |
| `category` | MUST | closed enum (§ 9.2) | Semantic class. A consumer that meets an unrecognized value **MUST NOT** fail (§ 9.4). |
| `code` | MUST | `[A-Z][A-Z0-9_]*` | Stable, tool‑defined identifier. **MUST** remain stable once published. |
| `message` | MUST | string, single line | Human‑readable. No ANSI, no stack trace, no secrets. Not machine‑parsed. |
| `retryable` | MUST | boolean | **Authoritative** retry disposition for this occurrence (§ 9.3). |
| `retry_after_ms` | MUST when known and `retryable:true`; **MUST NOT** appear when `retryable:false` | integer ≥ 0 | Minimum delay before retry, in milliseconds, relative to receipt. |
| `domain` | SHOULD | string | Namespace for `code`. Defaults to `aoi:meta.tool`. |
| `origin` | MUST when forwarded | string | Names 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. |
| `scope` | SHOULD | `process` \| `item` (default `process`) | `process` means the run is over; `item` means the stream continues. |
| `target` | SHOULD | string | The resource the error concerns: a path, URI, or id. |
| `item_index` / `line_number` | SHOULD when `scope:"item"` | integer | Correlation back to the input record. |
| `field` | MAY | string | Input path that failed, for `validation`. |
| `wrapped` | MAY | `{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 with two meanings is the collision this spec exists to prevent. |
| `hint` | MAY | string | Informational. Consumers **MUST NOT** execute it (§ 1.3). |
| `docs` | MAY | URL | Documentation for this condition. |
| `details` | MAY | object | Extension 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.

| Category | Default `retryable` | Meaning |
|---|---:|---|
| `usage` | false | Invalid CLI usage or missing required operand. |
| `validation` | false | Input parsed but failed schema/domain validation. |
| `config` | false | Missing or invalid configuration. |
| `authn` | false | Missing or expired credentials. Waiting does not refresh a token. |
| `authz` | false | Credentials valid but action not allowed. |
| `not_found` | false | Target resource does not exist. |
| `conflict` | false | Concurrent modification, version/ETag mismatch, or destination exists. Retry requires re‑reading state first, so the *identical* invocation is not retryable. |
| `precondition_failed` | false | System state must be explicitly fixed before this can succeed (e.g. `rmdir` on a non‑empty directory). |
| `unsupported` | false | The invocation is well‑formed but the capability does not exist. The version‑skew signal between an agent and a tool. |
| `rate_limited` | **true** | Time‑based throttling or quota. **SHOULD** carry `retry_after_ms`. |
| `unavailable` | **true** | Transient dependency, network, or service failure. |
| `timeout` | **true** | Operation exceeded a deadline. |
| `cancelled` | false | Interrupted by caller or system. Retry is the caller's decision. |
| `io` | false | Filesystem or stdio failure. Includes `BROKEN_PIPE`. Transient I/O (`EAGAIN`, `EINTR`) **MAY** override to true. |
| `internal` | false | Tool 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.

| gRPC | HTTP | AOI category | `retryable` |
|---|---:|---|---|
| `INVALID_ARGUMENT` | 400, 422 | `validation` | false |
| `UNAUTHENTICATED` | 401 | `authn` | false |
| `PERMISSION_DENIED` | 403 | `authz` | false |
| `NOT_FOUND` | 404, 410 | `not_found` | false |
| `UNIMPLEMENTED` | 405, 501 | `unsupported` | false |
| `DEADLINE_EXCEEDED` | 408, 504 | `timeout` | **true** |
| `ABORTED`, `ALREADY_EXISTS` | 409 | `conflict` | false |
| `FAILED_PRECONDITION` | 412, 428 | `precondition_failed` | false |
| `RESOURCE_EXHAUSTED` | 429 | `rate_limited` | **true** |
| `UNAVAILABLE` | 502, 503 | `unavailable` | **true** |
| `INTERNAL`, `UNKNOWN`, `DATA_LOSS` | 500 | `internal` | false |
| `CANCELLED` | 499 | `cancelled` | false |

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:

```bash
outline schema --output json
```

It **MAY** support:

```bash
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:

```json
{
  "$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:

```json
{
  "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:

```bash
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**, meaning 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:

```json
{"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:

```json
{
  "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](/direction).

### 13.2 Pipefail

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

```bash
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) as § 18.1 describes, 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:

```bash
--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:

```bash
--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.

```json
{"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

| Option | Status | Meaning |
|---|---:|---|
| `--help`, `-h` | MUST | Human‑readable help. |
| `--version` | SHOULD | Tool version. |
| `--output jsonl` / `--format jsonl` | MUST | Enter AOI JSONL mode unless the tool is AOI‑only. |
| `--schema-version VERSION` | SHOULD | Select supported schema version where applicable. |
| `--timeout SECONDS` | SHOULD | Bound external calls. |
| `--limit N` | SHOULD | Bound result count. |
| `--cursor TOKEN` | SHOULD | Continue from prior cursor. |
| `--dry-run` | SHOULD | Plan without side effects. |
| `--idempotency-key KEY` | SHOULD | Safe retry key. |
| `--confirm` | MUST¹ | Required for destructive operations. |
| `--confirm-count N` | SHOULD | Guard expected affected count. |
| `--no-color` | SHOULD | Disable color. Machine mode never uses color. |
| `--debug` | MAY | Extra 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

| Subcommand | Meaning |
|---|---|
| `schema` | Emit event schema JSON. Credential‑free and network‑free. |
| `input-schema` | Emit input schema JSON for a command or input mode. |
| `capabilities` | Emit supported commands, schema versions, event types, input modes, auth requirements. |
| `doctor` | Check credentials, dependencies, connectivity. Emits AOI events in machine mode. |

### 16.3 Resource subcommands

Use boring verbs:

| Subcommand | Meaning |
|---|---|
| `list` | Bounded collection. |
| `get` | One resource by stable key. |
| `search` | Ranked or filtered results. |
| `create` | Create resource. |
| `update` | Update resource. |
| `delete` | Delete resource. Confirmation required. |
| `move` | Move or rename resource. |
| `copy` | Copy resource. |
| `send` | Send message, email, notification. |
| `sync` | Reconcile state. Must state direction and planned changes. |
| `watch` | Unbounded stream. Must document completion semantics. |
| `status` | Current 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 value 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.

```json
{
  "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:

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

### 19.1 Finite search

```bash
outline search "agent operable" --output jsonl --limit 2
```

```jsonl
{"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

```bash
outline schema --output json --schema-version 1
```

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

### 19.3 Doctor

```bash
outline doctor --output jsonl
```

```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

```bash
outline import --input-jsonl - --output jsonl --continue-on-error
```

The 29 audit events for the imported records (seq 2 to 30) are omitted below. Tool-specific fields on `aoi:meta` go in `properties` (§ 8.1).

```jsonl
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","command":"import","properties":{"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":31,"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 specification define a standard `watch` heartbeat interval hint?

Open questions concerning forthcoming modes, profiles, and adjacent‑standard relationships are tracked separately in [Direction § 08](/direction#08).

---

## Cite as

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