1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
use clap::{Parser, Subcommand, ValueEnum, ValueHint};
use clap_complete::Shell;
use crate::principles::registry::ExceptionCategory;
use crate::skill_install::SkillHost;
#[derive(Parser)]
#[command(name = "anc", version, about = "The agent-native CLI linter")]
#[command(arg_required_else_help = true)]
#[command(
long_about = "The agent-native CLI linter — audits a CLI tool against the agent-readiness spec.
Runs three layers of audits against a target: behavioral (spawn the binary and inspect output), source (ast-grep over Rust/Python files), and project (manifest, completions, bundle presence). The result is a scorecard you can read interactively (text mode) or pipe into another tool (`--output json`).
Default output format is text; color and progress affordances auto-detect the TTY and disappear when stdout is piped or redirected (NO_COLOR-compatible). Use `--verbose` / `-v` to escalate diagnostic detail when debugging unexpected results.
Input model: targets are passed as positional path arguments or via `--command <name>`. Stdin is not consumed; `-` is reserved and behaves like a literal filename rather than a stdin sentinel."
)]
#[command(after_help = "Examples:
anc audit . # human scorecard for the current project
anc audit . --output json # JSON envelope for agents (--json works too)
anc audit --command ripgrep # audit a PATH-resolved binary by name
anc emit coverage-matrix # emit the spec coverage matrix
anc emit schema # print the scorecard JSON Schema
anc skill install claude_code # install the bundle into Claude Code
When the first argument is not a subcommand, `audit` is inserted automatically:
anc . ≡ anc audit .
anc --command ripgrep ≡ anc audit --command ripgrep
Bare `anc` (no arguments) prints this help and exits 2 — a deliberate guard
that prevents recursive self-invocation when agentnative audits itself.")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
/// Suppress non-essential output. Default: false (warnings and progress
/// notes are written to stderr).
#[arg(long, short = 'q', global = true, env = "AGENTNATIVE_QUIET")]
pub quiet: bool,
/// Escalate diagnostic detail. `-v` is shorthand for `--verbose`.
/// Mutually exclusive with `--quiet`; the last flag on the command line
/// wins when both appear.
#[arg(
long,
short = 'v',
global = true,
env = "AGENTNATIVE_VERBOSE",
conflicts_with = "quiet"
)]
pub verbose: bool,
/// Print a curated examples block and exit. Pairs with `--output json`
/// (or `--json`) so structured-output consumers can fetch the examples
/// without parsing the full `--help` body.
#[arg(long, global = true)]
pub examples: bool,
/// Emit JSON output. Short alias for `--output json` on subcommands that
/// support it. Per the agent-native convention (`p2-should-json-aliases`),
/// the short form works alongside the canonical `--output` enum.
#[arg(long, global = true)]
pub json: bool,
/// Color control for text output. `auto` (default) emits ANSI styling
/// when stdout is a terminal and `NO_COLOR` is unset. `always` forces
/// styling on; `never` strips it. Honors the `NO_COLOR` environment
/// variable in `auto` mode (https://no-color.org/).
#[arg(
long,
global = true,
value_name = "WHEN",
default_value = "auto",
env = "AGENTNATIVE_COLOR"
)]
pub color: ColorChoice,
/// Strip section headers, evidence lines, summary line, and badge hint
/// — emit only `id<TAB>status` per audit. Pipe-safe for grep, awk, and
/// downstream tooling that wants the raw verdict stream without prose.
/// Ignored in `--output json` mode.
#[arg(long, global = true)]
pub raw: bool,
}
/// `--color auto|always|never` — choice of when to emit ANSI styling.
/// Aligns with the cargo / rustc convention. `auto` consults TTY detection
/// and `NO_COLOR` at output time; the choice itself stays inert until
/// `format_text` queries `should_color()`.
#[derive(Clone, Copy, Debug, ValueEnum, Default, PartialEq, Eq)]
#[value(rename_all = "kebab-case")]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
#[derive(Subcommand)]
pub enum Commands {
/// Audit a CLI project or binary for agent-readiness
///
/// Reads the target's project layout (Cargo.toml / pyproject.toml),
/// language detection, and binary discovery. Stdin is not consumed.
/// Pass the target as a positional argument or via `--command <name>`
/// to resolve from PATH. `-` is reserved and behaves like any other
/// path argument (no special stdin meaning).
#[command(after_help = "Examples:
anc audit . # default: project at cwd
anc audit . --output json # JSON envelope for agents
anc audit . --output json --principle 2 # filter to P2 (Structured Output)
anc audit --command ripgrep # PATH-resolved binary
anc audit ./target/release/anc --binary # behavioral audits only
Defaults: path = `.`, output = text, no principle filter.")]
Audit {
/// Path to project directory or binary
#[arg(default_value = ".")]
path: std::path::PathBuf,
/// Resolve a command from PATH and run behavioral audits against it
#[arg(
long,
value_name = "NAME",
value_hint = ValueHint::CommandName,
conflicts_with = "path",
conflicts_with = "source",
)]
command: Option<String>,
/// Run only behavioral audits (skip source analysis)
#[arg(long)]
binary: bool,
/// Run only source audits (skip behavioral)
#[arg(long)]
source: bool,
/// Filter audits by principle number (1-8)
#[arg(long)]
principle: Option<u8>,
/// Output format
#[arg(long, default_value = "text")]
output: OutputFormat,
/// Include test code in source analysis
#[arg(long)]
include_tests: bool,
/// Exemption category for the target. Suppresses audits that do not
/// apply to this class of tool — e.g., TUI apps legitimately
/// intercept the TTY, so `--audit-profile human-tui` skips the
/// interactive-prompt MUSTs. Suppressed audits emit `Skip` with
/// structured evidence so readers see what was excluded.
#[arg(long, value_name = "CATEGORY")]
audit_profile: Option<AuditProfile>,
},
/// Generate shell completions
Completions {
/// Shell to generate for
shell: Shell,
},
/// Render build artifacts (coverage matrix, scorecard schema)
#[command(after_help = "Examples:
anc emit coverage-matrix # write docs/coverage-matrix.md + coverage/matrix.json
anc emit coverage-matrix --check # CI drift guard (non-zero on mismatch)
anc emit coverage-matrix --out /tmp/cov.md # custom output path
anc emit schema # print the scorecard JSON Schema to stdout
anc emit schema | jq '.title' # pipe into jq for inspection")]
Emit {
#[command(subcommand)]
artifact: EmitKind,
},
/// Install or manage the agentnative skill bundle
///
/// Namespace for bundle operations. `anc skill install <host>` clones
/// the agentnative-skill bundle into a host's canonical skills
/// directory; `anc skill update <host>` refreshes an existing install.
#[command(after_help = "Examples:
anc skill install claude_code # install bundle to Claude Code
anc skill install claude_code --dry-run # print the git command without spawning
anc skill install --all # install across every known host
anc skill update claude_code # refresh an existing install
anc skill install codex --output json # JSON envelope for agent consumption")]
Skill {
#[command(subcommand)]
cmd: SkillCmd,
},
}
#[derive(Subcommand)]
pub enum SkillCmd {
/// Install the skill bundle into a host's canonical skills directory.
///
/// If the site adds a host before this `anc` release knows about it, run
/// the manual fallback printed by `--dry-run` for any known host and
/// substitute the destination path:
///
/// git clone --depth 1 https://github.com/brettdavies/agentnative-skill.git <dest>
Install {
/// Target host (claude_code, codex, cursor, opencode). Required
/// unless `--all` is set.
host: Option<SkillHost>,
/// Install into every known host in one invocation.
#[arg(long, conflicts_with = "host")]
all: bool,
/// Print the resolved git command without spawning. Captures cleanly
/// via `eval $(anc skill install --dry-run <host>)`.
#[arg(long)]
dry_run: bool,
/// Output format for the result envelope.
#[arg(long, default_value = "text")]
output: OutputFormat,
},
/// Refresh an installed skill bundle to the latest upstream revision.
Update {
/// Target host. Required unless `--all` is set.
host: Option<SkillHost>,
/// Refresh every known host in one invocation.
#[arg(long, conflicts_with = "host")]
all: bool,
/// Print the resolved commands without spawning.
#[arg(long)]
dry_run: bool,
/// Output format for the result envelope.
#[arg(long, default_value = "text")]
output: OutputFormat,
},
}
#[derive(Subcommand)]
pub enum EmitKind {
/// Render the spec coverage matrix (registry → audits → artifact).
CoverageMatrix {
/// Path for the Markdown artifact. Defaults to `docs/coverage-matrix.md`.
#[arg(long, value_name = "PATH", default_value = "docs/coverage-matrix.md")]
out: std::path::PathBuf,
/// Path for the JSON artifact. Defaults to `coverage/matrix.json`.
#[arg(
long = "json-out",
value_name = "PATH",
default_value = "coverage/matrix.json"
)]
json_out: std::path::PathBuf,
/// Exit non-zero when committed artifacts differ from rendered output. CI drift guard.
#[arg(long)]
check: bool,
},
/// Print the scorecard JSON Schema (draft 2020-12) to stdout.
///
/// Consumers (site renderer, leaderboards, agent integrations) validate
/// scorecards against this contract instead of inferring shape from
/// sample output. The schema is the same document committed at
/// `schema/scorecard.schema.json` in this repo.
Schema,
}
#[derive(Clone, ValueEnum)]
pub enum OutputFormat {
Text,
Json,
}
/// Exemption category for `--audit-profile`. Mirrors
/// `ExceptionCategory` in the registry one-to-one; the `From` impl below
/// converts between them at the call site. Kept as a CLI-owned type so
/// clap controls the surface (`value_enum` validation, shell completions)
/// without leaking clap into the registry module.
#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)]
#[value(rename_all = "kebab-case")]
pub enum AuditProfile {
/// TUI-by-design tools (lazygit, k9s, btop). Suppresses
/// interactive-prompt MUSTs and SIGPIPE — their contract is the TTY.
HumanTui,
/// File-traversal utilities (fd, find). Reserved for subcommand-structure
/// relaxations as those audits land.
FileTraversal,
/// POSIX utilities (cat, sed, awk). P1 interactive-prompt MUSTs
/// satisfied vacuously via stdin-primary input.
PosixUtility,
/// Diagnostic tools (nvidia-smi, vmstat). No write operations, so the
/// P5 mutation-boundary MUSTs do not apply.
DiagnosticOnly,
}
impl From<AuditProfile> for ExceptionCategory {
fn from(p: AuditProfile) -> Self {
match p {
AuditProfile::HumanTui => ExceptionCategory::HumanTui,
AuditProfile::FileTraversal => ExceptionCategory::FileTraversal,
AuditProfile::PosixUtility => ExceptionCategory::PosixUtility,
AuditProfile::DiagnosticOnly => ExceptionCategory::DiagnosticOnly,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::principles::registry::ALL_EXCEPTION_CATEGORIES;
use clap::ValueEnum;
/// Every CLI `AuditProfile` variant must map to a distinct
/// `ExceptionCategory`, and every `ExceptionCategory` must be
/// reachable from at least one `AuditProfile` variant. Failing this
/// test means adding a category on one side without the other —
/// either the CLI accepts a profile that suppresses nothing, or the
/// registry defines a category no CLI user can reach.
#[test]
fn audit_profile_and_exception_category_variants_are_isomorphic() {
let cli_mapped: std::collections::HashSet<&'static str> = AuditProfile::value_variants()
.iter()
.map(|v| ExceptionCategory::from(*v).as_kebab_case())
.collect();
let registry_kebab: std::collections::HashSet<&'static str> = ALL_EXCEPTION_CATEGORIES
.iter()
.map(|c| c.as_kebab_case())
.collect();
assert_eq!(
cli_mapped, registry_kebab,
"AuditProfile (cli) and ExceptionCategory (registry) variants must be isomorphic. \
CLI-reachable: {cli_mapped:?}, registry: {registry_kebab:?}",
);
}
/// The kebab-case string clap renders for each `AuditProfile` variant
/// must equal the kebab-case the registry emits — otherwise the flag
/// value a user types on the CLI won't match the `audit_profile` field
/// echoed in JSON output.
#[test]
fn audit_profile_clap_name_matches_registry_kebab_case() {
for variant in AuditProfile::value_variants() {
let clap_name = variant
.to_possible_value()
.expect("AuditProfile variants have clap names")
.get_name()
.to_string();
let registry_name = ExceptionCategory::from(*variant).as_kebab_case();
assert_eq!(
clap_name, registry_name,
"clap value name and registry kebab-case must match for every variant",
);
}
}
}