diff --git a/.claude/skills/release-notes/SKILL.md b/.claude/skills/release-notes/SKILL.md new file mode 100644 index 00000000..f299bf1d --- /dev/null +++ b/.claude/skills/release-notes/SKILL.md @@ -0,0 +1,95 @@ +--- +name: release-notes +description: Turn the generated release notes into the ones a release publishes. Generates the entries with script/release-notes, then merges entries that describe one change, drops work that never shipped and internal changes, rewrites entries into one consistent voice, and writes the Highlights and Upgrading sections, with an audit that accounts for every generated entry. Use before tagging a release, or when asked to draft, clean up or review release notes. +--- + +# Release notes + +`script/release-notes` turns commit subjects into a change list. That list +is the input, not the release notes: it repeats one feature across the +commits that built it, lists fixes to code no release ever had, and mixes +internal work with what an admin notices. This skill is the pass between +the two. `docs/agent/dev.md` ("Releases") is the gate it sits in. + +## 1. Generate + +```bash +prev=$(git describe --tags --abbrev=0 --match 'v*') +script/release-notes "$prev..HEAD" > /tmp/generated.md +git log --reverse --format='%h %s%n%b%n---' "$prev..HEAD" > /tmp/commits.txt +``` + +The commit bodies are the source for any entry the subject does not +explain. Read the body before rewriting an entry, never guess from the +subject. + +## 2. Sort every generated entry + +Each entry ends in exactly one of four places: + +- **Keep**: an admin or a family member notices it, and it did not exist + in `$prev`. +- **Merge into **: it describes the same change as another entry + (a feature and its follow-ups, the commits of one PR, a feature and the + fixes to it made before the release). The merged entry says what is + true at the tag. +- **Drop, unreleased**: it changes or fixes something that did not exist + in `$prev`. Check with `git show "$prev:"` or by finding the commit + that introduced the thing. A rename of an unreleased command is not an + entry; the command appears once, under its final name. +- **Drop, internal**: nobody running famstack notices it (metrics, cache + keys, test tooling, refactors that slipped into a shown type). + +Action required entries are never dropped for being internal. Check each +against the rule in dev.md: breaking is measured against `$prev`. + +## 3. Write + +``` +## Highlights + +<3 to 5 short paragraphs: what this release makes possible, for the admin +and the family, in plain words. The most visible change first.> + +## Upgrading from + + + +### Security +### Added +### Fixed +### Documentation + +- **:** (#PR) +``` + +Entries: + +- One line, at most about 100 characters, starting with a verb in the + imperative, like the commit subjects: "Choose the AI server with + `./stack ai switch`". +- The admin's words: commands, rooms and pages they see. No function, + class, file or module names. +- Commands and paths in backticks. Keep the `(#N)` of every PR the entry + covers. +- No em dashes. No "now", "new" or "improved": every entry is new. +- Group by the rendered scope, as generated. Sort entries within a group + by how much an admin notices them. +- A section with no entries is left out. + +Plain, factual prose, as AGENTS.md asks of every text here: this one is +published on GitHub and on famstack.dev. + +## 4. Audit + +Next to the notes, never inside them, list every generated entry with its +fate: kept as is, rewritten as, merged into, or dropped (unreleased or +internal), with the reason. Nothing may be missing from the audit: the +count of audit lines equals the count of generated entries. + +## Output + +The notes as markdown, ready for `gh release create --notes-file`, and the +audit. Publishing is the maintainer's: the release gate in dev.md, step 6. diff --git a/.gitignore b/.gitignore index 0c491ee3..a94b1dcb 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,8 @@ tests/e2e/remote-results/ # AI assistant. Local settings and transcripts stay out, but the LSP plugin # is repo configuration: it makes agents read this codebase with a language # server instead of grep, so every clone should get it. The same goes for -# the pr skill, which applies this repo's pull request rules. +# the pr and release-notes skills, which apply this repo's pull request and +# release rules. # Git cannot re-include a file whose parent directory is excluded, so these # ignore directory *contents* and re-open each path that is tracked. .claude/* @@ -60,6 +61,7 @@ tests/e2e/remote-results/ .claude/skills/* !.claude/skills/famstack-lsp/ !.claude/skills/pr/ +!.claude/skills/release-notes/ # Extension stacklets live in ~/-extensions, not here. This is # the obvious place to try anyway, so keep anything dropped here out of diff --git a/docs/agent/dev.md b/docs/agent/dev.md index 172cad3c..a2ebf140 100644 --- a/docs/agent/dev.md +++ b/docs/agent/dev.md @@ -488,7 +488,8 @@ Pre-tag gate, in order. A published tag is never moved; anything missed here shi 3b. Every commit since the previous tag parses as a changelog entry. An unclassified subject blocks the tag: it would be missing from the release notes and from the website. Reword it if it has not shipped, add the entry by hand if it has. `script/release-notes` shows such a subject under Unclassified. 4. Fresh-instance install verified. 5. Stale references updated: README version callouts, docs links, blog "Try it" instructions. -6. Tag (`vX.Y.Z` / `vX.Y.Z-beta.N`, annotated), push main + tag, publish the GitHub release with Highlights and an "Upgrading from" section, followed by the change list from `script/release-notes --latest`. +6. Write the notes with the `release-notes` skill (`.claude/skills/release-notes/`): it generates the change list with `script/release-notes`, merges and drops entries, and writes Highlights and an "Upgrading from" section, with an audit that accounts for every generated entry. The generated list is its input, never the published text. +7. Tag (`vX.Y.Z` / `vX.Y.Z-beta.N`, annotated), push main + tag, publish the GitHub release with those notes. **The tag format is load-bearing.** `stack update` parses `v?MAJOR.MINOR.PATCH[-label.N]` and ignores anything else, so a tag spelled `v0.3.0.beta1` is invisible: `stack update` would keep offering the previous release and never mention it. Hyphen before the label, dot before its number, as SemVer has it. The prerelease sorts below the release it leads to, and label numbers compare as numbers, so `beta.10` is newer than `beta.9`.