Agent-First Data v0.25: One Spelling Per Concept
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.