Generate CHANGELOG entries
You are tasked with regenerating the ## [Unreleased] section of every affected CHANGELOG.md in the repository so it reflects all change since the last release tag — committed and uncommitted alike.
Input
$ARGUMENTS — optional --since <ref> flag. Empty/literal → range starts at last_tag: from the Metadata block.
Metadata
node "${SKILL_DIR}/../_shared/changelog-bootstrap.mjs"
in_repo: — yes or no. Used by Step 1.1.
last_tag: — last release tag, or (no tags). Used by Step 1.3 and Step 2.1 when no --since is supplied.
---changelogs--- block — paths of every tracked CHANGELOG.md (one per line, empty if none). Used by Step 1.2.
Workflow
- Bail-out checks
- Determine the change range
- Determine each CHANGELOG's scope and collect commits + uncommitted hunks
- Classify and draft entries
- Preview and confirm
- Apply
Step 1: Bail-out checks
- If
in_repo: is no, tell the user "This directory is not a git repository." and stop.
- If the
---changelogs--- block is empty, tell the user "No CHANGELOG.md found in the repository — create one (root or per-package) before running this skill." and stop.
- If
last_tag: is (no tags) AND $ARGUMENTS lacks --since <ref>, ask the user to supply --since <ref> and stop until they do.
Step 2: Determine the change range
- Parse the input for a
--since <ref> flag. If absent, use last_tag: from the Metadata block as SINCE.
- The range is
$SINCE..HEAD for committed changes, plus the current uncommitted+staged working tree.
Step 3: Determine each CHANGELOG's scope, then collect commits + uncommitted hunks
Each CHANGELOG.md discovered in Step 1.2 owns a path scope:
- Nested CHANGELOG (e.g.
packages/foo/CHANGELOG.md, apps/web/CHANGELOG.md): scope is its parent directory — packages/foo/, apps/web/.
- Root CHANGELOG (
CHANGELOG.md at repo root):
- If no nested CHANGELOGs exist: scope is the entire repository.
- If nested CHANGELOGs also exist: scope is the repository excluding every directory that owns a nested CHANGELOG. The root file captures repo-wide change (CI, build config, root README) that no per-package file would claim.
For each scope:
- Committed:
git log $SINCE..HEAD --pretty=format:"%H%x09%s%x09%b%x1e" -- <scope>. For root-with-exclusions, pass :(exclude)<dir> pathspecs for every nested-CHANGELOG directory. Records are \x1e-delimited; parse subject (%s) and body (%b).
- Uncommitted:
git diff HEAD -- <scope> and git diff --cached -- <scope> with the same pathspec rules. Treat the union as a single virtual "pending" change set with no commit message — the model classifies it from the diff itself.
- Skip CHANGELOGs whose scope has no committed and no uncommitted changes in range.
Step 4: Classify and draft entries
For each affected CHANGELOG, produce entries grouped under the Keep a Changelog 1.1.0 sections, in this order: Added, Changed, Deprecated, Removed, Fixed, Security, Performance. Append a Breaking / Upgrade Notes section only when a breaking change exists.
Conventional Commit → section mapping
feat: → Added
fix: → Fixed
perf: → Performance
refactor:, style:, build:, ci:, chore: → Changed
docs: → Changed (only if user-facing docs; skip internal thoughts/ or research notes)
test: → omit (not user-visible)
revert: → Changed (note what was reverted)
Always-skip commits
Skip any commit whose subject matches one of these — they are release pipeline housekeeping, not user-visible change:
Release v<x.y.z> or chore(release): v<x.y.z> (common release-bot patterns)
Add [Unreleased] section for next cycle
- Version-only bumps with no other content (
<x.y.z> as the entire subject)
- Merge commits with no diff content of their own
Breaking change detection
Flag a commit as breaking if any of these are true:
- The type carries an exclamation suffix (
feat!:, refactor!:, etc.)
- The commit body contains a
BREAKING CHANGE: footer
- The diff removes or renames an exported symbol, removes a CLI flag, or removes a public file
For each breaking change, add an entry to Breaking / Upgrade Notes in addition to the regular section, written as a one-line upgrade instruction.
Style rules — match Keep a Changelog 1.1.0 prose
- One short user-facing sentence per entry. Imperative mood ("Add", "Fix", "Remove").
- Write for the plugin's users, not its maintainers. No internal symbol names, file paths, regex literals, or precedent commit hashes inside entries.
- If a feature has a user-visible name (a slash command, a CLI flag, a skill name), name it in backticks. Example:
Added `--locale` flag for per-invocation language override.
- Group entries by category, not by commit. Merge duplicate-topic commits into one entry.
- If a commit reverses something earlier in the same
[Unreleased] window (e.g. add → remove → add-back), reflect only the net effect.
- Skip entries that have zero user-visible impact: dependency bumps with no behavior change, internal refactors invisible to users, test additions, type-only changes.
Worked example
Input commits in packages/api/:
abc1234 feat(api): add /v2/search endpoint with cursor pagination
def5678 feat(api): support webhook retries with exponential backoff
ghi9abc fix(api): rotate session secret on every JWT refresh
jkl0def docs(api): document rate-limit headers in OpenAPI spec
mno1234 chore(deps): bump @types/node to 20.11
pqr5678 test(api): coverage for cursor edge cases
stu9abc refactor(api): inline httpClient factory (no behavior change)
Output [Unreleased]:
## [Unreleased]
### Added
- `/v2/search` endpoint with cursor-based pagination.
- Webhook delivery retries with exponential backoff.
### Changed
- OpenAPI spec documents rate-limit response headers.
### Fixed
- JWT refresh rotates the session secret on every renewal.
What this example demonstrates:
- Two
feat: commits → two Added entries (one per user-visible feature).
docs: for a user-facing API spec → Changed (skip if the docs touched were internal notes).
fix: → Fixed, written as the corrected behavior in imperative mood, not as the bug.
chore(deps): with no behavior change → omitted.
test: → omitted (not user-visible).
refactor: flagged "no behavior change" → omitted (the rule is user-visible impact, not commit type).
- Commit hashes never appear in entries.
Step 5: Preview and confirm
- Print a per-CHANGELOG summary: file path, count by section, breaking-change flag.
- Print the proposed
[Unreleased] body for each affected CHANGELOG, in full.
- Call
ask_user_question:
- Question: "Apply regenerated
[Unreleased] to {N} CHANGELOG(s)?"
Header is capped at ≤16 characters (MAX_HEADER_LENGTH = 16 — longer values are rejected).
- Header: "Changelog"
- Options:
- "Apply (Recommended)" — Write the regenerated sections to disk. Refinement, if needed, happens afterward in normal chat (
Edit tool) or via git restore to roll back.
- "Show Preview" — For each affected CHANGELOG, render a unified diff between the current
[Unreleased] body on disk and the proposed regenerated body. Lines marked - are about to be removed; lines marked + are about to be added. After printing, re-ask this same question.
Step 6: Apply
For each affected CHANGELOG:
- Read the file.
- Locate the
## [Unreleased] heading. The block runs from that heading up to (but not including) the next ## [ heading — or end of file if no later version exists. If no ## [Unreleased] heading exists, insert one above the first ## [ heading (or after the file's intro prose if no version sections exist yet).
- Use
Edit to replace the entire block with ## [Unreleased]\n\n followed by the regenerated sections.
- Never touch any heading below
[Unreleased]. Released version sections are immutable.
After all writes complete, print the list of modified files and remind the user to commit them before invoking their release pipeline — most release scripts require a clean working tree.
Important Notes
- ALWAYS preview before writing. Never apply without the user's
ask_user_question confirmation.
- ALWAYS replace the full
[Unreleased] body, not append. The skill is idempotent regeneration, not accumulation.
- NEVER modify released version sections (anything below the first
## [x.y.z] heading).
- NEVER write Conventional Commit prefixes (
feat:, fix:, etc.) into the changelog body. They classify the entry; they don't appear in the prose.
- NEVER include commit hashes, PR numbers, or author names in entries. The audience is end users, not git archaeologists.
- NEVER pick or suggest a version number. The release pipeline owns the bump.
- NEVER invoke a release script from this skill. Authoring is a separate step from releasing.
- If a CHANGELOG has changes in the range but every commit is omit-worthy by the style rules (test-only, type-only, internal refactor), leave its
[Unreleased] body empty — do not invent entries.
1---2name: changelog3description: Regenerate the [Unreleased] section of every affected CHANGELOG.md in Keep a Changelog style. Reads commits since the last release tag plus any uncommitted or staged changes, classifies them by Conventional Commit prefix, and rewrites each [Unreleased] block. Works in single-package repos and monorepos (one CHANGELOG.md per package). Use when preparing a release or drafting changelog entries. Idempotent — safe to re-run as work lands.4---56# Generate CHANGELOG entries78You are tasked with regenerating the `## [Unreleased]` section of every affected `CHANGELOG.md` in the repository so it reflects all change since the last release tag — committed and uncommitted alike.910## Input1112`$ARGUMENTS` — optional `--since <ref>` flag. Empty/literal → range starts at `last_tag:` from the Metadata block.1314## Metadata1516```!17node "${SKILL_DIR}/../_shared/changelog-bootstrap.mjs"18```1920- `in_repo:` — `yes` or `no`. Used by Step 1.1.21- `last_tag:` — last release tag, or `(no tags)`. Used by Step 1.3 and Step 2.1 when no `--since` is supplied.22- `---changelogs---` block — paths of every tracked `CHANGELOG.md` (one per line, empty if none). Used by Step 1.2.2324## Workflow25261. Bail-out checks272. Determine the change range283. Determine each CHANGELOG's scope and collect commits + uncommitted hunks294. Classify and draft entries305. Preview and confirm316. Apply3233## Step 1: Bail-out checks34351. If `in_repo:` is `no`, tell the user "This directory is not a git repository." and stop.362. If the `---changelogs---` block is empty, tell the user "No `CHANGELOG.md` found in the repository — create one (root or per-package) before running this skill." and stop.373. If `last_tag:` is `(no tags)` AND `$ARGUMENTS` lacks `--since <ref>`, ask the user to supply `--since <ref>` and stop until they do.3839## Step 2: Determine the change range40411. Parse the input for a `--since <ref>` flag. If absent, use `last_tag:` from the Metadata block as `SINCE`.422. The range is `$SINCE..HEAD` for committed changes, plus the current uncommitted+staged working tree.4344## Step 3: Determine each CHANGELOG's scope, then collect commits + uncommitted hunks4546Each `CHANGELOG.md` discovered in Step 1.2 owns a path scope:4748- **Nested CHANGELOG** (e.g. `packages/foo/CHANGELOG.md`, `apps/web/CHANGELOG.md`): scope is its parent directory — `packages/foo/`, `apps/web/`.49- **Root CHANGELOG** (`CHANGELOG.md` at repo root):50 - If no nested CHANGELOGs exist: scope is the entire repository.51 - If nested CHANGELOGs also exist: scope is the repository **excluding** every directory that owns a nested CHANGELOG. The root file captures repo-wide change (CI, build config, root README) that no per-package file would claim.5253For each scope:54551. Committed: `git log $SINCE..HEAD --pretty=format:"%H%x09%s%x09%b%x1e" -- <scope>`. For root-with-exclusions, pass `:(exclude)<dir>` pathspecs for every nested-CHANGELOG directory. Records are `\x1e`-delimited; parse subject (`%s`) and body (`%b`).562. Uncommitted: `git diff HEAD -- <scope>` and `git diff --cached -- <scope>` with the same pathspec rules. Treat the union as a single virtual "pending" change set with no commit message — the model classifies it from the diff itself.573. Skip CHANGELOGs whose scope has no committed and no uncommitted changes in range.5859## Step 4: Classify and draft entries6061For each affected CHANGELOG, produce entries grouped under the Keep a Changelog 1.1.0 sections, in this order: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`, `Performance`. Append a `Breaking / Upgrade Notes` section only when a breaking change exists.6263### Conventional Commit → section mapping6465- `feat:` → **Added**66- `fix:` → **Fixed**67- `perf:` → **Performance**68- `refactor:`, `style:`, `build:`, `ci:`, `chore:` → **Changed**69- `docs:` → **Changed** (only if user-facing docs; skip internal `thoughts/` or research notes)70- `test:` → omit (not user-visible)71- `revert:` → **Changed** (note what was reverted)7273### Always-skip commits7475Skip any commit whose subject matches one of these — they are release pipeline housekeeping, not user-visible change:7677- `Release v<x.y.z>` or `chore(release): v<x.y.z>` (common release-bot patterns)78- `Add [Unreleased] section for next cycle`79- Version-only bumps with no other content (`<x.y.z>` as the entire subject)80- Merge commits with no diff content of their own8182### Breaking change detection8384Flag a commit as breaking if any of these are true:8586- The type carries an exclamation suffix (`feat!:`, `refactor!:`, etc.)87- The commit body contains a `BREAKING CHANGE:` footer88- The diff removes or renames an exported symbol, removes a CLI flag, or removes a public file8990For each breaking change, add an entry to **Breaking / Upgrade Notes** in addition to the regular section, written as a one-line upgrade instruction.9192### Style rules — match Keep a Changelog 1.1.0 prose9394- One short user-facing sentence per entry. Imperative mood ("Add", "Fix", "Remove").95- Write for the plugin's **users**, not its maintainers. No internal symbol names, file paths, regex literals, or precedent commit hashes inside entries.96- If a feature has a user-visible name (a slash command, a CLI flag, a skill name), name it in backticks. Example: `` Added `--locale` flag for per-invocation language override. ``97- Group entries by category, not by commit. Merge duplicate-topic commits into one entry.98- If a commit reverses something earlier in the same `[Unreleased]` window (e.g. add → remove → add-back), reflect only the net effect.99- Skip entries that have zero user-visible impact: dependency bumps with no behavior change, internal refactors invisible to users, test additions, type-only changes.100101### Worked example102103Input commits in `packages/api/`:104105```106abc1234 feat(api): add /v2/search endpoint with cursor pagination107def5678 feat(api): support webhook retries with exponential backoff108ghi9abc fix(api): rotate session secret on every JWT refresh109jkl0def docs(api): document rate-limit headers in OpenAPI spec110mno1234 chore(deps): bump @types/node to 20.11111pqr5678 test(api): coverage for cursor edge cases112stu9abc refactor(api): inline httpClient factory (no behavior change)113```114115Output `[Unreleased]`:116117```markdown118## [Unreleased]119120### Added121- `/v2/search` endpoint with cursor-based pagination.122- Webhook delivery retries with exponential backoff.123124### Changed125- OpenAPI spec documents rate-limit response headers.126127### Fixed128- JWT refresh rotates the session secret on every renewal.129```130131What this example demonstrates:132133- Two `feat:` commits → two **Added** entries (one per user-visible feature).134- `docs:` for a user-facing API spec → **Changed** (skip if the docs touched were internal notes).135- `fix:` → **Fixed**, written as the corrected behavior in imperative mood, not as the bug.136- `chore(deps):` with no behavior change → omitted.137- `test:` → omitted (not user-visible).138- `refactor:` flagged "no behavior change" → omitted (the rule is user-visible impact, not commit type).139- Commit hashes never appear in entries.140141## Step 5: Preview and confirm1421431. Print a per-CHANGELOG summary: file path, count by section, breaking-change flag.1442. Print the proposed `[Unreleased]` body for each affected CHANGELOG, in full.1453. Call `ask_user_question`:146 - Question: "Apply regenerated `[Unreleased]` to {N} CHANGELOG(s)?"147 - `Header` is capped at ≤16 characters (`MAX_HEADER_LENGTH = 16` — longer values are rejected).148 - Header: "Changelog"149 - Options:150 - "Apply (Recommended)" — Write the regenerated sections to disk. Refinement, if needed, happens afterward in normal chat (`Edit` tool) or via `git restore` to roll back.151 - "Show Preview" — For each affected CHANGELOG, render a unified diff between the **current** `[Unreleased]` body on disk and the **proposed** regenerated body. Lines marked `-` are about to be removed; lines marked `+` are about to be added. After printing, re-ask this same question.152153## Step 6: Apply154155For each affected CHANGELOG:1561571. Read the file.1582. Locate the `## [Unreleased]` heading. The block runs from that heading up to (but not including) the next `## [` heading — or end of file if no later version exists. If no `## [Unreleased]` heading exists, insert one above the first `## [` heading (or after the file's intro prose if no version sections exist yet).1593. Use `Edit` to replace the entire block with `## [Unreleased]\n\n` followed by the regenerated sections.1604. **Never** touch any heading below `[Unreleased]`. Released version sections are immutable.161162After all writes complete, print the list of modified files and remind the user to commit them before invoking their release pipeline — most release scripts require a clean working tree.163164## Important Notes165166- ALWAYS preview before writing. Never apply without the user's `ask_user_question` confirmation.167- ALWAYS replace the full `[Unreleased]` body, not append. The skill is idempotent regeneration, not accumulation.168- NEVER modify released version sections (anything below the first `## [x.y.z]` heading).169- NEVER write Conventional Commit prefixes (`feat:`, `fix:`, etc.) into the changelog body. They classify the entry; they don't appear in the prose.170- NEVER include commit hashes, PR numbers, or author names in entries. The audience is end users, not git archaeologists.171- NEVER pick or suggest a version number. The release pipeline owns the bump.172- NEVER invoke a release script from this skill. Authoring is a separate step from releasing.173- If a CHANGELOG has changes in the range but every commit is omit-worthy by the style rules (test-only, type-only, internal refactor), leave its `[Unreleased]` body empty — do not invent entries.