diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index afb2398..39ff473 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,9 @@ -# Mandatory review on anything that runs on user machines at install time. -# 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). -scripts/** @brettdavies -.github/workflows/** @brettdavies -.github/CODEOWNERS @brettdavies +scripts/** @brettdavies +.github/workflows/** @brettdavies +.github/rulesets/** @brettdavies +.github/CODEOWNERS @brettdavies diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..58b33b1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,47 @@ +--- +name: Bug report +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 or quoted bundle content; redact paths/credentials +``` + +## Environment + +- Bundle version (`cat VERSION` if cloned, or the tag you installed): vX.Y.Z +- Pinned spec version (`cat spec/VERSION`): vX.Y.Z +- Host (Claude Code / Cursor / Codex / other): +- `anc --version` (if a workflow involving anc is at issue): +- OS and shell: + +## 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..2fa2eaa --- /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 `templates/` +- [ ] New reference doc under `references/` +- [ ] Update to `SKILL.md` (entry-point structure or routing) +- [ ] Update to `getting-started.md` (new flow, new invocation) +- [ ] Idioms for a new language/framework in `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/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..fdead33 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,151 @@ +## Summary + + + +## Changelog + + + +### Added + +- + +### Changed + +- + +### Fixed + +- + +### Documentation + +- + +## Type of Change + + + +- [ ] `feat`: New feature (non-breaking change which adds functionality) +- [ ] `fix`: Bug fix (non-breaking change which fixes an issue) +- [ ] `refactor`: Code refactoring (no functional changes) +- [ ] `perf`: Performance improvement +- [ ] `docs`: Documentation update +- [ ] `test`: Adding or updating tests +- [ ] `chore`: Maintenance tasks (dependencies, config, etc.) +- [ ] `ci`: CI/CD configuration changes +- [ ] `style`: Code style/formatting changes +- [ ] `build`: Build system changes +- [ ] `BREAKING CHANGE`: Breaking API change (requires major version bump) + +## Related Issues/Stories + + + +- Story: +- Issue: +- Architecture: +- Related PRs: + +## Testing + + + +- [ ] Unit tests added/updated +- [ ] Integration tests added/updated +- [ ] Manual testing completed +- [ ] All tests passing + +**Test Summary:** + +- Unit tests: X passing +- Integration tests: Y passing +- Coverage: Z% + +## Files Modified + + + +**Modified:** + +**Created:** + +**Renamed:** + +**Deleted:** + +## Key Features + + + +- + +## Benefits + + + +- + +## Breaking Changes + + + +- [ ] No breaking changes +- [ ] Breaking changes described below: + +## Deployment Notes + + + +- [ ] No special deployment steps required +- [ ] Deployment steps documented below: + +## Screenshots/Recordings + + + +## Checklist + +- [ ] Code follows project conventions and style guidelines +- [ ] Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) +- [ ] Self-review of code completed +- [ ] Tests added/updated and passing +- [ ] No new warnings or errors introduced +- [ ] Changes are backward compatible (or breaking changes documented) + +## Additional Context + + + +--- + + diff --git a/.github/rulesets/protect-dev.json b/.github/rulesets/protect-dev.json new file mode 100644 index 0000000..33bbb1e --- /dev/null +++ b/.github/rulesets/protect-dev.json @@ -0,0 +1,31 @@ +{ + "name": "Protect dev", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/heads/dev" + ] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "required_signatures" + } + ], + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ] +} diff --git a/.github/rulesets/protect-main.json b/.github/rulesets/protect-main.json new file mode 100644 index 0000000..74f2fc6 --- /dev/null +++ b/.github/rulesets/protect-main.json @@ -0,0 +1,69 @@ +{ + "name": "Protect main", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/heads/main" + ] + } + }, + "rules": [ + { + "type": "creation" + }, + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "required_reviewers": [], + "require_code_owner_review": true, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": [ + "squash" + ] + } + }, + { + "type": "required_signatures" + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "do_not_enforce_on_create": false, + "required_status_checks": [ + { + "context": "markdownlint" + }, + { + "context": "shellcheck" + }, + { + "context": "guard-docs / check-forbidden-docs" + } + ] + } + }, + { + "type": "required_linear_history" + } + ], + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ] +} diff --git a/.github/rulesets/protect-tags.json b/.github/rulesets/protect-tags.json new file mode 100644 index 0000000..9f087b0 --- /dev/null +++ b/.github/rulesets/protect-tags.json @@ -0,0 +1,31 @@ +{ + "name": "Protect release tags", + "target": "tag", + "enforcement": "active", + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/tags/v*" + ] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "update" + } + ], + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 125b397..2876d28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,8 @@ on: - dev - "feat/**" - "fix/**" + - "chore/**" + - "release/**" pull_request: branches: - main @@ -39,11 +41,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Run ShellCheck + - 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 - additional_files: scripts/checks/_helpers.sh diff --git a/.github/workflows/guard-main-docs.yml b/.github/workflows/guard-main-docs.yml new file mode 100644 index 0000000..eeb3b61 --- /dev/null +++ b/.github/workflows/guard-main-docs.yml @@ -0,0 +1,17 @@ +# Source repo guard-main-docs wrapper — calls the centralized reusable workflow. +# Copy to .github/workflows/guard-main-docs.yml in repos with guarded docs. +# +# Blocks compound-engineering docs (plans, solutions, brainstorms, reviews) +# from being merged into main. Only needs pull-requests: read. +name: Guard main from engineering docs + +on: + pull_request: + branches: [main] + +permissions: + pull-requests: read + +jobs: + guard-docs: + uses: brettdavies/.github/.github/workflows/guard-main-docs.yml@main diff --git a/.gitignore b/.gitignore index 052da2b..5df039a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,9 @@ target/ node_modules/ dist/ build/ + +# Override global ignore for AGENTS.md. +# Global ~/.config/git/ignore excludes **/AGENTS.md (local agent setup is private +# by default). This producer repo IS the canonical home for how agents should +# operate against the bundle, so AGENTS.md ships here intentionally. +!AGENTS.md diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 1685e69..7e0d44d 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -80,6 +80,13 @@ 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. + - "spec/CHANGELOG.md" + # Generated artifact. scripts/generate-changelog.sh writes single-line + # bullets per PR; line-length wrapping would conflict with the generator. + # CHANGELOG.md is regenerated, never hand-edited. + - "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 new file mode 100644 index 0000000..3361655 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,109 @@ +# AGENTS.md + +Project-level agent instructions for `agentnative-skill` — the producer repo for the `agent-native-cli` skill. + +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 `spec/`). +The skill teaches agents how to use `anc` and supplies the surrounding context — spec, idioms, templates — that `anc` +findings reference. + +## Layout + +The repo ships to consumers via plain `git clone`. After install, the host (Claude Code, Codex, Cursor, OpenCode) +auto-discovers `SKILL.md` at the install root and ignores everything else. Producer-side files (`scripts/`, `docs/`, +`.github/`, `cliff.toml`, etc.) clone alongside the skill content but are inert at runtime. + +| Path | Read at runtime by host? | Purpose | +| ---------------------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| `SKILL.md` | ✓ | Skill metadata + entry-point pointer to `getting-started.md`. The host's first read. | +| `getting-started.md` | ✓ | Three working loops (existing CLI / new Rust / other language); canonical `anc check` invocations. | +| `bin/check-update` | ✓ | Consumer-side update-check script. Compares local `VERSION` to GitHub `main`; emits `UPGRADE_AVAILABLE` for the SKILL.md preamble. | +| `spec/` | ✓ | Vendored copy of `agentnative-spec`. Canonical principle text + machine-readable `requirements[]`. | +| `references/` | ✓ | Implementation guidance: framework idioms (Rust + others), project structure, Rust/clap patterns. | +| `templates/` | ✓ | Drop-in starter files for greenfield Rust CLIs (`clap-main.rs`, `error-types.rs`, `output-format.rs`, `agents-md-template.md`). | +| `VERSION` | ✓ | Single-line current version. `bin/check-update` reads this for the upgrade comparison. | +| `scripts/sync-spec.sh` | — | Vendor the latest `agentnative-spec` v\* tag into `spec/`. 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`, `README.md`, `SECURITY.md` | — | Producer-repo docs. | +| `.github/rulesets/` | — | Version-controlled GitHub repository rulesets. | +| `.github/workflows/` | — | CI: markdownlint, shellcheck. Plus `guard-main-docs.yml` to keep engineering docs off `main`. | +| `.github/ISSUE_TEMPLATE/` | — | Bug report + bundle-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 scripts/*.sh bin/* +actionlint .github/workflows/*.yml +``` + +The repo ships a local `.markdownlint-cli2.yaml` (canonical 120-char line length) and `.shellcheckrc` so CI and local +tooling agree. + +## Spec sync + +The canonical principle text lives in [`brettdavies/agentnative`](https://github.com/brettdavies/agentnative). This repo +vendors the latest released `v*` tag via `scripts/sync-spec.sh`. To resync: + +```bash +scripts/sync-spec.sh # queries the remote first; falls back to $HOME/dev/agentnative-spec if offline +git diff spec/ # review +``` + +Then commit the result with a message like `chore: bump spec to agentnative-spec@`. The vendored version is +recorded in `spec/VERSION`. Override `SPEC_REMOTE_URL` to query a different remote, or `SPEC_ROOT` to point at a +non-default local checkout. + +## Branch + release model + +`feat/* → dev (squash) → release/ from origin/main → main (squash)`. Cherry-pick the non-docs commits from `dev` +onto the `release/*` branch. `dev` and `main` are both forever branches; `release/*` branches are short-lived and +auto-deleted on merge. + +Engineering docs (`docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, `docs/reviews/`) live on `dev` only. +`guard-main-docs.yml` blocks any `added` or `modified` files under those paths from reaching `main`. The release-branch +cherry-pick pattern handles this naturally: docs commits stay on `dev`, only feature commits go onto `release/*`. + +See [`RELEASES.md`](./RELEASES.md) for the full workflow, version-bump procedure, and the verified status-check context +table. + +## What an agent should NEVER do + +- Edit anything under `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 skill 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 `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 for released versions. +- Add Rust/Cargo scaffolding. There is no Rust code in this repo and there should be none — the standard is + language-prescriptive but the skill itself is markdown + a tiny bash update-check. + +## Common pitfalls + +- The skill's `templates/agents-md-template.md` is for downstream Rust CLI tools (`cargo build`, `cargo test`, 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`. +- `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 + +- [`SKILL.md`](./SKILL.md) — skill entry point +- [`getting-started.md`](./getting-started.md) — agent's three working loops +- [`spec/README.md`](./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 +- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — how to propose changes + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index f8757d1..fe87cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,71 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to -[Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2026-04-29 + +### Added + +- Version-controlled GitHub repository rulesets for `main`, `dev`, and release tags (`v*`). Apply procedure documented in `.github/rulesets/README.md`. by @brettdavies in [#1](https://github.com/brettdavies/agentnative-skill/pull/1) +- `AGENTS.md` (root) describing the bundle layout, lint commands, branch model, and hard rules for agents working in this producer repo. by @brettdavies in [#2](https://github.com/brettdavies/agentnative-skill/pull/2) +- `RELEASES.md` (root) documenting a release procedure for this repo (later rewritten in #3 to the canonical full `release/*` pattern). +- `.github/pull_request_template.md` (canonical PR template). +- `.github/workflows/guard-main-docs.yml` caller for the `brettdavies/.github` reusable workflow that blocks `docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, `docs/reviews/` from PRs targeting `main`. +- `cliff.toml` — git-cliff configuration mirroring sibling repos. by @brettdavies in [#3](https://github.com/brettdavies/agentnative-skill/pull/3) +- `scripts/generate-changelog.sh` — release-time CHANGELOG generator. Reads PR-body `## Changelog` sections and prepends a curated, attributed `[X.Y.Z]` section. Authoritative; never hand-edit `CHANGELOG.md`. +- `CONTRIBUTING.md` — how to propose changes, link to release procedure. +- `.github/ISSUE_TEMPLATE/bug_report.md` — bug report template. +- `.github/ISSUE_TEMPLATE/principle_proposal.md` — substantive standards-change template. +- `**Renamed:**` subsection in `.github/pull_request_template.md` (sync of the canonical update at `~/dotfiles/stow/github/dot-config/github/pull_request_template.md`). Sister sync PRs landing in agentnative-cli (#30 there) and agentnative-site (already on dev as commit 4437435). +- Add vendored `bundle/spec/` tree (agentnative-spec @ v0.2.0): `VERSION`, `CHANGELOG.md`, `README.md`, and seven `principles/p*.md` files with machine-readable `requirements[]` frontmatter — canonical principle text the skill now points at instead of paraphrasing. by @brettdavies in [#4](https://github.com/brettdavies/agentnative-skill/pull/4) +- Add `bundle/getting-started.md` covering three working agent loops (existing CLI / new Rust / other language), canonical `anc check --output json` invocations, and a "where things live" map. +- Add `scripts/sync-spec.sh` so the bundle can re-vendor agentnative-spec on demand. +- `LICENSE-APACHE` — Apache 2.0 boilerplate, identical to the file in `agentnative-cli`. by @brettdavies in [#6](https://github.com/brettdavies/agentnative-skill/pull/6) +- `bundle/bin/check-update` — script that compares the consumer's local `VERSION` against the producer repo's `main` and emits `UPGRADE_AVAILABLE ` (or empty when up-to-date / snoozed / disabled). Adapts the gstack pattern with cache TTL (60min UP_TO_DATE / 720min UPGRADE_AVAILABLE) and a 3-level snooze (24h / 48h / 7d). State directory: `$HOME/.cache/agent-native-cli/`. by @brettdavies in [#8](https://github.com/brettdavies/agentnative-skill/pull/8) +- `bundle/SKILL.md` `## Update check` section — first non-frontmatter section after the intro. Documents how to invoke the script and inlines the AskUserQuestion-driven upgrade flow with three options ("Yes, upgrade now" / "Not now" / "Never ask again"). + +### Changed + +- `.gitignore` adds `!AGENTS.md` to override the global `**/AGENTS.md` ignore for this repo only. Other repos remain unaffected. by @brettdavies in [#2](https://github.com/brettdavies/agentnative-skill/pull/2) +- **Breaking (install layout):** Skill bundle moved into `bundle/` subdirectory. Installers must fetch `bundle/` rather than the entire repo. Consumer's installed skill directory shape is unchanged (`SKILL.md` at the root). by @brettdavies in [#3](https://github.com/brettdavies/agentnative-skill/pull/3) +- Adopted the full `release/*` cherry-pick release pattern (was lightweight `dev → main`). Plans on `dev` no longer conflict with release PRs because release branches cherry-pick only non-docs commits. +- `RELEASES.md` rewritten to the canonical pattern; broken `../../.claude/...` link removed. +- **Breaking (install layout):** Skill bundle no longer ships `bundle/scripts/` or `bundle/checklists/`. Installers and consumers should fetch only the surviving directories: `SKILL.md`, `getting-started.md`, `spec/`, `references/`, `templates/`. The consumer's installed skill-directory shape (`SKILL.md` at the root) is unchanged. by @brettdavies in [#4](https://github.com/brettdavies/agentnative-skill/pull/4) +- Rewrite `bundle/SKILL.md` to drop inline principle prose, link `bundle/getting-started.md` and `bundle/spec/principles/` for progressive disclosure, and frame the spec / `anc` / skill three-artifact ecosystem. +- Reframe `RELEASES.md` SemVer guidance around the bundle's actual surface (markdown + templates + vendored spec) rather than deleted shell-script exit codes; document the spec-bump-vs-skill-version distinction. +- License changed from MIT-only to dual MIT or Apache-2.0 (consumer's choice). The skill bundle, top-level scripts, and all repo content are now dual-licensed; no MIT compatibility regression. by @brettdavies in [#6](https://github.com/brettdavies/agentnative-skill/pull/6) +- Documentation now points at `https://anc.dev/skill` instead of `https://anc.dev/install` for skill installation instructions, the cross-repo re-pin process, and the `bundle/` consumer description. by @brettdavies in [#7](https://github.com/brettdavies/agentnative-skill/pull/7) +- `bundle/SKILL.md`, `bundle/getting-started.md`, `bundle/spec/README.md` — drop "pinned ref" / "pinned upstream tag" / "pinned SPEC_VERSION" framing in favor of "vendored snapshot, refreshed each release". The bundle's behavior is unchanged; the language was misleading because the install command never actually pinned at the consumer side. by @brettdavies in [#8](https://github.com/brettdavies/agentnative-skill/pull/8) +- **BREAKING (install layout):** Skill content moved out of `bundle/` to the repo root. After install, hosts find `SKILL.md` at the skill root (where Claude Code expects it), not at `/bundle/SKILL.md`. Plain `git clone --depth 1` and `git pull --ff-only` are now the load-bearing install + update commands; no sparse-checkout magic, no post-install scripts. by @brettdavies in [#9](https://github.com/brettdavies/agentnative-skill/pull/9) +- `bin/check-update`: `SKILL_DIR` is now one dir up from the script (was two), since there's no `bundle/` layer. +- `scripts/sync-spec.sh` writes to `spec/` (was `bundle/spec/`). +- README, AGENTS, CONTRIBUTING reframe the consumer/producer split from a directory boundary (`bundle/` vs everything else) to an audience boundary (host reads `SKILL.md` + `bin/` + `spec/` + `references/` + `templates/` + `VERSION`; ignores everything else). +- Spec content vendored under `spec/` re-vendored from `agentnative-spec` v0.2.0 to v0.3.0. All 7 principles flip `status: draft` → `status: active` (P1–P7 are now the shipped baseline); prose tightened across P1 (TUI parenthetical), P2 (sysexits acknowledgment), P4 (dependency-gating cleanup), P5 (`--dry-run` write-gate + retry hedge), P6 (SIGPIPE language-neutral + global-flags behavioral lead), P7 (LLM-vs-non-LLM cost generalization). No requirement IDs added/removed/renamed; no level changes. Full upstream context: agentnative `v0.3.0` CHANGELOG. by @brettdavies in [#10](https://github.com/brettdavies/agentnative-skill/pull/10) +- `scripts/sync-spec.sh` no longer accepts `SPEC_REF`. The script always vendors the latest `v*` tag, queried from `SPEC_REMOTE_URL` (default `https://github.com/brettdavies/agentnative.git`) via `git ls-remote --tags --sort=-version:refname` and shallow-cloned for extraction. On any remote failure, falls back to the existing `SPEC_ROOT`-based logic (default `$HOME/dev/agentnative-spec`). New env var `SPEC_REMOTE_URL` overrides the remote; the temp clone is auto-cleaned on script exit via trap. by @brettdavies in [#11](https://github.com/brettdavies/agentnative-skill/pull/11) +- `.markdownlint-cli2.yaml` excludes `CHANGELOG.md` from linting. Aligns its treatment with `spec/CHANGELOG.md` and reflects that the file is regenerated by `scripts/generate-changelog.sh`, not hand-edited. Per-line content is governed by PR-body bullets in source PRs, not by this repo's MD013 line-length rule. by @brettdavies in [#13](https://github.com/brettdavies/agentnative-skill/pull/13) + +### Fixed + +- Harden `bundle/bin/check-update` against malformed local `VERSION` (apply SemVer regex; malformed → silent exit) and against curl failure being cached as UP_TO_DATE (skip cache write on network failure so the next invocation retries). by @brettdavies in [#8](https://github.com/brettdavies/agentnative-skill/pull/8) +- Align table pipes in `SKILL.md` and `getting-started.md` after the `bundle/` path strip (markdownlint MD060). MD060 isn't auto-fixable, so violations slipped past the local PostToolUse hook and surfaced in CI. by @brettdavies in [#9](https://github.com/brettdavies/agentnative-skill/pull/9) + +### Documentation + +- `README.md` — License section rewritten to reflect dual licensing and link both LICENSE files; tree row updated. by @brettdavies in [#6](https://github.com/brettdavies/agentnative-skill/pull/6) +- `CONTRIBUTING.md` — License section rewritten: contributions are dual-licensed at the consumer's option, no CLA, with an explicit pointer to the Apache §3 patent grant. +- `bundle/spec/README.md` licensing reference catches drift from PR #6: was "MIT-licensed", now reflects the actual dual MIT/Apache-2.0 posture introduced in `18836d8`. by @brettdavies in [#8](https://github.com/brettdavies/agentnative-skill/pull/8) +- `RELEASES.md` gains a `## Spec re-vendoring` section between `## Why branch from main, not dev` and `## Version bump procedure`, documenting the `scripts/sync-spec.sh` re-vendor step. The script auto-resolves the latest upstream tag from the remote, so no manual version selection is needed at re-vendor time. by @brettdavies in [#10](https://github.com/brettdavies/agentnative-skill/pull/10) +- `AGENTS.md` `## Spec sync` section: rewritten — single-step recipe (`scripts/sync-spec.sh` then review); notes the remote-first / local-fallback behavior and the `SPEC_REMOTE_URL` / `SPEC_ROOT` overrides. Commit-message example uses `` placeholder instead of a hard-coded version. by @brettdavies in [#11](https://github.com/brettdavies/agentnative-skill/pull/11) +- `spec/README.md` `## Resync` section: rewritten similarly; drops the manually-maintained `**Current snapshot:**` line and points readers at `spec/VERSION` (which `sync-spec.sh` writes verbatim from upstream). +- `RELEASES.md` post-merge sequence ends at the GitHub Release; replaces deleted step 5 with a one-liner pointing consumers at `bin/check-update`. + +### Removed + +- Remove `bundle/scripts/check-compliance.sh` and 24 `bundle/scripts/checks/check-*.sh` files (plus `_helpers.sh`). `anc check --output json` is the canonical replacement. by @brettdavies in [#4](https://github.com/brettdavies/agentnative-skill/pull/4) +- Remove `bundle/references/principles-deep-dive.md` (419-line hand-typed paraphrase of the spec; canonical text now lives at `bundle/spec/principles/`). +- Remove `bundle/checklists/new-tool.md` (pre-anc manual checklist; replaced by `bundle/getting-started.md`). +- All SHA-pin claims from public-facing markdown (`RELEASES.md`, `AGENTS.md`, `README.md`, `spec/README.md`, `CONTRIBUTING.md`): pipeline diagram's "site re-pins to commit SHA" step, the post-merge "site re-pins via its own PR" step, the `protect-tags.json` / `install endpoints` claims that tags are pinned to install endpoints, and the spec-vendor "pinned ref" / "pinned `SPEC_REF`" / "current pin is recorded" vocabulary across all docs. by @brettdavies in [#11](https://github.com/brettdavies/agentnative-skill/pull/11) + +**Full Changelog**: [v0.1.0...v0.2.0](https://github.com/brettdavies/agentnative-skill/compare/v0.1.0...v0.2.0) ## [0.1.0] - 2026-04-27 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4bb642e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing to `agentnative-skill` + +Thanks for your interest. This repo is the agent-facing skill that pairs with two siblings: + +- [`agentnative`](https://github.com/brettdavies/agentnative) (the spec) — canonical principle text. Vendored here at + `spec/`. +- [`agentnative-cli`](https://github.com/brettdavies/agentnative-cli) (`anc`) — the canonical compliance checker. + +This skill 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 **Bundle 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) + +```text +feat/* → PR to dev (squash merge) + → cherry-pick non-docs commits to release/ + → PR release/* to main (squash merge) + → tag v + GitHub Release +``` + +`dev` is the integration branch. `main` is what consumers install. Engineering docs (`docs/plans/`, `docs/solutions/`, +`docs/brainstorms/`, `docs/reviews/`) live on `dev` only and are blocked from `main` by `guard-main-docs.yml`. Full +procedure in [`RELEASES.md`](./RELEASES.md). + +## Pull requests + +- **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. 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 scripts/*.sh bin/* + actionlint .github/workflows/*.yml + ``` + +## Repo layout + +The repo ships to consumers as a flat `git clone`. After install, the host (Claude Code, Codex, Cursor, OpenCode) +auto-discovers `SKILL.md` at the install root and ignores everything else. Producer-side files (`scripts/`, `docs/`, +`.github/`, `cliff.toml`, `AGENTS.md`, `CONTRIBUTING.md`, `RELEASES.md`) clone alongside but are inert at runtime. + +**Read at runtime by the host:** `SKILL.md`, `getting-started.md`, `bin/check-update`, `spec/`, `references/`, +`templates/`, `VERSION`. + +**Producer-side, inert at runtime:** `scripts/`, `docs/plans/`, `.github/`, `cliff.toml`, the producer-docs above. + +## Touching the skill content + +- **`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` after a new upstream tag lands. +- **`SKILL.md`** is the host-discovered entry point. Changes to its `name` or `description` frontmatter affect skill + discovery on every host — coordinate before changing. +- **`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. +- **`bin/check-update`** is the consumer-side update-check script. It compares local `VERSION` to GitHub `main` and + emits `UPGRADE_AVAILABLE` for the SKILL.md preamble. Treat as load-bearing — agents rely on it to detect staleness. +- **`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. +- **`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 + +See [`SECURITY.md`](./SECURITY.md). Do not file security issues in the public tracker — use the GitHub private security +advisories channel. + +## License + +By contributing, you agree your contributions are dual-licensed under the same terms as the rest of this repository: + +- MIT — see [`LICENSE-MIT`](./LICENSE-MIT) +- Apache License, Version 2.0 — see [`LICENSE-APACHE`](./LICENSE-APACHE) + +at the consumer's option. No CLA. The Apache-2.0 side carries the standard contributor patent grant under §3 of the +license. + +Vendored content under `spec/` is CC BY 4.0 (upstream); contributions to that directory should happen upstream in +[`agentnative-spec`](https://github.com/brettdavies/agentnative). + + diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..cb3a32b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,199 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please also get an + "Alarm" or "alarm" if your text editor autocorrects to "Alarm." + + Copyright 2026 Brett Davies + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE b/LICENSE-MIT similarity index 100% rename from LICENSE rename to LICENSE-MIT diff --git a/README.md b/README.md index 6bfb16c..fa72072 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,95 @@ # agentnative-skill -The producer repo for the [`agent-native-cli`](./SKILL.md) skill bundle — a north-star standard for CLI tools designed -to be operated by AI agents. - -This repo ships the bundle at the root so that `git clone` directly into a host's skills directory IS install. +The producer repo for the [`agent-native-cli`](./SKILL.md) skill — 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/ +├── SKILL.md skill entry point — host-discovered; points the agent at getting-started.md +├── getting-started.md three working loops; canonical anc invocations +├── bin/ +│ └── check-update consumer-side update-check script (gstack-style) +├── spec/ vendored from agentnative-spec (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) +├── VERSION single-line current version (read by bin/check-update) +├── scripts/ +│ ├── sync-spec.sh vendor the latest agentnative-spec v* tag into spec/ +│ └── 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 (producer-side) +├── 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) +├── cliff.toml git-cliff configuration +├── LICENSE-MIT MIT (one half of the dual license) +├── LICENSE-APACHE Apache 2.0 (the other half) +└── README.md this file +``` + +Consumer-facing files (`SKILL.md`, `getting-started.md`, `bin/`, `spec/`, `references/`, `templates/`, `VERSION`, +`LICENSE-*`) are read by the agent at runtime. Producer-side files (`scripts/`, `docs/`, `.github/`, `AGENTS.md`, +`CONTRIBUTING.md`, `RELEASES.md`, `cliff.toml`) ship to consumers via `git clone` but are inert at runtime — the host +discovers `SKILL.md` and ignores everything else. ## Install -See [anc.dev/install](https://anc.dev/install) for the cloned-in-place install model and supported hosts (Claude Code, -Cursor, Codex, etc.). - -## What's inside - -- [`SKILL.md`](./SKILL.md) — the standard itself: 7 agent-readiness principles, when to trigger, how to use. -- [`checklists/`](./checklists/) — task-shaped checklists (e.g., starting a new tool). -- [`references/`](./references/) — deep-dive references: principle specifications, framework idioms, project structure, - Rust/clap patterns. -- [`scripts/`](./scripts/) — automated compliance checker (`check-compliance.sh`) plus 24 individual checks across 9 - groups under `scripts/checks/`. -- [`templates/`](./templates/) — drop-in starting points (`AGENTS.md`, clap main, error types, output format). +See [anc.dev/skill](https://anc.dev/skill) for the supported hosts (Claude Code, Cursor, Codex, etc.) and the exact +install commands. The install model is plain `git clone --depth 1` into the host's skills directory — for example +`~/.claude/skills/agent-native-cli/`. The host auto-discovers `SKILL.md` at the install root; `SKILL.md` then points the +agent at `getting-started.md` for progressive disclosure. Updates are `git pull --ff-only` from inside the install dir, +prompted by `bin/check-update`. + +## Skill contents + +- [`SKILL.md`](./SKILL.md) — skill metadata + entry-point pointer. +- [`getting-started.md`](./getting-started.md) — three working loops (existing CLI / new Rust / other language); + canonical `anc check` invocations; "where things live" map. +- [`bin/check-update`](./bin/check-update) — periodic version check. Compares local `VERSION` to GitHub `main`, emits + `UPGRADE_AVAILABLE` so the agent can offer to `git pull`. +- [`spec/`](./spec/) — vendored canonical principle text from + [`agentnative-spec`](https://github.com/brettdavies/agentnative). See [`spec/README.md`](./spec/README.md) for the + resync procedure. **Do not edit by hand.** +- [`references/`](./references/) — implementation guidance: framework idioms (Rust + others), project structure, + Rust/clap patterns. Used when remediating `anc` findings. +- [`templates/`](./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`. ## Versioning Tagged releases follow [SemVer](https://semver.org/). The current version lives in [`VERSION`](./VERSION); release notes -are in [`CHANGELOG.md`](./CHANGELOG.md). +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-vendored spec version is in +[`spec/VERSION`](./spec/VERSION). ## Contributing -Issues and PRs welcome. 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 [`SKILL.md`](./SKILL.md) and -[`references/principles-deep-dive.md`](./references/principles-deep-dive.md). +Issues and PRs welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). Routing: -Branch model: `feat/*` off `dev`, squash-merged back to `dev`; `dev` → `main` PRs cut releases. +- **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 issues** (templates, references, getting-started, layout) → file here. + +Branch + release model documented in [`RELEASES.md`](./RELEASES.md). ## Security @@ -41,4 +97,16 @@ See [`SECURITY.md`](./SECURITY.md) for vulnerability disclosure. ## License -MIT — see [`LICENSE`](./LICENSE). +Dual-licensed under either of: + +- MIT — see [`LICENSE-MIT`](./LICENSE-MIT) +- Apache License, Version 2.0 — see [`LICENSE-APACHE`](./LICENSE-APACHE) + +at your option. Matches the licensing on `agentnative-cli` so producers can adapt the skill's content into their own +tooling without re-licensing friction. + +Vendored spec content under `spec/` is CC BY 4.0 (upstream from +[`brettdavies/agentnative`](https://github.com/brettdavies/agentnative)); attribution is in +[`spec/README.md`](./spec/README.md). + + diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..e9f7616 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,230 @@ +# Releasing `agentnative-skill` + +Every change reaches `main` via this pipeline. Direct commits to `dev` or `main` are not permitted — every change has a +PR number in its squash commit message, which keeps the history scannable, attributable, and changelog-ready. + +```text +feature branch (feat/*, fix/*, chore/*, docs/*) → PR to dev (squash merge) + → cherry-pick non-docs commits to release/ + → PR release/* to main (squash merge) + → tag v* on main → GitHub Release +``` + +This is the canonical brettdavies release pattern with `release/*` cherry-pick branches. Plans live on `dev` forever and +`guard-main-docs.yml` blocks any `added` or `modified` engineering-doc files in PRs targeting `main`. The release-branch +cherry-pick handles this cleanly: docs commits stay on `dev`, only feature/fix/chore commits go onto `release/*`. + +## Branches + +| Branch | Role | Lifetime | Protection | +| -------------------------------------- | ------------------------------------------------------- | ------------------------------------------- | ------------------------------------ | +| `main` | Released bundle. Only release-merged commits. | Forever. | `.github/rulesets/protect-main.json` | +| `dev` | Integration. All feature PRs land here. Default branch. | Forever. Never delete. | `.github/rulesets/protect-dev.json` | +| `feat/*`, `fix/*`, `chore/*`, `docs/*` | Feature work. | One PR's worth. Auto-deleted on merge. | None — squash into `dev` freely. | +| `release/*` | Head of a `release/* → main` PR. | One release's worth. Auto-deleted on merge. | None. | + +`dev` is a **forever branch**. Never delete it locally or remotely, even after a `release/* → main` merge. The next +release cycle reuses the same `dev`. The repo's `delete_branch_on_merge: true` setting doesn't touch `dev` because `dev` +is never the head of a PR — using a short-lived `release/*` head is what keeps the setting compatible with a forever +integration branch. + +## Daily development (feature → dev) + +```bash +git checkout dev && git pull +git checkout -b feat/short-description +# ... work ... +git push -u origin feat/short-description +gh pr create --base dev --title "feat(scope): what changed" +# CI passes → squash-merge (PR_BODY becomes the dev commit message) +``` + +- **Commit style**: [Conventional Commits](https://www.conventionalcommits.org/). +- **PR body**: follow `.github/pull_request_template.md`. The `## Changelog` section is the source of truth for + user-facing release notes — `CHANGELOG.md` entries derive from it directly. + +## Releasing dev to main + +Engineering docs (`docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, `docs/reviews/`) live on `dev` only. +`guard-main-docs.yml` blocks any `added` or `modified` files under those paths from reaching `main`. Branching from +`dev` and deleting docs on the way produces `add/add` merge conflicts whenever `dev` and `main` have diverged (the norm +after the first squash merge). The cherry-pick pattern avoids this. + +**Branch naming**: `release/v` (preferred) or `release/-`. Keep the slug short and descriptive. + +```bash +# 1. Cut release/* from main, NOT dev. Branching from dev causes add/add +# conflicts when dev and main have divergent histories. +git fetch origin +git checkout -b release/v origin/main + +# 2. List the dev commits not yet on main: +git log --oneline dev --not origin/main + +# 3. Cherry-pick non-docs commits onto release/v. Docs commits +# (anything that touched only docs/plans/, docs/solutions/, +# docs/brainstorms/, or docs/reviews/) stay on dev. +git cherry-pick ... + +# 4. Verify no guarded paths leaked through: +git diff origin/main --name-only | grep -E '^docs/(plans|solutions|brainstorms|reviews)/' && \ + echo 'FAIL: guarded docs in release branch' || echo 'OK: no guarded docs' + +# 5. Bump VERSION on the release branch. +echo '' > VERSION + +# 6. Generate CHANGELOG entries from PR bodies. NEVER hand-edit CHANGELOG.md — +# the script is authoritative. It reads cliff.toml + each cherry-picked PR's +# ## Changelog section and prepends a versioned [] entry. +scripts/generate-changelog.sh +# (the script extracts from the branch name release/v) + +# 7. Commit the version bump and generated changelog. +git add VERSION CHANGELOG.md +git commit -m "chore(release): v" + +# 8. Push and open the PR: +git push -u origin release/v +gh pr create --base main --head release/v --title "release: v" +``` + +When the PR merges: + +1. The squash commit lands on `main` with the PR body as its message. +2. `release/v` is auto-deleted. +3. Tag the new `main` HEAD: `git checkout main && git pull && git tag -a v -m "v" && git push origin + v`. +4. Create the GitHub Release using the generated CHANGELOG section: + + ```bash + gh release create v --title "v" \ + --notes "$(awk '/^## \[\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md)" + ``` + +Consumers detect the new release on their next `bin/check-update` run; nothing else to do here. + +`dev` keeps moving forward. Never reset or rebase `dev` after a release — it is forever. + +### CHANGELOG is generated, never hand-written + +`scripts/generate-changelog.sh` (with `cliff.toml`) is the only sanctioned way to update `CHANGELOG.md`. The script: + +- Runs `git-cliff` to prepend a versioned entry for commits since the last tag. +- Walks each squash-merged PR's body, extracts the `## Changelog` section's `### Added` / `### Changed` / `### Fixed` / + `### Documentation` subsections, and replaces the auto-generated bullets with the curated PR-body content (with author + and PR-link attribution). + +If a PR's `## Changelog` section is empty, that PR's entry is omitted from the changelog (the convention in +[`.github/pull_request_template.md`](.github/pull_request_template.md): empty section = no user-facing change). To fix a +wrong CHANGELOG entry, fix the input — edit the squash-merged PR body, then re-run the script. Do **not** edit +`CHANGELOG.md` directly. + +`scripts/generate-changelog.sh --check` verifies that `CHANGELOG.md` has a versioned section (not just `[Unreleased]`) — +wire this into the release-branch CI if/when one is added. + +### Why branch from main, not dev + +Branching from `dev` and then `gio trash`-ing the guarded paths seems simpler but produces `add/add` merge conflicts +whenever `dev` and `main` have diverged. The file appears as "added" on both sides with different content. Always branch +from `origin/main` and cherry-pick onto it. + +## Spec re-vendoring + +The bundle vendors a snapshot of [`agentnative-spec`](https://github.com/brettdavies/agentnative) under `spec/`. When +the spec ships a new tag (e.g., `v0.3.0`), this skill re-vendors via `scripts/sync-spec.sh` on the `release/v` +branch — same commit as the version bump, message `chore(spec): re-vendor spec to `. The script auto-resolves +the latest upstream tag from the remote, so no manual version selection is needed. Without re-vendoring, the bundle +ships stale spec content while consumers see the new version on `anc.dev`. + +## Version bump procedure + +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 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 `SKILL.md` frontmatter fields, restructuring directory + layout in ways that break existing skill installations, moving content between `` 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 + +Every PR **must** follow `.github/pull_request_template.md`. The template's `## Changelog` section has these +subsections: + +- `### Added` — new user-visible features or capabilities (new principles, new checks, new templates). +- `### Changed` — changes to existing behavior (e.g., a check's pass/fail criteria tightens). +- `### Fixed` — bug fixes (e.g., a check produces false positives). +- `### Removed` — removed features or APIs. +- `### Security` — security-relevant changes (e.g., a script that ran on user machines now refuses an unsafe path). + +A PR that lands with an empty or missing `## Changelog` section silently drops its user-facing notes from the next +release. If a PR truly has no user-facing impact (pure refactor, test-only, CI-only), leave the section empty — the PR +still appears in git history. + +## Branch protection + +Three rulesets are committed under `.github/rulesets/` and applied to the repo via the GitHub API: + +- **`protect-main.json`** — required signatures, linear history, squash-only merges via PR with CODEOWNERS review, + required status checks (`markdownlint`, `shellcheck`, `guard-docs / check-forbidden-docs`), creation/deletion blocked, + non-fast-forward blocked. +- **`protect-dev.json`** — required signatures, deletion blocked, non-fast-forward blocked. No PR-requirement at the + ruleset level; the PR-only norm is enforced by convention. +- **`protect-tags.json`** — `v*` tags: deletion, force-push (re-tag), and updates all blocked. Tags are immutable + historical anchors for released versions. + +### Apply (post-public-flip) + +The repo ships **PRIVATE** through the bootstrap window. GitHub's free tier does not allow rulesets on private repos. +After visibility flips to public from the agentnative-site session: + +```bash +gh api repos/brettdavies/agentnative-skill/rulesets -X POST --input .github/rulesets/protect-main.json +gh api repos/brettdavies/agentnative-skill/rulesets -X POST --input .github/rulesets/protect-dev.json +gh api repos/brettdavies/agentnative-skill/rulesets -X POST --input .github/rulesets/protect-tags.json +``` + +See [`.github/rulesets/README.md`](.github/rulesets/README.md) for verification + negative tests. + +### Updating a ruleset + +Edit the JSON locally, then sync to the remote (replacement, not patch): + +```bash +# Find the ruleset id +gh api repos/brettdavies/agentnative-skill/rulesets --jq '.[] | "\(.id)\t\(.name)"' + +# Replace by id +gh api -X PUT repos/brettdavies/agentnative-skill/rulesets/ --input .github/rulesets/protect-main.json +``` + +### Status-check context pitfall + +`required_status_checks[].context` strings must match exactly what GitHub publishes for each check. For this repo: + +| Check | Source | Context (verified) | +| ------------------ | ---------------------------------------------- | ----------------------------------- | +| `markdownlint` | inline job, `name: markdownlint` | `markdownlint` | +| `shellcheck` | inline job, `name: shellcheck` | `shellcheck` | +| `guard-docs / ...` | reusable workflow caller, job key `guard-docs` | `guard-docs / check-forbidden-docs` | + +Confirm post-CI with: + +```bash +gh api repos/brettdavies/agentnative-skill/commits//check-runs --jq '.check_runs[].name' +``` + +## Related docs + +- [`AGENTS.md`](./AGENTS.md) — repo layout, lint commands, what agents must not do. +- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — how to propose changes. +- [`.github/pull_request_template.md`](.github/pull_request_template.md) — PR body structure with changelog sections. +- [`.github/rulesets/README.md`](.github/rulesets/README.md) — ruleset apply + verify procedure. +- [`CHANGELOG.md`](./CHANGELOG.md) — released versions and their notes. diff --git a/SKILL.md b/SKILL.md index e766d2a..cd5340a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,270 +1,126 @@ --- 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 + `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 [`spec/`](./spec/) — snapshot refreshed each release. | +| [`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`. +## Update check -### P1: Non-Interactive by Default +On first invocation per session, run `bin/check-update`. It compares this bundle's `VERSION` against `main` on GitHub +and prints one of: -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. +| Output | Meaning | +| ------------------------------------ | ------------------------------------------------------------------------------- | +| (empty) | Up to date, snoozed, disabled, or check skipped (broken install, no network). | +| `UPGRADE_AVAILABLE ` | A newer release is on `main`. Surface the upgrade flow below before continuing. | -**Key requirements:** - -- 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. - -### P2: Structured, Parseable Output - -Agents consume output programmatically. Data goes to stdout, diagnostics to stderr. Output format is selectable. - -**Key requirements:** - -- `--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 - -See `templates/output-format.rs` and `templates/error-types.rs` for canonical implementations. - -### P3: Progressive Help Discovery - -Agents scan help text to learn how to use a tool. Examples must be findable after the flags section. - -**Key requirements:** - -- 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)]`) - -### 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:** +```bash +bash "$(dirname "$0")/bin/check-update" +``` -- 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) +Exit code is always 0; failures degrade silently. -**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`). +### Inline upgrade flow -See `templates/clap-main.rs` for the three-tier pattern. +When stdout is `UPGRADE_AVAILABLE `, ask the user via `AskUserQuestion`: -### P7: Bounded, High-Signal Responses +> `agent-native-cli` **v{remote}** is available (you're on v{local}). Upgrade now? -Agents have finite context windows. Output must be controllable and bounded. +Three options: -**Key requirements:** +- **"Yes, upgrade now"** — run `git -C pull --ff-only`. Report the new HEAD and the upgrade outcome. + The bundle root is the parent of ``; `git -C ../.. pull --ff-only` from `bin/` works for the default install layout + (`~//skills/agent-native-cli/`). If `--ff-only` rejects (uncommitted edits or divergent history), surface git's + error verbatim and stop — do not auto-stash. +- **"Not now"** — write `$HOME/.cache/agent-native-cli/update-snoozed` in the format ` `, where + `` is `1` (24h reminder), `2` (48h), or `3` (7 days), escalating each time the user defers. Tell the user the + next reminder window. +- **"Never ask again"** — `touch $HOME/.cache/agent-native-cli/disabled` and tell the user how to re-enable (`rm + $HOME/.cache/agent-native-cli/disabled`). -- `--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 +State directory: `$HOME/.cache/agent-native-cli/`. All three files (`last-update-check`, `update-snoozed`, `disabled`) +live there; the script auto-creates the directory on first slow-path fetch. -See `templates/output-format.rs` for the diag! macro and OutputConfig patterns. +## Start here -## Project Structure Requirements +→ **[`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. -Beyond the CLI interface, agent-native tools need proper project structure. See `references/project-structure.md` for -full details. +## The seven principles -**Required:** +Defined in [`spec/principles/`](./spec/principles/) (vendored from `agentnative-spec` — currently `v0.2.0`; see +[`spec/README.md`](./spec/README.md) for resync instructions). One file per principle, each with machine-readable +`requirements[]` frontmatter: -- `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 +| # | 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 | -See `templates/agents-md-template.md` for the AGENTS.md template. +Do not paraphrase the principles inside this skill — read the spec files directly. They are the source of truth. -## What Would You Like to Do? +## Implementation guidance (when fixing findings) -| 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 | +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: -## Compliance Checker +| 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) | -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. +## Starter code -### Run all checks +Drop-in starting points for greenfield Rust CLIs. Each encodes the relevant principles by construction. -```text -agent-native-cli/scripts/check-compliance.sh /path/to/repo -``` +| 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 | -### Run a single principle +## Compliance checking -```text -agent-native-cli/scripts/check-compliance.sh /path/to/repo --principle 3 -``` +Use `anc`. Install once: -### 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 +```bash +brew install brettdavies/tap/agentnative # binary is `anc` +cargo install agentnative ``` -### 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/VERSION b/VERSION index 6e8bf73..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/bin/check-update b/bin/check-update new file mode 100755 index 0000000..fc2d01c --- /dev/null +++ b/bin/check-update @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# bin/check-update — periodic version check for the agent-native-cli skill. +# +# Adapted from gstack/bin/gstack-update-check (https://github.com/garrytan/gstack). +# Strips telemetry, install-type detection, migrations, and config integration; +# keeps the VERSION-compare + cache + snooze core. +# +# Output (one line, or nothing): +# UPGRADE_AVAILABLE — remote VERSION differs from local +# (nothing) — up to date, snoozed, disabled, or check skipped +# +# Exit code is always 0 (failures degrade silently — no agent prompt on broken installs). +# +# Env overrides (for testing): +# AGENTNATIVE_SKILL_DIR — override auto-detected skill (install) root (defaults to script's parent dir) +# AGENTNATIVE_SKILL_REMOTE_URL — override remote VERSION URL +# AGENTNATIVE_SKILL_STATE_DIR — override $HOME/.cache/agent-native-cli state directory + +set -euo pipefail + +# Script is at /bin/check-update; the skill (= install) root is +# one dir up and holds VERSION. Consumers install via plain `git clone` +# so the full repo lands at the skill root. +SKILL_DIR="${AGENTNATIVE_SKILL_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" +STATE_DIR="${AGENTNATIVE_SKILL_STATE_DIR:-$HOME/.cache/agent-native-cli}" +CACHE_FILE="$STATE_DIR/last-update-check" +SNOOZE_FILE="$STATE_DIR/update-snoozed" +DISABLED_FILE="$STATE_DIR/disabled" +VERSION_FILE="$SKILL_DIR/VERSION" +REMOTE_URL="${AGENTNATIVE_SKILL_REMOTE_URL:-https://raw.githubusercontent.com/brettdavies/agentnative-skill/main/VERSION}" + +# ─── Step 0: Bail if disabled ───────────────────────────────── +# User opt-out: `touch $STATE_DIR/disabled`. Re-enable: `rm $STATE_DIR/disabled`. +if [ -f "$DISABLED_FILE" ]; then + exit 0 +fi + +# ─── Step 1: Bail if $HOME unavailable (degrades silently) ──── +if [ -z "${HOME:-}" ]; then + exit 0 +fi + +# ─── Snooze helper ──────────────────────────────────────────── +# check_snooze +# Returns 0 if snoozed (should stay quiet), 1 if not snoozed (should output). +# +# Snooze file format: +# Level durations: 1=24h, 2=48h, 3+=7d +# New version (version mismatch) resets snooze. +check_snooze() { + local remote_ver="$1" + if [ ! -f "$SNOOZE_FILE" ]; then + return 1 + fi + local snoozed_ver snoozed_level snoozed_epoch + snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)" + snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)" + snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)" + + if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then + return 1 + fi + case "$snoozed_level" in *[!0-9]*) return 1 ;; esac + case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac + + if [ "$snoozed_ver" != "$remote_ver" ]; then + return 1 + fi + + local duration + case "$snoozed_level" in + 1) duration=86400 ;; + 2) duration=172800 ;; + *) duration=604800 ;; + esac + + local now expires + now="$(date +%s)" + expires=$(( snoozed_epoch + duration )) + if [ "$now" -lt "$expires" ]; then + return 0 + fi + return 1 +} + +# ─── Step 2: Read local version ─────────────────────────────── +LOCAL="" +if [ -f "$VERSION_FILE" ]; then + LOCAL="$(tr -d '[:space:]' < "$VERSION_FILE" 2>/dev/null || true)" +fi +if [ -z "$LOCAL" ]; then + exit 0 +fi + +# Validate local: same shape we expect from remote. A malformed VERSION +# (corrupt install, shell-injection attempt) should fail closed silently. +if ! echo "$LOCAL" | grep -qE '^[0-9]+\.[0-9.]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then + exit 0 +fi + +# ─── Step 3: Check cache freshness ──────────────────────────── +# UP_TO_DATE: 60min TTL (detect new releases promptly) +# UPGRADE_AVAILABLE: 720min TTL (keep nagging on a workday cadence) +if [ -f "$CACHE_FILE" ]; then + CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)" + case "$CACHED" in + UP_TO_DATE*) CACHE_TTL=60 ;; + UPGRADE_AVAILABLE*) CACHE_TTL=720 ;; + *) CACHE_TTL=0 ;; + esac + + STALE=$(find "$CACHE_FILE" -mmin +$CACHE_TTL 2>/dev/null || true) + if [ -z "$STALE" ] && [ "$CACHE_TTL" -gt 0 ]; then + case "$CACHED" in + UP_TO_DATE*) + CACHED_VER="$(echo "$CACHED" | awk '{print $2}')" + if [ "$CACHED_VER" = "$LOCAL" ]; then + exit 0 + fi + ;; + UPGRADE_AVAILABLE*) + CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')" + if [ "$CACHED_OLD" = "$LOCAL" ]; then + CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')" + if check_snooze "$CACHED_NEW"; then + exit 0 + fi + echo "$CACHED" + exit 0 + fi + ;; + esac + fi +fi + +# ─── Step 4: Slow path — fetch remote version ───────────────── +# Graceful: read-only $STATE_DIR or other write failure → silent exit 0. +mkdir -p "$STATE_DIR" 2>/dev/null || exit 0 + +# Distinguish curl failure (don't cache; retry next invocation) from +# curl success with garbage response (cache as UP_TO_DATE to suppress +# retry storms when upstream is misconfigured). +CURL_RC=0 +REMOTE_RAW="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null)" || CURL_RC=$? +if [ "$CURL_RC" -ne 0 ]; then + exit 0 +fi +REMOTE="$(echo "$REMOTE_RAW" | tr -d '[:space:]')" + +# Validate: must look like a SemVer (allow pre-release / build metadata). +# Reject HTML error pages, garbage responses, etc. +if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then + echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE" 2>/dev/null || true + exit 0 +fi + +if [ "$LOCAL" = "$REMOTE" ]; then + echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE" 2>/dev/null || true + exit 0 +fi + +echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE" 2>/dev/null || true +if check_snooze "$REMOTE"; then + exit 0 +fi + +echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" diff --git a/checklists/new-tool.md b/checklists/new-tool.md deleted file mode 100644 index 4dd61e5..0000000 --- a/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/cliff.toml b/cliff.toml new file mode 100644 index 0000000..4176efc --- /dev/null +++ b/cliff.toml @@ -0,0 +1,25 @@ +[changelog] +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [Unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {{ commit.message | upper_first }}\ + {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\ + {% if commit.remote.pr_number %} in \ + [#{{ commit.remote.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.remote.pr_number }}){%- endif %} + {%- endfor %} +{% endfor %}\n +""" + +[remote.github] +owner = "brettdavies" +repo = "agentnative-skill" diff --git a/getting-started.md b/getting-started.md new file mode 100644 index 0000000..5cab6d8 --- /dev/null +++ b/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 +[`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 spec/principles/p-*.md — frontmatter `requirements[]`. + +# 3. Apply the fix. Patterns live in: +# references/rust-clap-patterns.md (Rust/clap) +# references/framework-idioms.md (Rust idioms) +# 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 /templates/clap-main.rs src/main.rs +cp /templates/error-types.rs src/error.rs +cp /templates/output-format.rs src/output.rs +cp /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 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 `spec/principles/p1-*.md` through `p7-*.md` for the language-agnostic requirements, and +`references/framework-idioms-other-languages.md` for per-framework idioms. + +## Installing anc + +```bash +brew install brettdavies/tap/agentnative # macOS / Linux +cargo install agentnative +``` + +Binary name: `anc`. Prebuilt releases at . + +## Where things live + +| Question | Where | +| ----------------------------------------------- | -------------------------------------------------- | +| What does P3 mean? | `spec/principles/p3-progressive-help-discovery.md` | +| What spec version does this bundle ship? | `spec/VERSION` | +| How do I implement `` in Rust/clap? | `references/rust-clap-patterns.md` | +| How do I implement `` in Python/Go/JS? | `references/framework-idioms-other-languages.md` | +| File a spec question or proposal | | +| File an `anc` bug | | +| File a skill-bundle issue | | diff --git a/references/principles-deep-dive.md b/references/principles-deep-dive.md deleted file mode 100644 index 45df90f..0000000 --- a/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/scripts/check-compliance.sh b/scripts/check-compliance.sh deleted file mode 100755 index e0a4a54..0000000 --- a/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/scripts/checks/_helpers.sh b/scripts/checks/_helpers.sh deleted file mode 100644 index 9cfbf2b..0000000 --- a/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/scripts/checks/check-code-env-flags.sh b/scripts/checks/check-code-env-flags.sh deleted file mode 100755 index babd06c..0000000 --- a/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/scripts/checks/check-code-naked-println.sh b/scripts/checks/check-code-naked-println.sh deleted file mode 100755 index 3193e5e..0000000 --- a/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/scripts/checks/check-code-unwrap.sh b/scripts/checks/check-code-unwrap.sh deleted file mode 100755 index 3c14c6c..0000000 --- a/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/scripts/checks/check-p1-headless-auth.sh b/scripts/checks/check-p1-headless-auth.sh deleted file mode 100755 index 4feefbc..0000000 --- a/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/scripts/checks/check-p1-non-interactive.sh b/scripts/checks/check-p1-non-interactive.sh deleted file mode 100755 index c9fa6b8..0000000 --- a/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/scripts/checks/check-p2-structured-output.sh b/scripts/checks/check-p2-structured-output.sh deleted file mode 100755 index b26697b..0000000 --- a/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/scripts/checks/check-p3-progressive-help.sh b/scripts/checks/check-p3-progressive-help.sh deleted file mode 100755 index 210e38a..0000000 --- a/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/scripts/checks/check-p3-version-flag.sh b/scripts/checks/check-p3-version-flag.sh deleted file mode 100755 index 87f3587..0000000 --- a/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/scripts/checks/check-p4-error-types.sh b/scripts/checks/check-p4-error-types.sh deleted file mode 100755 index 7a5cce5..0000000 --- a/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/scripts/checks/check-p4-exit-codes.sh b/scripts/checks/check-p4-exit-codes.sh deleted file mode 100755 index 9cb735b..0000000 --- a/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/scripts/checks/check-p4-process-exit.sh b/scripts/checks/check-p4-process-exit.sh deleted file mode 100755 index 4bb6ed6..0000000 --- a/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/scripts/checks/check-p4-try-parse.sh b/scripts/checks/check-p4-try-parse.sh deleted file mode 100755 index 5afe1c7..0000000 --- a/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/scripts/checks/check-p5-safe-retries.sh b/scripts/checks/check-p5-safe-retries.sh deleted file mode 100755 index e8dcd59..0000000 --- a/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/scripts/checks/check-p6-completions.sh b/scripts/checks/check-p6-completions.sh deleted file mode 100755 index e48624c..0000000 --- a/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/scripts/checks/check-p6-global-flags.sh b/scripts/checks/check-p6-global-flags.sh deleted file mode 100755 index c303e99..0000000 --- a/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/scripts/checks/check-p6-no-color.sh b/scripts/checks/check-p6-no-color.sh deleted file mode 100755 index fddb3aa..0000000 --- a/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/scripts/checks/check-p6-no-pager.sh b/scripts/checks/check-p6-no-pager.sh deleted file mode 100755 index c84d277..0000000 --- a/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/scripts/checks/check-p6-sigpipe.sh b/scripts/checks/check-p6-sigpipe.sh deleted file mode 100755 index 1c411dd..0000000 --- a/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/scripts/checks/check-p6-timeout.sh b/scripts/checks/check-p6-timeout.sh deleted file mode 100755 index be35e0b..0000000 --- a/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/scripts/checks/check-p6-tty-detection.sh b/scripts/checks/check-p6-tty-detection.sh deleted file mode 100755 index 960b367..0000000 --- a/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/scripts/checks/check-p7-output-clamping.sh b/scripts/checks/check-p7-output-clamping.sh deleted file mode 100755 index dda0aff..0000000 --- a/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/scripts/checks/check-p7-quiet-flag.sh b/scripts/checks/check-p7-quiet-flag.sh deleted file mode 100755 index 30cef14..0000000 --- a/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/scripts/checks/check-project-agents-md.sh b/scripts/checks/check-project-agents-md.sh deleted file mode 100755 index 9264b93..0000000 --- a/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/scripts/checks/check-project-dependencies.sh b/scripts/checks/check-project-dependencies.sh deleted file mode 100755 index 74e91ce..0000000 --- a/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/scripts/generate-changelog.sh b/scripts/generate-changelog.sh new file mode 100755 index 0000000..b111442 --- /dev/null +++ b/scripts/generate-changelog.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +# Generate or update CHANGELOG.md using git-cliff with PR body expansion. +# +# Usage: +# generate-changelog.sh [--tag vX.Y.Z] [repo-path] +# generate-changelog.sh --check [repo-path] +# +# Options: +# --tag vX.Y.Z Override version tag (default: extracted from branch name) +# --check Verify CHANGELOG.md has a versioned section (exit 1 if only [Unreleased]) +# +# The version tag is extracted from the branch name by matching the pattern +# release/vN.N.N (with optional suffix like release/v1.0.5:ci-migration). +# Use --tag to override when not on a release branch. +# +# Generates entries for commits since the last tag, prepends to existing +# CHANGELOG.md, then expands squash commit entries by fetching categorized +# changelog sections (### Added, ### Changed, ### Fixed, ### Documentation) +# from each PR body's ## Changelog section. +# +# Falls back to ## Changes (flat list) for PRs using the old template. +# +# Run this on a release branch before opening a PR to main. + +set -euo pipefail + +CHECK_MODE=false +REPO_PATH="." +TAG="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --check) + CHECK_MODE=true + shift + ;; + --tag) + TAG="$2" + shift 2 + ;; + *) + REPO_PATH="$1" + shift + ;; + esac +done + +cd "$REPO_PATH" + +# Verify prerequisites +if [[ ! -f cliff.toml ]]; then + echo "error: cliff.toml not found in $(pwd)" >&2 + exit 1 +fi + +if ! command -v git-cliff &>/dev/null; then + echo "error: git-cliff is not installed" >&2 + echo " Install: cargo install git-cliff" >&2 + echo " Or: brew install git-cliff" >&2 + exit 1 +fi + +if $CHECK_MODE; then + if [[ ! -f CHANGELOG.md ]]; then + echo "FAIL: CHANGELOG.md does not exist" >&2 + exit 1 + fi + + # Check for a versioned section (not just [Unreleased]) + LATEST_SECTION=$(awk '/^## \[/{print; exit}' CHANGELOG.md) + if echo "$LATEST_SECTION" | grep -q '\[Unreleased\]'; then + echo "FAIL: CHANGELOG.md has [Unreleased] instead of a versioned section" >&2 + echo "Run: generate-changelog.sh (on a release/vX.Y.Z branch)" >&2 + exit 1 + fi + + echo "OK: CHANGELOG.md has versioned section" + exit 0 +fi + +# Extract version from branch name if --tag not provided +if [[ -z "$TAG" ]]; then + BRANCH=$(git branch --show-current 2>/dev/null || true) + if [[ "$BRANCH" =~ ^release/v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + TAG="v${BASH_REMATCH[1]}" + echo "Detected version $TAG from branch $BRANCH" + else + echo "error: could not detect version from branch '$BRANCH'" >&2 + echo "Either use a release/vX.Y.Z branch or pass --tag vX.Y.Z" >&2 + exit 1 + fi +fi + +# Ensure GitHub token is available for remote integration (PR links, authors) +if [[ -z "${GITHUB_TOKEN:-}" ]]; then + if command -v gh &>/dev/null && gh auth status &>/dev/null 2>&1; then + export GITHUB_TOKEN + GITHUB_TOKEN=$(gh auth token) + fi +fi + +# Step 1: Run git-cliff to prepend entries tagged with the release version +CLIFF_ARGS=(--unreleased --tag "$TAG") +if [[ -f CHANGELOG.md ]]; then + CLIFF_ARGS+=(--prepend CHANGELOG.md) +else + CLIFF_ARGS+=(-o CHANGELOG.md) +fi +git cliff "${CLIFF_ARGS[@]}" + +# Step 2: Expand squash commit entries using PR body changelog sections +OWNER=$(awk -F'"' '/^\[remote\.github\]/{found=1} found && /^owner/{print $2; exit}' cliff.toml) +REPO=$(awk -F'"' '/^\[remote\.github\]/{found=1} found && /^repo/{print $2; exit}' cliff.toml) + +# Strip leading v for version matching in the changelog +VERSION="${TAG#v}" + +if [[ -z "$OWNER" || -z "$REPO" ]] || ! command -v gh &>/dev/null; then + echo "Updated CHANGELOG.md (skipping PR expansion — missing [remote.github] or gh CLI)" + echo "" + echo "Next steps:" + echo " git add CHANGELOG.md" + echo " git commit -m 'docs: update CHANGELOG.md'" + exit 0 +fi + +# Extract PR numbers from the new version section only +VERSION_SECTION=$(awk -v ver="$VERSION" ' + /^## \[/{ + if (found) exit + if (index($0, "[" ver "]")) found=1 + } + found{print} +' CHANGELOG.md) +PR_NUMBERS=$(echo "$VERSION_SECTION" | grep -oP '\(#\K\d+' | sort -un) + +if [[ -z "$PR_NUMBERS" ]]; then + echo "Updated CHANGELOG.md" + echo "" + echo "Next steps:" + echo " git add CHANGELOG.md" + echo " git commit -m 'docs: update CHANGELOG.md'" + exit 0 +fi + +# Pass PR numbers as comma-separated arg to python +PR_LIST=$(echo "$PR_NUMBERS" | tr '\n' ',' | sed 's/,$//') + +python3 - "$OWNER" "$REPO" "$PR_LIST" "CHANGELOG.md" "$VERSION" "$TAG" << 'PYEOF' +import json, re, subprocess, sys + +owner = sys.argv[1] +repo = sys.argv[2] +pr_numbers = [int(n) for n in sys.argv[3].split(',')] +changelog_path = sys.argv[4] +version = sys.argv[5] +tag = sys.argv[6] if len(sys.argv) > 6 else f'v{version}' +tag_prefix = 'v' if tag.startswith('v') else '' + +CATEGORIES = ['Added', 'Changed', 'Fixed', 'Documentation'] + +def fetch_pr(num): + """Fetch PR body and author from GitHub API.""" + try: + result = subprocess.run( + ['gh', 'api', f'repos/{owner}/{repo}/pulls/{num}', + '--jq', '{body: .body, author: .user.login}'], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0: + return json.loads(result.stdout) + except Exception: + pass + return None + +def extract_changelog_sections(body): + """Extract categorized bullets from ## Changelog section with ### subsections.""" + sections = {} + if not body: + return sections + + changelog_match = re.search(r'^## Changelog\s*$', body, re.MULTILINE) + if not changelog_match: + return sections + + rest = body[changelog_match.end():] + next_h2 = re.search(r'^## ', rest, re.MULTILINE) + changelog_content = rest[:next_h2.start()] if next_h2 else rest + + current_section = None + for line in changelog_content.split('\n'): + h3_match = re.match(r'^### (.+)', line) + if h3_match: + current_section = h3_match.group(1).strip() + if current_section not in sections: + sections[current_section] = [] + elif current_section and re.match(r'^- ', line): + sections[current_section].append(line) + elif current_section and sections.get(current_section) and re.match(r'^ \S', line): + # Continuation line (indented, part of previous bullet) — join to last bullet + sections[current_section][-1] = sections[current_section][-1].rstrip() + ' ' + line.strip() + + return sections + +def extract_flat_changes(body): + """Fallback: extract flat bullet list from ## Changes section.""" + bullets = [] + if not body: + return bullets + + changes_match = re.search(r'^## Changes\s*$', body, re.MULTILINE) + if not changes_match: + return bullets + + rest = body[changes_match.end():] + next_h2 = re.search(r'^## ', rest, re.MULTILINE) + changes_content = rest[:next_h2.start()] if next_h2 else rest + + for line in changes_content.split('\n'): + if re.match(r'^- ', line): + bullets.append(line) + elif bullets and re.match(r'^ \S', line): + # Continuation line (indented, part of previous bullet) — join to last bullet + bullets[-1] = bullets[-1].rstrip() + ' ' + line.strip() + + return bullets + +# Collect all categorized entries from PR bodies +all_entries = {} # category -> list of bullets +for num in pr_numbers: + pr_data = fetch_pr(num) + if not pr_data: + continue + + body = pr_data.get('body', '') or '' + author = pr_data.get('author', '') + attrib = f' by @{author} in [#{num}](https://github.com/{owner}/{repo}/pull/{num})' if author else '' + + # Try new template format first (## Changelog with ### subsections) + sections = extract_changelog_sections(body) + + if sections: + for category, bullets in sections.items(): + if not bullets: + continue + if category not in all_entries: + all_entries[category] = [] + first = True + for bullet in bullets: + if first and ' by @' not in bullet: + all_entries[category].append(bullet + attrib) + else: + all_entries[category].append(bullet) + first = False + else: + # Fallback: flat ## Changes section + flat = extract_flat_changes(body) + if flat: + category = 'Changed' + if category not in all_entries: + all_entries[category] = [] + first = True + for bullet in flat: + if first and ' by @' not in bullet: + all_entries[category].append(bullet + attrib) + else: + all_entries[category].append(bullet) + first = False + +if not all_entries: + sys.exit(0) + +# Read the changelog +with open(changelog_path, 'r') as f: + content = f.read() + +# Find the version section header line (preserve it with the date) +header_pattern = rf'^## \[{re.escape(version)}\].*$' +header_match = re.search(header_pattern, content, re.MULTILINE) +if not header_match: + sys.exit(0) + +header_line = header_match.group(0) + +# Build the new version section content +new_section = header_line + '\n' +for cat in CATEGORIES: + if cat in all_entries and all_entries[cat]: + new_section += f'\n### {cat}\n\n' + for bullet in all_entries[cat]: + new_section += bullet + '\n' + +# Include any categories not in the standard list +for cat in all_entries: + if cat not in CATEGORIES and all_entries[cat]: + new_section += f'\n### {cat}\n\n' + for bullet in all_entries[cat]: + new_section += bullet + '\n' + +# Find the previous version tag for the Full Changelog link +prev_match = re.search(rf'## \[{re.escape(version)}\].*?\n## \[([^\]]+)\]', content, re.DOTALL) +if prev_match: + prev_version = prev_match.group(1) + new_section += f'\n**Full Changelog**: [{tag_prefix}{prev_version}...{tag_prefix}{version}](https://github.com/{owner}/{repo}/compare/{tag_prefix}{prev_version}...{tag_prefix}{version})\n' + +# Replace the version section in the file +section_pattern = rf'## \[{re.escape(version)}\].*?(?=\n## \[|\Z)' +new_content = re.sub(section_pattern, new_section.rstrip() + '\n', content, count=1, flags=re.DOTALL) + +with open(changelog_path, 'w') as f: + f.write(new_content) +PYEOF + +echo "Updated CHANGELOG.md" +echo "" +echo "Next steps:" +echo " git add CHANGELOG.md" +echo " git commit -m 'docs: update CHANGELOG.md'" diff --git a/scripts/sync-spec.sh b/scripts/sync-spec.sh new file mode 100755 index 0000000..bd1a949 --- /dev/null +++ b/scripts/sync-spec.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Vendor agentnative-spec into spec/. +# +# Resolves the latest v* tag of agentnative-spec, preferring the remote +# repository, and falls back to a local checkout if the remote is +# unreachable. Extracts files via `git show :` so neither +# checkout's working tree is 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_ROOT=/path/to/agentnative-spec scripts/sync-spec.sh +# SPEC_REMOTE_URL=git@github.com:brettdavies/agentnative.git scripts/sync-spec.sh +# +# Env vars: +# SPEC_REMOTE_URL Remote URL to query first. +# Default: https://github.com/brettdavies/agentnative.git +# SPEC_ROOT Local checkout to fall back to when the remote is +# unreachable. Default: $HOME/dev/agentnative-spec +# +# Resync cadence: rerun after every new agentnative-spec tag. The remote +# query picks up new tags automatically; a local fallback only sees what +# the local checkout already has fetched. +# +# Stale orphan files in 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_REMOTE_URL="${SPEC_REMOTE_URL:-https://github.com/brettdavies/agentnative.git}" +SPEC_ROOT="${SPEC_ROOT:-$HOME/dev/agentnative-spec}" + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DEST_DIR="$REPO_ROOT/spec" +DEST_PRINCIPLES="$DEST_DIR/principles" + +# Cleanup hook for the temp clone (set only after mktemp succeeds). +tmp_root="" +cleanup() { + if [[ -n "$tmp_root" && -d "$tmp_root" ]]; then + rm -rf "$tmp_root" + fi +} +trap cleanup EXIT + +# === Remote-first resolution =========================================== +spec_source="" +spec_tag="" + +echo "querying $SPEC_REMOTE_URL for latest v* tag..." +remote_tag="$(git ls-remote --tags --sort='-version:refname' \ + "$SPEC_REMOTE_URL" 'refs/tags/v*' 2>/dev/null \ + | awk '{print $2}' \ + | sed 's|refs/tags/||' \ + | grep -v '\^{}$' \ + | head -n 1 || true)" + +if [[ -n "$remote_tag" ]]; then + tmp_root="$(mktemp -d -t agentnative-spec-XXXXXX)" + if git clone --depth 1 --branch "$remote_tag" --quiet \ + "$SPEC_REMOTE_URL" "$tmp_root" 2>/dev/null; then + spec_source="$tmp_root" + spec_tag="$remote_tag" + resolved_sha="$(git -C "$spec_source" rev-parse --short=7 "$spec_tag^{commit}")" + echo "vendoring $spec_tag ($resolved_sha) from remote $SPEC_REMOTE_URL" + fi +fi + +# === Local fallback ==================================================== +if [[ -z "$spec_source" ]]; then + if [[ ! -d "$SPEC_ROOT/.git" ]]; then + echo "error: remote unreachable and SPEC_ROOT is not a git repository: $SPEC_ROOT" >&2 + echo " remote: $SPEC_REMOTE_URL" >&2 + echo " set SPEC_ROOT to your agentnative-spec checkout, or check network access." >&2 + exit 1 + fi + echo "warning: remote query failed; falling back to local $SPEC_ROOT" >&2 + + spec_source="$SPEC_ROOT" + spec_tag="$(git -C "$spec_source" tag --list 'v*' --sort='-version:refname' | head -n 1)" + if [[ -z "$spec_tag" ]]; then + echo "error: no v* tags found in $SPEC_ROOT" >&2 + echo " try \`git -C $SPEC_ROOT fetch --tags\` to pick up upstream tags" >&2 + exit 1 + fi + resolved_sha="$(git -C "$spec_source" rev-parse --short=7 "$spec_tag^{commit}")" + echo "vendoring $spec_tag ($resolved_sha) from local $spec_source" +fi + +# === Verify + extract (works identically for remote and local sources) = +if ! git -C "$spec_source" cat-file -e "$spec_tag:principles" 2>/dev/null; then + echo "error: $spec_tag has no principles/ directory in $spec_source" >&2 + exit 1 +fi + +mkdir -p "$DEST_PRINCIPLES" + +# VERSION and CHANGELOG.md are top-level in the spec repo. +git -C "$spec_source" show "$spec_tag:VERSION" >"$DEST_DIR/VERSION" +git -C "$spec_source" show "$spec_tag:CHANGELOG.md" >"$DEST_DIR/CHANGELOG.md" + +# Enumerate principle files at the tag 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_source" show "$spec_tag:$path" >"$DEST_PRINCIPLES/$dest_name" + copied=$((copied + 1)) + ;; + esac +done < <(git -C "$spec_source" ls-tree --name-only "$spec_tag" principles/) + +if [[ "$copied" -eq 0 ]]; then + echo "error: no principles/p*-*.md files found at $spec_tag" >&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." diff --git a/spec/CHANGELOG.md b/spec/CHANGELOG.md new file mode 100644 index 0000000..d091c4b --- /dev/null +++ b/spec/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog + +All notable changes to this repository are documented here — governance, validator, release infrastructure, README, decision records. + +Changes to the standard itself — principle MUST/SHOULD/MAY tier moves, requirement IDs added/removed/renamed, applicability shifts — are tracked per-principle in `principles/p*-*.md` via the `last-revised:` calver frontmatter field and the `## Pressure test notes` section appended to each file. + +## [0.3.0] - 2026-04-28 + +### Added + +- `active` status value for principles, joining `draft | under-review | locked` as the default state for shipped principles. by @brettdavies in [#12](https://github.com/brettdavies/agentnative/pull/12) +- README `## Status` section and HN-scroller hook explaining the active-with-pressure-tests-welcome posture. + +### Changed + +- All 7 principles flipped from `status: draft` to `status: active` for the v0.3.0 release. by @brettdavies in [#12](https://github.com/brettdavies/agentnative/pull/12) +- `principles/AGENTS.md` pressure-test protocol updated for the new status lifecycle (`active` is the default; `under-review` is reserved for substantive critique cycles). +- README restructured for HN-visitor flow: new `## The trifecta` callout (spec + linter + leaderboard as equals); `## Quick start` lead with `brew install brettdavies/tap/agentnative`; `## Live leaderboard` preview table; admin sections (Versioning, Decision records, Related, Contributing, License) reordered below spec content. License section tightened from 4 paragraphs to 3 bullets; Contributing tightened from 4-bullet list to 1 paragraph. by @brettdavies in [#14](https://github.com/brettdavies/agentnative/pull/14) +- README leaderboard URLs corrected from bare `anc.dev` to `anc.dev/scorecards`. +- AGENTS.md adds `brettdavies/agentnative-skill` as a documented downstream consumer (introductory list + cross-repo context table); replaces the prior `~/.claude/skills/agent-native-cli/` row with the public `~/dev/agentnative-skill` row. + +### Documentation + +- G11 red-team pass on all 7 principle files via `compound-engineering:ce-adversarial-document-reviewer`. 25 findings: 11 prose edits applied (P1 TUI parenthetical, P2 sysexits acknowledgment, P4 dependency-gating cleanup, P5 `--dry-run` write-gate + retry hedge, P6 SIGPIPE language-neutral + global-flags behavioral lead, P7 LLM-vs-non-LLM cost generalization), 10 `[later]` notes appended for v0.4.0 follow-up, 2 `[wontfix]` notes, 2 skipped. No requirement IDs added/removed/renamed; no level/applicability changes; no `last-revised:` bumps; no VERSION bump triggered. by @brettdavies in [#13](https://github.com/brettdavies/agentnative/pull/13) +- Three summary-text tightenings (P4 `gating-before-network`, P6 `sigpipe`, P6 `global-flags`) introduce mild registry-readable drift documented in pressure-test notes for v0.4.0 follow-up. + +**Full Changelog**: [v0.2.0...v0.3.0](https://github.com/brettdavies/agentnative/compare/v0.2.0...v0.3.0) + +## [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/spec/README.md b/spec/README.md new file mode 100644 index 0000000..5a18373 --- /dev/null +++ b/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 the +latest upstream `v*` tag and ship inside the skill bundle so consumers carry the canonical principle text alongside the +skill metadata. Each release of this bundle re-vendors against the latest spec tag. The currently vendored version is +recorded in [`VERSION`](./VERSION). + +## Resync + +Run from the repo root: + +```bash +scripts/sync-spec.sh # queries the remote for the latest v* tag; falls back to local on network failure +``` + +The script queries `https://github.com/brettdavies/agentnative.git` for the latest `v*` tag and shallow-clones that tag +into a temp directory for extraction. If the remote is unreachable, it falls back to a local checkout +(`$HOME/dev/agentnative-spec` by default; override with `SPEC_ROOT`). Override `SPEC_REMOTE_URL` to query a different +remote. The script extracts files via `git show`, so neither source's working tree is perturbed. + +## Layout + +| Path | Source in `agentnative-spec` | Purpose | +| ------------------ | ---------------------------- | ------------------------------------------------------------- | +| `VERSION` | `VERSION` | Spec version string; the skill's vendored `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 +dual-licensed under MIT or Apache-2.0; 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/spec/VERSION b/spec/VERSION new file mode 100644 index 0000000..0d91a54 --- /dev/null +++ b/spec/VERSION @@ -0,0 +1 @@ +0.3.0 diff --git a/spec/principles/p1-non-interactive-by-default.md b/spec/principles/p1-non-interactive-by-default.md new file mode 100644 index 0000000..f93ad5a --- /dev/null +++ b/spec/principles/p1-non-interactive-by-default.md @@ -0,0 +1,130 @@ +--- +id: p1 +title: Non-Interactive by Default +last-revised: 2026-04-22 +status: active +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, or any TUI event loop that takes over the terminal). 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. + +## Pressure test notes + +### 2026-04-27 — Show HN launch red-team pass + +Adversarial review via `compound-engineering:ce-adversarial-document-reviewer` ahead of the v0.3.0 launch. Findings +recorded verbatim per `principles/AGENTS.md` § "Pressure-test protocol". + +- **[edit]** *Internal inconsistency.* "The `--no-interactive` MUST bullet says 'uses defaults, reads from stdin, or + exits with an actionable error' but the principle's behavioral framing (per decision record) covers TUI session init + too. The prose bullet only enumerates prompt libraries (`dialoguer`, `inquire`, `read_line`, `TTY::Prompt`, + `inquirer`), not TUI frameworks (`ratatui`, `bubbletea`) — readers will infer the MUST excludes TUIs, contradicting + the decision record's explicit 'blocking-interactive surface includes... TUI session initialization.'" Resolved: prose + bullet's parenthetical now includes "or any TUI event loop that takes over the terminal." Mirrors the behavioral + framing in [`docs/decisions/p1-behavioral-must.md`](../docs/decisions/p1-behavioral-must.md). No frontmatter change; + the summary already says "gates every prompt library call" and stays. +- **[wontfix]** *Prior art.* "RFC 8628 citation is correct in name but incomplete in framing. The prose says Device + Authorization Grant means 'the CLI prints a URL and a code; the user authorizes on another device' — this still + requires a human on another device, which an unattended agent does not have. An HN commenter will note this is a + *human-assisted* headless path, not an agent-headless path; true unattended agents need service-account / API-token + auth (which the principle doesn't mention)." Rationale: P1 scopes "headless" as "no local browser required," not "no + human anywhere." A human-in-the-loop is acceptable here. Service-account auth is orthogonal and would belong to a + separate principle if it were added. diff --git a/spec/principles/p2-structured-parseable-output.md b/spec/principles/p2-structured-parseable-output.md new file mode 100644 index 0000000..4f1ef7b --- /dev/null +++ b/spec/principles/p2-structured-parseable-output.md @@ -0,0 +1,124 @@ +--- +id: p2 +title: Structured, Parseable Output +last-revised: 2026-04-22 +status: active +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 | + + These codes blend the bash 0/1/2 convention with BSD `sysexits.h` 77/78 (`EX_NOPERM`, `EX_CONFIG`); the result is the + de-facto agent-facing dialect, not strict `sysexits.h` compliance. + +- 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. + +## Pressure test notes + +### 2026-04-27 — Show HN launch red-team pass + +Adversarial review via `compound-engineering:ce-adversarial-document-reviewer` ahead of the v0.3.0 launch. Findings +recorded verbatim per `principles/AGENTS.md` § "Pressure-test protocol". + +- **[edit]** *Prior art.* "The exit-code table conflicts with `sysexits.h`. `EX_NOPERM=77` is 'permission denied' + (close), but `EX_CONFIG=78` is correct. However, `sysexits.h` reserves `EX_USAGE=64`, `EX_DATAERR=65`, + `EX_NOINPUT=66`, `EX_UNAVAILABLE=69`, `EX_SOFTWARE=70` — P2 puts 'usage error' at 2 (bash convention), not 64. HN will + note the principle straddles two conventions (bash 0/1/2 + sysexits 77/78) without naming the hybrid." Resolved: added + one sentence under the exit-code table acknowledging the bash + `sysexits.h` blend. The same citation now appears in + P4's exit-code table (per Row #13 of the same review pass) so both files agree. +- **[later]** *Must-vs-should.* "A single-number-emitting CLI (e.g., `epoch`, `uuidgen`) plausibly violates the + `--output text|json|jsonl` MUST for a defensible reason. Universal applicability is a strong claim." Deferred: revisit + whether `applicability` should soften when the launch landscape clarifies actual single-number agent-facing CLIs. The + applicability change would fire coupled-release (CLI registry impact), so it is held for a v0.4.0 cleanup PR rather + than churned during launch week. diff --git a/spec/principles/p3-progressive-help-discovery.md b/spec/principles/p3-progressive-help-discovery.md new file mode 100644 index 0000000..55bf47b --- /dev/null +++ b/spec/principles/p3-progressive-help-discovery.md @@ -0,0 +1,106 @@ +--- +id: p3 +title: Progressive Help Discovery +last-revised: 2026-04-22 +status: active +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. + +## Pressure test notes + +### 2026-04-27 — Show HN launch red-team pass + +Adversarial review via `compound-engineering:ce-adversarial-document-reviewer` ahead of the v0.3.0 launch. Findings +recorded verbatim per `principles/AGENTS.md` § "Pressure-test protocol". + +- **[later]** *Internal inconsistency.* "The SHOULD on paired examples ('text then `--output json` equivalent') + implicitly gates P3 conformance on a `--output json` mode, which is P2's territory. A CLI without structured output + cannot satisfy this SHOULD even in spirit, yet `applicability: universal`." Deferred: narrowing `applicability` from + `universal` to conditional (`if: CLI exposes a structured-output mode`) fires the coupled-release norm (CLI registry + parses `applicability`). Bundled with other applicability cleanups for a v0.4.0 PR with explicit registry + coordination. +- **[later]** *Must-vs-should.* "'Top-level command ships 2–3 examples' as a universal MUST is too strong for genuinely + single-purpose CLIs (e.g., `cat`, `true`, a one-shot wrapper) where one canonical invocation is the entire surface. + The '2–3' count baked into a MUST will draw HN fire as cargo-culted." Deferred: softening to "at least one example, + and 2–3 when the tool has multiple primary use cases" is a MUST-content change that drifts the frontmatter summary. + Bundled with other MUST-content softenings for a v0.4.0 PR. +- **[later]** *Prior art.* "Principle is clap-flavored throughout (`after_help`, `about`/`long_about`, `///` doc + comments in Anti-Patterns) without a single sentence acknowledging the non-Rust analog (docopt usage block, `argparse` + epilog, `cobra` Example field, `gh`/`kubectl` Examples convention). HN will call this 'a clap style guide, not a CLI + standard.'" Deferred: a cross-framework analog appendix is a meaningful addition. The Definition / Why-Agents-Need-It + sections are framework-agnostic; the Evidence section is intentionally clap-keyed. Worth revisiting in v0.4.0 once the + standard's multi-language reach is clearer; site copy may also be a better home than the principle file itself. diff --git a/spec/principles/p4-fail-fast-actionable-errors.md b/spec/principles/p4-fail-fast-actionable-errors.md new file mode 100644 index 0000000..8e91deb --- /dev/null +++ b/spec/principles/p4-fail-fast-actionable-errors.md @@ -0,0 +1,141 @@ +--- +id: p4 +title: Fail Fast with Actionable Errors +last-revised: 2026-04-22 +status: active +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, failing at the earliest possible point. + - 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. Use 77 when the CLI has an auth surface and 78 when it has a config surface; + 0/1/2 are universal: + +| Code | Meaning | +| ---: | ----------------------- | +| 0 | Success | +| 1 | General command error | +| 2 | Usage / argument error | +| 77 | Auth / permission error | +| 78 | Configuration error | + + These codes blend the bash 0/1/2 convention with BSD `sysexits.h` 77/78 (`EX_NOPERM`, `EX_CONFIG`); the result is the + de-facto agent-facing dialect, not strict `sysexits.h` compliance. + +- 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, failing at the earliest possible point. The structural + three-tier definition (meta-commands, local-only commands, network commands) lives in P6 (`p6-should-tier-gating`); + this requirement specifies the network-call ordering consequence. +- 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. + +## Pressure test notes + +### 2026-04-27 — Show HN launch red-team pass + +Adversarial review via `compound-engineering:ce-adversarial-document-reviewer` ahead of the v0.3.0 launch. Findings +recorded verbatim per `principles/AGENTS.md` § "Pressure-test protocol". + +- **[edit]** *Internal inconsistency.* "Three-tier gating is labeled identically as a SHOULD in both P4 + (`p4-should-gating-before-network`) and P6 (`p6-should-tier-gating`) — same pattern, two homes, no cross-reference. + Readers can't tell which is canonical, and a CLI that satisfies one auto-satisfies the other." Resolved: P4's bullet + now focuses on the network-call ordering consequence and points to P6 as the canonical home of the structural + three-tier definition. Frontmatter summary tightened to match. Requirement ID is unchanged so CLI registry pinning is + unaffected. +- **[edit]** *Must-vs-should.* "`p4-must-exit-code-mapping` is `applicability: universal` and the prose says 'At + minimum' 0/1/2/77/78 — but a CLI with no auth surface and no config file legitimately has nothing to assign to either + 77 or 78, and the MUST forces empty-by-construction error variants. Same shape as P6, which correctly gates + `p6-must-timeout-network` behind `if: CLI makes network calls`." Resolved: prose now reads "Use 77 when the CLI has an + auth surface and 78 when it has a config surface; 0/1/2 are universal." Frontmatter summary stays universal because + the *mapping discipline* is universal even if the specific 77/78 codes are conditional. The summary-prose drift is a + known launch-week tradeoff; full alignment of the summary text is on the v0.4.0 punch list. + +- **[edit]** *Prior art.* "77/78 align with BSD `sysexits.h` (`EX_NOPERM`, `EX_CONFIG`) — the alignment is a strength + but neither P2 nor P4 cites BSD sysexits, leaving an HN commenter to 'discover' it as a gotcha." Resolved: added a + one-liner under the P4 exit-code table acknowledging the `sysexits.h` alignment. Same sentence added to P2's exit-code + table for consistency. +- **[later]** *Must-vs-should.* "`p4-must-try-parse` names a clap-specific Rust API in a `applicability: universal` + MUST. A Go/Python/Node CLI has no `try_parse()`. The underlying requirement — 'argument-parse failures route through + the same error/output formatter as runtime errors, not a library-internal `process::exit()`' — is universal; the API + name is not." Deferred: language-neutralizing the bullet ("Argument parsing returns a structured error rather than + calling `process::exit()` internally; in Rust+clap, this means `try_parse()` not `parse()`") drifts the frontmatter + summary. Bundled with P6's SIGPIPE and `global = true` rewrites for a coordinated v0.4.0 language-neutralization PR. diff --git a/spec/principles/p5-safe-retries-mutation-boundaries.md b/spec/principles/p5-safe-retries-mutation-boundaries.md new file mode 100644 index 0000000..882769b --- /dev/null +++ b/spec/principles/p5-safe-retries-mutation-boundaries.md @@ -0,0 +1,118 @@ +--- +id: p5 +title: Safe Retries and Explicit Mutation Boundaries +last-revised: 2026-04-22 +status: active +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 with write operations MUST support `--dry-run` so agents can preview a mutation before committing it. Commands +MUST make the read-vs-write distinction visible from name and `--help` alone, and destructive writes MUST require +explicit confirmation. 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 + +Agent harnesses commonly retry failed operations. 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. + +## Pressure test notes + +### 2026-04-27 — Show HN launch red-team pass + +Adversarial review via `compound-engineering:ce-adversarial-document-reviewer` ahead of the v0.3.0 launch. Findings +recorded verbatim per `principles/AGENTS.md` § "Pressure-test protocol". + +- **[edit]** *Internal inconsistency.* "Definition opens 'Every CLI MUST support `--dry-run`' as universal, but + `p5-must-dry-run` is gated on 'CLI has write operations' — read-only CLIs would falsely fail this prose claim." + Resolved: Definition sentence 1 narrowed to "Every CLI with write operations MUST support `--dry-run`..." Read-only + CLIs are no longer falsely accused by the prose. +- **[edit]** *Internal inconsistency.* "Definition's 'Write operations MUST clearly separate destructive actions from + read-only queries' garbles the read/write MUST. The actual requirement is read-vs-write distinction; 'destructive vs + read-only' is a different axis (writes can be non-destructive, e.g., `create`)." Resolved: Definition sentence 2 + rewritten to "Commands MUST make the read-vs-write distinction visible from name and `--help` alone, and destructive + writes MUST require explicit confirmation." The two axes are now stated separately. +- **[later]** *Internal inconsistency.* "`--force`/`--yes` MUST + P1 `--no-interactive` MUST should compose (agent path + is `--force --no-interactive`); composition isn't called out, leaving the 'without it, the tool refuses or enters + dry-run' clause ambiguous when stdin is non-TTY." Deferred: tightening the MUST to specify error-vs-dry-run behavior + under `--no-interactive` modifies the bullet's contract semantics. Bundled with other MUST-content cleanups for a + v0.4.0 PR. +- **[later]** *Must-vs-should.* "`read-write-distinction` MUST hinges on 'clear from command name and help text alone' — + subjective and unverifiable by `anc`. The `sync` anti-pattern proves the bar is taste, not a checkable property." + Deferred: rewriting to a verifiable form ("Help text for every write command MUST contain an explicit mutation + statement; command names SHOULD signal intent") creates a new SHOULD-shape claim, which is a `requirements[]` change. + Coupled-release fires firmly. Defer to v0.4.0 with explicit registry-coordination plan. +- **[later]** *Prior art.* "Principle prescribes flag *names* (`--dry-run`, `--force`, `--yes`) without naming the + contract behind them. kubectl `--dry-run=server|client`, Terraform `plan`/`apply`, apt `--simulate`, rsync `-n` all + satisfy the contract under different surfaces." Deferred: worth revisiting whether to add a + 'name-or-contract-equivalent' clause that names the contract first and treats canonical flag spelling as one + realization. Hold for v0.4.0 alongside the verifiability rewrite above. +- **[wontfix]** *Must-vs-should.* "'Why Agents Need It' leans on retry-safety, then idempotency lands as SHOULD. If + retries are the framing, idempotency-where-domain-allows is the load-bearing property; `--dry-run` is mitigation, not + cure." Rationale: domain-gated idempotency genuinely cannot be a universal MUST (some domains forbid it: append-only + logs, payment capture). The current SHOULD is correct; the prose framing in "Why Agents Need It" is fine because it + explains *why* idempotency matters when it is available, not that it is universally required. +- **[edit]** *Vague agent-native.* "'Agents retry failed operations by default' — true for Claude Code/Cursor/Aider tool + loops; not universally true for one-shot harnesses or human-in-the-loop agents." Resolved: "Why Agents Need It" hedged + to "Agent harnesses commonly retry failed operations." Same operational point; more accurate across harness shapes. diff --git a/spec/principles/p6-composable-predictable-command-structure.md b/spec/principles/p6-composable-predictable-command-structure.md new file mode 100644 index 0000000..0448d8e --- /dev/null +++ b/spec/principles/p6-composable-predictable-command-structure.md @@ -0,0 +1,161 @@ +--- +id: p6 +title: Composable and Predictable Command Structure +last-revised: 2026-04-22 +status: active +requirements: + - id: p6-must-sigpipe + level: must + applicability: universal + summary: SIGPIPE is handled so piping to `head`/`tail` does not crash the process (Rust example below; Python/Go/Node have language-specific equivalents). + - 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`) propagate to every subcommand (e.g., `global = true` in clap). + - 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:** + +- SIGPIPE is handled so that piping to `head`, `tail`, or any tool that closes the pipe early does not crash the + process. In Rust, restore the default SIGPIPE handler as the first executable statement in `main()`: + + ```rust + unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL); } + ``` + + Equivalents in other languages: Python — restore the default `SIGPIPE` handler at startup + (`signal.signal(signal.SIGPIPE, signal.SIG_DFL)`); Go — the runtime's default handling already exits cleanly on + EPIPE writes; Node.js — handle `EPIPE` on `process.stdout`. + +- 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`) propagate to + every subcommand automatically (e.g., `global = true` in clap). + +**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. + +## Pressure test notes + +### 2026-04-27 — Show HN launch red-team pass + +Adversarial review via `compound-engineering:ce-adversarial-document-reviewer` ahead of the v0.3.0 launch. Findings +recorded verbatim per `principles/AGENTS.md` § "Pressure-test protocol". + +- **[edit]** *Prior art / vague agent-native.* "The SIGPIPE MUST prescribes `unsafe { libc::signal(libc::SIGPIPE, + libc::SIG_DFL); }` as the first `main()` statement — that is a Rust-specific remedy. Python raises `BrokenPipeError` + by default (different fix), Go's runtime already exits cleanly on EPIPE writes (no fix needed), Node.js needs + `process.stdout.on('error')`. The MUST as written is correct in spirit but the prescription leaks Rust into a + universal-applicability rule." Resolved: prose bullet now leads with the language-neutral MUST ("SIGPIPE is handled so + that piping to `head`, `tail`, or any tool that closes the pipe early does not crash the process"); the Rust snippet + stays as the canonical example; per-language one-liners cover Python, Go, and Node. Frontmatter summary updated to + match. +- **[edit]** *Must-vs-should.* "The `global = true` MUST is a clap-API artifact — the behavioral requirement is 'agentic + flags propagate to every subcommand,' which is what the prose actually says. The frontmatter summary baking `global = + true` into a universal contract overfits to one library." Resolved: frontmatter summary and prose bullet now lead with + the behavioral requirement ("propagate to every subcommand"), with `global = true` cited as the clap-specific example. + Behavior unchanged; language-neutrality restored. diff --git a/spec/principles/p7-bounded-high-signal-responses.md b/spec/principles/p7-bounded-high-signal-responses.md new file mode 100644 index 0000000..a77da84 --- /dev/null +++ b/spec/principles/p7-bounded-high-signal-responses.md @@ -0,0 +1,133 @@ +--- +id: p7 +title: Bounded, High-Signal Responses +last-revised: 2026-04-22 +status: active +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 + +Unbounded CLI output is expensive for any agent — token cost and context-window capacity for LLM agents, parse cost and +memory pressure for scripts, schedulers, and other automation. Either way, the agent ends up truncating (losing +potentially important data) or consuming the full response (wasting cycles 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. + +## Pressure test notes + +### 2026-04-27 — Show HN launch red-team pass + +Adversarial review via `compound-engineering:ce-adversarial-document-reviewer` ahead of the v0.3.0 launch. Findings +recorded verbatim per `principles/AGENTS.md` § "Pressure-test protocol". + +- **[later]** *Internal inconsistency.* "`--timeout` is universal SHOULD in P7 but conditional MUST in P6 + (`p6-must-timeout-network`). For network CLIs the two compose (MUST wins), but P7's prose ('An agent waiting + indefinitely on a hung network call cannot proceed') only motivates the network case — the universal scope is + unjustified by its own rationale." Deferred: narrowing P7's `applicability` from `universal` to non-network + long-running operations only — or to `if: CLI has long-running operations` — fires the coupled-release norm (CLI + registry parses `applicability`). Bundled with other applicability cleanups for a v0.4.0 PR with explicit registry + coordination. +- **[later]** *Must-vs-should.* "The list-clamping MUST fires on every CLI with 'list-style commands' regardless of + natural cardinality. A tool whose list operation returns a bounded small set by construction (e.g., `anc principles + list` → exactly 7) gains nothing from a clamp + `\"truncated\": true` contract — the clamp is unreachable and the + truncation flag is dead schema." Deferred: narrowing the `if:` clause from "CLI has list-style commands" to "CLI has + list-style commands whose result set is unbounded or user-data-driven" changes the registry-parsed applicability + value. Bundled with the P3 / P7 applicability cleanups for v0.4.0. +- **[edit]** *Vague agent-native.* "'Every token of CLI output an agent consumes has a cost' plus 'context window' + framing dates the principle to current LLM-agent assumptions. Non-LLM agents (scripts, schedulers, future + architectures) still benefit from bounded output, but for throughput/parsing reasons, not tokens." Resolved: "Why + Agents Need It" generalized to acknowledge both costs ("token cost and context-window capacity for LLM agents, parse + cost and memory pressure for scripts, schedulers, and other automation"). Keeps the LLM example without making it the + sole rationale.