- Capture decisions from brainstorming: replace bullet renderer with `claude -p`, write straight to disk, hard-fail on missing/failed claude, drop internal section from release notes but keep collapsed in CHANGELOG.md - Document prompt input/output contract, affected files, test plan, and CI guard that fails tag-based releases when changelog fragments are still pending - Set scope boundaries (no caching, no SDK fallback, no `--no-polish` escape hatch)
8.8 KiB
AI-Polished Changelog Design
Date: 2026-05-02 Status: Approved, awaiting implementation plan
Problem
Today every user-visible PR drops a fragment under changes/. The
fragments are written by whoever shipped the PR, so the prose tone,
granularity, and noise level vary wildly. By the time a stable release
is cut, the assembled CHANGELOG section can run 40+ bullets and lean
heavily on internal area labels (Overlay:, Launcher:, etc.). End
users skim it, miss the actual story of the release, and the GitHub
release body inherits the same clutter.
Goal
Replace the mechanical renderGroupedChanges step in
scripts/build-changelog.ts with a single non-interactive claude -p
call that:
- Merges related fragments into coherent feature-level bullets.
- Drops PR-housekeeping noise that no user benefits from reading.
- Reorganizes by user-visible feature, not by
area:prefix. - Produces a tight
## v<version>body suitable for bothCHANGELOG.mdand the GitHub release notes.
The fragment file format and authoring workflow stay the same. Only the rendering step changes.
Decisions
- Pipeline: Replace the current bullet renderer.
changelog:buildandchangelog:prerelease-notesalways invokeclaude -p. There is no legacy fallback path. - Review: Write straight to
CHANGELOG.md/release/release-notes.md/release/prerelease-notes.md. The release engineer reviews the diff before tagging; no extra confirmation prompt. - CI: Local-only. The release workflow no longer auto-runs
changelog:buildwhen fragments are pending; instead it fails the tagged release with a clear message asking the user to run the build locally and commit the result. CI does not needclaudeon PATH or any Anthropic credentials. - Polish depth: Heavy rewrite plus dedupe. Claude is allowed to merge bullets, drop trivial ones, and reorder by feature.
- Internal section: Dropped from release notes entirely. Kept in
CHANGELOG.mdinside a<details><summary>Internal changes</summary>collapse so the historical record survives without taking over the page. - Prereleases: Same polish path. Beta and RC notes look as clean as stable.
- Claude invocation:
claude -p --bare --model sonnet --permission-mode bypassPermissions --output-format textover stdin.--bareskips hooks, MCP, auto-memory, and CLAUDE.md discovery so the call is self-contained and reproducible. - Failure mode: Hard fail. Missing
claudebinary, non-zero exit, empty/short output, or output missing the required section headers all abort the build.
Architecture
Affected files
scripts/build-changelog.ts— primary edit. ReplacerenderGroupedChangeswithpolishFragmentsWithClaude. Wire it intowriteChangelogArtifacts,writePrereleaseNotesForVersion, and any other path that currently callsrenderGroupedChanges.scripts/build-changelog.test.ts— addrunClaudeto the dep injection surface, stub it in unit tests, add new coverage for failure modes and mode='changelog' vs mode='release-notes'..github/workflows/release.yml(or wherever the auto-build lives) — remove the auto-changelog:buildfallback; add a guard that fails the run ifchanges/*.mdexist on a tag-based release.src/release-workflow.test.ts,src/prerelease-workflow.test.ts— update if they exercise the CI auto-run path.docs/RELEASING.md— document the new local-only build step and theclaudePATH requirement.changes/README.md— note that fragments will be merged and rewritten, so authors should write raw notes rather than polished prose.changes/<n>-ai-changelog-polish.md— fragment for this change itself (type: internal,area: release).
New function: polishFragmentsWithClaude
polishFragmentsWithClaude(
fragments: ChangeFragment[],
options: {
mode: 'changelog' | 'release-notes',
version: string,
date?: string, // changelog mode only
deps?: { runClaude?: (prompt: string) => string },
},
): string
- Filters out
internalfragments whenmode === 'release-notes'. - Serializes the surviving fragments into the prompt format below.
- Invokes
claude(viadeps.runClaudeor the defaultexecFileSyncwrapper). - Validates the output contains the expected section headers; throws on failure.
- Returns the Markdown body that will be inserted under the
## v<version>heading.
The existing prependReleaseSection, extractReleaseSectionBody,
writeReleaseNotesFile, and generateDocsChangelog functions stay
exactly as they are — they consume the polished body the same way
they consumed the rendered body.
Prompt input format
MODE: changelog | release-notes
VERSION: 0.13.0
DATE: 2026-05-02 (changelog mode only)
FRAGMENT changes/291-character-dictionary-selection.md
type: added
area: dictionary
breaking: false
- Added CLI and in-app AniList selection for character dictionary mismatches...
- Added launcher support through `subminer dictionary --candidates`...
FRAGMENT changes/293-interjection-annotation-filter.md
type: fixed
area: tokenizer
breaking: false
- Stopped standalone `あ` interjections from receiving subtitle annotation metadata...
...
Prompt output contract
Claude is instructed to emit Markdown only, no preamble, conforming to:
-
Sections in this order, omitting empty ones:
### Breaking Changes,### Added,### Changed,### Fixed,### Docs. -
For
mode === 'changelog', an additional terminal section:<details> <summary>Internal changes</summary> ### Internal - … - … </details> -
Bullets lead with a feature name (e.g.
Playlist browser:,Windows overlay:) — not with the rawarea:slug. -
Bullets may merge multiple fragments, but breaking changes must retain their substance and stay in
### Breaking Changes. -
Bullets that document only PR-housekeeping or CodeRabbit follow-ups are dropped unless they describe a user-visible behavior change.
After parsing, the body is passed unchanged to prependReleaseSection,
which wraps it under the standard ## v<version> (<date>) heading.
Determinism and review
Claude is non-deterministic. The mitigation is the existing release workflow: the polished CHANGELOG/release-notes are committed before tagging, so what's in the repo is what ships. The release engineer diffs the commit before pushing.
Testing
scripts/build-changelog.test.ts already exposes a deps injection
seam for fs operations. Extend it with runClaude?: (prompt: string) => string. Tests stub it to return canned Markdown; the default
implementation (used in production) wraps execFileSync('claude', …).
New cases to cover:
- Mode
'release-notes'filtersinternalfragments before sending to Claude. - Mode
'changelog'includesinternalfragments and the<details>wrapper expectation is propagated through to the prompt. - Missing
claudebinary throws a clear error. - Non-zero exit code from
claudethrows. - Empty / whitespace-only output throws.
- Output missing the expected section headers throws.
- Prerelease path uses release-notes mode and writes
release/prerelease-notes.mdwith the existing disclaimer. - The polished body still flows correctly through
prependReleaseSection,extractReleaseSectionBody, andgenerateDocsChangelog.
release-workflow.test.ts and prerelease-workflow.test.ts are
updated to cover the new "fragments pending on a tag" failure mode.
Documentation
docs/RELEASING.mdupdates the stable and prerelease sections to state thatchangelog:buildandchangelog:prerelease-notesinvokeclaudelocally, list the PATH requirement, and remove the note about CI auto-runningchangelog:build.changes/README.mdadds a short note: fragments will be merged and rewritten by Claude during release, so write raw notes — don't polish them.
Out of Scope (YAGNI)
- No caching of polished output between runs.
- No diff-and-confirm interactive prompt.
- No SDK fallback if the CLI is missing.
- No
--no-polishescape hatch. Revert the commit if you genuinely need the legacy renderer back. - No change to the fragment authoring schema or the
pr-checkenforcement.
Open Risks
- Claude regresses on a release. Mitigated by the
commit-before-tag review step. If the polish is bad, the release
engineer edits
CHANGELOG.mddirectly and re-commits before tagging. --baremode behavior changes upstream. Pinning to--model sonnetand--barereduces drift, but Claude Code is still an external tool. If invocation flags change, the release engineer will see a hard failure and can patch the script.- CI surface area increases. Removing the auto-
changelog:buildfallback means a tag pushed before running the local build will fail CI. The error message must clearly point at the fix.