Skip to content

Commit 05e74ca

Browse files
authored
feat(spec): sync v0.4.0 — add 11 requirement IDs across P1/P2/P4/P6/P8 (#50)
## Summary v0.4.0 spec sync companion. Ships live check implementations for 11 new requirement IDs across P1, P2, P4, P6, and the brand-new P8 (skill bundle discoverability), suppresses `p6-must-sigterm` under `--audit-profile human-tui` to mirror `p6-sigpipe`'s rationale, and bumps the CLI from 0.3.1 to 0.4.0. ## Changelog ### Added - Add P1 secret-handling check (`p1-must-secret-non-leaky-path`): scans target CLIs' `--help` for secret-bearing flag families (`--token`, `--password`, `--api-key`, `--secret`, `--auth`, `--credential`) and verifies each has either a `--*-file` companion or stdin path advertised. Vacuous Pass when no secret-bearing flag is detected. - Add P2 schema trio (`p2-must-schema-print`, `p2-should-schema-file`, `p2-should-json-aliases`): runtime-discoverable output schema via `schema` subcommand or `--schema` flag, file-export of schemas (`schema/*.json`, `*.schema.json` at repo root), and `--json` / `--jsonl` short aliases for `--output`. - Add P4 closed-set rejection check (`p4-should-enumerate-valid-set`, Rust + Python): detects clap `ValueEnum`, `PossibleValuesParser`, `value_parser!`, and Python `argparse.choices=` / `click.Choice()`. - Add P6 lifecycle and naming checks (`p6-must-sigterm`, Rust + Python; `p6-may-standard-names`): SIGTERM-handler detection across `signal_hook`, `tokio::signal::unix`, `signal.signal`, and `loop.add_signal_handler`; community-standard-verb allow-list applied to top-level subcommands. - Add P8 skill-bundle suite (`p8-should-bundle-exists`, `p8-must-bundle-install`, `p8-may-install-all`, `p8-may-bundle-update`): repo-root detection of `AGENTS.md` / `SKILL.md` with YAML frontmatter, plus help-surface probes for `skill install`, `skill install --all`, and `skill update` / `skill upgrade`. Brand-new principle in the registry. ### Changed - Bump CLI from 0.3.1 to 0.4.0 (MINOR; meaningful coverage growth across five principles, including a brand-new principle). ### Documentation - Document prose-scrubbing runbook in `RELEASES.md` for release-flow artifacts (PR bodies, `CHANGELOG.md`, release-PR bodies) using Vale + LanguageTool + unslop. - Add `## PR body` section to `RELEASES.md` codifying what belongs in PR bodies (NEW user-facing substance, six required template sections) and what does not (workflow recap, triple-diff output, pre-push gate results, CI status, AI attribution). ## Type of Change - [x] `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: agentnative-spec v0.4.0 (brettdavies/agentnative#25, brettdavies/agentnative#26) - Issue: n/a - Architecture: docs/plans/2026-05-07-001-feat-v0.4.0-spec-sync-plan.md - Related PRs: docs/plans/2026-05-07-002-feat-prose-tooling-import-plan.md (sibling, no governance deadline) ## Files Modified **Modified:** - `Cargo.toml`, `Cargo.lock`: version 0.3.1 to 0.4.0 - `RELEASES.md`: prose-scrubbing runbook + new `## PR body` section - `docs/coverage-matrix.md`, `coverage/matrix.json`: regenerated for 57 requirements - `src/principles/spec/**`: vendored from agentnative-spec v0.4.0 - `src/principles/registry.rs`: counter bumps, principle range to `1..=8`, `p6-sigterm` in `HumanTui` suppression - `src/types.rs`, `src/scorecard/mod.rs`: `CheckGroup::P8` variant + label/order - `src/checks/{behavioral,project,source/{rust,python}}/mod.rs`: register the 13 new check files - `tests/build_parser.rs`: integration test pin updated for v0.4.0 / 57 requirements **Created:** - `src/principles/spec/principles/p8-discoverable-skill-bundle.md` (vendored) - `src/checks/behavioral/secret_non_leaky_path.rs` (P1) - `src/checks/source/{rust,python}/enumerate_valid_set.rs` (P4) - `src/checks/behavioral/{schema_print,json_aliases}.rs`, `src/checks/project/schema_file.rs` (P2) - `src/checks/source/{rust,python}/sigterm.rs`, `src/checks/behavioral/standard_names.rs` (P6) - `src/checks/project/bundle_exists.rs`, `src/checks/behavioral/{bundle_install,install_all,bundle_update}.rs` (P8) **Renamed:** - None. **Deleted:** - None. ## Testing - [x] Unit tests added/updated - [x] Integration tests added/updated - [x] Manual testing completed (dogfood `anc check .`) - [x] All tests passing **Test Summary:** 495 unit tests pass; 51 integration tests pass (including the spec-version drift sentry, the `convention_check_result_constructed_only_in_run_body` rule, the `dangling_cover_ids` detector, and the matrix artifact drift gate); clippy `-Dwarnings` clean; `anc generate coverage-matrix --check` exits 0.
1 parent 86fd96e commit 05e74ca

46 files changed

Lines changed: 4820 additions & 206 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "agentnative"
3-
version = "0.3.1"
3+
version = "0.4.0"
44
edition = "2024"
55
description = "The agent-native CLI linter — check whether your CLI follows agent-readiness principles"
66
license = "MIT OR Apache-2.0"

RELEASES.md

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Releasing `agentnative`
22

3-
Every change reaches production via this pipeline. Direct commits to `dev` or `main` are not permitted — every change
4-
has a PR number in its squash commit message, which keeps the history scannable, attributable, and changelog-ready.
3+
Every change reaches production via this pipeline. Direct commits to `dev` or `main` are not permitted. Every change has
4+
a PR number in its squash commit message, which keeps the history scannable, attributable, and changelog-ready.
55

66
```text
77
feature branch → PR to dev (squash merge)
@@ -16,12 +16,12 @@ feature branch → PR to dev (squash merge)
1616
| -------------------------------------- | --------------------------------------- | ------------------------------------------- | ------------------------------------ |
1717
| `main` | Production. Only release commits. | Forever. | `.github/rulesets/protect-main.json` |
1818
| `dev` | Integration. All feature PRs land here. | Forever. Never delete. | `.github/rulesets/protect-dev.json` |
19-
| `feat/*`, `fix/*`, `chore/*`, `docs/*` | Feature work. | One PR's worth. Auto-deleted on merge. | None — squash into dev freely. |
19+
| `feat/*`, `fix/*`, `chore/*`, `docs/*` | Feature work. | One PR's worth. Auto-deleted on merge. | None. Squash into dev freely. |
2020
| `release/*` | Head of a dev → main PR. | One release's worth. Auto-deleted on merge. | None. |
2121

2222
`dev` is a **forever branch**. Never delete it locally or remotely, even after a `release/* → main` merge. The next
2323
release cycle reuses the same `dev`. The repo's `deleteBranchOnMerge: true` setting doesn't touch `dev` as long as `dev`
24-
is never the head of a PR — using a short-lived `release/*` head is what keeps the setting compatible with a forever
24+
is never the head of a PR. Using a short-lived `release/*` head is what keeps the setting compatible with a forever
2525
integration branch.
2626

2727
## Daily development (feature → dev)
@@ -38,6 +38,47 @@ gh pr create --base dev --title "feat(scope): what changed"
3838
- **Commit style**: [Conventional Commits](https://www.conventionalcommits.org/).
3939
- **PR body**: follow `.github/pull_request_template.md`. The `## Changelog` section is the source of truth for
4040
user-facing release notes — `git-cliff` extracts these bullets verbatim into `CHANGELOG.md` during release prep.
41+
- **PR body prose scrub**: `gh pr create` and `gh pr edit` send body text directly to GitHub; no automated prose check
42+
sees it. Save the body to `/tmp/`, run Vale + LanguageTool + unslop, fix findings, then submit via `--body-file`. See
43+
[§ Prose scrubbing](#prose-scrubbing).
44+
45+
## PR body
46+
47+
Every PR — feature, fix, docs, release — uses `.github/pull_request_template.md` verbatim. Six sections, no inventions:
48+
`## Summary`, `## Changelog`, `## Type of Change`, `## Related Issues/Stories`, `## Files Modified`, `## Testing`.
49+
50+
- **Summary** is the NEW user-facing substance the PR ships. What is changing for the consumer that was not already
51+
there. One short paragraph fits. Do NOT recap the workflow (cherry-pick / regenerate / pre-push gate / CI behavior is
52+
documented in this file and `.github/`). Do NOT paste triple-diff output, pre-push gate results, or CI check status
53+
into the body. Those are author verification artifacts that stay local; anomalies get fixed before push, not
54+
audit-trailed in the body.
55+
- **Changelog** subsections (`### Added` / `### Changed` / `### Fixed` / `### Documentation`) hold the user-facing
56+
entries. The template's RULES (in the HTML comment at the top of the section) are literal: 1-5 bullets, delete empty
57+
subsections entirely, each bullet starts with a verb. Prose-only edits leave the section empty or omit it.
58+
- **Type of Change** is one checkbox. Prefer `feat` / `fix` over `chore` when the change has any user-observable effect
59+
(config defaults, env vars, default behaviors). `cliff.toml` skips `^chore` (and `^style` / `^test` / `^ci` /
60+
`^build`) regardless of body content; mistyping a user-facing change as `chore` silently strips it from release notes.
61+
- **Related Issues/Stories** has four labels (`Story:` / `Issue:` / `Architecture:` / `Related PRs:`). All four are
62+
required even when empty — write `- None.` or `n/a` rather than deleting the label.
63+
- **Files Modified** has four sub-headers (`**Modified:**` / `**Created:**` / `**Renamed:**` / `**Deleted:**`). All four
64+
are required even when empty — `Renamed: None.` / `Deleted: None.`
65+
- **Internal tooling commits** (`chore(cliff): ...`, `chore(prose-check): ...`, etc.) do NOT appear in the PR body's `##
66+
Changelog`. They are not user-facing.
67+
- **Release PRs** repeat the entries from the upstream feature PRs they cherry-pick. The repetition is intentional and
68+
harmless: `cliff.toml`'s `^release` skip prevents the release-PR squash commit from being double-counted in any future
69+
regeneration.
70+
- **No AI attribution.** Never append `Co-Authored-By: Claude …`, `🤖 Generated with [Claude Code]`, or any similar
71+
AI-attribution trailer to PR bodies or commit messages. Commits and PRs stand on their own technical content.
72+
- **No hard line wraps.** Author each paragraph and each bullet as one logical line, however long. GitHub soft-wraps for
73+
display; hard wraps within prose produce visible mid-sentence breaks in some renderers and interfere with the
74+
prose-check pipeline (Vale's line-anchored output reports findings against split lines, LanguageTool's input handling
75+
can choke on certain control-char interactions). The auto-format hook skips `/tmp/` paths so the body keeps its
76+
authored shape — don't undo that with manual wrapping during composition. The same rule applies to commit messages
77+
composed via heredoc and to any markdown that ships verbatim to GitHub.
78+
79+
The PR body is read by humans reviewing what shipped. Workflow mechanics, verification output, and tool-fix provenance
80+
are noise from that perspective; they belong in this file (`RELEASES.md`), the script outputs, and the commit history
81+
respectively.
4182

4283
## Releasing dev to main
4384

@@ -139,7 +180,12 @@ git add src/skill_install/skill.json && \
139180
./scripts/generate-changelog.sh
140181

141182
# 9. Review CHANGELOG.md. See "CHANGELOG is generated, never hand-written" below
142-
# for the cliff.toml chore-skip footgun and how to recover. When clean, commit:
183+
# for the cliff.toml chore-skip footgun and how to recover. Then scrub the
184+
# generated content through Vale + LanguageTool + unslop — CHANGELOG.md is a
185+
# generated artifact built from upstream PR bodies and inherits whatever prose
186+
# those PR bodies carry. See "Prose scrubbing" below for the procedure. Fix
187+
# findings on the upstream PR body and re-run scripts/generate-changelog.sh,
188+
# not by hand-editing CHANGELOG.md. When clean, commit:
143189
git add CHANGELOG.md && git commit -m "docs: update CHANGELOG.md for v0.2.0"
144190

145191
# 10. Push and open the PR:
@@ -253,6 +299,52 @@ A PR that has no user-facing impact (pure refactor, test-only, CI-only) should l
253299
omit it. See "CHANGELOG is generated, never hand-written" above for how the script consumes these sections at release
254300
time and the cliff.toml chore-skip footgun.
255301

302+
## Prose scrubbing
303+
304+
Three release-flow artifacts live outside any automated prose check and need a manual scrub before they ship:
305+
306+
- **PR bodies.** `gh pr create` and `gh pr edit` send body text directly to GitHub; no automated prose check has reach
307+
there.
308+
- **`CHANGELOG.md`.** A generated artifact built from upstream PR bodies — it inherits whatever prose those PR bodies
309+
carry, so scrubbing happens at generation time on the release branch.
310+
- **Release-PR bodies.** The `release/v<version>` PR to `main` gets wrap-up text contributors edit after `CHANGELOG.md`
311+
has been generated, and the same out-of-repo gap applies.
312+
313+
The canonical Vale + LanguageTool rule packs and orchestrator behavior live in the spec repo at
314+
[`~/dev/agentnative-spec/docs/architecture/voice-enforcement.md`](../agentnative-spec/docs/architecture/voice-enforcement.md).
315+
Until those packs are vendored into this repo (a deferred follow-up tracked in the spec plan; expected to extend
316+
`scripts/sync-spec.sh`), point Vale at the spec checkout via `--config`.
317+
318+
The scrub procedure:
319+
320+
```bash
321+
# 1. Save the artifact to /tmp/. The auto-format hook skips /tmp paths, so the
322+
# body keeps its authored shape and no soft-wrapping is injected.
323+
gh pr view <num> --json body --jq .body > /tmp/body.md # for PR body edits
324+
# cp CHANGELOG.md /tmp/body.md # for changelog scrub
325+
326+
# 2. Vale (against the spec's rule packs — until vendored locally, point at the spec checkout).
327+
vale --no-global --config ~/dev/agentnative-spec/.vale.ini --output=line --minAlertLevel=error /tmp/body.md
328+
329+
# 3. LanguageTool (blocking categories: TYPOS|GRAMMAR|CONFUSED_WORDS, mirrors the orchestrator's whitelist).
330+
curl -sS -X POST "${LANGUAGETOOL_URL:-http://pool.tail42ba87.ts.net:8081}/v2/check" \
331+
--data-urlencode "language=en-US" --data-urlencode "text@/tmp/body.md" \
332+
| jaq '.matches[] | select(.rule.category.id | test("^(TYPOS|GRAMMAR|CONFUSED_WORDS)$"))'
333+
334+
# 4. unslop (em-dash density and AI-unique structural patterns Vale + LT do not catch).
335+
~/.claude/skills/unslop/scripts/score.py /tmp/body.md
336+
337+
# 5. Apply fixes per finding. Re-run until 0 blocking and unslop score is 0.
338+
339+
# 6. Apply the cleaned version:
340+
gh pr edit <num> --body-file /tmp/body.md # for PR body edits
341+
# ./scripts/generate-changelog.sh # for CHANGELOG.md (re-runs the
342+
# # PR-body fetch from GitHub)
343+
```
344+
345+
For a `CHANGELOG.md` finding, fix the upstream PR body (which `generate-changelog.sh` re-fetches every run) and
346+
regenerate. Hand-editing `CHANGELOG.md` directly produces drift the next regeneration overwrites.
347+
256348
## Branch protection
257349

258350
Two rulesets are committed under `.github/rulesets/` and applied to the repo via the GitHub API:

completions/anc.bash

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ _anc() {
2828
anc,help)
2929
cmd="anc__help"
3030
;;
31+
anc,skill)
32+
cmd="anc__skill"
33+
;;
3134
anc__generate,coverage-matrix)
3235
cmd="anc__generate__coverage__matrix"
3336
;;
@@ -52,17 +55,35 @@ _anc() {
5255
anc__help,help)
5356
cmd="anc__help__help"
5457
;;
58+
anc__help,skill)
59+
cmd="anc__help__skill"
60+
;;
5561
anc__help__generate,coverage-matrix)
5662
cmd="anc__help__generate__coverage__matrix"
5763
;;
64+
anc__help__skill,install)
65+
cmd="anc__help__skill__install"
66+
;;
67+
anc__skill,help)
68+
cmd="anc__skill__help"
69+
;;
70+
anc__skill,install)
71+
cmd="anc__skill__install"
72+
;;
73+
anc__skill__help,help)
74+
cmd="anc__skill__help__help"
75+
;;
76+
anc__skill__help,install)
77+
cmd="anc__skill__help__install"
78+
;;
5879
*)
5980
;;
6081
esac
6182
done
6283

6384
case "${cmd}" in
6485
anc)
65-
opts="-q -h -V --quiet --help --version check completions generate help"
86+
opts="-q -h -V --quiet --json --help --version check completions generate skill help"
6687
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
6788
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
6889
return 0
@@ -76,7 +97,7 @@ _anc() {
7697
return 0
7798
;;
7899
anc__check)
79-
opts="-q -h --command --binary --source --principle --output --include-tests --audit-profile --quiet --help [PATH]"
100+
opts="-q -h --command --binary --source --principle --output --include-tests --audit-profile --quiet --json --help [PATH]"
80101
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
81102
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
82103
return 0
@@ -106,7 +127,7 @@ _anc() {
106127
return 0
107128
;;
108129
anc__completions)
109-
opts="-q -h --quiet --help bash elvish fish powershell zsh"
130+
opts="-q -h --quiet --json --help bash elvish fish powershell zsh"
110131
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
111132
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
112133
return 0
@@ -120,7 +141,7 @@ _anc() {
120141
return 0
121142
;;
122143
anc__generate)
123-
opts="-q -h --quiet --help coverage-matrix help"
144+
opts="-q -h --quiet --json --help coverage-matrix help"
124145
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
125146
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
126147
return 0
@@ -134,7 +155,7 @@ _anc() {
134155
return 0
135156
;;
136157
anc__generate__coverage__matrix)
137-
opts="-q -h --out --json-out --check --quiet --help"
158+
opts="-q -h --out --json-out --check --quiet --json --help"
138159
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
139160
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
140161
return 0
@@ -198,7 +219,7 @@ _anc() {
198219
return 0
199220
;;
200221
anc__help)
201-
opts="check completions generate help"
222+
opts="check completions generate skill help"
202223
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
203224
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
204225
return 0
@@ -281,6 +302,108 @@ _anc() {
281302
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
282303
return 0
283304
;;
305+
anc__help__skill)
306+
opts="install"
307+
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
308+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
309+
return 0
310+
fi
311+
case "${prev}" in
312+
*)
313+
COMPREPLY=()
314+
;;
315+
esac
316+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
317+
return 0
318+
;;
319+
anc__help__skill__install)
320+
opts=""
321+
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
322+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
323+
return 0
324+
fi
325+
case "${prev}" in
326+
*)
327+
COMPREPLY=()
328+
;;
329+
esac
330+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
331+
return 0
332+
;;
333+
anc__skill)
334+
opts="-q -h --quiet --json --help install help"
335+
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
336+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
337+
return 0
338+
fi
339+
case "${prev}" in
340+
*)
341+
COMPREPLY=()
342+
;;
343+
esac
344+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
345+
return 0
346+
;;
347+
anc__skill__help)
348+
opts="install help"
349+
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
350+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
351+
return 0
352+
fi
353+
case "${prev}" in
354+
*)
355+
COMPREPLY=()
356+
;;
357+
esac
358+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
359+
return 0
360+
;;
361+
anc__skill__help__help)
362+
opts=""
363+
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
364+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
365+
return 0
366+
fi
367+
case "${prev}" in
368+
*)
369+
COMPREPLY=()
370+
;;
371+
esac
372+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
373+
return 0
374+
;;
375+
anc__skill__help__install)
376+
opts=""
377+
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
378+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
379+
return 0
380+
fi
381+
case "${prev}" in
382+
*)
383+
COMPREPLY=()
384+
;;
385+
esac
386+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
387+
return 0
388+
;;
389+
anc__skill__install)
390+
opts="-q -h --dry-run --output --quiet --json --help claude_code codex cursor factory kiro opencode"
391+
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
392+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
393+
return 0
394+
fi
395+
case "${prev}" in
396+
--output)
397+
COMPREPLY=($(compgen -W "text json" -- "${cur}"))
398+
return 0
399+
;;
400+
*)
401+
COMPREPLY=()
402+
;;
403+
esac
404+
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
405+
return 0
406+
;;
284407
esac
285408
}
286409

0 commit comments

Comments
 (0)