Agent-First Slug v0.5: Discovery Without Guessing

by Agent-First Kit Contributors

An agent meeting a CLI for the first time does the same three things: asks what it can do, asks what version it is, and runs something. afslug answered the first two in prose an agent had to parse and the third by mixing results and errors into one stream. v0.5 makes all three structured. `afslug --help` returns a scoped result — this command level only, globals marked global, `--recursive` for the whole tree. `afslug --version` returns a result, not a sentence. And `--output-to` decides whether errors land on stderr beside the results or join them in one ordered stream.

An agent meeting a CLI for the first time does the same three things in the same order: asks what the tool can do, asks what version it is, and then runs something and reads what came back.

afslug used to answer the first two in prose. --help printed clap’s usage block — indentation, column alignment, [default: -] in brackets — and the agent parsed English to find out that --charset takes three values. --version printed a line. Both are fine for a person at a terminal and both are a parsing task for everything else.

v0.5 makes all three structured.

Help is a result

$ afslug --help
{"kind":"result","result":{"code":"help","help":{
  "command_path":"afslug","scope":"one_level",
  "arguments":[{"name":"--output","value":"OUTPUT","default":"json","global":true,
    "help":"Output format: json, yaml, or plain"}, ...],
  "subcommands":[{"name":"slugify","about":"Generate a slug from input text"}, ...]
}},"trace":{}}

The shape comes from afdata v0.24’s help protocol, so it reads the same across every agent-first CLI. Three properties matter more than the schema:

Help is scoped. scope: "one_level" means you got this command and the names of its children, not the entire tree. Ask for what you need next: afslug slugify --help returns slugify’s flags, and lists afslug under inherited_arguments_from instead of repeating --output in every subcommand. Discovery is a walk, not a dump.

--recursive is the index. When you do want the whole surface at once — writing a wrapper, generating docs — afslug --help --recursive expands every nested subcommand in one compact payload. The tests hold it to a token budget proportional to the number of commands and arguments, so the full tree stays cheap enough to read in a single turn.

Humans still have a format. --output plain returns clap’s rendering unchanged; --output markdown is what generates docs/cli.md. The format is a request, not a guess about who’s asking.

The help pseudo-subcommand is gone. afslug help slugify was a second spelling for afslug slugify --help, and clap advertised it in every command listing as though it were part of the tool. And running afslug with no command no longer dumps the whole usage block at you — it returns a short structured error with a hint:

$ afslug
{"error":{"code":"cli_error","hint":"try: afslug --help",
  "message":"a command is required","retryable":false},"kind":"error","trace":{}}

An error should tell you what to do next, not spend a thousand tokens guessing what you meant.

One spelling for version

afslug --version now always returns the structured protocol result — name, display name, version, and the git SHA the binary was built from. There is no human variant and no -V shorthand:

$ afslug --version
{"kind":"result","result":{"build":"0f7dbe74","code":"version",
  "display_name":"Agent-First Slug","name":"afslug","version":"0.5.0"},"trace":{}}

v0.4 kept -V and a plain-text line for people. Two spellings of one concept is a decision every caller has to make and every wrapper has to handle. One spelling, one shape.

Where errors go

The last piece is which stream carries what. --output-to is global and defaults to split:

# split (default): results on stdout, errors on stderr
slug=$(afslug slugify "Hello, World!" --output plain) || echo "failed"

# stdout: one ordered stream of events, results and errors interleaved
afslug slugify "!!!" --validation url-path --output-to stdout

Under split, $(...) captures only successful output — a failed slugify leaves the variable empty instead of filling it with a JSON error object that the next command treats as a slug. Under stdout, you get a single ordered event stream, which is what you want when you’re piping everything into one reader. stderr sends both the other way for logging.

Install

cargo add agent-first-slug --no-default-features   # the library, no CLI deps
cargo install agent-first-slug                     # the afslug CLI
brew install agentfirstkit/tap/afslug              # prebuilt binaries

v0.5 also ships the release workflow that builds those binaries for macOS (Apple silicon and Intel), Linux, and Windows, so brew and scoop install afslug without a Rust toolchain.

The library API is unchanged. Everything here is about the ten seconds before an agent calls it for the first time.