Adopt.
Make your CLI agent-operable in an afternoon. Minimal working implementations in five languages, plus a wrapper pattern for adopting incrementally.
The conformance contract is small. Emit JSONL on stdout. Open with meta. Close with summary. Exit cleanly on broken pipes. Don't echo secrets. Don't pretend exit 0 means success when there's no terminal summary. Everything else is detail.
The minimum.
A conforming AOI-CLI tool, at minimum, emits a stream shaped like this. Three event types in order — meta first, hit (or any domain event) zero or more times, summary last — terminated by a clean exit. Everything else in the specification builds on this shape.
$ hello-aoi search "machine" --output jsonl{"type":"meta","tool":"hello-aoi","tool_version":"0.1.0","aoi_version":"0.2","schema_name":"com.example.hello.events","schema_version":"1.0.0","command":"search"}{"type":"hit","rank":1,"id":"doc_0","title":"Result 0"}{"type":"summary","ok":true,"count":1,"truncated":false}
- One JSON object per line. No pretty-printing. No arrays. No mixed prose. Newline-delimited. UTF-8.
- Every event carries a
typefield. That field is the discriminator a consumer dispatches on. - The first emission is
meta. It declares the schema name + version, the tool, the AOI version, and the command being run. - The last emission is
summary. Its absence at EOF is the cross-language crash signal. Its presence withok:trueis the only honest success signal.
Hello AOI in Python.
The reference greenfield implementation, in Python 3.10+. The same pattern translates to every other language below — compact JSON, line-buffered stdout, clean handling of BrokenPipeError on early pipe close.
#!/usr/bin/env python3
"""Minimal AOI-CLI: emits a typed JSONL search-result stream."""
import json
import sys
TOOL = "hello-aoi"
TOOL_VERSION = "0.1.0"
AOI_VERSION = "0.2"
SCHEMA_NAME = "com.example.hello.events"
SCHEMA_VERSION = "1.0.0"
def emit(event: dict) -> None:
"""One compact JSON line, flushed immediately so consumers can stream."""
sys.stdout.write(json.dumps(event, separators=(",", ":")) + "\n")
sys.stdout.flush()
def main() -> int:
emit({
"type": "meta",
"tool": TOOL,
"tool_version": TOOL_VERSION,
"aoi_version": AOI_VERSION,
"schema_name": SCHEMA_NAME,
"schema_version": SCHEMA_VERSION,
"command": "search",
})
for i in range(3):
emit({"type": "hit", "rank": i + 1, "id": f"doc_{i}", "title": f"Result {i}"})
emit({"type": "summary", "ok": True, "count": 3, "truncated": False})
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except BrokenPipeError:
# Downstream closed the pipe — exit cleanly, no stack trace.
sys.exit(141)Run it through a downstream consumer with set -o pipefail to confirm the contract holds end to end:
set -o pipefail
python3 hello_aoi.py | jq -e 'select(.type=="summary") | .ok == true'Hello AOI in TypeScript.
#!/usr/bin/env node
import { stdout, exit } from "node:process";
const TOOL = "hello-aoi";
const TOOL_VERSION = "0.1.0";
const AOI_VERSION = "0.2";
const SCHEMA_NAME = "com.example.hello.events";
const SCHEMA_VERSION = "1.0.0";
function emit(event: Record<string, unknown>): void {
stdout.write(JSON.stringify(event) + "\n");
}
// Treat downstream pipe-close as ordinary termination, no stack trace.
stdout.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EPIPE") exit(141);
throw err;
});
emit({
type: "meta",
tool: TOOL,
tool_version: TOOL_VERSION,
aoi_version: AOI_VERSION,
schema_name: SCHEMA_NAME,
schema_version: SCHEMA_VERSION,
command: "search",
});
for (let i = 0; i < 3; i++) {
emit({ type: "hit", rank: i + 1, id: `doc_${i}`, title: `Result ${i}` });
}
emit({ type: "summary", ok: true, count: 3, truncated: false });Hello AOI in Go.
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
)
const (
tool = "hello-aoi"
toolVersion = "0.1.0"
aoiVersion = "0.2"
schemaName = "com.example.hello.events"
schemaVersion = "1.0.0"
)
func emit(event map[string]any) {
line, _ := json.Marshal(event)
if _, err := fmt.Fprintln(os.Stdout, string(line)); err != nil {
// Treat broken pipe as normal termination.
if errors.Is(err, io.ErrClosedPipe) || errors.Is(err, os.ErrClosed) {
os.Exit(141)
}
os.Exit(74) // EX_IOERR
}
}
func main() {
emit(map[string]any{
"type": "meta",
"tool": tool,
"tool_version": toolVersion,
"aoi_version": aoiVersion,
"schema_name": schemaName,
"schema_version": schemaVersion,
"command": "search",
})
for i := 0; i < 3; i++ {
emit(map[string]any{
"type": "hit",
"rank": i + 1,
"id": fmt.Sprintf("doc_%d", i),
"title": fmt.Sprintf("Result %d", i),
})
}
emit(map[string]any{
"type": "summary",
"ok": true,
"count": 3,
"truncated": false,
})
}Hello AOI in Rust.
use serde_json::json;
use std::io::{self, Write};
use std::process::ExitCode;
const TOOL: &str = "hello-aoi";
const TOOL_VERSION: &str = "0.1.0";
const AOI_VERSION: &str = "0.2";
const SCHEMA_NAME: &str = "com.example.hello.events";
const SCHEMA_VERSION: &str = "1.0.0";
fn emit(event: serde_json::Value) -> io::Result<()> {
let line = event.to_string();
let mut out = io::stdout().lock();
writeln!(out, "{}", line)
}
fn run() -> io::Result<()> {
emit(json!({
"type": "meta",
"tool": TOOL,
"tool_version": TOOL_VERSION,
"aoi_version": AOI_VERSION,
"schema_name": SCHEMA_NAME,
"schema_version": SCHEMA_VERSION,
"command": "search",
}))?;
for i in 0..3 {
emit(json!({
"type": "hit",
"rank": i + 1,
"id": format!("doc_{}", i),
"title": format!("Result {}", i),
}))?;
}
emit(json!({
"type": "summary",
"ok": true,
"count": 3,
"truncated": false,
}))
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
// BrokenPipe — exit cleanly, no panic.
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => ExitCode::from(141),
Err(_) => ExitCode::from(74), // EX_IOERR
}
}Hello AOI in shell.
Shell is the lowest-friction way to convert a JSON-emitting upstream into an AOI tool. The trick is using jq -c -n to produce compact one-line objects, and keeping all human prose on stderr.
#!/usr/bin/env bash
set -euo pipefail
TOOL="hello-aoi"
TOOL_VERSION="0.1.0"
AOI_VERSION="0.2"
SCHEMA_NAME="com.example.hello.events"
SCHEMA_VERSION="1.0.0"
emit() {
jq -c -n "$1"
}
emit '{
"type": "meta",
"tool": "'"$TOOL"'",
"tool_version": "'"$TOOL_VERSION"'",
"aoi_version": "'"$AOI_VERSION"'",
"schema_name": "'"$SCHEMA_NAME"'",
"schema_version": "'"$SCHEMA_VERSION"'",
"command": "search"
}'
for i in 0 1 2; do
emit "{
\"type\": \"hit\",
\"rank\": $((i+1)),
\"id\": \"doc_$i\",
\"title\": \"Result $i\"
}"
done
emit '{"type":"summary","ok":true,"count":3,"truncated":false}'Adapt an existing CLI.
Rewriting a mature CLI to be AOI-native is rarely the right first step. The faster path is to ship a --output jsonl mode that wraps the existing implementation — either inside the tool, or as a sibling shell script.
Below: a shell wrapper that converts ls -la (decidedly not AOI) into a conforming stream. The same pattern works for any line-oriented or JSON-emitting upstream.
#!/usr/bin/env bash
# Wraps `ls -la` to emit AOI-CLI events instead of a human-readable table.
set -euo pipefail
emit() { jq -c -n "$1"; }
emit '{"type":"meta","tool":"ls-aoi","tool_version":"0.1.0","aoi_version":"0.2","schema_name":"com.example.ls.events","schema_version":"1.0.0","command":"list"}'
count=0
# Skip the "total" line from `ls -la`, parse each remaining line.
ls -la "$@" | tail -n +2 | while IFS= read -r line; do
# Naive parse — production would use `stat` for typed fields.
perms=$(echo "$line" | awk '{print $1}')
size=$(echo "$line" | awk '{print $5}')
name=$(echo "$line" | awk '{print $9}')
emit "{\"type\":\"entry\",\"name\":\"$name\",\"perms\":\"$perms\",\"size\":$size}"
count=$((count+1))
done
emit "{\"type\":\"summary\",\"ok\":true,\"count\":$count,\"truncated\":false}"The same pattern, generalized, is what the eventual machinemode uber-wrapper provides for common tools — one wrapper module per non-conforming CLI, all sharing the same AOI event vocabulary.
Test your conformance.
Until aoi-lint ships, the cheapest conformance test is a shell pipeline that asserts every line is valid JSON and that a terminal summary arrives with ok:true. Run this against your CLI:
# Replace `your-tool ...` with your actual command.
set -o pipefail
your-tool --output jsonl \
| tee /tmp/aoi-test.jsonl \
| jq -c '.' >/dev/null \
&& jq -e 'select(.type=="summary") | .ok == true' /tmp/aoi-test.jsonl \
&& echo "ok: conforming stream" \
|| echo "fail: invalid JSON or missing summary"The full conformance lint will check more than this — schema validity, signal handling, redaction, dry-run behavior — but the three checks above (valid JSONL · terminal summary · summary.ok) catch the vast majority of broken implementations.
For the full normative checklist, see §18 of the AOI‑CLI specification.
Declare conformance.
Tools that pass the conformance checklist may display the Machine Mode Ready badge. The badge is self-declared, not certified — there is no central authority, and there will not be one. The credibility of the claim lives with the tool's reputation and the consumer's ability to verify it (via the checklist or, in time, aoi-lint).
What's next.
- SDKs
- Per-language packages with the boilerplate above factored out: event emitters, schema helpers, redaction utilities, pipe-safe stdout, terminal-summary contracts. Initial packages: TypeScript, Python, Go, Rust. Repos under github.com/agentoperable.
- aoi-lint
- The conformance test as a real tool. Will run the §18 checklist against your CLI invocation and report structured pass/fail events — itself an AOI-CLI tool.
- machinemode (uber-wrapper)
machinemode <subcommand> <tool> ...args— invokes a non-AOI tool and wraps its output as AOI events. Per-tool adapter modules;machinemode curl,machinemode rg,machinemode find,machinemode git logas the seed set.- Registry
- A list of conforming tools so consumers can discover them. Self-submission via PR to a registry repo; the badge links back here.