Ouro AX checks the interface an agent uses from outside, the way an agent calls it (Ouro AX). To make your own CLI conform, copy in the kit, move your commands onto its declarations one at a time, and run the X-ray until it says the CLI conforms; then keep it that way in CI. Try the whole idea first in your browser: the Ouro AX page lets you be the agent, and shows the X-ray's report on a real app before and after it adopted the kit.
1. Copy in the kit
npx shadcn@latest add ourostack/ouro-ui/ouro-ax#v1.3.10
npx shadcn@latest add ourostack/ouro-ui/ouro-ax-tests#v1.3.10The files land in lib/ouro/ax/, as TypeScript that Node runs as it is.node Keep them as they are: when you need a change, make it in Ouro UI and copy the next tag, so your copy and the X-ray never drift apart.
2. Run the X-ray on your CLI as it is
node lib/ouro/ax/xray.ts --cli "node bin/tool.mjs" --home .xray-home --env "TOOL_URL=$MOCK_URL" --guard "node scripts/assert-mock.mjs" --md xray-before.mdStart before you change anything. The report lists each claim, the surfaces it holds on, and for each failure the call it made, what came back and the fix.effects Keep this first report: it is your "before". Point the CLI at a mock with made-up data on a loopback address, pass that address with --env so the X-ray can check it, and give the X-ray a guard that proves it is a mock; without that proof it skips every call other people would see.safety
3. Declare one command at a time
Write a defineCommand for one command: its name, a one-line summary, who sees its effect (only the person, or other people), its arguments and an example. Declare an existing command loose while its flags still pass through unchecked, and declare them next.lint Run lintCli(cli, { inventory }) with the names your dispatcher registers, and it lists the commands that still run outside the kit.lint The guide's tool is a whole CLI in one file, with a read and an action other people see.
What an agent gets from a declaration, in the guide's tool:tool
$ tasks assign t1 Ana
tasks: assign: other people will see this as the person's, so it needs --why: who asked you, and what they said. Fix: tasks assign t1 Ana --why "<who asked you, and what they said>".
$ tasks assign t1 Ana --why "Jo asked" --dry-run
Would give "Book the room" to Ana; the team will see it.4. Generate the inventory, and ask for conformance
node scripts/inventory.mjs > inventory.json # from your command table, never by hand
node lib/ouro/ax/xray.ts --cli "node bin/tool.mjs" --inventory inventory.json --conformance --home .xray-home --env "TOOL_URL=$MOCK_URL" --guard "node scripts/assert-mock.mjs" --json --md xray.md > xray.jsonA report proves only the surfaces it covered, so build the inventory from your CLI's own registration.inventory With --conformance, the X-ray exits 0 only when your CLI conforms.conformance Read xray.md for what still fails, fix the first must, and run it again.
5. Gate CI on it, and keep the report
Add the X-ray to CI against your mock, with --commit so the report names what it checked, and upload xray.json, xray.md and inventory.json as an artifact (the X-ray in CI). The report pins the claim set's version and hash, the X-ray's version and your commit.pin When you review a change, compare the failing checks of the base and the change, not their totals: a new command adds checks, so the totals move anyway.
For a CLI without an HTTP API, run step 4's command in CI with --commit and a home under $RUNNER_TEMP:
- name: Ouro AX X-ray
run: |
node scripts/inventory.mjs > inventory.json
node lib/ouro/ax/xray.ts --cli "node bin/tool.mjs" --inventory inventory.json --conformance \
--home "$RUNNER_TEMP/xray-home" --env "TOOL_URL=$MOCK_URL" --guard "node scripts/assert-mock.mjs" --commit "$GITHUB_SHA" --json --md xray.md > xray.json6. Give it a voice, if you want one
voice: {
banner: ["Rooms to talk in, notes to keep."],
empty: ["Nothing yet. Rooms fill up."],
},A voice is optional: a line of character after the facts, for a person at a terminal (a voice). The kit keeps it out of --json, pipes, --quiet, CI, stderr and errors, and checks each line when the tool is built.voice Write it in your product's own voice: call the agent "you" and the human "the person". Don't make the tool say "I", and don't give a brand character lines.
7. Check the tasks, not just the rules
Write each task an agent does with your CLI as a flow, the calls a good agent makes and what each must show, and run the flows in CI beside the X-ray (agent flows in CI). A command can keep every rule and still leave an agent stuck halfway through a task.flows
8. Watch real agents use it
Give fresh agents real tasks against your mock, with only your CLI allowed, and score each run from the end state (the evaluation). Admit a new must claim only when a run shows agents failing without it.
Today's gotchas
- **Text on stdin can't get through an allow-list.** An agent allowed only your CLI can't run
printf ... | tool, because the pipe's first command isn't your tool. Take text as--textor--file.text - **A fresh agent waits forever before its first call.** Pass
--no-config-pluginswith--allow-toolwhen you run an agent in prompt mode. With an allow-list, the agent CLI stops approving every tool, so the host's plugin extensions are denied and the run waits for an approval nobody can give until it times out.hang Give each run minutes, not seconds, on a busy machine. - **
nulland missing are different answers.** Print "nothing yet" for a field that isnulland "not known" for one that is absent, and test both. - **Unlisted means unfound.** Put every command in the help overview, and group commands on one line when the overview gets long, rather than leaving one out.v13
- **Real names leak.** Use made-up people and role names (
inbox-agent) in examples and fixtures, never the name of a real product or person.
Claim sources
- node: test:scripts/ax.test.mjs::the Ouro AX tests pass and cover every line, branch and function of Ouro AX
verify:testExists(scripts/ax.test.mjs::the Ouro AX tests pass and cover every line, branch and function of Ouro AX) - effects: test:registry/ouro/ax/xray.test.mts::a CLI that breaks the claims fails them, each with what it saw
verify:testExists(registry/ouro/ax/xray.test.mts::a CLI that breaks the claims fails them, each with what it saw) - safety: test:registry/ouro/ax/xray.test.mts::checkSafety fails closed: a remote address, the real home, a credential, a wrong or missing mock; test:registry/ouro/ax/xray.test.mts::without proof of a mock, nothing other people see is called, and it does not conform
verify:testExists(registry/ouro/ax/xray.test.mts::checkSafety fails closed: a remote address, the real home, a credential, a wrong or missing mock)verify:testExists(registry/ouro/ax/xray.test.mts::without proof of a mock, nothing other people see is called, and it does not conform) - lint: test:registry/ouro/ax/lint.test.mts::against the tool's own inventory: a registered command with no declaration, and a declaration with no command
verify:testExists(registry/ouro/ax/lint.test.mts::against the tool's own inventory: a registered command with no declaration, and a declaration with no command) - tool: test:scripts/ax.test.mjs::the guide shows the tasks tool as it is, and the answers it gives; test:registry/ouro/ax/xray.test.mts::the guide's tool conforms
verify:testExists(scripts/ax.test.mjs::the guide shows the tasks tool as it is, and the answers it gives)verify:testExists(registry/ouro/ax/xray.test.mts::the guide's tool conforms) - inventory: test:registry/ouro/ax/xray.test.mts::compareInventory names what is missing, unknown or listed twice
verify:testExists(registry/ouro/ax/xray.test.mts::compareInventory names what is missing, unknown or listed twice) - conformance: test:registry/ouro/ax/xray.test.mts::the kit's example tool conforms, and the X-ray acts on nothing; test:registry/ouro/ax/xray.test.mts::the command: claims, help, usage errors with the fix, exit codes, written reports
verify:testExists(registry/ouro/ax/xray.test.mts::the kit's example tool conforms, and the X-ray acts on nothing)verify:testExists(registry/ouro/ax/xray.test.mts::the command: claims, help, usage errors with the fix, exit codes, written reports) - pin: test:registry/ouro/ax/xray.test.mts::the kit's example tool conforms, and the X-ray acts on nothing
verify:testExists(registry/ouro/ax/xray.test.mts::the kit's example tool conforms, and the X-ray acts on nothing) - voice: test:registry/ouro/ax/cli.test.mts::the voice speaks only on a terminal: never under --json, --quiet, CI or a pipe, and never in an error; test:registry/ouro/ax/voice.test.mts::a line is one line of plain ASCII, at most 72 characters, checked when the tool is built
verify:testExists(registry/ouro/ax/cli.test.mts::the voice speaks only on a terminal: never under --json, --quiet, CI or a pipe, and never in an error)verify:testExists(registry/ouro/ax/voice.test.mts::a line is one line of plain ASCII, at most 72 characters, checked when the tool is built) - flows: evidence:docs/decisions/2026-10-30-ouro-ax/evidence/agent-flow-review.md
verify:fileLine(docs/decisions/2026-10-30-ouro-ax/evidence/agent-flow-review.md, /593 checks, 30 failing/) - text: evidence:docs/decisions/2026-10-30-ouro-ax/evidence/eval-baseline.md; evidence:docs/decisions/2026-10-30-ouro-ax/evidence/eval-after-440.md
verify:fileLine(docs/decisions/2026-10-30-ouro-ax/evidence/eval-baseline.md, /\| T4 Recovery \(ambiguous verb: edit\) \| 0\/2 \|/)verify:fileLine(docs/decisions/2026-10-30-ouro-ax/evidence/eval-after-440.md, /\| T4 Recovery \(ambiguous verb: edit\) \| 2\/2 \|/) - hang: evidence:docs/decisions/2026-10-30-ouro-ax/evidence/eval-method.md
verify:fileLine(docs/decisions/2026-10-30-ouro-ax/evidence/eval-method.md, /every prompt-mode run with .--allow-tool. hung after startup/) - v13: test:registry/ouro/ax/xray.test.mts::a voice in a pipe or --json, text without its refs, an unlisted command, long help, an unnamed audience, a closed pipe
verify:testExists(registry/ouro/ax/xray.test.mts::a voice in a pipe or --json, text without its refs, an unlisted command, long help, an unnamed audience, a closed pipe)
