Agent-First Data v0.25: One Spelling Per Concept

by Agent-First Kit Contributors

v0.24 made help a normal result. v0.25 is the follow-through: every remaining second spelling is gone. `--json` was `--output json` typed differently — it bought an agent nothing and cost applications a flag name, hijacking `--json <FILE>` and misreading its value as a subcommand. The help model also stopped saying things that weren't true: defaults an author had hidden, empty strings the project's own validator rejected, and a positional that contradicted its own usage line. Plus `redact_argv`, so a CLI can record its own invocation without writing a credential to its log.

The last release made --help answer in the CLI’s own output format, so an agent discovering an unfamiliar tool parses its surface instead of scraping columns. That change had a corollary we only half-applied: if help is a normal result, then every other convenience spelling around it is redundant too.

v0.25 finishes the job, and fixes four places where the help model said things that were not true.

--json had to go

AFDATA recognized --json as a shorthand for --output json. Two spellings, one meaning. An agent gains nothing from the shorter one — it does not count characters — and the cost turned out to be worse than redundancy.

Consider a CLI that legitimately wants --json <FILE> as an input:

mytool --json config.json build

The pre-parser claimed --json before the application ever saw it. It selected JSON output, and then — because it did not know the flag carried a value — read config.json as the subcommand. One flag name, silently stolen and misparsed.

So --output is now the only format selector AFDATA recognizes. --json belongs to the application. This is the same reasoning that retired -h and -V in v0.24: a byte-identical alias is not a convenience, it is a name taken out of the application’s namespace for nothing.

That principle now stops at the library’s own boundary, which is where it belongs. AFDATA’s handlers claim no short flags — that is what keeps -h free — but what an application does with -h afterward is its own decision. A tool imitating an established interface may owe its users -h for --host, and the convention has no business forbidding it.

The model must not lie

Structured help is only useful if it is exactly true. Four ways it wasn’t:

A hidden default was disclosed. An author who calls hide_default_value has said, explicitly, do not show this. Plain help honored it. JSON help printed the value anyway — including for a --password whose default the author had deliberately hidden.

Empty strings were emitted. about("") produced "about": "". The schema sets minLength: 1 on every optional string, so the crate could emit help that its own gate validator rejected. That is the most embarrassing kind of bug: the specification and the implementation disagreed, and only the specification was being checked.

A positional contradicted its own usage. An argument taking two values rendered usage: <KEY> <VALUE> while arguments listed only KEY. The second placeholder vanished. Such a positional now lists every placeholder, and the validator requires the first to match the argument’s name.

Usage carried terminal wrapping. One SDK built its usage line by scraping a parser’s rendered output, which wraps at terminal width and pads continuations — so a single-line schema field shipped embedded newlines and column alignment. Now the schema forbids newlines, the validator enforces it, and a fixture reproduces the exact wrapped text so the gate can prove it catches it.

The pattern in all four: the rule existed in prose, and nothing checked it. Each is now a schema constraint plus a validator case.

A CLI that logs its own invocation

New in all four SDKs: redact_argv (RedactArgv, redactArgv).

Any tool that records how it was called — startup diagnostics, an audit trail, a crash report — will write a credential into that record unless something stops it:

mytool --api-key-secret sk-live-xxxx --domain example.com
{"argv": ["--api-key-secret", "***", "--domain", "example.com"]}

It redacts by flag name, following the same _secret rule as every other AFDATA surface, and it deliberately never scans free text: a bare api_key_secret=sk-live positional, or a secret-looking token after an unrelated flag, is left alone. AFDATA decides sensitivity from names, not from guessing at value shapes — rename the flag instead.

The behavior was already specified in spec/fixtures/redact_argv.json, complete with custom secret_names and an off switch. Nothing had read that file since the function was removed in 0.16, and in the meantime three of the four example CLIs had each hand-rolled the same helper — two of them incorrectly, logging the secret verbatim. The fixture is now the cross-language contract it was written to be, and all four SDKs run it.

Named after your own subcommand

A smaller fix worth naming, because it only bites tools with a particular shape.

The help pre-parser accepted arguments with or without argv[0], and told the two cases apart by checking whether the first entry matched a known subcommand. A binary named after one of its own subcommands — tool with a tool subcommand — therefore parsed its own program path as that subcommand and rendered the wrong help, reporting command_path: "tool tool".

Only the safe half of that leniency remains: argv[0] can never begin with -, so a leading dash still means the vector is already stripped. Version and help now share one rule instead of disagreeing about it.

What we shipped by accident

v0.24.0’s published crate is 2.4 MB. It should be about 420 KB.

The difference is a 3.5 MB macOS binary that go build had left beside its source in an example directory, plus some Python bytecode. A release stages its spore with a blanket git add, and a Rust crate without include/exclude packages every git-tracked file under its root — so an artifact that nobody noticed became a permanent part of a published version. crates.io is immutable; that tarball stays exactly as it is.

The guard now runs immediately before staging, for every spore, and it detects artifacts by content — a NUL byte in the first block, the same heuristic git diff uses — rather than by a list of names the next unfamiliar toolchain would walk straight past. It refuses while refusing is still free.

v0.25.0 is the first clean tarball since.

Where it lands

None of this is new capability, mostly. It is the same surface, saying fewer things and meaning them more precisely: one spelling per concept, a model that matches its own schema, and secrets that stay redacted on the one path that had been quietly leaking them.