diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c3c07f8..39ff473 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1,8 @@ -# Mandatory review on anything that runs on user machines at install time, and -# on producer ops that determine what ships. -# See: master plan risk row "Shell scripts in producer repo execute on user machines". +# Mandatory review on producer-ops paths that determine what ships and how. +# The bundle/ tree is content (markdown + templates) and is reviewed via normal +# PR rules; bundle/spec/ specifically is vendored and not edited by hand +# (see scripts/sync-spec.sh and bundle/spec/README.md). -bundle/scripts/** @brettdavies scripts/** @brettdavies .github/workflows/** @brettdavies .github/rulesets/** @brettdavies diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 90919fc..d270679 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,39 +1,47 @@ --- name: Bug report -about: A check produces wrong results, a script fails, or a doc says one thing while the code does another. +about: A bundle file has a wrong example, a stale path, a broken cross-reference, or contradicts the spec. title: "bug: " labels: bug --- + + ## What happened - + ## What you expected - + ## How to reproduce 1. 1. 1. ```bash -# exact commands; redact paths/credentials +# exact commands or quoted bundle content; redact paths/credentials ``` ## Environment - Bundle version (`cat bundle/VERSION` if cloned, or the tag you installed): vX.Y.Z +- Pinned spec version (`cat bundle/spec/VERSION`): vX.Y.Z - Host (Claude Code / Cursor / Codex / other): -- Target tool the checker was run against (if applicable): owner/repo @ commit +- `anc --version` (if a workflow involving anc is at issue): - OS and shell: -- `bundle/scripts/check-compliance.sh --principle N` output (if relevant): - -```text - -``` -## Why this is a bundle bug, not a target-tool bug +## Why this is a bundle bug, not a spec or anc bug - + diff --git a/.github/ISSUE_TEMPLATE/bundle_proposal.md b/.github/ISSUE_TEMPLATE/bundle_proposal.md new file mode 100644 index 0000000..6479eca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bundle_proposal.md @@ -0,0 +1,66 @@ +--- +name: Bundle proposal +about: Propose a new template, reference doc, getting-started flow, or other change to the skill bundle. +title: "proposal: " +labels: proposal +--- + + + +## Problem statement + + + +## Proposal + + + +## Type of change + +- [ ] New starter template under `bundle/templates/` +- [ ] New reference doc under `bundle/references/` +- [ ] Update to `bundle/SKILL.md` (entry-point structure or routing) +- [ ] Update to `bundle/getting-started.md` (new flow, new invocation) +- [ ] Idioms for a new language/framework in `bundle/references/framework-idioms-other-languages.md` +- [ ] Other (describe) + +## Prior art + + + +- - + +## Draft of the change + + + +```diff + +``` + +## Compatibility + +- [ ] Additive — no existing bundle content needs to change +- [ ] Replaces existing content — list what gets removed/superseded +- [ ] Coordinated with a spec or anc change — link the upstream issue/PR + +## Open questions + + + +- diff --git a/.github/ISSUE_TEMPLATE/principle_proposal.md b/.github/ISSUE_TEMPLATE/principle_proposal.md deleted file mode 100644 index 7f233a9..0000000 --- a/.github/ISSUE_TEMPLATE/principle_proposal.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: Principle proposal -about: Propose a new principle, a substantive change to an existing principle, a new check, or a new template. -title: "proposal: " -labels: proposal ---- - -## Problem statement - - - -## Proposal - - - -## Type of change - -- [ ] New principle (P8 or later) -- [ ] Tightening an existing principle's MUST/SHOULD/MAY semantics -- [ ] Removing or relaxing an existing principle (major version bump) -- [ ] New automated check under `bundle/scripts/checks/` -- [ ] New template under `bundle/templates/` -- [ ] Other (describe) - -## Prior art - - - -- - - -## Draft of the change - -### `bundle/SKILL.md` diff sketch - -```diff - -``` - -### `bundle/references/principles-deep-dive.md` diff sketch - -```diff - -``` - -### Check / template additions - - - -## Backward compatibility - -- [ ] Backward-compatible (existing tools that pass today still pass after this change) -- [ ] Tightens — some currently-passing tools would start failing or warning. List representative examples: -- [ ] Breaking — explicit major version bump justified because: - -## Open questions - - - -- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0876fc..2876d28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,17 +41,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Run ShellCheck (bundle scripts — ship to consumers) - uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 - env: - # style severity surfaces all suggestions; CI fails on error+warning by default - SHELLCHECK_OPTS: "--severity=style" - with: - scandir: ./bundle/scripts - additional_files: bundle/scripts/checks/_helpers.sh - name: Run ShellCheck (producer scripts) + # The bundle is markdown-only (spec, references, templates, getting-started). + # Only producer-ops scripts under ./scripts/ are subject to shellcheck. uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 env: + # style severity surfaces all suggestions; CI fails on error+warning by default SHELLCHECK_OPTS: "--severity=style" with: scandir: ./scripts diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 1685e69..351dff1 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -80,6 +80,9 @@ ignores: - "target/**" # Rust build artifacts; harmless for non-Rust projects - ".git/**" - "*.min.md" + # Vendored agentnative-spec content. Edited upstream; we do not enforce + # this repo's lint rules on it. Resync via scripts/sync-spec.sh. + - "bundle/spec/CHANGELOG.md" # Fix automatically when --fix is used fix: true diff --git a/.shellcheckrc b/.shellcheckrc deleted file mode 100644 index 7e371dd..0000000 --- a/.shellcheckrc +++ /dev/null @@ -1,23 +0,0 @@ -# Shellcheck configuration for the agent-native-cli skill bundle. -# -# The bundle is a byte-faithful copy from a longer-lived private source; existing -# style-level findings are tolerated rather than rewritten in v0.1.0. Each disable -# below is narrowly scoped and explained. Re-evaluate when the underlying scripts -# are next refactored (post-bootstrap, on a feat/* branch). - -# SC1091: "Not following: ./_helpers.sh was not specified as input." -# Every check-*.sh sources scripts/checks/_helpers.sh at runtime. The path is -# correct; shellcheck cannot verify dynamic sources without -x. The runtime -# behaviour is exercised by scripts/check-compliance.sh. -disable=SC1091 - -# SC2034: " appears unused." -# _helpers.sh exports common variables (e.g., SRC_DIR) that individual checks -# reference selectively. Unused-by-this-file is the intended pattern for a -# shared sourced helper. -disable=SC2034 - -# SC2125: "Brace expansions and globs are literal in assignments." -# scripts/check-compliance.sh stores a glob expression and lets the shell -# expand it at the use site. The behaviour is intentional and tested. -disable=SC2125 diff --git a/AGENTS.md b/AGENTS.md index 3a2b7a2..cd2d83a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,40 +2,56 @@ Project-level agent instructions for `agentnative-skill` — the producer repo for the `agent-native-cli` skill bundle. -This repo is **not** a Rust CLI tool. It is a content + script bundle plus producer-side ops scaffolding. The bundle -defines the standard for agent-native CLI design and ships an automated compliance checker that consumers run against -their own tools. +This repo is **not** a Rust CLI tool and **not** a compliance checker. It is the agent-facing guide that pairs with +[`anc`](https://github.com/brettdavies/agentnative-cli) (the canonical checker) and +[`agentnative-spec`](https://github.com/brettdavies/agentnative) (the canonical principle text, vendored at +`bundle/spec/`). The bundle teaches agents how to use `anc` and supplies the surrounding context — spec, idioms, +templates — that `anc` findings reference. ## Layout The repo is split into **the bundle** (what consumers install) and **producer-side ops** (governance, CI, plans). Consumers only see the bundle. -| Path | Ships to consumers? | Purpose | -| -------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | -| `bundle/SKILL.md` | ✓ | The standard itself: 7 agent-readiness principles, when to trigger, how to use. | -| `bundle/checklists/` | ✓ | Task-shaped checklists for downstream consumers (e.g., `new-tool.md`). | -| `bundle/references/` | ✓ | Deep-dive references: principle specifications, framework idioms, project structure, Rust/clap patterns. | -| `bundle/scripts/check-compliance.sh` | ✓ | Driver script that runs all 24 compliance checks against a target Rust CLI repo. | -| `bundle/scripts/checks/` | ✓ | Individual check scripts (`check-p1-*.sh`, `check-p4-*.sh`, etc.) plus shared `_helpers.sh`. | -| `bundle/templates/` | ✓ | Drop-in starting points for downstream tools (`agents-md-template.md`, clap main, error types, output format). | -| `AGENTS.md`, `RELEASES.md`, `CONTRIBUTING.md` | — | Producer-repo docs. Not part of the skill. | -| `.github/rulesets/` | — | Version-controlled GitHub repository rulesets (applied post-public-flip — see `.github/rulesets/README.md`). | -| `.github/workflows/` | — | CI: markdownlint, shellcheck. Plus `guard-main-docs.yml` to keep engineering docs off `main`. | -| `.github/ISSUE_TEMPLATE/` | — | Bug report + principle proposal templates. | -| `docs/plans/` | — | Engineering plans (`dev`-only — guarded out of `main`). | -| `.markdownlint-cli2.yaml`, `.shellcheckrc`, `.gitattributes`, `.gitignore` | — | Local lint configs and repo metadata. | +| Path | Ships to consumers? | Purpose | +| ---------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `bundle/SKILL.md` | ✓ | Skill metadata + entry-point pointer to `getting-started.md`. | +| `bundle/getting-started.md` | ✓ | Three working loops (existing CLI / new Rust / other language); canonical `anc check` invocations. | +| `bundle/spec/` | ✓ | Vendored copy of `agentnative-spec` at a pinned ref. Canonical principle text + machine-readable `requirements[]`. | +| `bundle/references/` | ✓ | Implementation guidance: framework idioms (Rust + others), project structure, Rust/clap patterns. | +| `bundle/templates/` | ✓ | Drop-in starter files for greenfield Rust CLIs (`clap-main.rs`, `error-types.rs`, `output-format.rs`, `agents-md-template.md`). | +| `scripts/sync-spec.sh` | — | Vendor `agentnative-spec` into `bundle/spec/` at a pinned `SPEC_REF`. Mirror of the agentnative-cli script. | +| `scripts/generate-changelog.sh` | — | Release-time CHANGELOG generator (git-cliff + PR-body extraction). | +| `AGENTS.md`, `RELEASES.md`, `CONTRIBUTING.md` | — | Producer-repo docs. Not part of the skill. | +| `.github/rulesets/` | — | Version-controlled GitHub repository rulesets (applied post-public-flip — see `.github/rulesets/README.md`). | +| `.github/workflows/` | — | CI: markdownlint, shellcheck. Plus `guard-main-docs.yml` to keep engineering docs off `main`. | +| `.github/ISSUE_TEMPLATE/` | — | Bug report + principle proposal templates. | +| `docs/plans/` | — | Engineering plans (`dev`-only — guarded out of `main`). | +| `.markdownlint-cli2.yaml`, `.shellcheckrc`, `.gitattributes`, `.gitignore`, `cliff.toml` | — | Local lint configs, git-cliff config, and repo metadata. | ## Lint & Format ```bash markdownlint-cli2 '**/*.md' '!node_modules/**' -shellcheck --severity=style bundle/scripts/check-compliance.sh bundle/scripts/checks/*.sh +shellcheck --severity=style scripts/*.sh actionlint .github/workflows/*.yml ``` -The repo ships a local `.markdownlint-cli2.yaml` (canonical 120-char line length) and `.shellcheckrc` (three narrow -disables documented inline) so CI and local tooling agree. +The repo ships a local `.markdownlint-cli2.yaml` (canonical 120-char line length) and `.shellcheckrc` so CI and local +tooling agree. The bundle has no shell scripts to lint — `anc` is the checker and lives in its own repo. + +## Spec sync + +The canonical principle text lives in [`brettdavies/agentnative`](https://github.com/brettdavies/agentnative). This repo +vendors it via `scripts/sync-spec.sh` at a pinned `SPEC_REF`. To bump: + +```bash +SPEC_REF=v0.2.1 scripts/sync-spec.sh # pulls from $HOME/dev/agentnative-spec by default +git diff bundle/spec/ # review +``` + +Then commit the result with a message like `chore: bump bundle/spec to agentnative-spec@v0.2.1`. The current pin is +recorded in [`bundle/spec/README.md`](./bundle/spec/README.md) and the version itself is in `bundle/spec/VERSION`. ## Branch + release model @@ -52,15 +68,17 @@ table. ## What an agent should NEVER do -- Edit shell scripts in `bundle/scripts/checks/` casually. They run on user machines at install time. CODEOWNERS gates - `bundle/scripts/**` and `.github/workflows/**` for that reason. +- Edit anything under `bundle/spec/` by hand. It is vendored from `agentnative-spec`. Any required change is a PR + against the spec repo, then a `scripts/sync-spec.sh` bump here. +- Reimplement `anc`. The bundle does not contain shell-script duplicates of `anc`'s checks. If you find yourself writing + `rg`-based grep checks, you're rebuilding what `anc` already does — use `anc check --output json` instead. - Commit anything under `docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, or `docs/reviews/` directly to a `release/*` branch — those paths are filtered by the cherry-pick pattern. Add to `dev` instead. - Modify `bundle/SKILL.md`'s `name` or `description` frontmatter without coordinating with consumers — those fields drive skill discovery on every host. - Re-tag a published version. Tags are immutable historical anchors that the install endpoints pin to. - Add Rust/Cargo scaffolding. There is no Rust code in this repo and there should be none — the standard is - language-prescriptive but the bundle itself is shell + markdown. + language-prescriptive but the bundle itself is markdown. - Move producer-ops files into `bundle/`. The split exists deliberately so consumers don't pull repo-management artifacts into their skills directories. @@ -70,12 +88,16 @@ table. etc.). This top-level `AGENTS.md` describes the producer repo and is intentionally different. - `markdownlint-cli2` does NOT consult a global config — every repo needs its own `.markdownlint-cli2.yaml`. If line wrapping looks wrong, the local copy has drifted from `~/.markdownlint-cli2.yaml`. -- The `_helpers.sh` file in `bundle/scripts/checks/` is sourced (`source _helpers.sh`), not executed. It is - intentionally not marked +x. +- `bundle/spec/` is a vendored copy, not a symlink or submodule. Stale orphan files can appear if the upstream spec + renames or removes a principle. `git status` after `scripts/sync-spec.sh` surfaces them; resolve by deletion. +- `CHANGELOG.md` is generated by `scripts/generate-changelog.sh` (git-cliff + PR-body extraction). Never hand-edit it — + fix the input (PR body's `## Changelog` section) and re-run. ## References -- [`bundle/SKILL.md`](./bundle/SKILL.md) — the standard +- [`bundle/SKILL.md`](./bundle/SKILL.md) — skill entry point +- [`bundle/getting-started.md`](./bundle/getting-started.md) — agent's three working loops +- [`bundle/spec/README.md`](./bundle/spec/README.md) — vendored-spec resync procedure - [`README.md`](./README.md) — what this repo is, repo layout, install pointer - [`SECURITY.md`](./SECURITY.md) — vulnerability disclosure - [`RELEASES.md`](./RELEASES.md) — release procedure diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 915cd48..095e29a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,24 @@ # Contributing to `agentnative-skill` -Thanks for your interest. This repo defines a north-star standard for agent-native CLI tools and ships an automated -compliance checker. Substantive proposals should engage with the principles directly. +Thanks for your interest. This repo is the agent-facing skill bundle that pairs with two siblings: -## Where to start +- [`agentnative`](https://github.com/brettdavies/agentnative) (the spec) — canonical principle text. Vendored here at + `bundle/spec/`. +- [`agentnative-cli`](https://github.com/brettdavies/agentnative-cli) (`anc`) — the canonical compliance checker. -| You want to… | Read first | -| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| Understand the standard | [`bundle/SKILL.md`](./bundle/SKILL.md) and [`bundle/references/principles-deep-dive.md`](./bundle/references/principles-deep-dive.md) | -| File a bug in the compliance checker or a check | Open an issue with the **Bug report** template | -| Propose a new principle, check, or template | Open an issue with the **Principle proposal** template | -| Add or fix something concrete | Read [`RELEASES.md`](./RELEASES.md) for the branch model, then open a PR | -| Operate as an agent in this repo | [`AGENTS.md`](./AGENTS.md) (lint commands, hard rules, common pitfalls) | +This skill bundle does **not** define principles (the spec does) and does **not** check compliance (`anc` does). It +teaches agents how to use them and supplies surrounding context (idioms, templates, getting-started). Route +contributions accordingly. + +## Where to file what + +| You want to… | Where | +| --------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Propose a new principle, change MUST/SHOULD/MAY tiers, etc. | [`brettdavies/agentnative`](https://github.com/brettdavies/agentnative) (the spec) | +| Report an `anc check` bug, or propose a new checker feature | [`brettdavies/agentnative-cli`](https://github.com/brettdavies/agentnative-cli) | +| Improve a starter template, add a language idiom, fix the guide | This repo — issue + PR. Templates: **Bug report** or **Principle proposal**. | +| Bump the vendored spec to a newer tag | This repo — run `scripts/sync-spec.sh` and PR the diff. | +| Operate as an agent in this repo | Read [`AGENTS.md`](./AGENTS.md) first (lint commands, hard rules, common pitfalls). | ## Branch model (TL;DR) @@ -30,15 +37,15 @@ procedure in [`RELEASES.md`](./RELEASES.md). - **Title format**: [Conventional Commits](https://www.conventionalcommits.org/) — `type(scope): description`. - **Body**: follow [`.github/pull_request_template.md`](.github/pull_request_template.md). The `## Changelog` section is - the source of truth for `CHANGELOG.md` entries — write for users, not implementers. -- **Scope**: keep PRs small and single-purpose where possible. A bundle change, a check tightening, and a docs refactor - should be three PRs, not one. + the source of truth for `CHANGELOG.md` entries — write for users, not implementers. Never hand-edit `CHANGELOG.md`; + it's generated by `scripts/generate-changelog.sh` from PR bodies at release time. +- **Scope**: keep PRs small and single-purpose where possible. - **Tests**: there is no test runner in this repo, but every PR must pass `markdownlint`, `shellcheck`, and (when targeting `main`) `guard-docs / check-forbidden-docs`. Run locally before pushing: ```bash markdownlint-cli2 '**/*.md' '!node_modules/**' - shellcheck --severity=style bundle/scripts/check-compliance.sh bundle/scripts/checks/*.sh + shellcheck --severity=style scripts/*.sh actionlint .github/workflows/*.yml ``` @@ -46,26 +53,28 @@ procedure in [`RELEASES.md`](./RELEASES.md). The repository is split into: -- **`bundle/`** — what consumers install via `anc.dev/install`. SKILL.md, checklists, references, scripts, templates. -- **Everything else** — producer-side ops: governance (`AGENTS.md`, `RELEASES.md`, `CONTRIBUTING.md`, `SECURITY.md`), CI - (`.github/workflows/`), rulesets (`.github/rulesets/`), engineering plans (`docs/plans/`). +- **`bundle/`** — what consumers install via `anc.dev/install`. `SKILL.md`, `getting-started.md`, vendored `spec/`, + `references/`, `templates/`. +- **Everything else** — producer-side ops: governance (`AGENTS.md`, `RELEASES.md`, `CONTRIBUTING.md`, `SECURITY.md`), + release tooling (`scripts/`, `cliff.toml`), CI (`.github/workflows/`), rulesets (`.github/rulesets/`), engineering + plans (`docs/plans/`). Do not move producer-ops files into `bundle/`. Do not move bundle content out of `bundle/`. The split is what keeps consumer skill directories clean. -## Substantive changes to the standard - -The 7 principles are stable. Proposing a new principle, removing one, or materially changing existing semantics -requires: - -1. An issue using the **Principle proposal** template, including the problem statement, prior art, and a draft of how - `bundle/SKILL.md` and `bundle/references/principles-deep-dive.md` would change. -2. Discussion in the issue. Maintainer signoff before any PR. -3. A PR that updates SKILL.md, the deep-dive, the relevant `bundle/scripts/checks/check-p*-*.sh` scripts, the templates, - and the `CHANGELOG.md` together. Partial coverage isn't merged. +## Touching the bundle -Tightening a check's pass criteria is a `Changed` (minor or major depending on how many existing tools regress). Adding -a new check is `Added`. Removing a check is `Removed` (major). See SemVer guidance in `RELEASES.md`. +- **`bundle/spec/`** is vendored. Do not edit by hand. Substantive principle changes happen in + `brettdavies/agentnative`; bring them here by re-running `scripts/sync-spec.sh` at a new `SPEC_REF`. +- **`bundle/SKILL.md`** is the host-discovered entry point. Changes to its `name` or `description` frontmatter affect + skill discovery on every host — coordinate before changing. +- **`bundle/getting-started.md`** is the agent's first read after `SKILL.md`. Keep it short and concrete; cite spec + paths and `anc` invocations rather than restating the principles. +- **`bundle/references/`** holds implementation guidance (Rust/clap patterns, framework idioms, project structure). When + `anc --fix` lands upstream, these may shrink — they exist today because the agent has to apply remediations by hand. +- **`bundle/templates/`** are starter files. They encode principles by construction. Changes here should be informed by + `agentnative-cli`'s reference patterns to avoid drift; the cross-repo alignment story is documented in the spec repo's + `AGENTS.md`. ## Security @@ -75,4 +84,5 @@ advisories channel. ## License By contributing, you agree your contributions are licensed under the MIT license that covers this repository (see -[`LICENSE`](./LICENSE)). +[`LICENSE`](./LICENSE)). Vendored content under `bundle/spec/` is CC BY 4.0 (upstream); contributions to that directory +should happen upstream in `agentnative-spec`. diff --git a/README.md b/README.md index fc957b9..12b927d 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,40 @@ # agentnative-skill -The producer repo for the [`agent-native-cli`](./bundle/SKILL.md) skill bundle — a north-star standard for CLI tools -designed to be operated by AI agents. +The producer repo for the [`agent-native-cli`](./bundle/SKILL.md) skill bundle — an agent-facing guide to designing, +building, and auditing CLI tools for use by AI agents. + +This skill is the third artifact in a three-repo ecosystem: + +| Repo | Role | +| --------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [`agentnative`](https://github.com/brettdavies/agentnative) (the spec) | Canonical text of the seven principles. CC BY 4.0. | +| [`agentnative-cli`](https://github.com/brettdavies/agentnative-cli) (`anc`) | The compliance checker. MIT / Apache-2.0. | +| **This repo** (`agentnative-skill`) | The agent-facing guide. Vendors the spec; teaches `anc` usage. | ## Repository layout ```text agentnative-skill/ -├── bundle/ ← THE SKILL — what consumers install -│ ├── SKILL.md skill metadata + the standard itself -│ ├── checklists/ task-shaped checklists (new-tool.md) -│ ├── references/ deep-dive specs, framework idioms, project structure -│ ├── scripts/ compliance checker + 24 individual checks -│ └── templates/ drop-in starter files (AGENTS, clap-main, error-types, output-format) -├── docs/plans/ engineering plans (dev-only — guarded out of main) -├── .github/ workflows, rulesets, issue templates, PR template -├── AGENTS.md project-level agent instructions FOR THIS REPO (not the bundle) -├── CONTRIBUTING.md how to propose changes -├── RELEASES.md release procedure (cherry-pick from dev → release/* → main) -├── SECURITY.md vulnerability disclosure -├── CHANGELOG.md released versions -├── VERSION single-line current version -├── LICENSE MIT -└── README.md this file +├── bundle/ ← THE SKILL — what consumers install +│ ├── SKILL.md skill metadata + entry-point pointer to getting-started.md +│ ├── getting-started.md three working loops; canonical anc invocations +│ ├── spec/ vendored from agentnative-spec at a pinned ref (do not edit) +│ ├── references/ implementation guidance: framework idioms, project structure, Rust/clap patterns +│ └── templates/ drop-in starter files (clap-main, error-types, output-format, agents-md-template) +├── scripts/ +│ ├── sync-spec.sh vendor agentnative-spec into bundle/spec/ at a pinned ref +│ └── generate-changelog.sh release-time CHANGELOG generator (git-cliff + PR-body extraction) +├── docs/plans/ engineering plans (dev-only — guarded out of main) +├── .github/ workflows, rulesets, issue templates, PR template +├── AGENTS.md project-level agent instructions FOR THIS REPO (not the bundle) +├── CONTRIBUTING.md how to propose changes +├── RELEASES.md release procedure (cherry-pick from dev → release/* → main) +├── SECURITY.md vulnerability disclosure +├── CHANGELOG.md released versions (generated, never hand-edited) +├── VERSION single-line current version +├── cliff.toml git-cliff configuration +├── LICENSE MIT +└── README.md this file ``` The skill bundle is **`bundle/`**. Everything outside `bundle/` is producer-side ops and **does not ship to consumers**. @@ -38,25 +50,27 @@ The install fetches `bundle/` (only) at a tagged commit SHA into the host's skil ```text ~/.claude/skills/agent-native-cli/ ├── SKILL.md -├── checklists/ +├── getting-started.md +├── spec/ ├── references/ -├── scripts/ └── templates/ ``` -The host then auto-discovers `SKILL.md` at the root of the skill directory. +The host auto-discovers `SKILL.md` at the root of the skill directory; `SKILL.md` then points the agent at +`getting-started.md` for progressive disclosure. ## Bundle contents -- [`bundle/SKILL.md`](./bundle/SKILL.md) — the standard itself: 7 agent-readiness principles, when to trigger, how to - use. -- [`bundle/checklists/`](./bundle/checklists/) — task-shaped checklists (e.g., starting a new tool). -- [`bundle/references/`](./bundle/references/) — deep-dive references: principle specifications, framework idioms, - project structure, Rust/clap patterns. -- [`bundle/scripts/`](./bundle/scripts/) — automated compliance checker (`check-compliance.sh`) plus 24 individual - checks across 9 groups under `bundle/scripts/checks/`. -- [`bundle/templates/`](./bundle/templates/) — drop-in starting points (`agents-md-template.md`, clap main, error types, - output format). +- [`bundle/SKILL.md`](./bundle/SKILL.md) — skill metadata + entry-point pointer. +- [`bundle/getting-started.md`](./bundle/getting-started.md) — three working loops (existing CLI / new Rust / other + language); canonical `anc check` invocations; "where things live" map. +- [`bundle/spec/`](./bundle/spec/) — vendored canonical principle text from + [`agentnative-spec`](https://github.com/brettdavies/agentnative). See + [`bundle/spec/README.md`](./bundle/spec/README.md) for the pin and resync procedure. **Do not edit by hand.** +- [`bundle/references/`](./bundle/references/) — implementation guidance: framework idioms (Rust + others), project + structure, Rust/clap patterns. Used when remediating `anc` findings. +- [`bundle/templates/`](./bundle/templates/) — drop-in starting points for greenfield Rust CLIs (`clap-main.rs`, + `error-types.rs`, `output-format.rs`, `agents-md-template.md`). The principles are also published as a stable web reference at [anc.dev/p1](https://anc.dev/p1) through `/p7`. @@ -65,12 +79,20 @@ The principles are also published as a stable web reference at [anc.dev/p1](http Tagged releases follow [SemVer](https://semver.org/). The current version lives in [`VERSION`](./VERSION); release notes are in [`CHANGELOG.md`](./CHANGELOG.md). Each tag has a corresponding GitHub Release with the same notes. +The skill's own version is independent of the spec it vendors. The currently-pinned spec version is in +[`bundle/spec/VERSION`](./bundle/spec/VERSION). + ## Contributing -Issues and PRs welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). The bundle's content is the authoritative source of -truth for what "agent-native CLI" means in this ecosystem; substantive proposals should engage with the principles in -[`bundle/SKILL.md`](./bundle/SKILL.md) and -[`bundle/references/principles-deep-dive.md`](./bundle/references/principles-deep-dive.md). +Issues and PRs welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). Routing: + +- **Spec questions or principle proposals** → file in + [`brettdavies/agentnative`](https://github.com/brettdavies/agentnative) (the spec repo). This skill vendors the spec; + substantive principle changes happen there first. +- **`anc` bugs or feature requests** → file in + [`brettdavies/agentnative-cli`](https://github.com/brettdavies/agentnative-cli). The skill teaches `anc` usage but + doesn't implement the checker. +- **Skill-bundle issues** (templates, references, getting-started, layout) → file here. Branch + release model documented in [`RELEASES.md`](./RELEASES.md). @@ -80,4 +102,5 @@ See [`SECURITY.md`](./SECURITY.md) for vulnerability disclosure. ## License -MIT — see [`LICENSE`](./LICENSE). +MIT — see [`LICENSE`](./LICENSE). Vendored spec content under `bundle/spec/` is CC BY 4.0; attribution is in +[`bundle/spec/README.md`](./bundle/spec/README.md). diff --git a/RELEASES.md b/RELEASES.md index 9d649d7..39d1b69 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -134,11 +134,17 @@ from `origin/main` and cherry-pick onto it. The version bump and CHANGELOG generation both happen on the `release/v` branch (steps 5–6 of the cherry-pick flow above). There is no separate version-bump PR to `dev`. Picking the version is the only manual decision: -- **Patch** — doc updates, internal cleanups, non-substantive script tweaks. -- **Minor** — new principles, new checks, new templates, new bundle files (backward-compatible additions). -- **Major** — breaking changes to the bundle's contract: renaming `bundle/SKILL.md` frontmatter fields, changing exit - codes of `bundle/scripts/check-compliance.sh`, restructuring directory layout in ways that break existing skill - installations, or moving content between `bundle/` and the producer-ops root. +- **Patch** — doc updates, internal cleanups, non-substantive template edits, vendoring a patch-level spec bump. +- **Minor** — new templates, new reference docs, new bundle files (backward-compatible additions), vendoring a + minor-level spec bump that adds requirements without tightening existing tiers. +- **Major** — breaking changes to the bundle's contract: renaming `bundle/SKILL.md` frontmatter fields, restructuring + directory layout in ways that break existing skill installations, moving content between `bundle/` and the + producer-ops root, or vendoring a major-level spec bump (renamed/removed principles or tightened MUSTs that would + regress existing consumers). + +The skill's version is independent of the spec it vendors. A spec bump that doesn't affect the skill's surface (e.g., +prose-only edits) can ship as a patch even when the spec went minor. Use the SemVer guidance above against the *skill's* +observable behaviour, not the spec's. ## PRs and changelog generation diff --git a/bundle/SKILL.md b/bundle/SKILL.md index e766d2a..90bb354 100644 --- a/bundle/SKILL.md +++ b/bundle/SKILL.md @@ -1,270 +1,89 @@ --- name: agent-native-cli description: >- - North-star standard for agent-native CLI tools. Defines 7 agent-readiness principles (non-interactive, structured - output, progressive help, actionable errors, safe retries, composable structure, bounded responses), Rust/clap - implementation patterns, project structure requirements, and an automated compliance checker (24 checks across 9 - groups). Use when designing a new CLI tool, reviewing an existing tool for agent-readiness, or running compliance - checks. Triggers on agentic CLI, agent-native, CLI design, CLI standard, agent-first, CLI for agents, agent-friendly - CLI, CLI compliance. + Guide to designing, building, and auditing CLI tools for use by AI agents. Pairs with + [`anc`](https://github.com/brettdavies/agentnative-cli) (the canonical compliance checker) and + [`agentnative-spec`](https://github.com/brettdavies/agentnative) (the canonical principle text, vendored at + `bundle/spec/`). Provides starter templates, language-specific implementation idioms, and a short + getting-started guide that points agents at `anc check --output json`. Use when designing a new CLI tool, + reviewing one for agent-readiness, or remediating findings from `anc`. Triggers on agentic CLI, agent-native, + CLI design, CLI standard, agent-first, CLI for agents, agent-friendly CLI, CLI compliance, anc. --- -# Agent-Native CLI Standard +# Agent-Native CLI -The north-star standard for CLI tools designed to be operated by AI agents. Defines what an agent-native CLI must look -like — from interface design through project structure — and provides an automated compliance checker that produces -deterministic pass/warn/fail scorecards (24 checks across 9 groups). +The standard for CLI tools designed to be operated by AI agents. Three artifacts work together: -This skill is **prescriptive** ("what to build"). Its complement, the `cli-agent-readiness-reviewer` agent, is -**evaluative** ("how well did you build it"). For release infrastructure (CI/CD, distribution, Homebrew), see the -`rust-tool-release` skill. +| Artifact | Role | +| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`agentnative-spec`](https://github.com/brettdavies/agentnative) | Canonical text of the seven principles. Frontmatter `requirements[]` is the machine-readable contract. Vendored into [`bundle/spec/`](./spec/) at a pinned ref. | +| [`anc`](https://github.com/brettdavies/agentnative-cli) | The compliance checker. Reads target source/binary, emits a JSON scorecard whose entries cite spec `requirement_id`s. The runtime authority. | +| **This skill** (`agent-native-cli`) | The agent-facing guide. Tells the agent how to invoke `anc`, how to navigate the spec when remediating findings, and where the implementation patterns and starter templates live. | -## The 7 Principles +The skill does **not** implement principles checking. `anc` does. The skill teaches agents to use `anc` and supplies the +surrounding context (spec, idioms, templates) that `anc`'s findings reference. -Every agent-native CLI tool must satisfy these seven principles. Each principle below gives the key requirements; for -full MUST/SHOULD/MAY specifications, see `references/principles-deep-dive.md`. +## Start here -### P1: Non-Interactive by Default +→ **[`getting-started.md`](./getting-started.md)** — the three working loops (existing CLI / new Rust CLI / other +language), the canonical `anc check` invocations, and a "where things live" map. -All automation paths work without human input. Interactive prompts (dialoguer, inquire) are gated behind a -`--no-interactive` flag. Every flag has an env var override via clap's `env` attribute for scriptability. +## The seven principles -**Key requirements:** +Defined in [`bundle/spec/principles/`](./spec/principles/) (vendored from `agentnative-spec` @ `v0.2.0` — see +[`bundle/spec/README.md`](./spec/README.md) for the pin and resync instructions). One file per principle, each with +machine-readable `requirements[]` frontmatter: -- No interactive prompts on the default code path -- `--no-interactive` global flag if any prompt exists -- Boolean env vars use `FalseyValueParser` so `TOOL_QUIET=0` correctly disables -- If the CLI has auth, it MUST support headless auth via `--no-browser` (canonical flag name, RFC 8628 device - authorization grant). Agents cannot open browsers for OAuth. +| # | File | Subject | +| --- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| P1 | [`p1-non-interactive-by-default.md`](./spec/principles/p1-non-interactive-by-default.md) | Non-Interactive by Default | +| P2 | [`p2-structured-parseable-output.md`](./spec/principles/p2-structured-parseable-output.md) | Structured, Parseable Output | +| P3 | [`p3-progressive-help-discovery.md`](./spec/principles/p3-progressive-help-discovery.md) | Progressive Help Discovery | +| P4 | [`p4-fail-fast-actionable-errors.md`](./spec/principles/p4-fail-fast-actionable-errors.md) | Fail Fast, Actionable Errors | +| P5 | [`p5-safe-retries-mutation-boundaries.md`](./spec/principles/p5-safe-retries-mutation-boundaries.md) | Safe Retries, Mutation Boundaries | +| P6 | [`p6-composable-predictable-command-structure.md`](./spec/principles/p6-composable-predictable-command-structure.md) | Composable, Predictable Structure | +| P7 | [`p7-bounded-high-signal-responses.md`](./spec/principles/p7-bounded-high-signal-responses.md) | Bounded, High-Signal Responses | -### P2: Structured, Parseable Output +Do not paraphrase the principles inside this skill — read the spec files directly. They are the source of truth. -Agents consume output programmatically. Data goes to stdout, diagnostics to stderr. Output format is selectable. +## Implementation guidance (when fixing findings) -**Key requirements:** +Once `anc check` reports a failure, the agent has the cited `requirement_id` and the spec text. The next question is +"how do I write code that satisfies this requirement?" — answered by: -- `--output text|json|jsonl` global flag with env override -- `OutputConfig` struct threaded through all command handlers — never naked `println!` -- Errors print as structured JSON when `--output json` -- Exit codes are structured: 0=success, 1=command error, 77=auth, 78=config +| Need | File | +| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Rust/clap-specific patterns per principle | [`references/rust-clap-patterns.md`](./references/rust-clap-patterns.md) | +| General Rust idioms (output, errors, dependency gating) | [`references/framework-idioms.md`](./references/framework-idioms.md) | +| Idioms in Python, Go, JS, Ruby | [`references/framework-idioms-other-languages.md`](./references/framework-idioms-other-languages.md) | +| Required project structure (modules, tests, AGENTS.md) | [`references/project-structure.md`](./references/project-structure.md) | -See `templates/output-format.rs` and `templates/error-types.rs` for canonical implementations. +## Starter code -### P3: Progressive Help Discovery +Drop-in starting points for greenfield Rust CLIs. Each encodes the relevant principles by construction. -Agents scan help text to learn how to use a tool. Examples must be findable after the flags section. +| File | Encodes | +| ---------------------------------------------------------------------- | -------------------------------------------------------------------- | +| [`templates/clap-main.rs`](./templates/clap-main.rs) | `try_parse`, SIGPIPE fix, three-tier dependency gating, global flags | +| [`templates/error-types.rs`](./templates/error-types.rs) | `thiserror` enum + `exit_code()` mapping | +| [`templates/output-format.rs`](./templates/output-format.rs) | `OutputConfig`, `OutputFormat`, `diag!`, NO_COLOR / IsTerminal | +| [`templates/agents-md-template.md`](./templates/agents-md-template.md) | Project-level AGENTS.md scaffold | -**Key requirements:** +## Compliance checking -- Both `after_help` AND `long_about` are required (not just one) — `after_help` for examples, `long_about` for extended - description -- `about` alone is insufficient — agents scan past flags to find examples in `after_help` -- Env vars visible in `--help` output via clap's `env` attribute -- `--version` flag MUST be present (clap `#[command(version)]`) +Use `anc`. Install once: -### P4: Fail Fast with Actionable Errors - -When something goes wrong, agents need structured error output to decide their next action. - -**Key requirements:** - -- `try_parse()` not `parse()` — parse() calls `process::exit()`, bypassing custom error handlers -- Structured error enum with `exit_code()` mapping (agents parse exit codes to decide: retry, re-auth, or report) -- Error messages include: what failed, why, and what to do next -- No `process::exit()` outside of main() - -See `templates/clap-main.rs` for the try_parse pattern and `templates/error-types.rs` for structured errors. - -### P5: Safe Retries and Explicit Mutation Boundaries - -Agents retry commands. Every CLI MUST support `--dry-run` regardless of command type — agents need a safe way to verify -behavior before committing to actions. - -**Key requirements:** - -- `--dry-run` MUST be supported on all CLIs (not just write-heavy ones) -- Destructive operations MUST require `--force` or `--yes` -- Idempotent design where possible - -### P6: Composable and Predictable Structure - -Agents pipe, redirect, and compose CLI tools. The tool must behave predictably in pipelines. - -**Key requirements:** - -- SIGPIPE fix as first line of `main()` (prevents panics when piping to `head`) -- TTY detection via `std::io::IsTerminal` respecting `NO_COLOR` and `TERM=dumb` -- Shell completions via `clap_complete` -- `--no-pager` or pager disable mechanism if the CLI uses a pager -- `--timeout` for network CLIs -- `global = true` on all agentic flags (`--output`, `--quiet`, `--dry-run`, `--no-interactive`) when subcommands are - used -- Three-tier dependency gating: meta-commands (no deps) -> local commands (config only) -> network commands (auth - required) - -**Flags vs. subcommands guidance:** Use subcommands for distinct operations (`list`, `get`, `delete`), flags for -modifiers (`--output`, `--limit`), and global flags for cross-cutting concerns (`--quiet`, `--dry-run`, `--output`). - -See `templates/clap-main.rs` for the three-tier pattern. - -### P7: Bounded, High-Signal Responses - -Agents have finite context windows. Output must be controllable and bounded. - -**Key requirements:** - -- `--quiet` flag suppresses non-essential output (diagnostics, progress) -- `diag!` macro gates all non-essential stderr output -- `--limit`/`--max-results` on endpoints returning lists -- `.clamp()` on pagination values to prevent runaway responses - -See `templates/output-format.rs` for the diag! macro and OutputConfig patterns. - -## Project Structure Requirements - -Beyond the CLI interface, agent-native tools need proper project structure. See `references/project-structure.md` for -full details. - -**Required:** - -- `AGENTS.md` at repo root — build commands, test commands, architecture, exit codes, conventions -- Error types in a dedicated module with exit code mapping -- Output config in a dedicated module with format-aware printing -- Integration tests using wiremock (API mocking) and TestEnv pattern (XDG isolation) -- README showing both human and agent usage - -See `templates/agents-md-template.md` for the AGENTS.md template. - -## What Would You Like to Do? - -| Intent | Resource | -| ------ | -------- | -| Design a new CLI tool from scratch | `checklists/new-tool.md` | -| Understand the 7 principles in depth | `references/principles-deep-dive.md` | -| Implement principles in Rust/clap | `references/rust-clap-patterns.md` | -| Set up project structure | `references/project-structure.md` | -| Get Rust/clap framework idioms | `references/framework-idioms.md` | -| Get idioms for other languages | `references/framework-idioms-other-languages.md` | -| Run compliance checks on a repo | See "Compliance Checker" below | -| Copy template files into a new project | `templates/` directory | - -## Compliance Checker - -The automated compliance checker scans a Rust CLI repo using static analysis (rg patterns) and produces a deterministic -scorecard. 24 checks across 9 groups: P1-P7 (one group per principle), Code Quality, and Project Structure. - -### Run all checks - -```text -agent-native-cli/scripts/check-compliance.sh /path/to/repo -``` - -### Run a single principle - -```text -agent-native-cli/scripts/check-compliance.sh /path/to/repo --principle 3 +```bash +brew install brettdavies/tap/agentnative # binary is `anc` +cargo install agentnative # any platform with Rust ``` -### Interpret results - -Each check gets PASS, WARN, or FAIL with evidence. Results are grouped by section. - -**Exit codes:** - -- 0 = all PASS -- 1 = any WARN, no FAIL (acceptable for most tools) -- 2 = any FAIL (action needed) - -**Example scorecard:** - -```text -╔══════════════════════════════════════════════════════════╗ -║ Agent-Native CLI Compliance — bird -╚══════════════════════════════════════════════════════════╝ - - P1: Non-Interactive - ────────────────────────────────────────────────────── - WARN Headless auth Auth delegated to subprocess - PASS Non-interactive No interactive prompts found - - P2: Structured Output - ────────────────────────────────────────────────────── - PASS Structured output OutputFormat enum + serde_json present - - P3: Progressive Help - ────────────────────────────────────────────────────── - FAIL Progressive help No after_help or long_about - PASS Version flag #[command(version)] found - - P4: Actionable Errors - ────────────────────────────────────────────────────── - WARN Error types Manual Error impl — migrate to thiserror - PASS Exit codes Named exit code constants found - PASS No process::exit leaks Confined to main.rs - WARN try_parse from_arg_matches() — migrate to try_parse() - - P5: Safe Retries - ────────────────────────────────────────────────────── - WARN Safe retries (--dry-run) Write commands present without --dry-run - - P6: Composable Structure - ────────────────────────────────────────────────────── - PASS Shell completions clap_complete found in Cargo.toml - PASS Global flags global = true on agentic flags - PASS NO_COLOR support NO_COLOR env check found - PASS No pager blocking No pager invocation found - PASS SIGPIPE fix reset_sigpipe() found in main - FAIL Network timeout No --timeout flag found - PASS TTY detection IsTerminal trait found - - P7: Bounded Responses - ────────────────────────────────────────────────────── - PASS Output clamping .clamp() found on pagination - PASS Quiet flag --quiet flag found - - Code Quality - ────────────────────────────────────────────────────── - PASS Env flag overrides env = attributes found on flags - FAIL No naked println! println! found outside output module - PASS No unwrap() in prod No unwrap() in src/ (excluding tests) - - Project Structure - ────────────────────────────────────────────────────── - PASS AGENTS.md AGENTS.md found at repo root - PASS Dependencies Required crates present - -════════════════════════════════════════════════════════════ - Score: 18/24 PASS, 3 WARN, 3 FAIL -``` - -### Adding new checks - -Drop a new `check-*.sh` script in `scripts/checks/`. Follow the output protocol: emit one line -`STATUS|LABEL|EVIDENCE` to stdout, exit 0/1/2. Name checks with a group prefix — `check-p1-*`, `check-p4-*`, -`check-code-*`, `check-project-*` — the orchestrator groups them automatically by prefix. The orchestrator -auto-discovers new checks via glob. - -## Reference Index - -**Principles:** `references/principles-deep-dive.md` — full MUST/SHOULD/MAY specification for all 7 principles - -**Implementation:** - -- `references/rust-clap-patterns.md` — Rust/clap-specific implementation guidance per principle -- `references/project-structure.md` — required files and project layout -- `references/framework-idioms.md` — Rust/clap idioms (primary) -- `references/framework-idioms-other-languages.md` — Click, argparse, Cobra, Commander, yargs, oclif, Thor - -**Templates:** `templates/clap-main.rs`, `templates/error-types.rs`, `templates/output-format.rs`, -`templates/agents-md-template.md` - -**Checklists:** `checklists/new-tool.md` — phased checklist for new tool creation or retrofit - -**Scripts:** `scripts/check-compliance.sh` (orchestrator), `scripts/checks/` (individual checks) +Recommended invocations and the full agent loop are in [`getting-started.md`](./getting-started.md). Do not write shell +scripts to grep for principle violations — `anc` already implements (and supersedes) every check that approach could +produce. ## Sources -- Eric Zakariasson, "Building CLIs for agents" — concise overview of agent-friendly CLI patterns -- `cli-agent-readiness-reviewer` agent (compound-engineering plugin) — evaluative 7-principle rubric -- Institutional learnings from bird and xurl-rs in `docs/solutions/` — battle-tested patterns -- "The Emerging Harness Engineering" (ignorance.ai) — AGENTS.md as living doc, architecture as guardrails +- [`agentnative-spec`](https://github.com/brettdavies/agentnative) — canonical principle text (CC BY 4.0) +- [`agentnative-cli`](https://github.com/brettdavies/agentnative-cli) — `anc`, the canonical checker (MIT / Apache-2.0) +- [`agentnative-skill`](https://github.com/brettdavies/agentnative-skill) — this repo (MIT) diff --git a/bundle/checklists/new-tool.md b/bundle/checklists/new-tool.md deleted file mode 100644 index 4dd61e5..0000000 --- a/bundle/checklists/new-tool.md +++ /dev/null @@ -1,117 +0,0 @@ -# New Agent-Native CLI Tool Checklist - -Use this checklist when creating a new Rust CLI tool or retrofitting an existing one for agent-native compliance. Each -item has a concrete verification method — no subjective judgment. - -## Phase 0: Prerequisites - -- [ ] Rust toolchain installed (`rustc --version`) -- [ ] `cargo init` or existing Cargo project -- [ ] `rg` (ripgrep) installed for compliance checks -- [ ] Read `agent-native-cli/SKILL.md` for principles overview -- [ ] Read `agent-native-cli/references/principles-deep-dive.md` for full specification - -## Phase 1: Scaffolding - -- [ ] Cargo.toml has `clap` with `derive` and `env` features -- [ ] Cargo.toml has `serde` + `serde_json` -- [ ] Cargo.toml has `thiserror` -- [ ] Cargo.toml has `libc` (for SIGPIPE fix) -- [ ] Cargo.toml has `clap_complete` (for shell completions) -- [ ] Copy `templates/clap-main.rs` structure into `src/main.rs` -- [ ] Copy `templates/error-types.rs` structure into `src/error.rs` -- [ ] Copy `templates/output-format.rs` structure into `src/output.rs` -- [ ] Verify: `cargo check` passes - -## Phase 2: Agent-Native Interface - -### P1: Non-interactive - -- [ ] No `dialoguer`/`inquire`/`read_line` calls without `--no-interactive` guard -- [ ] If CLI has auth, support `--no-browser` for headless auth (RFC 8628 device-code grant) -- [ ] Verify: `rg "dialoguer|inquirer|read_line" --type rust src/` returns 0 matches OR all gated -- [ ] Verify: `rg "no.browser" --type rust src/` returns matches (if auth is present) - -### P2: Structured output - -- [ ] `OutputFormat` enum with Text/Json/Jsonl variants -- [ ] `OutputConfig` struct threaded through all command handlers -- [ ] `--output text|json|jsonl` global flag with env override -- [ ] Data to stdout, diagnostics to stderr -- [ ] Errors print as JSON when `--output json` -- [ ] Verify: `rg "OutputFormat" --type rust src/` returns matches - -### P3: Progressive help - -- [ ] Both `after_help` AND `long_about` required (not just one) -- [ ] `after_help` on each subcommand with subcommand-specific examples -- [ ] `--version` via `#[command(version)]` -- [ ] Env vars visible in `--help` output (via `env` attribute) -- [ ] Verify: `rg "long_about" --type rust src/` returns matches -- [ ] Verify: `rg "after_help" --type rust src/` returns matches - -### P4: Actionable errors - -- [ ] `try_parse()` in main, not `parse()` -- [ ] thiserror error enum (canonical) with `exit_code()` method -- [ ] Exit codes: 0=success, 1=command, 77=auth, 78=config -- [ ] Error messages include: what failed, why, what to do -- [ ] No `process::exit()` outside main -- [ ] Verify: `rg "exit_code" --type rust src/` returns matches - -### P5: Safe retries - -- [ ] `--dry-run` flag present -- [ ] Destructive operations have `--force`/`--yes` confirmation -- [ ] Idempotent design where possible -- [ ] Verify: `rg "dry.run|force|yes" --type rust src/` returns matches - -### P6: Composable structure - -- [ ] SIGPIPE fix as first line of main() -- [ ] TTY detection via `std::io::IsTerminal` (canonical, stdlib since Rust 1.70) -- [ ] `NO_COLOR` environment variable respected -- [ ] Shell completions via `clap_complete` -- [ ] Three-tier dependency gating in main() -- [ ] If CLI uses pager: `--no-pager` or PAGER disable mechanism -- [ ] If CLI makes HTTP requests: `--timeout` flag with default (e.g., 30s) -- [ ] All agentic flags (output, quiet, no-interactive, timeout) have `global = true` -- [ ] Verify: `rg "SIGPIPE|SIG_DFL" --type rust src/` returns matches - -### P7: Bounded responses - -- [ ] `--quiet` flag suppresses diagnostics -- [ ] `diag!` macro for all non-essential output -- [ ] `--limit`/`--max-results` on endpoints returning lists -- [ ] `.clamp()` on pagination values -- [ ] `--timeout` for network operations -- [ ] Verify: `rg "diag!|clamp|suppress_diag" --type rust src/` returns matches - -### Code Quality - -- [ ] No `println!` outside main.rs (use OutputConfig) -- [ ] No `.unwrap()` in src/ (use `?` or explicit error handling) -- [ ] `env = "TOOL_*"` on all agentic flags -- [ ] `FalseyValueParser::new()` on boolean env var flags -- [ ] Verify: `rg "println!" --type rust src/ --glob '!main.rs'` returns 0 matches -- [ ] Verify: `rg "\.unwrap\(\)" --type rust src/` returns 0 matches -- [ ] Verify: `rg 'env\s*=' --type rust src/` returns matches on all flag definitions -- [ ] Verify: `rg "FalseyValueParser" --type rust src/` returns matches - -## Phase 3: Project Structure - -- [ ] `AGENTS.md` (plural, canonical) at repo root (copy from `templates/agents-md-template.md`, fill in) -- [ ] Error types in dedicated module (`src/error.rs` or `src/errors/`) -- [ ] Output config in dedicated module (`src/output.rs`) -- [ ] Integration tests using wiremock (API tests) or TestEnv pattern -- [ ] README shows both human and agent usage -- [ ] Verify: `AGENTS.md` exists and has Build, Test, Architecture, Exit Codes sections - -## Phase 4: Automated Compliance - -- [ ] Run: `agent-native-cli/scripts/check-compliance.sh /path/to/repo` -- [ ] All 24 checks show PASS or acceptable WARN -- [ ] No FAIL results remain -- [ ] Address any WARN items — WARN tier flags non-canonical patterns (e.g., manual Error impl instead of thiserror, - `is-terminal` crate instead of stdlib `IsTerminal`) -- [ ] Verify: exit code is 0 (all PASS) or 1 (WARNs only, no FAILs) diff --git a/bundle/getting-started.md b/bundle/getting-started.md new file mode 100644 index 0000000..c97022b --- /dev/null +++ b/bundle/getting-started.md @@ -0,0 +1,69 @@ +# Getting started + +This skill teaches agents how to design, build, or audit a CLI for use by other agents. The work is one of three loops; +pick the one that matches your starting point. The canonical checker is +[`anc`](https://github.com/brettdavies/agentnative-cli); the canonical principle text is in +[`bundle/spec/principles/`](./spec/principles/). + +## You have an existing CLI + +```bash +# 1. Run the checker. +anc check --output json . > scorecard.json + +# 2. For each FAIL, look up the cited requirement_id (e.g. `p1-must-no-interactive`) +# in bundle/spec/principles/p-*.md — frontmatter `requirements[]`. + +# 3. Apply the fix. Patterns live in: +# bundle/references/rust-clap-patterns.md (Rust/clap) +# bundle/references/framework-idioms.md (Rust idioms) +# bundle/references/framework-idioms-other-languages.md (Click, argparse, Cobra, Commander, yargs, oclif, Thor) +# Re-run `anc check` until the scorecard is clean. +``` + +Useful flags: `--principle N` to focus on one principle, `--audit-profile ` to suppress checks that don't +apply (e.g. `human-tui` for tools that legitimately intercept the TTY), `--binary` / `--source` to scope. + +## You're building from scratch (Rust) + +```bash +cargo init my-tool && cd my-tool + +# Starter files. Encode P1–P7 by construction. +cp /bundle/templates/clap-main.rs src/main.rs +cp /bundle/templates/error-types.rs src/error.rs +cp /bundle/templates/output-format.rs src/output.rs +cp /bundle/templates/agents-md-template.md AGENTS.md # fill placeholders + +# Add to Cargo.toml: clap (derive, env), serde, serde_json, thiserror, +# libc (SIGPIPE), clap_complete. See bundle/references/project-structure.md. + +anc check --output json # run continuously as you build +``` + +## You're building in another language + +`anc`'s source-analysis layer is Rust-only; its behavioral layer (`anc check --command `) runs against any +compiled binary on `PATH`. Read `bundle/spec/principles/p1-*.md` through `p7-*.md` for the language-agnostic +requirements, and `bundle/references/framework-idioms-other-languages.md` for per-framework idioms. + +## Installing anc + +```bash +brew install brettdavies/tap/agentnative # macOS / Linux +cargo install agentnative # any platform with Rust +``` + +Binary name: `anc`. Prebuilt releases at . + +## Where things live + +| Question | Where | +| ----------------------------------------------- | --------------------------------------------------------- | +| What does P3 mean? | `bundle/spec/principles/p3-progressive-help-discovery.md` | +| What spec version is this skill pinned to? | `bundle/spec/VERSION` | +| How do I implement `` in Rust/clap? | `bundle/references/rust-clap-patterns.md` | +| How do I implement `` in Python/Go/JS? | `bundle/references/framework-idioms-other-languages.md` | +| File a spec question or proposal | | +| File an `anc` bug | | +| File a skill-bundle issue | | diff --git a/bundle/references/principles-deep-dive.md b/bundle/references/principles-deep-dive.md deleted file mode 100644 index 45df90f..0000000 --- a/bundle/references/principles-deep-dive.md +++ /dev/null @@ -1,419 +0,0 @@ -# Agent-Readiness Principles: Deep Dive - -This reference is the full specification for the 7 agent-readiness principles that define an agent-native CLI tool. Each -principle includes a definition, the cost of violation, tiered requirements using RFC 2119 language (MUST/SHOULD/MAY), -evidence patterns for code review, and anti-patterns to reject. For Rust/clap implementation details, see the template -files referenced within each principle. - ---- - -## P1: Non-Interactive by Default - -### Definition - -All automation paths MUST work without human input. A CLI tool that blocks on a TTY prompt is invisible to an agent — -the agent hangs, the user sees nothing, and the operation times out silently. - -### Why Agents Need It - -When a tool prompts for confirmation or credentials interactively, an agent cannot respond. The agent's process stalls -until timeout, wasting tokens and wall-clock time. Worse, the agent has no structured signal that interaction was -requested — it cannot distinguish "waiting for input" from "still processing." Interactive prompts in automation paths -are the single most common cause of agent-tool deadlocks. - -### Requirements - -**MUST:** - -- All flags MUST be settable via environment variables. Use `FalseyValueParser` for boolean env vars so that - `TOOL_QUIET=0` correctly disables the flag rather than treating any non-empty value as truthy. See - `templates/output-format.rs` for the canonical pattern. -- Tools that include any interactive prompts (dialoguer, inquire, `read_line`) MUST gate them behind a - `--no-interactive` flag. When `--no-interactive` is set or the env var equivalent is truthy, the tool MUST either use - defaults, read from stdin, or fail with an actionable error — never block. -- If the CLI includes authentication (OAuth, token management, credential flows), it MUST support a headless auth path. - The canonical flag is `--no-browser`, which triggers the OAuth 2.0 Device Authorization Grant (RFC 8628) — the CLI - prints a URL and code, the user authorizes on another device. Agents cannot open browsers. Non-canonical alternatives - (`--device-code`, `--remote`, `--headless`) are acceptable but should be migrated to `--no-browser`. - -**SHOULD:** - -- Tools SHOULD auto-detect non-interactive contexts via TTY detection (`IsTerminal`) and suppress prompts when stderr is - not a terminal, even without an explicit `--no-interactive` flag. -- Default values for prompted inputs SHOULD be documented in `--help` output so agents can pass them explicitly. - -**MAY:** - -- Tools MAY offer rich interactive experiences (spinners, progress bars, multi-select menus) when a TTY is detected and - `--no-interactive` is not set, provided the non-interactive path remains fully functional. - -### Evidence Patterns - -- `--no-interactive` flag definition in the CLI struct with an env var binding -- `FalseyValueParser::new()` on all boolean flags that have env var overrides -- TTY guard wrapping any `dialoguer` or `inquire` call -- Every flag has a corresponding `env = "TOOL_..."` attribute -- `--no-browser` flag definition for headless/device-code authentication - -### Anti-Patterns - -- Bare `dialoguer::Confirm::new().interact()` without a `--no-interactive` check -- Boolean env var flags using clap's default string parser (where `TOOL_QUIET=false` is truthy because it is non-empty) -- `stdin().read_line()` in a code path reachable during normal operation without a TTY check -- Hard-coded credentials prompts with no env var or config file alternative -- OAuth flow that unconditionally opens a browser with no headless escape hatch - ---- - -## P2: Structured, Parseable Output - -### Definition - -Tools MUST separate data from diagnostics and offer machine-readable output formats. Agents parse stdout -programmatically — mixing status messages with data forces fragile regex extraction that breaks on any format change. - -### Why Agents Need It - -An agent calling a CLI tool needs three things: the data, the error (if any), and the exit code. When data goes to -stdout, diagnostics go to stderr, and errors include machine-readable fields, the agent can parse output reliably -without heuristics. When these channels are mixed or output is human-formatted only, the agent must resort to -best-effort text parsing that fails unpredictably across versions, locales, and edge cases. - -### Requirements - -**MUST:** - -- Tools MUST support `--output text|json|jsonl` for selecting the output format. Text is the default for human use; JSON - and JSONL are the agent-facing formats. See `templates/output-format.rs` for the `OutputFormat` enum and - `OutputConfig` struct. -- Data MUST go to stdout. Diagnostics, progress indicators, and warnings MUST go to stderr. An agent consuming JSON from - stdout must never encounter an interleaved progress message. -- Exit codes MUST be structured and documented: 0 for success, 1 for general command errors, 2 for usage errors (bad - arguments), 77 for authentication/permission errors, 78 for configuration errors. See `templates/error-types.rs` for - the canonical exit code mapping. -- When `--output json` is active, errors MUST also be emitted as JSON (to stderr) with at minimum `error`, `kind`, and - `message` fields. See `templates/error-types.rs` for the `print()` method that respects `OutputConfig`. - -**SHOULD:** - -- The `OutputConfig` struct SHOULD be threaded through the entire call stack so that every function producing output - respects the chosen format. Naked `println!` calls bypass format selection and leak unstructured text into stdout. -- JSON output SHOULD include a consistent envelope: a top-level object with predictable keys that agents can rely on - across commands. - -**MAY:** - -- Tools MAY support additional output formats (CSV, TSV, YAML) beyond the core three, as long as the core three are - always available. -- Tools MAY include a `--raw` flag for unformatted output suitable for piping to other tools. - -### Evidence Patterns - -- `OutputFormat` enum with `Text`, `Json`, `Jsonl` variants deriving `ValueEnum` -- `OutputConfig` struct with `format`, `use_color`, and `quiet` fields -- `serde_json` in `Cargo.toml` dependencies -- No `println!` in `src/` outside of the output module (all output goes through `OutputConfig`) -- Exit code constants or match arms mapping error variants to distinct numeric codes -- `eprintln!` (or `diag!` macro) for all diagnostic output - -### Anti-Patterns - -- `println!` scattered across command handlers instead of routing through `OutputConfig` -- A single exit code (1) for all error types — agents cannot distinguish auth failures from config errors -- Status messages ("Fetching data...") printed to stdout where they contaminate JSON output -- `process::exit()` calls in library code that bypass structured error propagation -- Human-formatted tables as the only output mode with no JSON alternative - ---- - -## P3: Progressive Help Discovery - -### Definition - -Help text MUST be layered so agents (and humans) can drill from a short summary to detailed usage examples without -reading the entire manual. The critical layer is `after_help` — the section that appears after the flags list — because -that is where agents look for concrete invocation patterns. - -### Why Agents Need It - -Agents discover how to use a tool by calling `--help` and scanning the output. They skip past flag definitions (which -describe what is possible) and look for examples (which describe what to do). Clap's `about` and `long_about` attributes -populate the description above the flags list — useful for orientation, but insufficient for invocation guidance. The -`after_help` attribute populates the section below the flags list, which is where usage examples belong. Without -`after_help`, an agent sees flags but no examples of how to combine them, leading to trial-and-error invocations that -waste tokens and often fail. - -### Requirements - -**MUST:** - -- Every subcommand MUST have an `after_help` (or `after_long_help`) attribute containing at least one concrete - invocation example showing the command with realistic arguments. -- The top-level command MUST have `after_help` showing the most common workflows (2-3 examples covering the primary use - cases). - -**SHOULD:** - -- Examples in `after_help` SHOULD show both human and agent invocations side by side (e.g., a text-output example - followed by its `--output json` equivalent). -- Help text SHOULD use short `about` for command list summaries and reserve `long_about` for detailed descriptions that - appear with `--help` but not `-h`. - -**MAY:** - -- Tools MAY include a dedicated `examples` subcommand or `--examples` flag that outputs a curated set of usage patterns - for agent consumption. - -### Evidence Patterns - -- `after_help` or `after_long_help` attribute on the top-level `Parser` struct -- `after_help` or `after_long_help` attribute on each subcommand enum variant or struct -- Example invocations in `after_help` text that include realistic arguments -- Both `about` (short) and `after_help` (examples) present on subcommands - -### Anti-Patterns - -- Relying solely on doc comments (`///`) which only populate `about` and `long_about` — no examples appear after the - flags section -- A single `about` string serving as both summary and usage documentation -- Examples buried in a README or man page but absent from `--help` output -- `after_help` containing only prose descriptions without concrete invocation examples - ---- - -## P4: Fail Fast with Actionable Errors - -### Definition - -Tools MUST detect invalid state early, exit with a structured error, and tell the caller what failed, why, and what to -do next. An error message that says "operation failed" gives the agent nothing to act on. - -### Why Agents Need It - -Agents operate in a retry loop: attempt, observe result, decide next action. When an error is vague ("something went -wrong") or unstructured (a stack trace on stdout), the agent cannot determine whether to retry, re-authenticate, fix -configuration, or escalate to the user. Structured errors with distinct exit codes and actionable messages let agents -make correct decisions immediately. The difference between exit code 77 (re-authenticate) and exit code 78 (fix config) -determines whether the agent retries OAuth or asks the user to check their config file — getting this wrong wastes -entire conversation turns. - -### Requirements - -**MUST:** - -- Tools MUST use `try_parse()` instead of `parse()` for CLI argument parsing. Clap's `parse()` calls `process::exit()` - directly, bypassing any custom error handler. When an agent passes `--output json`, it expects parse errors in JSON — - `parse()` makes that impossible. See `templates/clap-main.rs` for the canonical pattern. -- Error types MUST map to distinct exit codes. At minimum: 0 (success), 1 (command error), 2 (usage/argument error), 77 - (auth/permission), 78 (configuration). See `templates/error-types.rs` for the `AppError` enum with `exit_code()` - method. -- Every error message MUST include three components: what failed (the operation), why it failed (the cause), and what to - do next (the remediation). Example: "Authentication failed: token expired (expires_at: 2026-03-25T00:00:00Z). Run - `tool auth refresh` or set TOOL_TOKEN." - -**SHOULD:** - -- Error types SHOULD use a structured enum (via `thiserror`) with variant-to-kind mapping for JSON serialization, so - agents can match on error kinds programmatically rather than parsing message text. -- Config and auth validation SHOULD happen before any network call or expensive operation. The three-tier dependency - gating pattern (meta commands, local commands, network commands) ensures the tool fails at the earliest possible - point. See `templates/clap-main.rs` for the tiered structure. -- Error output SHOULD respect `--output json` by emitting JSON-formatted errors to stderr when JSON output is selected. - -### Evidence Patterns - -- `Cli::try_parse()` in `main()` instead of `Cli::parse()` -- Error enum with `#[derive(Error)]` and distinct variants for config, auth, and command errors -- `exit_code()` method on the error type returning variant-specific codes -- `kind()` method returning a machine-readable string for JSON serialization -- `run()` function returning `Result<(), AppError>` (not calling `process::exit()` internally) -- Error messages containing remediation steps ("run X" or "set Y") - -### Anti-Patterns - -- `Cli::parse()` anywhere in the codebase — it silently prevents JSON error output -- `process::exit()` in library code or command handlers (only acceptable in `main()` after all error handling) -- A single catch-all error variant that maps everything to exit code 1 -- Error messages that state the symptom without the cause or fix: "Error: request failed" -- Panics (`unwrap()`, `expect()`) on recoverable errors in production code paths - ---- - -## P5: Safe Retries and Explicit Mutation Boundaries - -### Definition - -Every CLI MUST support `--dry-run` so agents can preview the effect of any command before committing. Write operations -MUST clearly separate destructive actions from read-only queries. An agent that cannot distinguish a safe read from a -dangerous write will either avoid the tool entirely or execute mutations blindly. - -### Why Agents Need It - -Agents retry failed operations by default. If a write operation is not idempotent, retrying it may create duplicates, -corrupt data, or trigger rate limits. When destructive operations require explicit confirmation (`--force`, `--yes`) and -support preview (`--dry-run`), agents can safely explore what a command would do before committing to it. Read-only -tools are inherently safe for retries, but they still benefit from clear documentation that no mutation occurs. - -### Requirements - -**MUST:** - -- Destructive operations (delete, overwrite, bulk modify) MUST require an explicit `--force` or `--yes` flag. Without - the flag, the tool MUST either refuse the operation or enter dry-run mode. -- The distinction between read and write commands MUST be clear from the command name and help text. An agent reading - `--help` output should immediately know whether a command mutates state. -- All CLIs MUST support a `--dry-run` flag. When set, commands validate inputs and report what they would do without - executing mutations. The output format MUST respect `--output json` so agents can parse the preview programmatically. - -**SHOULD:** - -- Write operations SHOULD be idempotent where the domain allows it — running the same command twice produces the same - result rather than duplicating the effect. - -### Evidence Patterns - -- `--dry-run` flag on commands that create, update, or delete resources -- `--force` or `--yes` flag on destructive commands -- Command names that signal intent: `add`, `remove`, `delete`, `create` for writes; `list`, `show`, `get`, `search` for - reads -- Dry-run output showing what would change without executing - -### Anti-Patterns - -- A `delete` command that executes immediately without `--force` or confirmation -- Write commands with the same name pattern as read commands (e.g., `sync` that silently overwrites local state) -- No `--dry-run` option on bulk operations where a preview would prevent costly mistakes -- Operations that fail on retry because the first attempt partially succeeded (non-idempotent writes without rollback) - ---- - -## P6: Composable and Predictable Command Structure - -### Definition - -Tools MUST integrate cleanly with pipes, scripts, and other CLI tools. This means fixing SIGPIPE handling, detecting -TTY for color/formatting decisions, supporting stdin for piped input, and maintaining a consistent, predictable -subcommand structure. - -### Why Agents Need It - -Agents compose CLI tools into pipelines: `tool list --output json | jaq '.[] | .id' | xargs tool get`. Every link in -this chain must behave predictably. A tool that panics on SIGPIPE when piped to `head` breaks the pipeline. A tool that -emits ANSI color codes into a pipe pollutes downstream JSON parsing. A tool with inconsistent subcommand naming forces -the agent to memorize exceptions rather than applying patterns. Composability is what makes a CLI tool a building block -rather than a dead end. - -### Requirements - -**MUST:** - -- The SIGPIPE fix MUST be the first executable statement in `main()`. Without it, piping output to `head`, `tail`, or - any tool that closes the pipe early causes a panic ("broken pipe"). See `templates/clap-main.rs` for the - `libc::signal(libc::SIGPIPE, libc::SIG_DFL)` pattern. -- Tools MUST detect TTY and respect `NO_COLOR` and `TERM=dumb` environment variables for disabling color output. When - stdout or stderr is not a terminal, color codes MUST be suppressed automatically. See `templates/output-format.rs` for - the TTY detection logic in `OutputConfig::new()`. -- Shell completions MUST be available via a `completions` subcommand using `clap_complete`. This is a Tier 1 - meta-command that works without config, auth, or network. See `templates/clap-main.rs` for the three-tier dependency - gating pattern. -- Network CLIs (those depending on reqwest, hyper, ureq, or similar HTTP crates) MUST provide a `--timeout` flag with a - sensible default (30 seconds). Agents operating under their own time budgets need to fail fast rather than block on - slow upstreams. -- If the CLI uses a pager (less, more, PAGER env), it MUST support `--no-pager` or respect `PAGER=""` to disable. Pagers - block headless execution indefinitely. -- When the CLI uses subcommands, all agentic flags (`--output`, `--quiet`, `--no-interactive`, `--timeout`) MUST have - `global = true` in their clap attribute so they propagate to all subcommands automatically. - -**SHOULD:** - -- Commands that accept input SHOULD support reading from stdin when no file argument is provided, enabling pipeline - composition. -- Subcommand naming SHOULD follow a consistent `noun verb` or `verb noun` convention throughout the tool. Mixing - patterns (e.g., `list-users` alongside `user show`) forces agents to learn exceptions. -- The three-tier dependency gating pattern SHOULD be used: Tier 1 (meta-commands like `completions` and `version`) needs - nothing; Tier 2 (local commands) needs config; Tier 3 (network commands) needs config + auth. This ensures that - `completions` and `version` always work, even in broken environments. -- Operations SHOULD be modeled as subcommands, not flags. `tool search "query"` is correct; `tool --search "query"` is - wrong. Flags are for behavior modifiers (`--quiet`, `--output json`), not for selecting which operation to perform. - -**MAY:** - -- Tools MAY support `--color auto|always|never` for explicit color control beyond TTY auto-detection. - -### Evidence Patterns - -- `libc::signal(libc::SIGPIPE, libc::SIG_DFL)` as the first statement in `main()` -- `IsTerminal` trait usage (either `std::io::IsTerminal` or the `is-terminal` crate) -- `NO_COLOR` environment variable check -- `TERM=dumb` check -- `clap_complete` in `Cargo.toml` dependencies -- A `completions` subcommand in the CLI enum -- Tiered match arms in `main()` separating meta-commands from config-dependent commands - -### Anti-Patterns - -- Missing SIGPIPE handler — `cargo run -- list | head` panics with "broken pipe" -- Hard-coded ANSI escape codes without TTY detection -- Color output in JSON mode — ANSI codes inside JSON string values break parsing -- A `completions` command that requires authentication or config to run -- No stdin support on commands where piped input is a natural use case - ---- - -## P7: Bounded, High-Signal Responses - -### Definition - -Tools MUST provide mechanisms to control output volume. Agent context windows are finite and expensive — a tool that -dumps 10,000 lines of unfiltered output wastes tokens and may exceed the context limit entirely, causing the agent to -lose track of the conversation. - -### Why Agents Need It - -Every token of CLI output consumed by an agent has a cost — both monetary (API tokens) and cognitive (context window -capacity). A tool that returns unbounded output forces the agent to either truncate (losing potentially important data) -or consume the full response (wasting context on noise). Bounded output with `--quiet`, `--verbose`, and `--limit` flags -gives the agent precise control over how much data it receives, keeping responses high-signal and within budget. - -### Requirements - -**MUST:** - -- Tools MUST support `--quiet` to suppress non-essential output (progress indicators, informational messages, decorative - formatting). When `--quiet` is set, only the requested data and errors appear. See `templates/output-format.rs` for - the `diag!` macro that gates diagnostic output behind the quiet flag. -- Tools MUST clamp unbounded list operations to a sensible default maximum. A `list` command without `--limit` MUST NOT - return more than a configurable ceiling (e.g., 100 items). If more items exist, the output MUST indicate truncation - (e.g., `"truncated": true` in JSON, or a stderr message in text mode). - -**SHOULD:** - -- Tools SHOULD support `--verbose` (or `-v` / `-vv`) for increasing diagnostic detail, useful when agents need to debug - failures. -- Tools SHOULD support `--limit` or `--max-results` to let callers request exactly the number of items they need. -- Tools SHOULD support `--timeout` to bound execution time. An agent waiting indefinitely for a hung network call cannot - proceed. - -**MAY:** - -- Tools MAY support cursor-based pagination flags (`--after`, `--before`) for efficient traversal of large result sets. -- Tools MAY automatically reduce output verbosity when detecting a non-TTY context (similar to how `--quiet` behaves in - JSON mode). - -### Evidence Patterns - -- `--quiet` flag with `FalseyValueParser` and env var binding -- `diag!` macro usage for all non-essential stderr output -- `--limit` or `--max-results` flag on list/search commands -- Pagination clamping logic (e.g., `min(requested, MAX_RESULTS)`) -- `--timeout` flag with a sensible default -- `--verbose` flag for diagnostic escalation -- `suppress_diag()` method that returns true when quiet is set or output format is JSON/JSONL - -### Anti-Patterns - -- List commands that return all results with no default limit — an agent listing 50,000 items floods its context window -- No `--quiet` flag — agents consuming JSON output still receive interleaved diagnostic text on stderr -- `--verbose` as the only output control (no way to reduce output, only increase it) -- Progress bars or spinners that write to stderr in non-TTY contexts, adding noise to agent logs -- No `--timeout` on network operations — a stalled request blocks the agent indefinitely diff --git a/bundle/scripts/check-compliance.sh b/bundle/scripts/check-compliance.sh deleted file mode 100755 index e0a4a54..0000000 --- a/bundle/scripts/check-compliance.sh +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env bash -# Agent-native CLI compliance checker — orchestrator. -# -# Discovers and runs all check-*.sh scripts in scripts/checks/, parses their -# STATUS|LABEL|EVIDENCE output, and produces a grouped scorecard. -# -# Usage: -# check-compliance.sh # Run all checks -# check-compliance.sh --principle N # Run only check-pN -# -# Exit codes: -# 0 = all PASS -# 1 = any WARN (no FAIL) -# 2 = any FAIL -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CHECKS_DIR="$SCRIPT_DIR/checks" - -# --- Argument parsing --- -REPO_PATH="" -PRINCIPLE="" - -while [[ $# -gt 0 ]]; do - case "$1" in - --principle) - PRINCIPLE="$2" - shift 2 - ;; - -*) - echo "Unknown option: $1" >&2 - echo "Usage: $(basename "$0") [--principle N]" >&2 - exit 2 - ;; - *) - REPO_PATH="$1" - shift - ;; - esac -done - -if [[ -z "$REPO_PATH" ]]; then - echo "Usage: $(basename "$0") [--principle N]" >&2 - exit 2 -fi - -if [[ ! -d "$REPO_PATH" ]]; then - echo "Error: '$REPO_PATH' is not a directory" >&2 - exit 2 -fi - -# Require Cargo.toml (Rust projects only for now) -if [[ ! -f "$REPO_PATH/Cargo.toml" ]]; then - echo "Error: No Cargo.toml found in '$REPO_PATH'" >&2 - echo " This checker currently supports Rust projects only." >&2 - echo " Non-Rust support can be added when needed." >&2 - exit 2 -fi - -# --- Group definitions (display order) --- -# Maps group prefix to display name. Checks are grouped by filename prefix. -declare -a GROUP_ORDER=("p1" "p2" "p3" "p4" "p5" "p6" "p7" "code" "project") -declare -A GROUP_NAMES=( - [p1]="P1: Non-Interactive" - [p2]="P2: Structured Output" - [p3]="P3: Progressive Help" - [p4]="P4: Actionable Errors" - [p5]="P5: Safe Retries" - [p6]="P6: Composable Structure" - [p7]="P7: Bounded Responses" - [code]="Code Quality" - [project]="Project Structure" -) - -# --- Discover checks --- -checks=() -if [[ -n "$PRINCIPLE" ]]; then - # Single principle mode - target="$CHECKS_DIR/check-p${PRINCIPLE}-"*.sh - # shellcheck disable=SC2086 - for f in $target; do - if [[ -f "$f" ]]; then - checks+=("$f") - else - echo "Error: No check script found for principle $PRINCIPLE" >&2 - echo " Expected: $CHECKS_DIR/check-p${PRINCIPLE}-*.sh" >&2 - exit 2 - fi - done -else - # All checks mode — glob for check-*.sh (excludes _helpers.sh by prefix convention) - for f in "$CHECKS_DIR"/check-*.sh; do - [[ -f "$f" ]] && checks+=("$f") - done -fi - -if [[ ${#checks[@]} -eq 0 ]]; then - echo "Error: No check scripts found in $CHECKS_DIR" >&2 - exit 2 -fi - -# --- Extract group from check filename --- -# check-p1-foo.sh → p1, check-code-bar.sh → code, check-project-baz.sh → project -get_group() { - local name - name=$(basename "$1" .sh) - name=${name#check-} # strip "check-" prefix - # Match p1-p7 first, then code, then project - if [[ "$name" =~ ^p[1-7] ]]; then - echo "${name:0:2}" - elif [[ "$name" =~ ^code ]]; then - echo "code" - elif [[ "$name" =~ ^project ]]; then - echo "project" - else - echo "other" - fi -} - -# --- Run checks and collect results --- -pass_count=0 -warn_count=0 -fail_count=0 - -# Store results keyed by group: group → array of formatted lines -declare -A GROUP_RESULTS - -for check in "${checks[@]}"; do - group=$(get_group "$check") - - # Run the check, capturing stdout and exit code - output="" - exit_code=0 - output=$("$check" "$REPO_PATH" 2>/dev/null) || exit_code=$? - - # Parse STATUS|LABEL|EVIDENCE - if [[ -n "$output" ]]; then - status=$(echo "$output" | cut -d'|' -f1) - label=$(echo "$output" | cut -d'|' -f2) - evidence=$(echo "$output" | cut -d'|' -f3-) - else - status="FAIL" - label="$(basename "$check" .sh)" - evidence="Check produced no output (exit $exit_code)" - fi - - # Count results - case "$status" in - PASS) pass_count=$((pass_count + 1)) ;; - WARN) warn_count=$((warn_count + 1)) ;; - FAIL) fail_count=$((fail_count + 1)) ;; - esac - - # Format and append to group - line=$(printf " %-4s %-24s %s" "$status" "$label" "$evidence") - GROUP_RESULTS[$group]="${GROUP_RESULTS[$group]:-}${line}"$'\n' -done - -# --- Print grouped scorecard --- -repo_name=$(basename "$REPO_PATH") -echo "" -echo "╔══════════════════════════════════════════════════════════╗" -echo "║ Agent-Native CLI Compliance — $repo_name" -echo "╚══════════════════════════════════════════════════════════╝" - -for group in "${GROUP_ORDER[@]}"; do - if [[ -n "${GROUP_RESULTS[$group]:-}" ]]; then - echo "" - echo " ${GROUP_NAMES[$group]}" - echo " ──────────────────────────────────────────────────────" - printf "%s" "${GROUP_RESULTS[$group]}" - fi -done - -# Print any ungrouped checks (future-proofing) -if [[ -n "${GROUP_RESULTS[other]:-}" ]]; then - echo "" - echo " Other" - echo " ──────────────────────────────────────────────────────" - printf "%s" "${GROUP_RESULTS[other]}" -fi - -total=$((pass_count + warn_count + fail_count)) -echo "" -echo "════════════════════════════════════════════════════════════" -printf " Score: %d/%d PASS" "$pass_count" "$total" -[[ "$warn_count" -gt 0 ]] && printf ", %d WARN" "$warn_count" -[[ "$fail_count" -gt 0 ]] && printf ", %d FAIL" "$fail_count" -echo "" -echo "" - -# --- Exit code --- -if [[ "$fail_count" -gt 0 ]]; then - exit 2 -elif [[ "$warn_count" -gt 0 ]]; then - exit 1 -else - exit 0 -fi diff --git a/bundle/scripts/checks/_helpers.sh b/bundle/scripts/checks/_helpers.sh deleted file mode 100644 index 9cfbf2b..0000000 --- a/bundle/scripts/checks/_helpers.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# Shared helpers for agent-native-cli compliance checks. -# Source this file — do not execute directly. - -set -euo pipefail - -emit_result() { - local status="$1" label="$2" evidence="$3" - echo "${status}|${label}|${evidence}" - case "$status" in - PASS) exit 0 ;; - WARN) exit 1 ;; - FAIL) exit 2 ;; - *) echo "BUG: unknown status '$status'" >&2; exit 2 ;; - esac -} - -validate_repo_path() { - if [[ -z "${1:-}" ]]; then - echo "Usage: $(basename "$0") " >&2 - exit 2 - fi - if [[ ! -d "$1" ]]; then - echo "Error: '$1' is not a directory" >&2 - exit 2 - fi - if [[ ! -d "$1/src" ]]; then - echo "Error: '$1/src' not found — is this a Rust project?" >&2 - exit 2 - fi -} - -validate_repo_path "${1:-}" -REPO_PATH="$1" -SRC_DIR="$REPO_PATH/src" diff --git a/bundle/scripts/checks/check-code-env-flags.sh b/bundle/scripts/checks/check-code-env-flags.sh deleted file mode 100755 index babd06c..0000000 --- a/bundle/scripts/checks/check-code-env-flags.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# Code quality: Env var overrides on agentic flags -# All agentic flags (output, quiet, no-interactive, timeout) must have -# env = "TOOL_*" attributes so agents can set them via environment. -# Boolean env vars must use FalseyValueParser so TOOL_QUIET=0 disables. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Check for env attribute on flags (env = "...") -env_attrs=$(rg -c 'env\s*=\s*"' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -# Check for FalseyValueParser on boolean flags -falsey=$(rg -c 'FalseyValueParser' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$env_attrs" -gt 0 && "$falsey" -gt 0 ]]; then - emit_result "PASS" "Env flag overrides" "env attributes ($env_attrs) + FalseyValueParser present" -elif [[ "$env_attrs" -gt 0 ]]; then - emit_result "FAIL" "Env flag overrides" "Has env attrs but missing FalseyValueParser for boolean flags" -else - emit_result "FAIL" "Env flag overrides" "No env = \"...\" attributes on flags — agents need env var overrides" -fi diff --git a/bundle/scripts/checks/check-code-naked-println.sh b/bundle/scripts/checks/check-code-naked-println.sh deleted file mode 100755 index 3193e5e..0000000 --- a/bundle/scripts/checks/check-code-naked-println.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# Code quality: No naked println! outside main.rs -# All output should go through OutputConfig so --quiet and --output json work. -# main.rs is exempt for meta-commands (version, completions). -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -violations=$(rg -c 'println!' --type rust "$SRC_DIR" --glob '!main.rs' 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$violations" -eq 0 ]]; then - emit_result "PASS" "No naked println!" "println! confined to main.rs" -else - files=$(rg -l 'println!' --type rust "$SRC_DIR" --glob '!main.rs' 2>/dev/null \ - | xargs -I{} basename {} | paste -sd, -) - emit_result "FAIL" "No naked println!" "println! found outside main.rs ($violations occurrences): $files" -fi diff --git a/bundle/scripts/checks/check-code-unwrap.sh b/bundle/scripts/checks/check-code-unwrap.sh deleted file mode 100755 index 3c14c6c..0000000 --- a/bundle/scripts/checks/check-code-unwrap.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -# Code quality: No .unwrap() in production code -# unwrap() panics crash the agent's subprocess with no structured error. -# Acceptable in tests (#[cfg(test)]) but not in src/ production code. -# Checks src/ excluding lines inside #[cfg(test)] modules. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Count .unwrap() in src/, excluding test files and test modules -# We exclude files in tests/ dir and look only at src/ -unwraps=$(rg -c '\.unwrap\(\)' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$unwraps" -eq 0 ]]; then - emit_result "PASS" "No unwrap() in prod" "Zero .unwrap() calls in src/" -else - files=$(rg -l '\.unwrap\(\)' --type rust "$SRC_DIR" 2>/dev/null \ - | xargs -I{} basename {} | paste -sd, -) - emit_result "FAIL" "No unwrap() in prod" ".unwrap() found in src/ ($unwraps occurrences): $files" -fi diff --git a/bundle/scripts/checks/check-p1-headless-auth.sh b/bundle/scripts/checks/check-p1-headless-auth.sh deleted file mode 100755 index 4feefbc..0000000 --- a/bundle/scripts/checks/check-p1-headless-auth.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# P1b: Headless auth (device-code / no-browser flow) -# If a CLI has authentication, agents cannot open browsers for OAuth. -# -# Canonical flag: --no-browser (describes the constraint, not the mechanism). -# -# PASS — no auth detected (not applicable) -# PASS — --no-browser flag present (canonical) -# WARN — non-canonical alternative (--device-code, --remote, --headless) -# WARN — auth delegated to subprocess (passthrough/Command spawning auth) -# FAIL — auth present, no headless flow, no delegation -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Detect auth-related code (OAuth, login, token management) -auth_patterns=$(rg -c "oauth|OAuth|auth.*login|login.*auth|token.*store|credential|authenticate" \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$auth_patterns" -eq 0 ]]; then - emit_result "PASS" "Headless auth" "No auth detected — not applicable" -fi - -# Tier 1: canonical --no-browser flag -canonical=$(rg -c "no.browser|no_browser" --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) -if [[ "$canonical" -gt 0 ]]; then - emit_result "PASS" "Headless auth" "Canonical --no-browser flag present" -fi - -# Tier 2: non-canonical headless alternatives -alt=$(rg -c "device.code|DeviceCode|device_authorization|headless" \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) -if [[ "$alt" -gt 0 ]]; then - emit_result "WARN" "Headless auth" "Non-canonical headless auth — rename to --no-browser" -fi - -# Tier 3: auth delegated to subprocess (passthrough/Command spawning auth) -delegated=$(rg -c 'passthrough.*auth|auth.*passthrough|Command::new.*auth|spawn.*"auth"' \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) -if [[ "$delegated" -gt 0 ]]; then - emit_result "WARN" "Headless auth" "Auth delegated to subprocess — expose --no-browser or document external auth" -fi - -emit_result "FAIL" "Headless auth" "Auth detected but no headless flow — agents cannot open browsers" diff --git a/bundle/scripts/checks/check-p1-non-interactive.sh b/bundle/scripts/checks/check-p1-non-interactive.sh deleted file mode 100755 index c9fa6b8..0000000 --- a/bundle/scripts/checks/check-p1-non-interactive.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Check for interactive prompt libraries/patterns -interactive_matches=$(rg -c "dialoguer|inquirer|read_line" --type rust "$SRC_DIR" 2>/dev/null | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$interactive_matches" -eq 0 ]]; then - emit_result "PASS" "Non-interactive" "No interactive prompts found in source" -fi - -# Interactive prompts found — check for --no-interactive guard -guard_matches=$(rg -c "no.interactive" --type rust "$SRC_DIR" 2>/dev/null | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$guard_matches" -gt 0 ]]; then - emit_result "PASS" "Non-interactive" "Interactive prompts gated by --no-interactive flag" -else - emit_result "FAIL" "Non-interactive" "Interactive prompts found without --no-interactive guard" -fi diff --git a/bundle/scripts/checks/check-p2-structured-output.sh b/bundle/scripts/checks/check-p2-structured-output.sh deleted file mode 100755 index b26697b..0000000 --- a/bundle/scripts/checks/check-p2-structured-output.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Signal 1: Output format enum/flag -format_flag=$(rg -c "OutputFormat|ValueEnum.*Text.*Json|output.*json.*jsonl" --type rust "$SRC_DIR" 2>/dev/null | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -# Signal 2: serde_json dependency -serde_json=$(rg -c "serde_json" "$REPO_PATH/Cargo.toml" 2>/dev/null || echo 0) - -if [[ "$format_flag" -gt 0 && "$serde_json" -gt 0 ]]; then - emit_result "PASS" "Structured output" "OutputFormat enum + serde_json present" -elif [[ "$serde_json" -gt 0 ]]; then - emit_result "WARN" "Structured output" "serde_json present but no OutputFormat enum found" -else - emit_result "FAIL" "Structured output" "No structured output support (missing OutputFormat and serde_json)" -fi diff --git a/bundle/scripts/checks/check-p3-progressive-help.sh b/bundle/scripts/checks/check-p3-progressive-help.sh deleted file mode 100755 index 210e38a..0000000 --- a/bundle/scripts/checks/check-p3-progressive-help.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# P3: Progressive help discovery -# Requires all three tiers of clap help text: -# - about: one-line summary (shown in parent's subcommand list) -# - long_about: extended description (shown before flags in --help) -# - after_help: usage examples (shown after flags — where agents look) -# FAIL if after_help or long_about is missing. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -after_help=$(rg -c "after_help|after_long_help" --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) -long_about=$(rg -c "long_about" --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$after_help" -gt 0 && "$long_about" -gt 0 ]]; then - emit_result "PASS" "Progressive help" "after_help ($after_help) + long_about ($long_about) present" -elif [[ "$after_help" -gt 0 ]]; then - emit_result "FAIL" "Progressive help" "Has after_help but missing long_about for extended descriptions" -elif [[ "$long_about" -gt 0 ]]; then - emit_result "FAIL" "Progressive help" "Has long_about but missing after_help with usage examples" -else - emit_result "FAIL" "Progressive help" "No after_help or long_about — agents cannot discover usage examples" -fi diff --git a/bundle/scripts/checks/check-p3-version-flag.sh b/bundle/scripts/checks/check-p3-version-flag.sh deleted file mode 100755 index 87f3587..0000000 --- a/bundle/scripts/checks/check-p3-version-flag.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# P3b: --version flag -# Agents need to know what version they're running for compatibility. -# clap provides this for free with #[command(version)] on the derive. -# -# PASS — version attribute or #[command(version)] found -# FAIL — no version support detected -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Check for clap version support: -# - #[command(version)] or #[command(version, ...)] in derive attributes -# - version = in Cli struct clap attributes -# - Version variant in Commands enum (subcommand) -# - CARGO_PKG_VERSION usage (manual version output) -version_support=$(rg -c 'command.*version|version\s*=|Version|CARGO_PKG_VERSION' \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$version_support" -gt 0 ]]; then - emit_result "PASS" "Version flag" "--version or version subcommand present" -else - emit_result "FAIL" "Version flag" "No --version support — add #[command(version)] to clap derive" -fi diff --git a/bundle/scripts/checks/check-p4-error-types.sh b/bundle/scripts/checks/check-p4-error-types.sh deleted file mode 100755 index 7a5cce5..0000000 --- a/bundle/scripts/checks/check-p4-error-types.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# P4a: Structured error types -# Canonical: thiserror crate (standard derive, source chaining, Display for free). -# Non-canonical: manual Error impl with exit_code() method (works but more boilerplate). -# -# PASS — thiserror in Cargo.toml (canonical) -# WARN — manual Error impl with exit_code() method (non-canonical) -# FAIL — neither -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Tier 1: canonical thiserror -thiserror=$(rg -c "thiserror" "$REPO_PATH/Cargo.toml" 2>/dev/null || echo 0) -if [[ "$thiserror" -gt 0 ]]; then - emit_result "PASS" "Error types" "thiserror in Cargo.toml (canonical)" -fi - -# Tier 2: manual Error impl with structured exit codes -manual_error=$(rg -c "fn exit_code|impl.*Display.*for.*Error|impl.*Error.*for" \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) -if [[ "$manual_error" -gt 0 ]]; then - emit_result "WARN" "Error types" "Manual Error impl — migrate to thiserror for standard derive pattern" -fi - -emit_result "FAIL" "Error types" "No structured error types — add thiserror to Cargo.toml" diff --git a/bundle/scripts/checks/check-p4-exit-codes.sh b/bundle/scripts/checks/check-p4-exit-codes.sh deleted file mode 100755 index 9cb735b..0000000 --- a/bundle/scripts/checks/check-p4-exit-codes.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -# P4b: Named exit code constants -# Requires named exit codes (EXIT_* constants or exit_code() method), -# not magic numbers scattered in match arms. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Check for named constants (EXIT_SUCCESS, EXIT_AUTH, etc.) or exit_code() method -named_codes=$(rg -c "EXIT_[A-Z]|fn exit_code|exit_code\(\)" --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$named_codes" -gt 0 ]]; then - emit_result "PASS" "Exit codes" "Named exit code constants or exit_code() method found" -else - emit_result "FAIL" "Exit codes" "No named exit codes — use EXIT_* constants or exit_code() method" -fi diff --git a/bundle/scripts/checks/check-p4-process-exit.sh b/bundle/scripts/checks/check-p4-process-exit.sh deleted file mode 100755 index 4bb6ed6..0000000 --- a/bundle/scripts/checks/check-p4-process-exit.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# P4d: No process::exit() outside main.rs -# Library code calling process::exit() skips destructors and error formatting. -# Only main.rs may call process::exit() or return ExitCode. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Search all .rs files except main.rs for process::exit -violations=$(rg -c "process::exit" --type rust "$SRC_DIR" --glob '!main.rs' 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$violations" -eq 0 ]]; then - emit_result "PASS" "No process::exit leaks" "process::exit() confined to main.rs" -else - files=$(rg -l "process::exit" --type rust "$SRC_DIR" --glob '!main.rs' 2>/dev/null \ - | xargs -I{} basename {} | paste -sd, -) - emit_result "FAIL" "No process::exit leaks" "process::exit() found outside main.rs: $files" -fi diff --git a/bundle/scripts/checks/check-p4-try-parse.sh b/bundle/scripts/checks/check-p4-try-parse.sh deleted file mode 100755 index 5afe1c7..0000000 --- a/bundle/scripts/checks/check-p4-try-parse.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# P4c: try_parse() instead of parse() -# Cli::parse() calls process::exit() on error, bypassing custom error handlers. -# Canonical: try_parse() — the standard clap method for fallible parsing. -# Non-canonical: from_arg_matches() — equally safe but more manual. -# -# PASS — try_parse() in main.rs (canonical) -# WARN — from_arg_matches() in main.rs (safe but non-canonical) -# FAIL — neither (using bare parse()) -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -main_rs="$SRC_DIR/main.rs" -if [[ ! -f "$main_rs" ]]; then - emit_result "FAIL" "try_parse" "No src/main.rs found" -fi - -# Tier 1: canonical try_parse() -try_parse=$(rg -c "try_parse" "$main_rs" 2>/dev/null || echo 0) -if [[ "$try_parse" -gt 0 ]]; then - emit_result "PASS" "try_parse" "try_parse() in main.rs (canonical)" -fi - -# Tier 2: non-canonical from_arg_matches() (safe but more manual) -from_arg=$(rg -c "from_arg_matches" "$main_rs" 2>/dev/null || echo 0) -if [[ "$from_arg" -gt 0 ]]; then - emit_result "WARN" "try_parse" "from_arg_matches() used — migrate to try_parse() for simplicity" -fi - -emit_result "FAIL" "try_parse" "Missing try_parse() — parse() bypasses custom error handlers" diff --git a/bundle/scripts/checks/check-p5-safe-retries.sh b/bundle/scripts/checks/check-p5-safe-retries.sh deleted file mode 100755 index e8dcd59..0000000 --- a/bundle/scripts/checks/check-p5-safe-retries.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -# P5: Safe retries — --dry-run required -# Every CLI must support --dry-run so agents can preview the effect of -# any command before committing to it. No exceptions based on -# perceived destructiveness. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -dry_run=$(rg -c 'dry.run|dry_run' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$dry_run" -gt 0 ]]; then - emit_result "PASS" "Safe retries (--dry-run)" "--dry-run flag found" -else - emit_result "FAIL" "Safe retries (--dry-run)" "Missing --dry-run flag" -fi diff --git a/bundle/scripts/checks/check-p6-completions.sh b/bundle/scripts/checks/check-p6-completions.sh deleted file mode 100755 index e48624c..0000000 --- a/bundle/scripts/checks/check-p6-completions.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# P6d: Shell completions via clap_complete -# Agents and humans both benefit from shell completions. -# Required as a Cargo dependency. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -clap_complete=$(rg -c "clap_complete" "$REPO_PATH/Cargo.toml" 2>/dev/null || echo 0) - -if [[ "$clap_complete" -gt 0 ]]; then - emit_result "PASS" "Shell completions" "clap_complete in Cargo.toml" -else - emit_result "FAIL" "Shell completions" "Missing clap_complete dependency" -fi diff --git a/bundle/scripts/checks/check-p6-global-flags.sh b/bundle/scripts/checks/check-p6-global-flags.sh deleted file mode 100755 index c303e99..0000000 --- a/bundle/scripts/checks/check-p6-global-flags.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# P6g: Global flags on agentic modifiers -# When a CLI uses subcommands, the four agentic flags (output, quiet, -# no-interactive, timeout) must have global = true so they propagate -# to all subcommands. Without this, agents must discover per-subcommand -# which flags are accepted. -# -# PASS — no subcommands (not applicable) or global = true present -# FAIL — subcommands exist but no global = true found -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Check if the CLI uses subcommands -subcommands=$(rg -c "Subcommand|subcommand" --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$subcommands" -eq 0 ]]; then - emit_result "PASS" "Global flags" "No subcommands — not applicable" -fi - -# Subcommands exist — check for global = true on flags -global_flags=$(rg -c "global\s*=\s*true" --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$global_flags" -ge 2 ]]; then - emit_result "PASS" "Global flags" "global = true on $global_flags flag(s)" -elif [[ "$global_flags" -eq 1 ]]; then - emit_result "WARN" "Global flags" "Only 1 global flag — agentic flags (output, quiet, no-interactive, timeout) should all be global" -else - emit_result "FAIL" "Global flags" "Subcommands present but no global = true — agentic flags won't propagate" -fi diff --git a/bundle/scripts/checks/check-p6-no-color.sh b/bundle/scripts/checks/check-p6-no-color.sh deleted file mode 100755 index fddb3aa..0000000 --- a/bundle/scripts/checks/check-p6-no-color.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# P6c: NO_COLOR environment variable support -# https://no-color.org/ — independent of TTY detection. -# When NO_COLOR is set, all ANSI color output must be suppressed. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -no_color=$(rg -c "NO_COLOR" --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$no_color" -gt 0 ]]; then - emit_result "PASS" "NO_COLOR support" "NO_COLOR env var checked in source" -else - emit_result "FAIL" "NO_COLOR support" "Missing NO_COLOR check — see no-color.org" -fi diff --git a/bundle/scripts/checks/check-p6-no-pager.sh b/bundle/scripts/checks/check-p6-no-pager.sh deleted file mode 100755 index c84d277..0000000 --- a/bundle/scripts/checks/check-p6-no-pager.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# P6e: No pager in headless environments -# If a CLI uses a pager (less, more, PAGER), it must have a --no-pager -# flag or respect PAGER="" to disable. A pager blocks headless execution -# indefinitely — the agent waits for input that never comes. -# -# PASS if no pager usage detected (not applicable). -# PASS if pager detected AND disable mechanism present. -# FAIL if pager detected WITHOUT disable mechanism. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Detect pager usage (spawning less/more, reading PAGER env, pager crate) -pager_usage=$(rg -c 'pager|::less|"less"|"more"|Command::new.*less|spawn.*pager' \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$pager_usage" -eq 0 ]]; then - emit_result "PASS" "No pager blocking" "No pager usage detected — not applicable" -fi - -# Pager exists — check for disable mechanism -disable=$(rg -c 'no.pager|no_pager|NO_PAGER|PAGER.*""' \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$disable" -gt 0 ]]; then - emit_result "PASS" "No pager blocking" "Pager with --no-pager or PAGER disable mechanism" -else - emit_result "FAIL" "No pager blocking" "Pager detected without disable mechanism — blocks headless execution" -fi diff --git a/bundle/scripts/checks/check-p6-sigpipe.sh b/bundle/scripts/checks/check-p6-sigpipe.sh deleted file mode 100755 index 1c411dd..0000000 --- a/bundle/scripts/checks/check-p6-sigpipe.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# P6a: SIGPIPE fix in main() -# Without this, piping to `head` panics with "broken pipe". -# Must be first thing in main(). -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -main_rs="$SRC_DIR/main.rs" -if [[ ! -f "$main_rs" ]]; then - emit_result "FAIL" "SIGPIPE fix" "No src/main.rs found" -fi - -sigpipe=$(rg -c "SIGPIPE|SIG_DFL" "$main_rs" 2>/dev/null || echo 0) - -if [[ "$sigpipe" -gt 0 ]]; then - emit_result "PASS" "SIGPIPE fix" "SIGPIPE/SIG_DFL handling in main.rs" -else - emit_result "FAIL" "SIGPIPE fix" "Missing SIGPIPE fix — pipe to head will panic" -fi diff --git a/bundle/scripts/checks/check-p6-timeout.sh b/bundle/scripts/checks/check-p6-timeout.sh deleted file mode 100755 index be35e0b..0000000 --- a/bundle/scripts/checks/check-p6-timeout.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# P6f: --timeout for network CLIs -# If a CLI makes HTTP requests, a hung request without a timeout is the -# #1 agent failure mode — the agent waits forever. -# -# PASS — no network crates (not applicable) -# PASS — timeout flag or configuration found -# FAIL — network crates present without timeout support -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Detect network crates in Cargo.toml -network_crates=$(rg -c "reqwest|hyper|ureq|surf|isahc|attohttpc" "$REPO_PATH/Cargo.toml" 2>/dev/null || echo 0) - -if [[ "$network_crates" -eq 0 ]]; then - emit_result "PASS" "Network timeout" "No HTTP crates detected — not applicable" -fi - -# Network crates present — check for timeout support -timeout_flag=$(rg -c 'timeout|Timeout|TIMEOUT' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$timeout_flag" -gt 0 ]]; then - emit_result "PASS" "Network timeout" "--timeout or timeout configuration present" -else - emit_result "FAIL" "Network timeout" "HTTP crates present but no --timeout flag — agents may hang forever" -fi diff --git a/bundle/scripts/checks/check-p6-tty-detection.sh b/bundle/scripts/checks/check-p6-tty-detection.sh deleted file mode 100755 index 960b367..0000000 --- a/bundle/scripts/checks/check-p6-tty-detection.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# P6b: TTY detection -# Canonical: std::io::IsTerminal (stable since Rust 1.70, stdlib). -# Non-canonical: is-terminal crate or deprecated atty crate. -# -# PASS — std::io::IsTerminal in source (canonical) -# WARN — is-terminal or atty crate (works but use stdlib) -# FAIL — no TTY detection -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -# Tier 1: canonical std::io::IsTerminal (trait import or method call) -stdlib_terminal=$(rg -c "std::io::IsTerminal|use std::io::IsTerminal|\.is_terminal\(\)" \ - --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) -if [[ "$stdlib_terminal" -gt 0 ]]; then - emit_result "PASS" "TTY detection" "std::io::IsTerminal (canonical, stdlib)" -fi - -# Tier 2: non-canonical is-terminal crate or deprecated atty -crate_terminal=$(rg -c "is.terminal|IsTerminal" "$REPO_PATH/Cargo.toml" 2>/dev/null || echo 0) -atty_crate=$(rg -c "atty" "$REPO_PATH/Cargo.toml" 2>/dev/null || echo 0) -if [[ $((crate_terminal + atty_crate)) -gt 0 ]]; then - emit_result "WARN" "TTY detection" "Using crate for TTY detection — migrate to std::io::IsTerminal (Rust 1.70+)" -fi - -emit_result "FAIL" "TTY detection" "No TTY detection — use std::io::IsTerminal" diff --git a/bundle/scripts/checks/check-p7-output-clamping.sh b/bundle/scripts/checks/check-p7-output-clamping.sh deleted file mode 100755 index dda0aff..0000000 --- a/bundle/scripts/checks/check-p7-output-clamping.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# P7b: Output clamping for bounded responses -# List endpoints must have --limit/--max-results AND .clamp() on values. -# Prevents unbounded API responses from flooding agent context. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -limit_flag=$(rg -c 'long.*=.*"limit"|long.*=.*"max-results"|max.results.*Option' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) -clamp_usage=$(rg -c '\.clamp\(' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$limit_flag" -gt 0 && "$clamp_usage" -gt 0 ]]; then - emit_result "PASS" "Output clamping" "--limit/--max-results with .clamp() present" -elif [[ "$limit_flag" -gt 0 ]]; then - emit_result "FAIL" "Output clamping" "Has limit flag but missing .clamp() on values" -elif [[ "$clamp_usage" -gt 0 ]]; then - emit_result "FAIL" "Output clamping" "Has .clamp() but missing --limit/--max-results flag" -else - emit_result "FAIL" "Output clamping" "No output clamping — add --limit/--max-results with .clamp()" -fi diff --git a/bundle/scripts/checks/check-p7-quiet-flag.sh b/bundle/scripts/checks/check-p7-quiet-flag.sh deleted file mode 100755 index 30cef14..0000000 --- a/bundle/scripts/checks/check-p7-quiet-flag.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# P7a: --quiet flag definition -# Required for agents to suppress non-essential output. -# Searches for the flag definition, not propagation sites. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -quiet_def=$(rg -c 'long.*=.*"quiet"|short.*q.*quiet|pub\s+quiet.*bool' --type rust "$SRC_DIR" 2>/dev/null \ - | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || echo 0) - -if [[ "$quiet_def" -gt 0 ]]; then - emit_result "PASS" "Quiet flag" "--quiet flag defined" -else - emit_result "FAIL" "Quiet flag" "Missing --quiet flag definition" -fi diff --git a/bundle/scripts/checks/check-project-agents-md.sh b/bundle/scripts/checks/check-project-agents-md.sh deleted file mode 100755 index 9264b93..0000000 --- a/bundle/scripts/checks/check-project-agents-md.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# Project: AGENTS.md at repo root -# Canonical: AGENTS.md (plural, agent-agnostic, Anthropic standard). -# Non-canonical: AGENT.md (singular), CLAUDE.md (tool-specific). -# -# CLAUDE.md serves a different purpose (Claude Code harness instructions) -# but some projects use it as a substitute for AGENTS.md. It should not -# replace AGENTS.md — the standard file is agent-agnostic. -# -# PASS — AGENTS.md exists (canonical) -# WARN — AGENT.md or CLAUDE.md exists without AGENTS.md (rename/add) -# FAIL — none of the above -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Override _helpers.sh validation — AGENTS.md check doesn't need src/ -if [[ -z "${1:-}" ]]; then - echo "Usage: $(basename "$0") " >&2 - exit 2 -fi -if [[ ! -d "$1" ]]; then - echo "Error: '$1' is not a directory" >&2 - exit 2 -fi -REPO_PATH="$1" - -emit_result() { - local status="$1" label="$2" evidence="$3" - echo "${status}|${label}|${evidence}" - case "$status" in - PASS) exit 0 ;; - WARN) exit 1 ;; - FAIL) exit 2 ;; - *) echo "BUG: unknown status '$status'" >&2; exit 2 ;; - esac -} - -# Tier 1: canonical AGENTS.md (plural) -if [[ -f "$REPO_PATH/AGENTS.md" ]]; then - emit_result "PASS" "AGENTS.md" "AGENTS.md exists at repo root (canonical)" -fi - -# Tier 2: non-canonical alternatives -if [[ -f "$REPO_PATH/AGENT.md" ]]; then - emit_result "WARN" "AGENTS.md" "Found AGENT.md (singular) — rename to AGENTS.md" -fi -if [[ -f "$REPO_PATH/CLAUDE.md" ]]; then - emit_result "WARN" "AGENTS.md" "Found CLAUDE.md but no AGENTS.md — add AGENTS.md for agent-agnostic docs" -fi - -emit_result "FAIL" "AGENTS.md" "Missing AGENTS.md — agents need build/test/convention docs" diff --git a/bundle/scripts/checks/check-project-dependencies.sh b/bundle/scripts/checks/check-project-dependencies.sh deleted file mode 100755 index 74e91ce..0000000 --- a/bundle/scripts/checks/check-project-dependencies.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# Project: Required Cargo.toml dependencies for agent-native CLI -# All are hard requirements — no partial credit. -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/_helpers.sh" - -cargo_toml="$REPO_PATH/Cargo.toml" -missing=() - -# Required dependencies -rg -q 'clap.*derive' "$cargo_toml" 2>/dev/null || missing+=("clap(derive)") -rg -q 'serde_json' "$cargo_toml" 2>/dev/null || missing+=("serde_json") -rg -q 'thiserror' "$cargo_toml" 2>/dev/null || missing+=("thiserror") -rg -q 'libc' "$cargo_toml" 2>/dev/null || missing+=("libc") -rg -q 'clap_complete' "$cargo_toml" 2>/dev/null || missing+=("clap_complete") - -if [[ ${#missing[@]} -eq 0 ]]; then - emit_result "PASS" "Dependencies" "All required crates present in Cargo.toml" -else - list=$(IFS=", "; echo "${missing[*]}") - emit_result "FAIL" "Dependencies" "Missing required crates: $list" -fi diff --git a/bundle/spec/CHANGELOG.md b/bundle/spec/CHANGELOG.md new file mode 100644 index 0000000..e536cbc --- /dev/null +++ b/bundle/spec/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to the agent-native CLI standard are documented here. + +## [0.2.0] - 2026-04-23 + +### Added + +- Per-principle `requirements[]` frontmatter contract: 46 stable requirement IDs (`p1-must-env-var` … `p7-may-auto-verbosity`) with `level`, `applicability`, and `summary`. by @brettdavies in [#3](https://github.com/brettdavies/agentnative/pull/3) +- `status: draft | under-review | locked` field on every principle. +- `principles/AGENTS.md` authoring conventions and pressure-test protocol. +- `docs/decisions/` named records: P1 behavioral-MUST doctrine, naming rationale. +- `scripts/generate-changelog.sh` — two-stage release-note generator that runs `git-cliff` for the skeleton and a Python post-processor to fetch PR bodies from the GitHub API and expand each entry with `### Added / Changed / Fixed / Removed / Security` subsections. Ported from `brettdavies/agentnative`. by @brettdavies in [#9](https://github.com/brettdavies/agentnative/pull/9) + +### Changed + +- Requirement IDs are now sourced from this repo; `agentnative-cli` will vendor the spec and drift-check against it (previously the CLI embedded the list in `src/principles/registry.rs`). by @brettdavies in [#3](https://github.com/brettdavies/agentnative/pull/3) +- `CONTRIBUTING.md`: versioning rule now covers frontmatter-shape changes as MINOR. +- `cliff.toml` switched from fragile commit-body-header parsing (which broke when markdown headers got stripped during cherry-picks) to subject-line-with-PR-link rendering. The PR body is now the source of truth for release notes. by @brettdavies in [#9](https://github.com/brettdavies/agentnative/pull/9) + +**Full Changelog**: [v0.1.1...v0.2.0](https://github.com/brettdavies/agentnative/compare/v0.1.1...v0.2.0) + +## [0.1.1] - 2026-04-20 + +### Added + +- Seven agent-native principles (P1–P7) published with `last-revised: 2026-04-20` per-principle calver. +- Governance model: three-repo architecture (spec / CLI / site), AI disclosure on all contributions, human co-sign on + principle edits and PRs, coupled-release protocol between spec and checker. + +### Changed + +- P1 "Non-Interactive by Default" — applicability gates added (help-on-bare-invocation, agentic flag, + stdin-as-primary-input). diff --git a/bundle/spec/README.md b/bundle/spec/README.md new file mode 100644 index 0000000..b93fc55 --- /dev/null +++ b/bundle/spec/README.md @@ -0,0 +1,40 @@ +# Vendored agentnative-spec + +This directory is a **vendored copy** of [`brettdavies/agentnative`](https://github.com/brettdavies/agentnative) — the +canonical specification of agent-native CLI principles. Files here are not edited by hand; they are mirrored from a +pinned upstream tag and ship inside the skill bundle so consumers carry the canonical principle text alongside the skill +metadata. + +**Current pin:** `v0.2.0` + +## Resync + +Run from the repo root: + +```bash +scripts/sync-spec.sh # default: SPEC_REF=v0.2.0 +SPEC_REF=v0.2.1 scripts/sync-spec.sh # bump to a newer tag +``` + +The script extracts files at the named git ref via `git show`, so the spec checkout's working tree is not perturbed. +Override `SPEC_ROOT` if your spec checkout is not at `$HOME/dev/agentnative-spec`. + +## Layout + +| Path | Source in `agentnative-spec` | Purpose | +| ------------------ | ---------------------------- | ------------------------------------------------------------- | +| `VERSION` | `VERSION` | Spec version string; the skill's pinned `SPEC_VERSION` | +| `CHANGELOG.md` | `CHANGELOG.md` | Spec change history; informational | +| `principles/p*.md` | `principles/p*.md` | Frontmatter `requirements[]` is the machine-readable contract | + +Each principle file has a YAML frontmatter block with `id`, `title`, `last-revised`, `status`, and `requirements[]`. +Each `requirements[]` entry carries a stable `id` (e.g. `p1-must-no-interactive`), a `level` (`must`/`should`/`may`), an +`applicability` (`universal` or `{if: }`), and a one-sentence `summary`. The `anc` checker +([brettdavies/agentnative-cli](https://github.com/brettdavies/agentnative-cli)) emits these IDs in its scorecard so +agents can navigate from a finding directly to the requirement. + +## Licensing + +Upstream content is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). This skill bundle is +MIT-licensed; vendoring a CC BY 4.0 source requires attribution only, satisfied by this README plus the upstream project +link in each principle's frontmatter `id` field. diff --git a/bundle/spec/VERSION b/bundle/spec/VERSION new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/bundle/spec/VERSION @@ -0,0 +1 @@ +0.2.0 diff --git a/bundle/spec/principles/p1-non-interactive-by-default.md b/bundle/spec/principles/p1-non-interactive-by-default.md new file mode 100644 index 0000000..7743803 --- /dev/null +++ b/bundle/spec/principles/p1-non-interactive-by-default.md @@ -0,0 +1,106 @@ +--- +id: p1 +title: Non-Interactive by Default +last-revised: 2026-04-22 +status: draft +requirements: + - id: p1-must-env-var + level: must + applicability: universal + summary: Every flag settable via environment variable (falsey-value parser for booleans). + - id: p1-must-no-interactive + level: must + applicability: universal + summary: "`--no-interactive` flag gates every prompt library call; when set or stdin is not a TTY, use defaults/stdin or exit with an actionable error." + - id: p1-must-no-browser + level: must + applicability: + if: CLI authenticates against a remote service + summary: Headless authentication path (`--no-browser` / OAuth Device Authorization Grant). + - id: p1-should-tty-detection + level: should + applicability: universal + summary: Auto-detect non-interactive context via TTY detection; suppress prompts when stderr is not a terminal. + - id: p1-should-defaults-in-help + level: should + applicability: universal + summary: Document default values for prompted inputs in `--help` output. + - id: p1-may-rich-tui + level: may + applicability: universal + summary: Rich interactive experiences (spinners, progress bars, menus) when TTY is detected and `--no-interactive` is not set. +--- + +# P1: Non-Interactive by Default + +## Definition + +Every automation path MUST run without human input. A CLI tool that blocks on an interactive prompt is invisible to an +agent — the agent hangs, the user sees nothing, and the operation times out silently. + +**Decision record:** this principle's MUST is worded in terms of observable behavior rather than enumerated APIs. +[`docs/decisions/p1-behavioral-must.md`](../docs/decisions/p1-behavioral-must.md) records the reasoning and names the +verification boundary: automated checks verify behavior under non-TTY stdin; TTY-driving-agent scenarios are covered by +the MUST but are not PTY-probed at the current scale. + +## Why Agents Need It + +An agent calling a CLI cannot type. When the tool prompts for a confirmation or a credential, the agent's process stalls +until timeout: no tokens recovered, no structured signal that interaction was requested, and no way to distinguish +"waiting for input" from "still processing." Interactive prompts in automation paths are the single most common cause of +agent-tool deadlock. + +## Requirements + +**MUST:** + +- Every flag settable via environment variable. Use a falsey-value parser for booleans so that `TOOL_QUIET=0` and + `TOOL_QUIET=false` correctly disable the flag rather than being treated as truthy non-empty strings. In Rust / clap: + + ```rust + #[arg(long, env = "TOOL_QUIET", global = true, + value_parser = FalseyValueParser::new())] + quiet: bool, + ``` + +- A `--no-interactive` flag gating every prompt library call (`dialoguer`, `inquire`, `read_line`, `TTY::Prompt`, + `inquirer`, equivalents in other frameworks). When the flag is set, or when stdin is not a TTY, the tool uses + defaults, reads from stdin, or exits with an actionable error. It never blocks. +- A headless authentication path if the CLI authenticates. The canonical flag is `--no-browser`, which triggers the + OAuth 2.0 Device Authorization Grant ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)): the CLI prints a URL and a + code; the user authorizes on another device. Agents cannot open browsers. Non-canonical alternatives (`--device-code`, + `--remote`, `--headless`) are acceptable but should migrate toward `--no-browser`. + +**SHOULD:** + +- Auto-detect non-interactive context via TTY detection (`std::io::IsTerminal` in Rust 1.70+, `process.stdin.isTTY` in + Node, `sys.stdout.isatty()` in Python) and suppress prompts when stderr is not a terminal, even without an explicit + `--no-interactive` flag. +- Document default values for prompted inputs in `--help` output so agents can pass them explicitly instead of accepting + whatever default ships. + +**MAY:** + +- Offer rich interactive experiences — spinners, progress bars, multi-select menus — when a TTY is detected and + `--no-interactive` is not set, provided the non-interactive path remains fully functional. + +## Evidence + +- `--no-interactive` flag in the CLI struct with an env-var binding. +- Boolean env vars parsed with a falsey-value parser (not the default string parser). +- TTY guard wrapping every `dialoguer`, `inquire`, or equivalent prompt call. +- `--no-browser` flag present on authenticated CLIs. +- `env = "TOOL_..."` attribute on every flag that takes user input. + +## Anti-Patterns + +- Bare `dialoguer::Confirm::new().interact()` with no TTY check and no `--no-interactive` override — agents hang + indefinitely. +- Boolean environment variables parsed as plain strings, so `TOOL_QUIET=false` is truthy because the string is + non-empty. +- `stdin().read_line()` in a code path reached during normal operation without a TTY check first. +- Hard-coded credentials prompts with no env-var or config-file alternative. +- OAuth flow that unconditionally opens a browser with no headless escape hatch. + +Measured by check IDs `p1-non-interactive` (behavioral) and `p1-non-interactive-source` (source). Run `agentnative check + --principle 1 .` against your CLI to see both. diff --git a/bundle/spec/principles/p2-structured-parseable-output.md b/bundle/spec/principles/p2-structured-parseable-output.md new file mode 100644 index 0000000..d479915 --- /dev/null +++ b/bundle/spec/principles/p2-structured-parseable-output.md @@ -0,0 +1,102 @@ +--- +id: p2 +title: Structured, Parseable Output +last-revised: 2026-04-22 +status: draft +requirements: + - id: p2-must-output-flag + level: must + applicability: universal + summary: "`--output text|json|jsonl` flag selects output format; `OutputFormat` enum threaded through output paths." + - id: p2-must-stdout-stderr-split + level: must + applicability: universal + summary: Data goes to stdout; diagnostics/progress/warnings go to stderr — never interleaved. + - id: p2-must-exit-codes + level: must + applicability: universal + summary: Exit codes are structured and documented (0 success, 1 general, 2 usage, 77 auth, 78 config). + - id: p2-must-json-errors + level: must + applicability: universal + summary: When `--output json` is active, errors are emitted as JSON (to stderr) with at least `error`, `kind`, and `message` fields. + - id: p2-should-consistent-envelope + level: should + applicability: universal + summary: JSON output uses a consistent envelope — a top-level object with predictable keys — across every command. + - id: p2-may-more-formats + level: may + applicability: universal + summary: Additional output formats (CSV, TSV, YAML) beyond the core three. + - id: p2-may-raw-flag + level: may + applicability: universal + summary: "`--raw` flag for unformatted output suitable for piping to other tools." +--- + +# P2: Structured, Parseable Output + +## Definition + +CLI tools MUST separate data from diagnostics and offer machine-readable output formats. Mixing status messages with +data forces agents into fragile regex extraction that breaks on any format change. + +## Why Agents Need It + +An agent calling a CLI needs three things from each invocation: the data, the error (if any), and the exit code. When +data goes to stdout, diagnostics go to stderr, and errors carry machine-readable fields, the agent parses the result +reliably without heuristics. Mix these channels or ship human-formatted output only, and the agent falls back to +best-effort text parsing that fails unpredictably across versions, locales, and edge cases — silently at first, +catastrophically later. + +## Requirements + +**MUST:** + +- A `--output text|json|jsonl` flag selects the output format. Text is the default for humans; JSON and JSONL are the + agent-facing formats. Implementation surfaces an `OutputFormat` enum and an `OutputConfig` struct threaded through + every function that produces output. +- Data goes to stdout. Diagnostics, progress indicators, and warnings go to stderr. An agent consuming JSON from stdout + must never encounter an interleaved progress message. +- Exit codes are structured and documented: + + | Code | Meaning | + | ---: | -------------------------------------------- | + | 0 | Success | + | 1 | General command error | + | 2 | Usage error (bad arguments) | + | 77 | Authentication / permission error | + | 78 | Configuration error | + +- When `--output json` is active, errors are emitted as JSON (to stderr) with at least `error`, `kind`, and `message` + fields. Plain-text errors in a JSON run break the agent's parser on the only output it was told to expect. + +**SHOULD:** + +- JSON output uses a consistent envelope — a top-level object with predictable keys — across every command so agents can + rely on the same shape. + +**MAY:** + +- Additional output formats (CSV, TSV, YAML) beyond the core three. The core three remain mandatory. +- A `--raw` flag for unformatted output suitable for piping to other tools. + +## Evidence + +- `OutputFormat` enum with `Text`, `Json`, `Jsonl` variants deriving `ValueEnum`. +- `OutputConfig` struct with `format`, `use_color`, and `quiet` fields. +- `serde_json` in `Cargo.toml`. +- No `println!` in `src/` outside the output module — every print goes through `OutputConfig`. +- Exit-code constants or match arms mapping error variants to distinct numeric codes. +- `eprintln!` (or an equivalent diagnostic macro) for every diagnostic line. + +## Anti-Patterns + +- `println!` scattered across handlers instead of routing through the output config. +- A single exit code (1) for everything — agents cannot distinguish auth failures from config errors. +- Status lines ("Fetching data…") printed to stdout where they contaminate JSON output. +- `process::exit()` in library code, bypassing structured error propagation. +- Human-formatted tables as the only output mode with no JSON alternative. + +Measured by check IDs `p2-output-json`, `p2-output-format`, `p2-stderr-diagnostics`. Run +`agentnative check --principle 2 .` against your CLI to see each. diff --git a/bundle/spec/principles/p3-progressive-help-discovery.md b/bundle/spec/principles/p3-progressive-help-discovery.md new file mode 100644 index 0000000..85642a4 --- /dev/null +++ b/bundle/spec/principles/p3-progressive-help-discovery.md @@ -0,0 +1,81 @@ +--- +id: p3 +title: Progressive Help Discovery +last-revised: 2026-04-22 +status: draft +requirements: + - id: p3-must-subcommand-examples + level: must + applicability: + if: CLI uses subcommands + summary: Every subcommand ships at least one concrete invocation example (`after_help` in clap). + - id: p3-must-top-level-examples + level: must + applicability: universal + summary: The top-level command ships 2–3 examples covering the primary use cases. + - id: p3-should-paired-examples + level: should + applicability: universal + summary: Examples show human and agent invocations side by side (text then `--output json` equivalent). + - id: p3-should-about-long-about + level: should + applicability: universal + summary: Short `about` for command-list summaries; `long_about` reserved for detailed descriptions visible with `--help`. + - id: p3-may-examples-subcommand + level: may + applicability: universal + summary: Dedicated `examples` subcommand or `--examples` flag for curated usage patterns. +--- + +# P3: Progressive Help Discovery + +## Definition + +Help text MUST be layered so agents (and humans) can drill from a short summary to concrete usage examples without +reading the entire manual. The critical layer is the one that appears **after** the flags list, because that is where +readers look for invocation patterns. + +## Why Agents Need It + +Agents discover how to use a tool by calling `--help` and scanning the output. They skip past flag definitions (which +describe what is *possible*) and hunt for examples (which describe what to *do*). A flags list alone is enough rope to +produce a failed invocation; examples are what turn discovery into action. Without examples in the help output, an agent +trial-and-errors its way into a working call, burning tokens and sometimes landing on a wrong-but-silent success. + +## Requirements + +**MUST:** + +- Every subcommand ships at least one concrete invocation example showing the command with realistic arguments, rendered + in the section that appears after the flags list. In clap this is the `after_help` attribute. +- The top-level command ships 2–3 examples covering the primary use cases. + +**SHOULD:** + +- Examples show human and agent invocations side by side — a text-output example followed by its `--output json` + equivalent. Readers see the pair; agents see the JSON form. +- Short `about` for command-list summaries; `long_about` reserved for detailed descriptions visible with `--help` but + not `-h`. + +**MAY:** + +- A dedicated `examples` subcommand or `--examples` flag that outputs a curated set of usage patterns for agent + consumption. + +## Evidence + +- `after_help` (or `after_long_help`) attribute on the top-level parser struct. +- `after_help` attribute on every subcommand variant. +- Example invocations in `after_help` text that include realistic arguments, not placeholder `` tokens. +- Both `about` (short) and `after_help` (examples) present on each subcommand. + +## Anti-Patterns + +- Relying solely on `///` doc comments — those populate `about` / `long_about`, not `after_help`, so no examples render + after the flags list. +- A single `about` string serving as both summary and usage documentation. +- Examples buried in a README or man page but absent from `--help` output. +- `after_help` text that describes the flags in prose instead of demonstrating them in code. + +Measured by check IDs `p3-help`, `p3-after-help`, `p3-version`. Run `agentnative check --principle 3 .` against +your CLI to see each. diff --git a/bundle/spec/principles/p4-fail-fast-actionable-errors.md b/bundle/spec/principles/p4-fail-fast-actionable-errors.md new file mode 100644 index 0000000..73d34be --- /dev/null +++ b/bundle/spec/principles/p4-fail-fast-actionable-errors.md @@ -0,0 +1,104 @@ +--- +id: p4 +title: Fail Fast with Actionable Errors +last-revised: 2026-04-22 +status: draft +requirements: + - id: p4-must-try-parse + level: must + applicability: universal + summary: Parse arguments with `try_parse()` instead of `parse()` so `--output json` can emit JSON parse errors. + - id: p4-must-exit-code-mapping + level: must + applicability: universal + summary: Error types map to distinct exit codes (0, 1, 2, 77, 78). + - id: p4-must-actionable-errors + level: must + applicability: universal + summary: Every error message contains what failed, why, and what to do next. + - id: p4-should-structured-enum + level: should + applicability: universal + summary: Error types use a structured enum (via `thiserror` in Rust) with variant-to-kind mapping for JSON serialization. + - id: p4-should-gating-before-network + level: should + applicability: + if: CLI makes network calls + summary: Config and auth validation happen before any network call (three-tier dependency gating). + - id: p4-should-json-error-output + level: should + applicability: universal + summary: "Error output respects `--output json`: JSON-formatted errors go to stderr when JSON output is selected." +--- + +# P4: Fail Fast with Actionable Errors + +## Definition + +CLI tools MUST detect invalid state early, exit with a structured error, and tell the caller three things: what failed, +why, and what to do next. An error that says "operation failed" gives an agent nothing to act on. + +## Why Agents Need It + +Agents operate in a retry loop: attempt, observe, decide. When an error is vague or unstructured — a bare stack trace, a +one-word failure, a mixed-channel splurge — the agent cannot tell whether to retry, re-authenticate, fix configuration, +or escalate to the user. Distinct exit codes with actionable messages let the agent act correctly on the first read. The +difference between exit code 77 (re-authenticate) and exit code 78 (fix config) determines whether the agent retries +OAuth or asks the user to check their config file. Getting that wrong wastes entire conversation turns. + +## Requirements + +**MUST:** + +- Parse arguments with `try_parse()` instead of `parse()`. Clap's `parse()` calls `process::exit()` directly, bypassing + custom error handlers — which means `--output json` cannot emit JSON parse errors. `try_parse()` returns a `Result` + the tool can format: + + ```rust + let cli = Cli::try_parse()?; + ``` + +- Error types map to distinct exit codes. At minimum: + + | Code | Meaning | + | ---: | ----------------------------- | + | 0 | Success | + | 1 | General command error | + | 2 | Usage / argument error | + | 77 | Auth / permission error | + | 78 | Configuration error | + +- Every error message contains **what failed**, **why**, and **what to do next**. Example: + + ```text + Authentication failed: token expired (expires_at: 2026-03-25T00:00:00Z). + Run `tool auth refresh` or set TOOL_TOKEN. + ``` + +**SHOULD:** + +- Error types use a structured enum (via `thiserror` in Rust) with variant-to-kind mapping for JSON serialization. + Agents match on error kinds programmatically rather than parsing message text. +- Config and auth validation happen before any network call. A three-tier dependency gating pattern (meta-commands, + local-only commands, network commands) fails at the earliest possible point. +- Error output respects `--output json`: JSON-formatted errors go to stderr when JSON output is selected. + +## Evidence + +- `Cli::try_parse()` in `main()`, not `Cli::parse()`. +- Error enum with `#[derive(Error)]` and distinct variants for config, auth, and command errors. +- `exit_code()` method on the error type returning variant-specific codes. +- `kind()` method returning a machine-readable string for JSON serialization. +- `run()` function returning `Result<(), AppError>`, not calling `process::exit()` internally. +- Error messages containing remediation steps ("run X" or "set Y") alongside the cause. + +## Anti-Patterns + +- `Cli::parse()` anywhere in the codebase — it silently prevents JSON error output. +- `process::exit()` in library code or command handlers. Only `main()` may call it, after all error handling. +- A single catch-all error variant that maps everything to exit code 1. +- Error messages that state the symptom without the cause or fix ("Error: request failed"). +- Panics (`unwrap()`, `expect()`) on recoverable errors in production code paths. + +Measured by check IDs `p4-bad-args`, `p4-process-exit`, `p4-unwrap`, `p4-exit-codes`. Run +`agentnative check --principle 4 .` against your CLI to see each. diff --git a/bundle/spec/principles/p5-safe-retries-mutation-boundaries.md b/bundle/spec/principles/p5-safe-retries-mutation-boundaries.md new file mode 100644 index 0000000..c65ec4a --- /dev/null +++ b/bundle/spec/principles/p5-safe-retries-mutation-boundaries.md @@ -0,0 +1,77 @@ +--- +id: p5 +title: Safe Retries and Explicit Mutation Boundaries +last-revised: 2026-04-22 +status: draft +requirements: + - id: p5-must-force-yes + level: must + applicability: + if: CLI has destructive operations + summary: Destructive operations (delete, overwrite, bulk modify) require an explicit `--force` or `--yes` flag. + - id: p5-must-read-write-distinction + level: must + applicability: + if: CLI has both read and write operations + summary: The distinction between read and write commands is clear from the command name and help text alone. + - id: p5-must-dry-run + level: must + applicability: + if: CLI has write operations + summary: A `--dry-run` flag is present on every write command; dry-run output respects `--output json`. + - id: p5-should-idempotency + level: should + applicability: + if: CLI has write operations + summary: Write operations are idempotent where the domain allows it — running the same command twice produces the same result. +--- + +# P5: Safe Retries and Explicit Mutation Boundaries + +## Definition + +Every CLI MUST support `--dry-run` so agents can preview any command before committing it. Write operations MUST clearly +separate destructive actions from read-only queries. An agent that cannot distinguish a safe read from a dangerous write +will either avoid the tool or execute mutations blindly — both are failure modes. + +## Why Agents Need It + +Agents retry failed operations by default. If a write operation is not idempotent, a retry creates duplicates, corrupts +data, or trips rate limits. When destructive operations require explicit confirmation (`--force`, `--yes`) and support +preview (`--dry-run`), an agent can safely explore what a command would do before committing to it. Read-only tools are +inherently safe for retries, but they still benefit from help text that names the mutation contract — "this does not +modify state" is a better sentence to put in `--help` than to assume. + +## Requirements + +**MUST:** + +- Destructive operations (delete, overwrite, bulk modify) require an explicit `--force` or `--yes` flag. Without it, the + tool refuses the operation or enters dry-run mode — never mutates silently. +- The distinction between read and write commands is clear from the command name and help text alone. An agent reading + `--help` immediately knows whether a command mutates state. +- A `--dry-run` flag is present on every write command. When set, the command validates inputs and reports what it would + do without executing. Dry-run output respects `--output json` so agents can parse the preview programmatically. + +**SHOULD:** + +- Write operations are idempotent where the domain allows it — running the same command twice produces the same result + rather than doubling the effect. + +## Evidence + +- `--dry-run` flag on commands that create, update, or delete resources. +- `--force` or `--yes` flag on destructive commands. +- Command names that signal intent: `add`, `remove`, `delete`, `create` for writes; `list`, `show`, `get`, `search` for + reads. +- Dry-run output that shows what *would* change without executing. + +## Anti-Patterns + +- A `delete` command that executes immediately without `--force` or confirmation. +- Write commands sharing a name pattern with read commands (e.g., a `sync` that silently overwrites local state). +- No `--dry-run` option on bulk operations, where a preview prevents costly mistakes. +- Operations that fail on retry because the first attempt partially succeeded — non-idempotent writes without rollback. + +Measured by check IDs `p5-dry-run`, `p5-destructive-guard`. Run `agentnative check --principle 5 .` against your +CLI to see each. diff --git a/bundle/spec/principles/p6-composable-predictable-command-structure.md b/bundle/spec/principles/p6-composable-predictable-command-structure.md new file mode 100644 index 0000000..9858dd4 --- /dev/null +++ b/bundle/spec/principles/p6-composable-predictable-command-structure.md @@ -0,0 +1,136 @@ +--- +id: p6 +title: Composable and Predictable Command Structure +last-revised: 2026-04-22 +status: draft +requirements: + - id: p6-must-sigpipe + level: must + applicability: universal + summary: SIGPIPE fix is the first executable statement in `main()` — piping output to `head`/`tail` must not panic. + - id: p6-must-no-color + level: must + applicability: universal + summary: TTY detection plus support for `NO_COLOR` and `TERM=dumb` — color codes suppressed when stdout/stderr is not a terminal. + - id: p6-must-completions + level: must + applicability: universal + summary: Shell completions available via a `completions` subcommand (Tier 1 meta-command — needs no config/auth/network). + - id: p6-must-timeout-network + level: must + applicability: + if: CLI makes network calls + summary: Network CLIs ship a `--timeout` flag with a sensible default (e.g., 30 seconds). + - id: p6-must-no-pager + level: must + applicability: + if: CLI invokes a pager for output + summary: If the CLI uses a pager (`less`, `more`, `$PAGER`), it supports `--no-pager` or respects `PAGER=""`. + - id: p6-must-global-flags + level: must + applicability: + if: CLI uses subcommands + summary: Agentic flags (`--output`, `--quiet`, `--no-interactive`, `--timeout`) are `global = true` so they propagate to every subcommand. + - id: p6-should-stdin-input + level: should + applicability: + if: CLI has commands that accept input data + summary: Commands that accept input read from stdin when no file argument is provided. + - id: p6-should-consistent-naming + level: should + applicability: + if: CLI uses subcommands + summary: Subcommand naming follows a consistent `noun verb` or `verb noun` convention throughout the tool. + - id: p6-should-tier-gating + level: should + applicability: universal + summary: "Three-tier dependency gating: Tier 1 (meta) needs nothing, Tier 2 (local) needs config, Tier 3 (network) needs config + auth." + - id: p6-should-subcommand-operations + level: should + applicability: + if: CLI performs multiple distinct operations + summary: Operations are modeled as subcommands, not flags (`tool search "q"`, not `tool --search "q"`). + - id: p6-may-color-flag + level: may + applicability: universal + summary: "`--color auto|always|never` flag for explicit color control beyond TTY auto-detection." +--- + +# P6: Composable and Predictable Command Structure + +## Definition + +CLI tools MUST integrate cleanly with pipes, scripts, and other tools. That means handling SIGPIPE, detecting TTY for +color and formatting decisions, supporting stdin for piped input, and maintaining a consistent, predictable subcommand +structure. + +## Why Agents Need It + +Agents compose CLI tools into pipelines: + +```bash +tool list --output json | jaq '.[] | .id' | xargs tool get +``` + +Every link in that chain has to behave predictably. A tool that panics on SIGPIPE when piped to `head` breaks the +pipeline. A tool that emits ANSI color codes into a pipe pollutes downstream JSON parsing. A tool with inconsistent +subcommand naming forces the agent to memorize exceptions rather than apply patterns. Composability is what makes a CLI +tool a building block rather than a dead end. + +## Requirements + +**MUST:** + +- A SIGPIPE fix is the first executable statement in `main()`. Without it, piping output to `head`, `tail`, or any tool + that closes the pipe early causes a panic: + + ```rust + unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL); } + ``` + +- TTY detection, plus support for `NO_COLOR` and `TERM=dumb`. When stdout or stderr is not a terminal, color codes are + suppressed automatically. +- Shell completions available via a `completions` subcommand (clap_complete in Rust; equivalents elsewhere). This is a + Tier 1 meta-command — it works without config, auth, or network. +- Network CLIs ship a `--timeout` flag with a sensible default (30 seconds). Agents operating under their own time + budgets need to fail fast rather than block on a slow upstream. +- If the CLI uses a pager (`less`, `more`, `$PAGER`), it supports `--no-pager` or respects `PAGER=""`. Pagers block + headless execution indefinitely. +- When the CLI uses subcommands, agentic flags (`--output`, `--quiet`, `--no-interactive`, `--timeout`) are `global = + true` so they propagate to every subcommand automatically. + +**SHOULD:** + +- Commands that accept input read from stdin when no file argument is provided. Pipeline composition depends on it. +- Subcommand naming follows a consistent `noun verb` or `verb noun` convention throughout the tool. Mixing patterns + (e.g., `list-users` alongside `user show`) forces agents to learn exceptions. +- A three-tier dependency gating pattern: Tier 1 (meta-commands like `completions`, `version`) needs nothing; Tier 2 + (local commands) needs config; Tier 3 (network commands) needs config + auth. `completions` and `version` always work, + even in broken environments. +- Operations are modeled as subcommands, not flags. `tool search "query"` is correct; `tool --search "query"` is wrong. + Flags modify behavior (`--quiet`, `--output json`); subcommands select operations. + +**MAY:** + +- A `--color auto|always|never` flag for explicit color control beyond TTY auto-detection. + +## Evidence + +- `libc::signal(libc::SIGPIPE, libc::SIG_DFL)` (or the equivalent in the target language) as the first statement of + `main()`. +- `IsTerminal` trait usage (`std::io::IsTerminal` or the `is-terminal` crate). +- `NO_COLOR` and `TERM=dumb` checks. +- `clap_complete` in `Cargo.toml`. +- A `completions` subcommand in the CLI enum. +- Tiered match arms in `main()` separating meta-commands from config-dependent commands. + +## Anti-Patterns + +- Missing SIGPIPE handler — `cargo run -- list | head` panics with "broken pipe". +- Hard-coded ANSI escape codes without TTY detection. +- Color output in JSON mode — ANSI codes inside JSON string values break downstream parsing. +- A `completions` command that requires auth or config to run. +- No stdin support on commands where piped input is a natural use case. + +Measured by check IDs `p6-sigpipe`, `p6-no-color`, `p6-completions`, `p6-timeout`, `p6-agents-md`. Run +`agentnative check --principle 6 .` against your CLI to see each. diff --git a/bundle/spec/principles/p7-bounded-high-signal-responses.md b/bundle/spec/principles/p7-bounded-high-signal-responses.md new file mode 100644 index 0000000..586fc54 --- /dev/null +++ b/bundle/spec/principles/p7-bounded-high-signal-responses.md @@ -0,0 +1,105 @@ +--- +id: p7 +title: Bounded, High-Signal Responses +last-revised: 2026-04-22 +status: draft +requirements: + - id: p7-must-quiet + level: must + applicability: universal + summary: A `--quiet` flag suppresses non-essential output; only requested data and errors appear. + - id: p7-must-list-clamping + level: must + applicability: + if: CLI has list-style commands + summary: "List operations clamp to a sensible default maximum; when truncated, indicate it (`\"truncated\": true` in JSON, stderr note in text)." + - id: p7-should-verbose + level: should + applicability: universal + summary: A `--verbose` flag (or `-v` / `-vv`) escalates diagnostic detail when agents need to debug failures. + - id: p7-should-limit + level: should + applicability: + if: CLI has list-style commands + summary: A `--limit` or `--max-results` flag lets callers request exactly the number of items they want. + - id: p7-should-timeout + level: should + applicability: universal + summary: A `--timeout` flag bounds execution time so agents are not blocked indefinitely. + - id: p7-may-cursor-pagination + level: may + applicability: + if: CLI returns paginated results + summary: Cursor-based pagination flags (`--after`, `--before`) for efficient traversal of large result sets. + - id: p7-may-auto-verbosity + level: may + applicability: universal + summary: Automatic verbosity reduction in non-TTY contexts (same behavior `--quiet` explicitly requests). +--- + +# P7: Bounded, High-Signal Responses + +## Definition + +CLI tools MUST provide mechanisms to control output volume. Agent context windows are finite and expensive — a tool that +dumps 10,000 lines of unfiltered output wastes tokens and may exceed the context limit entirely, breaking the +conversation that invoked it. + +## Why Agents Need It + +Every token of CLI output an agent consumes has a cost — both monetary (API tokens) and cognitive (context window +capacity). Unbounded output forces the agent to either truncate (losing potentially important data) or consume the full +response (wasting context on noise). Bounded output with `--quiet`, `--verbose`, and `--limit` flags gives the agent +precise control over how much data arrives, keeping responses high-signal and inside budget. + +## Requirements + +**MUST:** + +- A `--quiet` flag suppresses non-essential output: progress indicators, informational messages, decorative formatting. + When `--quiet` is set, only requested data and errors appear. Implementations typically route diagnostics through a + macro that short-circuits when quiet is on: + + ```rust + macro_rules! diag { + ($cfg:expr, $($arg:tt)*) => { + if !$cfg.quiet { eprintln!($($arg)*); } + } + } + ``` + +- List operations clamp to a sensible default maximum. A `list` without `--limit` does not return more than a + configurable ceiling (e.g., 100 items). If more items exist, the output indicates truncation — `"truncated": true` in + JSON, a stderr note in text mode. + +**SHOULD:** + +- A `--verbose` flag (or `-v` / `-vv`) escalates diagnostic detail when agents need to debug failures. +- A `--limit` or `--max-results` flag lets callers request exactly the number of items they want. +- A `--timeout` flag bounds execution time. An agent waiting indefinitely on a hung network call cannot proceed. + +**MAY:** + +- Cursor-based pagination flags (`--after`, `--before`) for efficient traversal of large result sets. +- Automatic verbosity reduction in non-TTY contexts (the same behavior `--quiet` explicitly requests). + +## Evidence + +- `--quiet` flag with a falsey-value parser and env-var binding. +- A diagnostic macro (or equivalent gate) that short-circuits when `quiet` is true. +- `--limit` or `--max-results` on every list / search command. +- Pagination clamping logic (e.g., `min(requested, MAX_RESULTS)`). +- `--timeout` flag with a sensible default. +- `--verbose` flag for diagnostic escalation. +- A `suppress_diag()` method that returns true when quiet is set or when the output format is JSON / JSONL. + +## Anti-Patterns + +- List commands that return all results with no default limit — an agent listing 50,000 items floods its context window. +- No `--quiet` flag — agents consuming JSON output still receive interleaved diagnostic text on stderr. +- `--verbose` as the only output control. If there is no way to reduce output, bounded responses do not exist. +- Progress bars or spinners that write to stderr in non-TTY contexts, adding noise to agent logs. +- No `--timeout` on network operations. A stalled request blocks the agent indefinitely. + +Measured by check IDs `p7-quiet`, `p7-limit`, `p7-timeout`. Run `agentnative check --principle 7 .` against +your CLI to see each. diff --git a/scripts/sync-spec.sh b/scripts/sync-spec.sh new file mode 100755 index 0000000..dbb17a1 --- /dev/null +++ b/scripts/sync-spec.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Vendor agentnative-spec into bundle/spec/. +# +# Extracts files at a pinned git ref via `git show :` so the user's +# spec working tree is not perturbed. The vendored tree ships as part of the +# skill bundle so consumers carry the canonical principle text alongside the +# skill metadata. +# +# Usage: +# scripts/sync-spec.sh +# SPEC_REF=v0.2.1 scripts/sync-spec.sh +# SPEC_ROOT=/path/to/agentnative-spec scripts/sync-spec.sh +# +# Env vars: +# SPEC_ROOT Path to agentnative-spec checkout. Default: $HOME/dev/agentnative-spec +# SPEC_REF Git ref (tag, branch, or SHA) to vendor. Default: v0.2.0 +# +# Resync cadence: rerun after every new agentnative-spec tag. Stale orphan +# files in bundle/spec/principles/ (e.g., from a spec rename) are accepted; +# `git status` surfaces them at commit time. +# +# Mirror of agentnative-cli/scripts/sync-spec.sh; only DEST_DIR differs. + +set -euo pipefail + +SPEC_ROOT="${SPEC_ROOT:-$HOME/dev/agentnative-spec}" +SPEC_REF="${SPEC_REF:-v0.2.0}" + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DEST_DIR="$REPO_ROOT/bundle/spec" +DEST_PRINCIPLES="$DEST_DIR/principles" + +if [[ ! -d "$SPEC_ROOT/.git" ]]; then + echo "error: SPEC_ROOT is not a git repository: $SPEC_ROOT" >&2 + echo " set SPEC_ROOT to your agentnative-spec checkout, or clone it to the default" >&2 + echo " location: \$HOME/dev/agentnative-spec" >&2 + exit 1 +fi + +if ! git -C "$SPEC_ROOT" rev-parse --verify --quiet "$SPEC_REF^{commit}" >/dev/null; then + echo "error: SPEC_REF does not resolve to a commit in $SPEC_ROOT: $SPEC_REF" >&2 + echo " try \`git -C $SPEC_ROOT fetch --tags\` or check the ref name" >&2 + exit 1 +fi + +resolved_sha="$(git -C "$SPEC_ROOT" rev-parse --short=7 "$SPEC_REF^{commit}")" +echo "vendoring $SPEC_REF ($resolved_sha) from $SPEC_ROOT" + +# Verify the principles/ tree exists at the ref. +if ! git -C "$SPEC_ROOT" cat-file -e "$SPEC_REF:principles" 2>/dev/null; then + echo "error: $SPEC_REF has no principles/ directory in $SPEC_ROOT" >&2 + exit 1 +fi + +mkdir -p "$DEST_PRINCIPLES" + +# VERSION and CHANGELOG.md are top-level in the spec repo. +git -C "$SPEC_ROOT" show "$SPEC_REF:VERSION" >"$DEST_DIR/VERSION" +git -C "$SPEC_ROOT" show "$SPEC_REF:CHANGELOG.md" >"$DEST_DIR/CHANGELOG.md" + +# Enumerate principle files at the ref and extract each one. +copied=0 +while IFS= read -r path; do + case "$path" in + principles/p*-*.md) + dest_name="${path#principles/}" + git -C "$SPEC_ROOT" show "$SPEC_REF:$path" >"$DEST_PRINCIPLES/$dest_name" + copied=$((copied + 1)) + ;; + esac +done < <(git -C "$SPEC_ROOT" ls-tree --name-only "$SPEC_REF" principles/) + +if [[ "$copied" -eq 0 ]]; then + echo "error: no principles/p*-*.md files found at $SPEC_REF" >&2 + exit 1 +fi + +echo "wrote $copied principle file(s) to $DEST_PRINCIPLES" +echo "wrote VERSION + CHANGELOG.md to $DEST_DIR" +echo +echo "next: review \`git diff\` for unexpected changes, then commit."