Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afb93e2
docs: spec for `qualifier agents` agent-bootstrap subcommand
akesling May 6, 2026
aea68de
docs: implementation plan for `qualifier agents` subcommand
akesling May 6, 2026
6a9e32d
feat(agents): scaffold qualifier agents subcommand
akesling May 6, 2026
01f2ec5
test(agents): cover unknown-topic exit path
akesling May 6, 2026
7d36350
feat(agents): registry and per-topic dispatch with placeholder pages
akesling May 6, 2026
6f81fab
test(agents): drop redundant single-topic test, parameterized covers it
akesling May 6, 2026
df69530
feat(agents): render overview with topic index
akesling May 6, 2026
bb4a43e
feat(agents): surface agents group at top of --help
akesling May 6, 2026
0a37d3e
style: cargo fmt fixes for agents module
akesling May 6, 2026
a286f3b
docs(agents): write topical pages — concepts, workflows, pitfalls
akesling May 6, 2026
11c8435
docs(agents): tighten topical-page accuracy after review
akesling May 6, 2026
260d17a
docs(agents): write per-subcommand pages
akesling May 6, 2026
072ddbd
docs(agents): clarify --supersedes does not expand prefixes in record
akesling May 6, 2026
628c601
chore: bump to 0.5.0 and document agents subcommand
akesling May 6, 2026
a05d6f2
docs: AGENTS-CLI 0.1 protocol and frontmatter migration spec
akesling May 6, 2026
dd8d552
docs: implementation plan for AGENTS-CLI cross-linking + frontmatter …
akesling May 6, 2026
fc5b0be
docs: link AGENTS-CLI from README
akesling May 6, 2026
3a3d0d5
docs(agents): declare AGENTS-CLI 0.1 conformance in orientation page
akesling May 6, 2026
aac824d
refactor(agents): generate page registry from TOML frontmatter
akesling May 6, 2026
0273d46
fix(agents): trim leading newline from page bodies; tighten Page visi…
akesling May 6, 2026
9cbbadc
test(agents): assert every page has a non-empty summary
akesling May 6, 2026
6f1385e
docs: changelog for 0.5.1 (AGENTS-CLI + frontmatter)
akesling May 6, 2026
7aee1e1
docs: align build.rs doc comment with non-pub consts; note order change
akesling May 6, 2026
7e6ce38
docs(agents): trim noise-encouraging guidance from orientation and ex…
akesling May 6, 2026
009e127
docs(agents): reframe resolve as user-directed; agents surface, do no…
akesling May 6, 2026
de6cd65
docs: reflow AGENTS-CLI.md to 80-col hard wrap
akesling May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions AGENTS-CLI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# AGENTS-CLI 0.1

A convention for command-line tools that self-describe to AI coding agents.

> **Status:** Draft. Breaking changes are allowed until 1.0. Feedback and
> adoption reports welcome via issues on any conforming tool's repository.

## Audience

CLI tool authors who want their tool to be productively usable by an AI
coding agent on first contact, with no separately-installed prompt
bundle, MCP server, or external skill.

## Motivation

A coding agent dropped into an unfamiliar shell typically does one of two
things when it encounters a CLI: relies on prior training-data knowledge
of the tool, or runs `<tool> --help` and pattern-matches. Neither path
teaches the agent the *judgment* it needs — when to use the tool, when
not to, what the common workflows look like, what mistakes to avoid.
That judgment is what separates an agent that uses your tool well from
one that strings invocations together by syntactic mimicry.

AGENTS-CLI defines the smallest surface that lets a tool ship its own
onboarding for AI agents, version-locked to the binary, discovered
through the existing `--help` channel that agents already inspect.

## Conformance

This document uses the requirement levels from RFC 2119: **MUST**,
**MUST NOT**, **SHOULD**, **SHOULD NOT**, **MAY**.

### Required (MUST)

A conforming tool **MUST** satisfy all of the following.

1. **Subcommand exists.** The tool exposes a subcommand named `agents`.
Examples: `qualifier agents`, `gh agents`, `cargo agents`.

2. **Bare invocation prints an orientation page.** Running `<tool>
agents` with no further arguments prints a self-contained orientation
document to standard output and exits with status 0. The orientation
document MUST be sufficient on its own to teach an agent (a) what the
tool is, (b) when to use it, (c) when not to use it, and (d) how to
drill into per-topic detail.

3. **Topic invocation prints the named page.** Running `<tool> agents
<topic>` prints the page identified by `<topic>` to standard output
and exits with status 0. The orientation page MUST list the available
topics.

4. **Unknown topic produces a structured error.** Running `<tool> agents
<unknown-topic>` MUST exit with status 2, and MUST write a message to
standard error of the form:

```
<tool> agents: no such topic '<unknown>'. Available: <name1>, <name2>, ...
```

The available list MUST contain every topic name that would succeed
under rule 3.

5. **`--help` discoverability.** The output of `<tool> --help` MUST
surface the `agents` subcommand in a way that signals to a coding
agent that it is the agent-targeted entry point. The conformance test
is informal but operational: *an LLM scanning the help output
reliably identifies `agents` as the entry point intended for AI
agents.* In practice this means a labeled section header ("For AI
agents:"), a banner line, or an equivalent affordance — not just an
unmarked entry buried in a generic command list.

### Recommended (SHOULD)

A conforming tool **SHOULD** satisfy the following. These are
common-sense quality bars; departing from them is allowed if there's a
clear reason.

1. **Output is markdown.** Pages SHOULD be UTF-8 markdown text. Each
page SHOULD begin with a single `# ` heading naming the page (e.g.,
`# <tool> record`).

2. **Page set covers concepts, workflows, pitfalls, and per-subcommand
detail.** At minimum, the topic set SHOULD include:

- A concept primer (often `concepts`) — the tool's data model, key
invariants, and any vocabulary an agent must understand to use the
tool well.
- Common workflows (often `workflows`) — three to five worked recipes
covering the most common tasks.
- Common pitfalls (often `pitfalls`) — mistakes agents make and how
to avoid them.
- One page per non-trivial subcommand, named after the subcommand.

3. **Pages are hand-written.** Pages SHOULD be authored with judgment,
not auto-generated from `--help` output. Agents already have access
to `--help` for syntax; AGENTS-CLI exists to convey the *when* and
*why* that flag tables cannot.

4. **Pages are version-locked to the binary.** Pages SHOULD ship inside
the binary or alongside it such that the guidance an agent receives
matches the tool it can actually invoke. Network fetches at runtime
are discouraged.

### Out of scope for 0.1

The following are deferred to later versions of this protocol or to
companion specifications. Tools MAY implement them, but doing so is not
part of 0.1 conformance:

- A discovery handshake for sweeping `$PATH` (e.g., `<tool> agents
--probe`).
- Structured (JSON) output for programmatic consumption.
- Internationalization or per-locale page sets.
- A standard format for storing pages in the source tree (each tool
chooses).
- A registry of conforming tools.

## Reference implementation

**qualifier** (0.5.0 and later) is the reference implementation:
<https://github.com/empathic/qualifier>.

`qualifier agents`, `qualifier agents concepts`, `qualifier agents
<subcommand>`, and `qualifier agents <unknown>` exercise every MUST in
this document. The "For AI agents:" group at the top of `qualifier
--help` is the discoverability mechanism for rule 5. Pages live at
`src/cli/commands/agents/pages/*.md`, embedded into the binary at
compile time.

## Versioning

AGENTS-CLI uses semantic versioning. The current version is 0.1. While
the major version is 0, breaking changes are allowed between minor
versions; downstream tools should expect to update conformance as the
protocol stabilizes. Tools MAY indicate which version they conform to
in any way they choose at 0.1 (a footer in the orientation page is the
conventional choice). A standardized declaration mechanism is deferred
to 1.0.

## Acknowledgements

The protocol distills practice from man pages (orientation + per-topic
drill-down), shell help conventions (machine-discoverable invocation
surface), and the more recent `AGENTS.md` repo-level convention (the
audience signal). It exists because no single one of those mechanisms
gives a coding agent everything it needs on first contact, and because
the friction of bolting on a separate skill bundle for every CLI is
incompatible with how agents actually arrive at a codebase.
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@ All notable changes to this project are documented here. Format follows
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (with
the pre-1.0 caveat that any breaking change bumps the minor version).

## [0.5.1] — unreleased

### Added

- **AGENTS-CLI 0.1 protocol document** (`AGENTS-CLI.md`) — a draft
cross-tool convention for CLI tools that self-describe to AI coding
agents. qualifier is named as the reference implementation. The
protocol defines five MUST rules (`agents` subcommand, bare
orientation, topic dispatch, exit-2 unknown-topic, `--help`
discoverability) and four SHOULD recommendations.

### Changed

- **Internal:** the `qualifier agents` page registry is now generated
at build time from TOML frontmatter on each `pages/*.md` file
instead of being a hand-coded `&[Page]` array in `mod.rs`. New
per-page fields: `name`, `summary`, `sees_also`, `since`.
User-visible CLI behavior is unchanged.
- **Topic display order:** the topic index in `qualifier agents` and the
available-topics list in unknown-topic errors are now in lexicographic
order (file-sorted), where they were previously hand-ordered. This is
cosmetic; the same set of topics is exposed.

## [0.5.0] — unreleased

### Added

- **`qualifier agents`** — self-contained guide for AI coding agents.
Bare `qualifier agents` prints an orientation page with an index of
topics; `qualifier agents <topic>` (e.g. `concepts`, `workflows`,
`record`) drills into per-topic detail. The agent group also appears
at the top of `qualifier --help` so models reading the help text
discover the entry point on their own.

## [0.4.0] — unreleased

This release is a substantial reshape: the CLI surface narrowed, scoring
Expand Down
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "qualifier"
version = "0.4.0"
version = "0.5.1"
edition = "2024"
description = "Deterministic quality annotations for software artifacts"
license = "MIT OR Apache-2.0"
Expand All @@ -9,6 +9,10 @@ readme = "README.md"
keywords = ["quality", "annotation", "code-review", "ci"]
categories = ["command-line-utilities", "development-tools"]

[build-dependencies]
toml = "0.8"
serde = { version = "1", features = ["derive"] }

[features]
default = ["cli"]
cli = ["dep:clap", "dep:comfy-table", "dep:figment", "dep:rand"]
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,17 @@ qualifier resolve a1b2
qualifier review src/parser.rs
```

> **For AI coding agents:** qualifier implements [AGENTS-CLI 0.1](AGENTS-CLI.md). Run `qualifier agents` for a self-contained guide.

## CLI

**Record observations**
### For AI agents

| Command | Description |
|---------|-------------|
| `qualifier agents [topic]` | Self-contained guide for AI coding agents (start here) |

### Record observations

| Command | Description |
|---------|-------------|
Expand All @@ -60,7 +68,7 @@ qualifier review src/parser.rs
is a path with an optional span (`src/foo.rs:42`, `src/foo.rs:42:58`).
`<target>` is an id-prefix (≥4 chars) or a `<location>`.

**Inspect annotations**
### Inspect annotations

| Command | Description |
|---------|-------------|
Expand All @@ -69,7 +77,7 @@ is a path with an optional span (`src/foo.rs:42`, `src/foo.rs:42:58`).
| `qualifier praise <artifact>` | Show who annotated and why (alias: `blame`) |
| `qualifier review [subject]` | Check freshness of span-bound annotations |

**Maintain**
### Maintain

| Command | Description |
|---------|-------------|
Expand Down
131 changes: 131 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Build-time generator for the `qualifier agents` page registry.
//!
//! Walks `src/cli/commands/agents/pages/*.md`, parses TOML frontmatter
//! delimited by `+++` lines, and emits `$OUT_DIR/agents_pages.rs`
//! containing:
//!
//! - `const OVERVIEW: &str = "...";`
//! - `const PAGES: &[Page] = &[...];`
//!
//! `Page` is defined in `src/cli/commands/agents/mod.rs`. The generated
//! file is `include!`d there.

use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::PathBuf;

use serde::Deserialize;

const PAGES_DIR: &str = "src/cli/commands/agents/pages";

#[derive(Deserialize)]
struct PageMeta {
name: String,
summary: Option<String>,
#[serde(default)]
sees_also: Vec<String>,
since: Option<String>,
}

fn main() {
println!("cargo:rerun-if-changed={PAGES_DIR}");
println!("cargo:rerun-if-changed=build.rs");

let mut entries: Vec<_> = fs::read_dir(PAGES_DIR)
.unwrap_or_else(|e| panic!("read {PAGES_DIR}: {e}"))
.filter_map(Result::ok)
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("md"))
.collect();
entries.sort_by_key(|e| e.path());

let mut overview_body: Option<String> = None;
let mut topic_entries: Vec<String> = Vec::new();
let mut seen_names: HashSet<String> = HashSet::new();

for entry in entries {
let path = entry.path();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_else(|| panic!("non-utf8 filename: {}", path.display()))
.to_string();
let raw =
fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));

let after_open = raw.strip_prefix("+++\n").unwrap_or_else(|| {
panic!(
"{}: file must begin with '+++' frontmatter delimiter on its first line",
path.display()
)
});
let close_offset = after_open.find("\n+++\n").unwrap_or_else(|| {
panic!(
"{}: missing closing '+++' frontmatter delimiter",
path.display()
)
});
let frontmatter = &after_open[..close_offset];
let body = after_open[close_offset + "\n+++\n".len()..].trim_start_matches('\n');

let meta: PageMeta = toml::from_str(frontmatter)
.unwrap_or_else(|e| panic!("{}: invalid TOML frontmatter: {e}", path.display()));

if meta.name != stem {
panic!(
"{}: frontmatter name '{}' does not match filename stem '{}'",
path.display(),
meta.name,
stem
);
}
if !seen_names.insert(meta.name.clone()) {
panic!("duplicate page name '{}'", meta.name);
}

if stem.starts_with('_') {
if stem == "_overview" {
overview_body = Some(body.to_string());
} else {
panic!(
"{}: unsupported internal page; only '_overview' is recognized",
path.display()
);
}
} else {
let summary = meta.summary.unwrap_or_else(|| {
panic!(
"{}: topic page is missing required 'summary' field",
path.display()
)
});
let sees_also_lit = meta
.sees_also
.iter()
.map(|s| format!("{s:?}"))
.collect::<Vec<_>>()
.join(", ");
let since_lit = match meta.since {
Some(v) => format!("Some({v:?})"),
None => "None".into(),
};
topic_entries.push(format!(
" Page {{ name: {:?}, summary: {:?}, sees_also: &[{sees_also_lit}], since: {since_lit}, body: {:?} }},",
meta.name, summary, body,
));
}
}

let overview = overview_body.unwrap_or_else(|| {
panic!("{PAGES_DIR}/_overview.md is required but not found");
});

let generated = format!(
"const OVERVIEW: &str = {overview:?};\n\nconst PAGES: &[Page] = &[\n{}\n];\n",
topic_entries.join("\n"),
);

let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("agents_pages.rs");
fs::write(&out_path, generated)
.unwrap_or_else(|e| panic!("write {}: {e}", out_path.display(),));
}
Loading
Loading