diff --git a/AGENTS-CLI.md b/AGENTS-CLI.md new file mode 100644 index 0000000..8809604 --- /dev/null +++ b/AGENTS-CLI.md @@ -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 ` --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 ` + 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 ` agents + ` prints the page identified by `` to standard output + and exits with status 0. The orientation page MUST list the available + topics. + +4. **Unknown topic produces a structured error.** Running ` agents + ` MUST exit with status 2, and MUST write a message to + standard error of the form: + + ``` + agents: no such topic ''. Available: , , ... + ``` + + The available list MUST contain every topic name that would succeed + under rule 3. + +5. **`--help` discoverability.** The output of ` --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., + `# 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., ` 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: +. + +`qualifier agents`, `qualifier agents concepts`, `qualifier agents +`, and `qualifier agents ` 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index e89aebd..26da0ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` (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 diff --git a/Cargo.lock b/Cargo.lock index bf69309..a6c7822 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,7 +674,7 @@ dependencies = [ [[package]] name = "qualifier" -version = "0.4.0" +version = "0.5.1" dependencies = [ "blake3", "chrono", @@ -688,6 +688,7 @@ dependencies = [ "serde_json", "tempfile", "thiserror", + "toml", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9c855de..d337fdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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"] diff --git a/README.md b/README.md index 528553f..68ef8ca 100644 --- a/README.md +++ b/README.md @@ -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 | |---------|-------------| @@ -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`). `` is an id-prefix (≥4 chars) or a ``. -**Inspect annotations** +### Inspect annotations | Command | Description | |---------|-------------| @@ -69,7 +77,7 @@ is a path with an optional span (`src/foo.rs:42`, `src/foo.rs:42:58`). | `qualifier praise ` | Show who annotated and why (alias: `blame`) | | `qualifier review [subject]` | Check freshness of span-bound annotations | -**Maintain** +### Maintain | Command | Description | |---------|-------------| diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..37a4789 --- /dev/null +++ b/build.rs @@ -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, + #[serde(default)] + sees_also: Vec, + since: Option, +} + +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 = None; + let mut topic_entries: Vec = Vec::new(); + let mut seen_names: HashSet = 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::>() + .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(),)); +} diff --git a/src/cli/commands/agents/mod.rs b/src/cli/commands/agents/mod.rs new file mode 100644 index 0000000..8fd2c2b --- /dev/null +++ b/src/cli/commands/agents/mod.rs @@ -0,0 +1,79 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +pub struct Args { + /// Topic name (e.g. `record`, `concepts`, `workflows`). Omit to print the orientation page. + pub topic: Option, +} + +struct Page { + name: &'static str, + summary: &'static str, + /// Cross-references to related topics. Stored for future rendering + /// (a "See also: ..." footer is planned); not yet read. + #[allow(dead_code)] + sees_also: &'static [&'static str], + /// Version when the topic was first present. Stored for future + /// version-aware navigation; not yet read. + #[allow(dead_code)] + since: Option<&'static str>, + body: &'static str, +} + +include!(concat!(env!("OUT_DIR"), "/agents_pages.rs")); + +fn topic_names() -> String { + PAGES.iter().map(|p| p.name).collect::>().join(", ") +} + +pub fn run(args: Args) -> crate::Result<()> { + match args.topic.as_deref() { + None => { + print!("{}", render_overview()); + Ok(()) + } + Some(name) => { + if let Some(page) = PAGES.iter().find(|p| p.name == name) { + print!("{}", page.body); + Ok(()) + } else { + eprintln!( + "qualifier agents: no such topic '{name}'. Available: {}", + topic_names() + ); + std::process::exit(2); + } + } + } +} + +fn render_overview() -> String { + // Replace the {{TOPICS}} sentinel with a `name — summary` listing. + let topics_block: String = PAGES + .iter() + .map(|p| format!("- `{}` — {}", p.name, p.summary)) + .collect::>() + .join("\n"); + OVERVIEW.replace("{{TOPICS}}", &topics_block) +} + +#[cfg(test)] +mod tests { + #[test] + fn overview_contains_topics_sentinel() { + // If this fails, the orientation page lost the {{TOPICS}} substitution + // anchor and the rendered overview will no longer list children. + assert!(super::OVERVIEW.contains("{{TOPICS}}")); + } + + #[test] + fn all_pages_have_non_empty_summaries() { + for page in super::PAGES { + assert!( + !page.summary.is_empty(), + "page '{}' has empty summary", + page.name + ); + } + } +} diff --git a/src/cli/commands/agents/pages/_overview.md b/src/cli/commands/agents/pages/_overview.md new file mode 100644 index 0000000..136ea3f --- /dev/null +++ b/src/cli/commands/agents/pages/_overview.md @@ -0,0 +1,55 @@ ++++ +name = "_overview" ++++ + +# qualifier — guide for AI coding agents + +You are an AI coding agent in a user's repository. The `qualifier` CLI is +installed. This page tells you what qualifier is, when to use it, and how to +get more detail on any specific feature. + +## What it is + +Qualifier records *annotations* — structured, content-addressed notes about +software artifacts (files, directories, line ranges). Annotations live in +`.qual` files alongside the code, are intended to be committed to version +control, and form a durable, reviewable record of quality observations +attached to specific places in the codebase. + +## When to use it + +- You found a bug, smell, risk, or stylistic concern that survives this + edit and is worth surfacing for whoever touches the code next. Record it. + +## When NOT to use it + +- One-off scratch debugging notes. Use scratch space. +- Information that belongs in the commit message (what *this* change does + and why). Use git. +- Project-wide policy or architecture decisions. Those belong in + `docs/` or an ADR, not as an annotation on a file. + +## Quickstart + +```bash +# Record a concern about a function in src/foo.rs +qualifier record concern src/foo.rs --message "tight coupling to the cache" + +# See what's annotated on a file +qualifier show src/foo.rs + +# After fixing it, resolve the annotation by id-prefix or location +qualifier resolve src/foo.rs --message "refactored to inject the cache" +``` + +## Available topics + +Run `qualifier agents ` for any of: + +{{TOPICS}} + +## Reference + +For exact flag tables on any subcommand, run `qualifier --help`. +For the JSONL wire format and library API, see `SPEC.md` in this repo (if +present) or the published spec. diff --git a/src/cli/commands/agents/pages/compact.md b/src/cli/commands/agents/pages/compact.md new file mode 100644 index 0000000..ddeda5b --- /dev/null +++ b/src/cli/commands/agents/pages/compact.md @@ -0,0 +1,75 @@ ++++ +name = "compact" +summary = "Compact a .qual file" +sees_also = ["review"] +since = "0.5.0" ++++ + +# qualifier compact + +## Purpose + +Prune superseded records from a `.qual` file, or collapse the full history +to a single epoch record. + +## When to use it + +`.qual` files grow as annotations are added, superseded, and resolved. +`compact` is a maintenance command for trimming that growth. Run it +occasionally to keep `.qual` files readable and diff-friendly — not after +every annotation. The default prune mode removes records that have been +superseded by a later record in the same file. The `--snapshot` mode goes +further, collapsing all remaining records into a single epoch record that +preserves the final state as a summary. Snapshot is appropriate when you +want a clean break in the history (e.g., after a major release audit); +prune alone is the lighter-weight routine operation. + +## Common invocations + +```bash +# Prune superseded records for one artifact +qualifier compact src/auth.rs + +# Preview what would be pruned, without writing +qualifier compact src/auth.rs --dry-run + +# Snapshot one artifact's history to a single epoch record +qualifier compact src/auth.rs --snapshot + +# Compact every .qual file in the project +qualifier compact --all + +# Snapshot all files (use with care — removes detailed history) +qualifier compact --all --snapshot +``` + +## Flags worth knowing + +**`--dry-run`** prints what would be removed or collapsed without modifying +any files. Always run this first when operating on a shared repo so you can +review the impact before committing. + +**`--snapshot`** collapses all remaining (non-superseded) records into one +`epoch` record with `issuer: "urn:qualifier:compact"` and +`issuer_type: tool`. The original individual annotation IDs are no longer +present after snapshotting, so downstream tools cannot reference them. Use +`--dry-run` first and ensure no active ids are referenced by open +`--supersedes` or `--references` chains. + +**`--all`** discovers and compacts every `.qual` file under the project root. +Combine with `--dry-run` to audit before committing the result. + +## Gotchas + +- The argument to `compact` is the **artifact name** (e.g., `src/auth.rs`), + not the `.qual` file path. The command resolves the relevant `.qual` file + from the artifact name. +- Compacting removes superseded records permanently from the file on disk. + Ensure the `.qual` file is tracked in VCS before compacting so the history + is recoverable if needed. +- `--snapshot` is destructive to annotation identity: individual record IDs + disappear. Any external system that stored those IDs (e.g., a ticket + linked to an annotation) will have broken references. Only snapshot when + you are certain no one depends on the old IDs. +- If nothing is superseded, `compact` reports "nothing to compact" and makes + no changes, so it is safe to run idempotently. diff --git a/src/cli/commands/agents/pages/concepts.md b/src/cli/commands/agents/pages/concepts.md new file mode 100644 index 0000000..ace43ac --- /dev/null +++ b/src/cli/commands/agents/pages/concepts.md @@ -0,0 +1,164 @@ ++++ +name = "concepts" +summary = "Annotation model, kinds, supersession, IDs, .qual layout" +since = "0.5.0" ++++ + +# qualifier — key concepts + +## The annotation envelope + +Every qualifier record is a single-line JSON object using the **Metabox envelope**. +The envelope fields appear in a fixed order and are identical for every record type: + +(formatted for readability; each record is a single line in the .qual file) + +```json +{ + "metabox": "1", + "type": "annotation", + "subject": "src/auth.rs", + "issuer": "mailto:agent@example.com", + "issuer_type": "ai", + "created_at": "2026-03-01T10:00:00Z", + "id": "", + "body": { ... } +} +``` + +Field notes: + +- `metabox` — always `"1"`. Validation rejects any other value. +- `type` — `"annotation"` (default, may be omitted in files), `"epoch"`, `"dependency"`, or any non-empty string for custom types (URI recommended). +- `subject` — the artifact being annotated. A path (`src/auth.rs`), module, build target, or package name. Opaque to qualifier. +- `issuer` — who or what wrote the record. Must be a URI (see §Issuer URIs below). +- `issuer_type` — optional; one of `human`, `ai`, `tool`, `unknown`. +- `created_at` — RFC 3339 timestamp. +- `id` — BLAKE3 hash of the canonical form (see §Content addressing). Never hand-edit. +- `body` — type-specific payload; body fields are serialized in alphabetical order. + +You do not construct this JSON by hand. Use `qualifier record`, `qualifier reply`, +`qualifier resolve`, or `qualifier emit` — they compute the correct `id` for you. + +## Kinds + +The `kind` field in an annotation body identifies the nature of the signal. +Choose the most specific kind that fits; downstream filtering and polarity depend on it. + +| Kind | When to use | +|--------------|-------------| +| `concern` | A non-blocking issue worth tracking (e.g., a code smell, a risk, a TODO that matters). | +| `blocker` | A blocking issue that must be resolved before release. | +| `fail` | The artifact does not meet a stated quality bar. | +| `pass` | The artifact meets a stated quality bar. | +| `comment` | An observation or discussion point with no polarity impact. | +| `praise` | Positive recognition — marks intentional design for future readers. | +| `suggestion` | A proposed improvement, typically paired with `--suggested-fix`. | +| `waiver` | An acknowledged issue explicitly accepted with rationale. | +| `resolve` | Closes a prior record via supersession. Prefer `qualifier resolve` instead of recording this directly. | + +Any other string is valid as a custom kind. The CLI warns if it looks like a +typo of a built-in kind (edit distance ≤ 2). + +Polarity summary: `pass`, `praise`, `waiver` are positive; `comment` and `resolve` +are neutral; `concern`, `suggestion`, `fail`, `blocker` are negative. + +## Supersession + +Records are **immutable once written**. To update or retract a signal, write a +new annotation with `supersedes` pointing to the old record's `id`. + +Rules the system enforces: + +- The superseding and superseded records **must share the same `subject`**. + Cross-subject supersession is rejected. +- Supersession chains must be **acyclic**. A → B → A is detected and rejected. +- Only the **tip** of a supersession chain is active. Superseded records are + hidden by `qualifier show` and `qualifier ls`. +- Dangling `supersedes` references (pointing to IDs not present in the + current file set) are allowed — the referencing record stays active. + +The canonical way to close an issue is `qualifier resolve `, which writes +a `resolve`-kind annotation that supersedes the target. The original record +is no longer surfaced; the resolve record stands as a visible tombstone. + +## `.qual` file layout and discovery + +A `.qual` file is UTF-8 JSONL (one record per line). Three layouts coexist: + +| Layout | Example file | Trade-off | +|--------|-------------|-----------| +| Per-directory (recommended) | `src/.qual` | Clean tree, good merge behavior | +| Per-file | `src/parser.rs.qual` | Maximum merge isolation, noisy tree | +| Per-project | `.qual` at repo root | Simplest, but high merge contention | + +When you run `qualifier record concern src/auth.rs ...`, the CLI writes to +`src/.qual` by default (or `src/auth.rs.qual` if that file already exists). +Override with `--file `. + +**Discovery** walks from the project root (found by searching upward for +`.git`, `.hg`, `.jj`, `.pijul`, `_FOSSIL_`, or `.svn`). It collects every +file named `.qual` or ending in `.qual`. + +**Ignore rules** are applied by default: +- `.gitignore` at any level (including global gitignore and `.git/info/exclude`). +- `.qualignore` — same syntax as `.gitignore`, qualifier-specific. Place + anywhere in the tree to exclude vendored code, generated files, or examples. + +Pass `--no-ignore` to bypass all ignore rules on any discovery command +(`show`, `ls`, `compact`, `review`, `praise`). + +Hidden directories (e.g., `.git`, `.vscode`) are always skipped. Hidden +*files* like `.qual` are not skipped. + +## Issuer URIs and `issuer_type` + +The `issuer` field identifies who or what created a record. It must be a URI +— specifically, it must contain `:`. Validation rejects bare strings without a colon. + +Common forms: + +``` +mailto:agent@example.com # typical for an AI agent or human +https://ci.example.com # CI job or tool with a URL +urn:qualifier:compact # reserved for the compact command +``` + +When `--issuer` is omitted, the CLI detects the VCS user identity: +- Git: `git config user.email` → wrapped as `mailto:` +- Mercurial: `hg config ui.username` → wrapped as `mailto:` +- Fallback: `mailto:$USER@localhost` + +As an agent, always pass `--issuer "mailto:your-agent-id@example.com"` and +`--issuer-type ai` so records are traceable back to you. + +The `issuer_type` field is optional but strongly recommended: + +| Value | Use when | +|-----------|----------| +| `human` | A person wrote this record | +| `ai` | An AI agent wrote this record | +| `tool` | An automated scanner or CI job wrote this record | +| `unknown` | Origin is unclear | + +## Content addressing (BLAKE3) + +A record's `id` is the BLAKE3 hash of its **Metabox Canonical Form (MCF)**: +the record serialized with envelope fields in fixed order, body fields in +alphabetical order, all optional absent fields omitted, and `id` set to `""` +during hashing. + +Implications: + +- Identical inputs always produce the same `id`, on every machine and implementation. +- Changing any canonical field (summary, kind, span, issuer, timestamp, ...) changes the `id`. +- **Do not hand-edit `.qual` files.** Editing a field invalidates the `id`, + and `qualifier` will reject the record. Use `qualifier record`, `qualifier emit`, + or the compactor to write records. +- Span normalization is part of MCF: a span with no `end` has `end` set equal + to `start` before hashing, so `{"start":{"line":42}}` and + `{"start":{"line":42},"end":{"line":42}}` produce the same `id`. + +The `id` is a stable handle you can use to target a record with +`qualifier reply ` or `qualifier resolve `. +Four characters of prefix are the minimum; use more if the file has many records. diff --git a/src/cli/commands/agents/pages/emit.md b/src/cli/commands/agents/pages/emit.md new file mode 100644 index 0000000..21b22da --- /dev/null +++ b/src/cli/commands/agents/pages/emit.md @@ -0,0 +1,77 @@ ++++ +name = "emit" +summary = "Emit a raw record of any type" +sees_also = ["record"] +since = "0.5.0" ++++ + +# qualifier emit + +## Purpose + +Emit a raw record of any type, bypassing the higher-level annotation +scaffolding. + +## When to use it + +Most agents should use `record`, `reply`, and `resolve` for day-to-day +annotation work — those commands construct well-formed annotation envelopes +and validate the body against the spec. `emit` is for cases that those +commands do not cover: writing `epoch` or `dependency` records, emitting +extension types defined by a custom pipeline (e.g., `license`, +`security-advisory`, `perf-measurement`, or any custom URI type), or +forwarding a pre-built record from an external tool. The body JSON is passed +through unchanged, so the caller is responsible for correctness. When +`record_type` is `annotation`, `emit` does run standard annotation validation; +for all other types, the body is stored verbatim. + +## Common invocations + +```bash +# Emit an epoch record to checkpoint the annotation history +qualifier emit epoch src/auth.rs \ + --body '{"summary":"Reviewed at v2.3.0","refs":["git:3aba500"]}' + +# Emit a dependency record linking two artifacts +qualifier emit dependency src/auth.rs \ + --body '{"depends_on":["src/session.rs"]}' + +# Emit a custom extension record type +qualifier emit https://example.com/types/perf-measurement src/hot_path.rs \ + --body '{"p99_ms":14,"baseline_ms":12}' + +# Batch-emit pre-built records from a JSONL file +cat records.jsonl | qualifier emit --stdin +``` + +## Flags worth knowing + +**`--body ''`** is required in single-record mode. The value must be a +valid JSON object; the CLI will reject non-object values. For annotation +records the body must conform to the annotation body schema; for other types +the body is stored as-is. Quote the argument carefully — shell word splitting +on embedded spaces or braces is a common source of errors. + +**`--stdin`** reads complete JSONL records from stdin, one per line. Each +line must be a full record (envelope + body). The positional `record_type` +and `subject` arguments, when provided alongside `--stdin`, act as defaults +applied to lines that are missing those fields. + +**`--issuer`** and **`--issuer-type`** work the same as in `record`. When +emitting machine-generated records such as pipeline measurements, set +`--issuer-type tool` to distinguish them from human annotations. + +## Gotchas + +- `emit` is intentionally low-level. Prefer `record` / `reply` / `resolve` + for annotation work — they guard against common mistakes (missing `kind`, + invalid supersession, etc.) that `emit` does not catch. +- When `record_type` is `annotation`, the body must include at least `kind` + and `summary`; the command will validate and reject non-conforming bodies. + For non-annotation types, no body validation is run — a malformed body will + be stored silently. +- The subject positional is the artifact name (e.g., `src/auth.rs`), not a + `.qual` file path. Span notation in the subject is not parsed; `emit` is + the low-level shape. +- Lines starting with `//` are treated as comments and skipped in `--stdin` + mode. Blank lines are also skipped. diff --git a/src/cli/commands/agents/pages/ls.md b/src/cli/commands/agents/pages/ls.md new file mode 100644 index 0000000..35c12df --- /dev/null +++ b/src/cli/commands/agents/pages/ls.md @@ -0,0 +1,64 @@ ++++ +name = "ls" +summary = "List artifacts by kind" +sees_also = ["show"] +since = "0.5.0" ++++ + +# qualifier ls + +## Purpose + +List all artifacts that have annotations, with an optional filter by +annotation kind. + +## When to use it + +`ls` gives a project-wide inventory of annotated artifacts — useful for an +initial orientation or for finding every artifact with a given kind of +annotation. It does not show the content of the annotations; it only shows +subject names and counts. For the annotations themselves, follow up with +`qualifier show `. Compare with `show`, which requires a specific +artifact name and shows full annotation text, and `praise`, which focuses +on authorship for one artifact. + +## Common invocations + +```bash +# List all annotated artifacts in the project +qualifier ls + +# Filter to artifacts that have at least one "concern" annotation +qualifier ls --kind concern + +# Machine-readable JSON output +qualifier ls --format json +``` + +## Flags worth knowing + +**`--kind `** filters the output to artifacts that have at least one +annotation of the given kind (e.g., `concern`, `pass`, `blocker`). Only +standard and custom kind strings stored in `body.kind` are matched; envelope +`type` filtering is not available here (for that, use `qualifier show --type` +per artifact). + +**`--format json`** emits a JSON array where each element has `subject`, +`annotation_count`, and `kinds` (an array of all kind strings recorded for +that artifact, including duplicates). This is useful for scripts that need +to walk every annotated artifact or count by kind. + +**`--no-ignore`** bypasses `.gitignore` and `.qualignore` filtering when +discovering `.qual` files. Use this if you suspect an artifact is being +hidden by an ignore rule. + +## Gotchas + +- `ls` operates on `.qual` files discovered from the project root. It does + not scan source files for artifacts that lack annotations — the + `--unqualified` flag exists as a placeholder but is not yet implemented. +- Counts include all records in the `.qual` file, not just active ones. + An artifact that was annotated and then fully resolved will still appear + in `ls` output with a non-zero count. +- The output is sorted alphabetically by subject. There is no sort-by-count + option; pipe through your shell's `sort` if needed. diff --git a/src/cli/commands/agents/pages/pitfalls.md b/src/cli/commands/agents/pages/pitfalls.md new file mode 100644 index 0000000..e9294f0 --- /dev/null +++ b/src/cli/commands/agents/pages/pitfalls.md @@ -0,0 +1,66 @@ ++++ +name = "pitfalls" +summary = "Common mistakes agents make with qualifier" +since = "0.5.0" ++++ + +# qualifier — common pitfalls + +- **Recording without `--span` when the concern is about specific lines.** + A concern about a particular function or block is most actionable when it + points at the exact lines. Without a span, the annotation attaches to the + whole file and `qualifier review` cannot detect drift when the code changes. + Do this instead: `qualifier record concern src/foo.rs:42:58 "..."`. + +- **Conflating `reply` (add context) with `resolve` (close the issue).** + `qualifier reply` writes a new `comment` that *references* the target — both + records remain active. `qualifier resolve` writes a `resolve` record that + *supersedes* the target, removing it from active views. Use `reply` when you + have more information to add; use `resolve` when the concern is addressed. + +- **Recording an annotation when a commit message would do.** + Annotations are for observations that survive the current change and need to + be visible to whoever touches the code next. If the information is only + relevant to *this* commit (what changed and why), put it in the commit + message instead. Annotations accumulate; don't add noise that expires + immediately. + +- **Editing `.qual` files by hand instead of using `record` or `emit`.** + Record IDs are BLAKE3 hashes of the canonical form. Editing any field + directly invalidates the `id`, and qualifier will reject the record on next + read. Always use `qualifier record`, `qualifier emit`, `qualifier reply`, + or `qualifier resolve` to write records. + +- **Choosing `bug` or another custom kind when a built-in kind fits.** + The built-in kinds (`concern`, `blocker`, `fail`, `pass`, `comment`, + `praise`, `suggestion`, `waiver`, `resolve`) carry polarity semantics used + by downstream tools for filtering and display. A custom kind like `bug` is + valid but invisible to anything that filters by polarity or standard kind. + Prefer `concern` for non-blocking bugs and `blocker` for must-fix issues; + reserve custom kinds for genuinely domain-specific signals. + +- **Resolving annotations the user has not directed you to close.** + `qualifier resolve` *closes* a record — the original concern is hidden + from `qualifier show`, `qualifier ls`, and other active views. An agent + resolving an annotation it does not fully understand silently buries a + concern the user may still want to act on. Resolution is a user + decision: surface the annotation (e.g., with `qualifier show`) and let + the user decide whether it is addressed. + +- **Adding positive annotations the user did not ask for.** + An agent volunteering `praise`, `pass`, or other positive-polarity + annotations without explicit user direction creates review noise the + user has to triage. Annotations exist to surface things the user needs + to act on; reflexively recording approval of code you happen to be + reading does the opposite. Record a positive annotation only when the + user explicitly asks for it, or when documenting an intentional + non-obvious design choice that future readers would otherwise mistake + for a bug. + +- **Using a non-URI issuer (must contain `:`).** + Validation rejects any `issuer` value that does not contain a colon. A bare + email address like `agent@example.com` will fail. Wrap it: + `--issuer "mailto:agent@example.com"`. If your agent has an HTTP identity, + use that directly: `--issuer "https://agents.example.com/review-bot"`. + + diff --git a/src/cli/commands/agents/pages/praise.md b/src/cli/commands/agents/pages/praise.md new file mode 100644 index 0000000..fc156c4 --- /dev/null +++ b/src/cli/commands/agents/pages/praise.md @@ -0,0 +1,67 @@ ++++ +name = "praise" +summary = "Show who annotated an artifact and why" +sees_also = ["show"] +since = "0.5.0" ++++ + +# qualifier praise + +## Purpose + +Show who annotated an artifact and why, with authorship detail for each +active record. + +## When to use it + +`praise` is the attribution command. Where `show` presents a threaded view +of open quality signals, `praise` focuses on who left each annotation, when, +and with what issuer type — making it easy to see whether a review was done +by a human, an AI agent, or a tool. It is useful when auditing the annotation +history of a file or when you need to find the contact for an annotation +before replying. The alias `blame` also works, but the CLI will print a hint +suggesting `praise` — the format is designed to surface helpers rather than +assign fault. + +## Common invocations + +```bash +# Show attribution for all active annotations on a file +qualifier praise src/auth.rs + +# Machine-readable JSON output (includes issuer_type, span, detail) +qualifier praise src/auth.rs --format json + +# Invoke via the alias (produces a hint, then runs normally) +qualifier blame src/auth.rs + +# Use VCS blame on the underlying .qual file instead +qualifier praise src/auth.rs --vcs +``` + +## Flags worth knowing + +**`--format json`** returns a JSON object with `subject` and a `records` +array. Each entry includes `id`, `kind`, `summary`, `issuer`, +`created_at`, and optionally `issuer_type`, `detail`, `suggested_fix`, and +`span`. This is the right mode for an agent that needs to programmatically +find who to notify or which annotations came from other agents. + +**`--vcs`** delegates to the VCS `blame` / `annotate` command on the `.qual` +file itself (git or hg), showing which VCS commit last touched each line. +This is complementary to the record-based view: `--vcs` shows commit +authorship at the `.qual` file level, not the annotation-level issuer field. +It requires a supported VCS to be detected. + +## Gotchas + +- `praise` only shows **active** records — superseded and resolved + annotations are filtered out. If you need the full history, use + `qualifier show --all`. +- The `issuer` field is a URI (e.g., `mailto:alice@example.com`). The human + output strips the `mailto:` prefix and the domain to show a short name; + the JSON output always includes the full URI. +- `--vcs` requires git or hg. For other VCS systems the command exits with + an error and suggests running your VCS tool directly on the `.qual` file. +- `praise` exits with an error if no records are found for the artifact, the + same as `show`. diff --git a/src/cli/commands/agents/pages/record.md b/src/cli/commands/agents/pages/record.md new file mode 100644 index 0000000..e05676c --- /dev/null +++ b/src/cli/commands/agents/pages/record.md @@ -0,0 +1,80 @@ ++++ +name = "record" +summary = "Record a new annotation" +sees_also = ["reply", "resolve", "emit"] +since = "0.5.0" ++++ + +# qualifier record + +## Purpose + +Record a new annotation against a source artifact. + +## When to use it + +`record` is the primary write verb — use it whenever you want to leave a +quality signal on an artifact. It replaced the earlier per-kind verbs +(`comment`, `flag`, `suggest`, `approve`, `reject`, `attest`); all of those +kinds are now values for the first argument rather than separate subcommands. +Use `reply` when you are responding to an existing annotation thread (it +sets `body.references` automatically). Use `resolve` when you are closing an +open concern. + +## Common invocations + +```bash +# Leave a concern on a file +qualifier record concern src/auth.rs "Missing rate-limit on login endpoint" + +# Annotate a specific line with extended detail +qualifier record concern src/auth.rs:42 "Null check missing" \ + --detail "The function returns early but does not reset the session token." \ + --suggested-fix "Add session.reset() before the return." + +# Record a suggestion with explicit AI issuer identity +qualifier record suggestion src/auth.rs "Replace inline regex with a named constant" \ + --issuer "mailto:agent@ci.example.com" \ + --issuer-type ai + +# Batch-record from a JSONL file +cat findings.jsonl | qualifier record --stdin +``` + +## Flags worth knowing + +**`--span `** overrides any line range parsed from ``. When +you already know the exact span (e.g., `42:58`) and want to keep the location +clean, pass it here rather than embedding it in the location string. The CLI +auto-computes a `content_hash` so that `qualifier review` can later detect +drift. + +**`--supersedes `** marks this record as superseding a prior annotation. +Use this to update or correct an existing annotation rather than leaving both +visible — the superseded record is filtered out by `show`, `praise`, and +`review`. The value is stored verbatim — no prefix expansion is performed +here. Pass the full 64-character ID. (Short prefixes are only resolved by +`reply` and `resolve`, not `record`.) + +**`--issuer-type `** takes `human`, `ai`, `tool`, or `unknown`. Always +set `--issuer-type ai` when writing from an agent; this lets human reviewers +distinguish machine-generated annotations from their own. + +**`--stdin`** switches to batch mode. Each stdin line must be a JSON object +with at least `kind`, `location`, and `message` keys, plus any optional +overrides. Lines starting with `//` are ignored. Useful for emitting many +annotations in one pass. + +## Gotchas + +- All three positional arguments (``, ``, ``) are + required in non-batch mode. Missing any one of them produces a validation + error rather than a prompt. +- The issuer defaults to your VCS user email wrapped in `mailto:`. If running + in a CI environment with no git config, the fallback is + `mailto:unknown@localhost` — set `--issuer` explicitly so records are + attributable. +- Cross-subject supersession is rejected: a new record can only supersede a + record with the same subject. +- Span lines are 1-indexed. Passing `--span 0` or `--span 0:5` will store + line 0, which is outside any real file — validate your line numbers first. diff --git a/src/cli/commands/agents/pages/reply.md b/src/cli/commands/agents/pages/reply.md new file mode 100644 index 0000000..07d4c65 --- /dev/null +++ b/src/cli/commands/agents/pages/reply.md @@ -0,0 +1,73 @@ ++++ +name = "reply" +summary = "Reply to an existing record" +sees_also = ["resolve", "record"] +since = "0.5.0" ++++ + +# qualifier reply + +## Purpose + +Add a follow-up annotation to an existing record, continuing a thread. + +## When to use it + +Use `reply` when you want to comment on or respond to an existing annotation +without closing it. The new record is linked to the target via +`body.references` — `qualifier show` will render replies indented under their +parent. This is distinct from `resolve`, which closes an annotation by +writing a record with `kind: resolve` and `body.supersedes` pointing at the +target. If you only want to add context, a correction, or acknowledge +something that still needs action, use `reply`. If the issue has been fixed +and you want it to stop appearing in active annotation lists, use `resolve`. + +## Common invocations + +```bash +# Reply to a record by id-prefix (4+ characters required) +qualifier reply a1b2c3d4 "Confirmed — also affects the logout path" + +# Reply to the most-recent active annotation at a location +qualifier reply src/auth.rs:42 "Fixed in PR #88 but needs backport to v2" + +# Reply with a suggested fix and ai issuer-type +qualifier reply a1b2c3d4 "Here is a safer pattern" \ + --suggested-fix "Use constant-time comparison: crypto.timingSafeEqual(a, b)" \ + --issuer-type ai + +# Reply with a non-default kind +qualifier reply src/auth.rs "This is intentional per security policy" \ + --kind waiver +``` + +## Flags worth knowing + +**``** accepts either an id-prefix (at least 4 hex characters) or a +location string like `src/auth.rs` or `src/auth.rs:42`. The id-prefix form +matches any record in the project whose ID starts with those characters — if +more than one record matches, the command fails with a list of candidates so +you can narrow it. The location form resolves to the most-recent active record +at that location; if multiple records share the newest timestamp, the command +also fails with a disambiguation list. + +**`--kind `** overrides the default kind of `comment`. Any standard +kind (`concern`, `suggestion`, `waiver`, etc.) or custom string is accepted. +This lets a reply carry semantic weight — for example, a `waiver` reply is +meaningful to tools that consume the annotation graph. + +**`--format json`** prints the emitted record as JSON on stdout, which is +useful when you need to capture the new record's ID for a subsequent +`resolve` or `reply`. + +## Gotchas + +- A reply does **not** close the parent record. To close, use `resolve`. +- The `body.references` field is set automatically to the target record's + full ID — you do not pass `--references` yourself. +- Location resolution only considers **active** records (those not superseded + by a later annotation). If the record at a location has already been + resolved, the location will return "no active record" rather than the + resolved one. Use an id-prefix instead if you need to target a closed record. +- The minimum id-prefix length is 4 characters. Passing 3 or fewer produces a + validation error. diff --git a/src/cli/commands/agents/pages/resolve.md b/src/cli/commands/agents/pages/resolve.md new file mode 100644 index 0000000..2192533 --- /dev/null +++ b/src/cli/commands/agents/pages/resolve.md @@ -0,0 +1,79 @@ ++++ +name = "resolve" +summary = "Resolve (close) a record" +sees_also = ["reply", "record"] +since = "0.5.0" ++++ + +# qualifier resolve + +## Purpose + +Close an existing record by emitting a `resolve`-kind annotation that +supersedes it. + +## When to use it + +`resolve` is a user-directed action. As an agent, do not invoke `resolve` +unless the user has explicitly told you to close a specific annotation. +Closing a record hides it from `qualifier show`, `qualifier ls`, and other +active views; if you close a concern you do not fully understand, you +silently bury something the user may still want to act on. When you +encounter an annotation that *might* be addressable, surface it to the +user (e.g., quote it, or print `qualifier show ` for them) and +let them decide. + +When the user does direct you to resolve, this is what the command does: +it writes a new annotation with `kind: resolve` and `body.supersedes` +pointing at the target. There is no deletion; the full history is +preserved in the `.qual` file. If you instead want to *add context* to a +record (a comment, a status update, an acknowledgment that work remains), +use `reply` — `reply` does not close anything. + +## Common invocations + +```bash +# Resolve a record by id-prefix with the default message "Resolved" +qualifier resolve a1b2c3d4 + +# Resolve with a descriptive message +qualifier resolve a1b2c3d4 "Fixed in commit 3aba500 — added null check before session reset" + +# Resolve the most-recent active annotation at a location +qualifier resolve src/auth.rs:42 "Rate limit added in middleware" + +# Resolve and emit the result as JSON (to capture the new record's ID) +qualifier resolve a1b2c3d4 "Done" --format json +``` + +## Flags worth knowing + +**``** follows the same resolution rules as `reply`: an id-prefix (4+ +chars) or a location string. An id-prefix that matches more than one record, +or a location with multiple tied active records, surfaces a disambiguation +list and exits without writing. + +**`[message]`** is optional. When omitted, the resolution record's summary is +set to `"Resolved"`. Provide a message when the reason for closure carries +useful context — the message ends up in the annotation history visible to +`praise` and `show --all`. + +**`--ref `** pins a VCS commit reference (e.g., `git:3aba500`) to +the resolution record. This is useful when you want reviewers to be able to +jump to the exact commit that addressed the issue. + +**`--issuer-type ai`** should be set whenever you do close a record on +behalf of the user, so the resolution is attributable to a machine rather +than a human and the user can review what their agent closed. + +## Gotchas + +- `resolve` writes a *new* record with `body.supersedes`; it does not modify + or delete the original. `qualifier show` hides the resolved record from its + default view, but `qualifier show --all` still shows it. +- Cross-subject supersession is rejected: the target and the new resolve + record must share the same subject. +- If the target was already resolved (already superseded), the supersession + cycle check may reject the new record. Inspect with + `qualifier show --all ` first. +- The minimum id-prefix is 4 characters, same as `reply`. diff --git a/src/cli/commands/agents/pages/review.md b/src/cli/commands/agents/pages/review.md new file mode 100644 index 0000000..847e81c --- /dev/null +++ b/src/cli/commands/agents/pages/review.md @@ -0,0 +1,67 @@ ++++ +name = "review" +summary = "Check freshness of annotations against current code" +sees_also = ["show", "compact"] +since = "0.5.0" ++++ + +# qualifier review + +## Purpose + +Check whether span-bound annotations still point at the same code they were +written against, reporting each annotation as fresh, drifted, or missing. + +## When to use it + +Run `review` after editing any file that has span-addressed annotations. +When `qualifier record` writes a span annotation, it computes a +`content_hash` of the referenced lines and stores it in `body.span`. Over +time, edits shift line numbers and change content. `review` re-reads each +spanned region and compares it against the stored hash, flagging annotations +whose code has moved or changed. This is not a general record-listing +command — it only operates on active, span-addressed annotations that have a +`content_hash`. Annotations without a span (or with a span but no hash) are +silently skipped. + +## Common invocations + +```bash +# Check all span-bound annotations in the project +qualifier review + +# Check only annotations on one file +qualifier review src/auth.rs + +# Machine-readable output for scripting +qualifier review --format json +``` + +## Flags worth knowing + +**`[subject]`** is optional. When supplied, only annotations whose subject +matches the given string are checked. Useful in a pre-commit or CI step to +limit the check to files that were actually changed (e.g., iterate over +`git diff --name-only` and call `qualifier review ` per path). + +**`--format json`** emits a JSON array. Each element has `subject`, +`location` (e.g., `src/auth.rs:42`), `kind`, `summary`, `status` +(`"fresh"`, `"drifted"`, or `"missing"`), and a `detail` object with +`expected`/`actual` hashes for drifted results, or a `reason` string for +missing ones. Parse this when automating annotation triage after a refactor. + +## Gotchas + +- Only annotations with **both** a span **and** a `content_hash` are + checked. Annotations recorded without a span, or recorded using an older + version of the tool before content hashing was added, are silently skipped. + The summary line ("N annotations checked") counts only the subset that had + hashes to check. +- A `"drifted"` status means the lines still exist but their content has + changed. A `"missing"` status means the file could not be read or the + line range is now out of bounds. Neither status causes a non-zero exit + code — `review` is informational. Act on drifted or missing annotations + by re-recording them with updated spans (`qualifier record --supersedes`) + or resolving them if they are no longer relevant. +- `review` does not fix annotations automatically. It is a diagnostic + tool; remediation is the caller's responsibility. diff --git a/src/cli/commands/agents/pages/show.md b/src/cli/commands/agents/pages/show.md new file mode 100644 index 0000000..66889ea --- /dev/null +++ b/src/cli/commands/agents/pages/show.md @@ -0,0 +1,78 @@ ++++ +name = "show" +summary = "Show annotations for an artifact" +sees_also = ["ls", "praise", "review"] +since = "0.5.0" ++++ + +# qualifier show + +## Purpose + +Display the active annotations for a specific artifact, threaded by reply +chain. + +## When to use it + +`show` is the primary read command for a single artifact. It filters out +superseded records by default and renders replies indented under their parent, +giving a conversational view of all open concerns, comments, and suggestions +on that file. Use `ls` when you want a cross-artifact overview (what +artifacts have any annotations at all). Use `praise` when you want authorship +detail — who wrote each annotation and when, with issuer type shown. Use +`review` when you need to know whether span-bound annotations are still +pointing at the right code. + +## Common invocations + +```bash +# Show active annotations on a file +qualifier show src/auth.rs + +# Show with source context rendered inline (compiler-diagnostic style) +qualifier show src/auth.rs --pretty + +# Show all records including resolved ones +qualifier show src/auth.rs --all + +# Programmatic output — returns JSON object with a "records" array +qualifier show src/auth.rs --format json + +# Show only epoch records for an artifact +qualifier show src/auth.rs --type epoch +``` + +## Flags worth knowing + +**`--format json`** emits a JSON object `{"subject": "...", "records": [...]}`. +Each record is the full envelope and body. This is the right mode when an +agent needs to inspect annotation IDs, `body.references` chains, or span +details programmatically. + +**`--pretty`** adds source context around each span-addressed annotation, +rendered in the same style as compiler diagnostics (filename, line numbers, +and a few lines of surrounding code). Useful for human review; for agents +parsing output, `--format json --pretty` adds a `"context"` field to each +span record. + +**`--all`** disables the default filter that hides superseded records and +`resolve` tombstones. Use it when you need to audit the full annotation +history or diagnose a supersession chain. + +**`--type `** filters by envelope record type (`annotation`, `epoch`, +`dependency`, or any custom URI). This is distinct from filtering by +annotation `kind` — `--type epoch` shows epoch records; `--kind concern` +is not a flag on `show` (use `ls --kind` for kind-based filtering across +artifacts). + +## Gotchas + +- `show` exits with an error if no records are found for the artifact. An + empty result is not silent — test the exit code when scripting. +- Dependency records are hidden in human output (they are graph metadata, not + quality signals) but appear in `--format json` output. +- Resolved records are hidden by default. If an artifact appears to have no + annotations but you expect some, try `--all` to check whether they were + resolved. +- The `artifact` argument is the subject name as stored in the `.qual` file + (e.g., `src/auth.rs`), not a filesystem glob. There is no wildcard matching. diff --git a/src/cli/commands/agents/pages/workflows.md b/src/cli/commands/agents/pages/workflows.md new file mode 100644 index 0000000..a61a4aa --- /dev/null +++ b/src/cli/commands/agents/pages/workflows.md @@ -0,0 +1,117 @@ ++++ +name = "workflows" +summary = "Worked recipes for common tasks" +since = "0.5.0" ++++ + +# qualifier — common workflows + +## Record a finding during code review + +When you notice a concern, smell, or risk in code you're reviewing, record it +immediately. Use `--span` to pin it to the exact lines so it stays actionable +after future edits, and `--suggested-fix` if you know what to do. + +```bash +qualifier record concern src/auth.rs:42:58 \ + "Token validation skips expiry check when issuer is internal" \ + --suggested-fix "Check exp claim unconditionally; remove the issuer shortcut" \ + --tag security \ + --issuer "mailto:review-agent@example.com" \ + --issuer-type ai +``` + +The CLI writes to `src/.qual` (or `src/auth.rs.qual` if it exists), prints +the new record's id, and exits zero. Use `qualifier show src/auth.rs` to +verify the annotation is visible. + +## Reply to an existing observation with new info + +When a record already exists and you have additional context — a root cause, +a related finding, a status update — reply rather than recording a new +standalone annotation. A reply threads to the original so readers see the +conversation together. + +`qualifier reply` accepts either a 4+ character id-prefix or a location +(`file:line`). The default kind is `comment`; override with `--kind`. + +```bash +# The original concern has id starting with a1b2c3d4 +qualifier reply a1b2 \ + "Root cause: the issuer allow-list is populated from an env var that CI never sets" \ + --issuer "mailto:review-agent@example.com" \ + --issuer-type ai + +# Or target by location if you know the span +qualifier reply src/auth.rs:42 \ + "Root cause: the issuer allow-list is populated from an env var that CI never sets" \ + --issuer "mailto:review-agent@example.com" \ + --issuer-type ai +``` + +The id-prefix form matches by prefix. The location form resolves to the most-recent active record at that location. +If multiple records share the newest timestamp, the CLI exits non-zero with +a disambiguation list showing id-prefix, kind, line, and summary. + +## Surface drifted annotations after a refactor + +Span-addressed annotations include a `content_hash` (BLAKE3 of the spanned +lines at write time). After a refactor, those hashes may no longer match the +current file content. `qualifier review` checks this and reports each +annotation's freshness status. As an agent, your job here is to surface +the results to the user. Closing or rewriting a drifted annotation +requires understanding the original concern; the user is better +positioned to make that call. + +```bash +# Check all annotations in the project +qualifier review + +# Check a specific subject +qualifier review src/auth.rs + +# Machine-readable output for programmatic triage +qualifier review --format json +``` + +Possible statuses: + +- `FRESH` — the spanned lines are unchanged. No action needed. +- `DRIFTED` — the lines changed. Surface the annotation to the user and + let them decide whether it still applies and what to do about it. +- `MISSING` — the file or span no longer exists. Surface this to the + user; do not assume the annotation is stale. + +Annotations without a span or without a `content_hash` are not checked. +Whole-file annotations and older records written without `--span` fall into +this category. + +## Compact a noisy `.qual` file + +Append-only files accumulate superseded records over time. `qualifier compact` +prunes them. With `--snapshot`, it goes further and collapses all surviving +records for each subject into a single epoch record, making the file minimal. + +Pass the artifact (subject) name — typically the same path you used when recording — not the path to a .qual file. + +```bash +# Preview without writing +qualifier compact src/auth.rs --dry-run + +# Prune superseded records only +qualifier compact src/auth.rs + +# Collapse to a single epoch per subject (smallest possible file) +qualifier compact src/auth.rs --snapshot + +# Compact every .qual file in the project +qualifier compact --all + +# Compact everything, preview first +qualifier compact --all --dry-run +``` + +Compaction is always explicit and user-initiated; it never happens silently. +Records of unrecognized types are preserved unchanged. After compaction the +file is still valid JSONL — no special reader support is needed. VCS history +retains the full pre-compaction records if you need to trace back. diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs index 84f2e67..5dbe398 100644 --- a/src/cli/commands/mod.rs +++ b/src/cli/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod agents; pub mod compact; pub mod emit; pub mod freshness; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a8454be..d85980b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -14,6 +14,9 @@ const HELP_TEMPLATE: &str = "\ {about-with-newline} {usage-heading} {usage} +For AI agents: + agents Self-contained guide for AI coding agents (start here) + Record observations: record Record an annotation: `qualifier record [message]` reply Reply to an existing record (id-prefix or location) @@ -53,6 +56,9 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { + /// Self-contained guide for AI coding agents (start here) + Agents(commands::agents::Args), + /// Record an annotation: `qualifier record [message]` Record(Box), /// Reply to an existing record (id-prefix or location) @@ -92,6 +98,7 @@ pub fn run() { } let result: crate::Result<()> = match cli.command { + Commands::Agents(args) => commands::agents::run(args), Commands::Record(args) => commands::record::run(*args), Commands::Reply(args) => commands::reply::run(args), Commands::Resolve(args) => commands::resolve::run(args), diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index be42fdb..b466af1 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -2314,3 +2314,123 @@ fn test_compact_preserves_unknown_record_type() { "snapshot should produce an epoch record:\n{after_snapshot}" ); } + +// --- qualifier agents --- + +#[test] +fn test_agents_bare_invocation_succeeds() { + let dir = tempfile::tempdir().unwrap(); + let (stdout, stderr, code) = run_qualifier(dir.path(), &["agents"]); + assert_eq!(code, 0, "agents should succeed: stderr={stderr}"); + assert!(!stdout.is_empty(), "agents should print something"); +} + +#[test] +fn test_agents_unknown_topic_exits_2() { + let dir = tempfile::tempdir().unwrap(); + let (_stdout, stderr, code) = run_qualifier(dir.path(), &["agents", "bogus-topic"]); + assert_eq!(code, 2, "unknown topic should exit 2: stderr={stderr}"); + assert!( + stderr.contains("no such topic"), + "stderr should explain: {stderr}" + ); + assert!( + stderr.contains("bogus-topic"), + "stderr should name the bad topic: {stderr}" + ); +} + +#[test] +fn test_agents_all_registered_topics_render() { + // Each topic in the registry must produce non-empty stdout with exit 0. + // If you add a topic, add it here too. + let topics = [ + "concepts", + "workflows", + "pitfalls", + "record", + "reply", + "resolve", + "emit", + "show", + "ls", + "praise", + "review", + "compact", + ]; + let dir = tempfile::tempdir().unwrap(); + for topic in topics { + let (stdout, stderr, code) = run_qualifier(dir.path(), &["agents", topic]); + assert_eq!(code, 0, "agents {topic} should succeed: stderr={stderr}"); + assert!(!stdout.is_empty(), "agents {topic} should print body"); + } +} + +#[test] +fn test_agents_overview_renders_topics_index() { + let dir = tempfile::tempdir().unwrap(); + let (stdout, _stderr, code) = run_qualifier(dir.path(), &["agents"]); + assert_eq!(code, 0); + // Every registered topic name should appear in the rendered overview. + for topic in [ + "concepts", + "workflows", + "pitfalls", + "record", + "reply", + "resolve", + "emit", + "show", + "ls", + "praise", + "review", + "compact", + ] { + assert!( + stdout.contains(topic), + "overview should mention topic '{topic}': {stdout}" + ); + } + // The literal sentinel must not leak through. + assert!( + !stdout.contains("{{TOPICS}}"), + "sentinel should be substituted: {stdout}" + ); +} + +#[test] +fn test_agents_orientation_summaries_match_pages() { + // Lock in the contract that the orientation page renders the topic + // index from frontmatter summaries (rather than hard-coded ones in + // mod.rs). Each topic's summary must appear in bare-agents output. + let dir = tempfile::tempdir().unwrap(); + let (stdout, _stderr, code) = run_qualifier(dir.path(), &["agents"]); + assert_eq!(code, 0); + for needle in [ + "Annotation model, kinds, supersession", // concepts + "Worked recipes for common tasks", // workflows + "Common mistakes agents make with qualifier", // pitfalls + "Record a new annotation", // record + ] { + assert!( + stdout.contains(needle), + "orientation should include summary '{needle}': {stdout}" + ); + } +} + +#[test] +fn test_top_level_help_shows_agents_group() { + let dir = tempfile::tempdir().unwrap(); + let (stdout, _stderr, code) = run_qualifier(dir.path(), &["--help"]); + assert_eq!(code, 0); + assert!( + stdout.contains("For AI agents:"), + "help should show the agents group header: {stdout}" + ); + // The agents row should appear under that header, with the "start here" nudge. + assert!( + stdout.contains("agents") && stdout.contains("start here"), + "help should mention the agents subcommand: {stdout}" + ); +}