diff --git a/changes/README.md b/changes/README.md index 34cd5ca2..c22364a6 100644 --- a/changes/README.md +++ b/changes/README.md @@ -35,6 +35,7 @@ How fragments turn into a release: - At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that. - The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries. +- GitHub release notes and prerelease notes use short top-level items with nested bullets for the change, user benefit, and any useful action note. The stable `CHANGELOG.md` can stay in compact single-line bullets. - `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `
` block) but are dropped from the GitHub release notes entirely. - The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something. diff --git a/changes/release-notes-tooling.md b/changes/release-notes-tooling.md index 060bb3fb..d6aa7e6a 100644 --- a/changes/release-notes-tooling.md +++ b/changes/release-notes-tooling.md @@ -3,3 +3,4 @@ area: release - Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes or breakages into the final user-facing change. - Prerelease note generation reuses existing reviewed notes and merges only new fragment material, and `make clean` preserves `release/prerelease-notes.md`. +- Release-note polishing now asks Claude to write short, nested highlight bullets so longer changes are easier to scan. diff --git a/scripts/build-changelog.test.ts b/scripts/build-changelog.test.ts index 3a5518f7..c45a1f85 100644 --- a/scripts/build-changelog.test.ts +++ b/scripts/build-changelog.test.ts @@ -43,6 +43,14 @@ function fragmentTypesInPrompt(input: string): string[] { .map((line) => line.slice('type: '.length).trim()); } +function assertReleaseNotesPromptRequestsNestedBullets(input: string): void { + assert.match(input, /In MODE: release-notes, use short top-level change bullets/); + assert.match(input, /Nested bullets should cover the change, user benefit, and any user action/); + assert.match(input, /Do not require the exact nested labels/); + assert.match(input, /Keep nested bullets short, concrete, and readable by non-technical users/); + assert.match(input, /Avoid paragraph-style release-note bullets/); +} + function defaultPolishedBody(input: string): string { const mode = modeFromPrompt(input); const types = fragmentTypesInPrompt(input); @@ -437,6 +445,12 @@ test('writeChangelogArtifacts prompts Claude to summarize the final stable outco /Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/, ); } + + const releaseNotesPrompt = stub.calls.find( + (call) => modeFromPrompt(call.input) === 'release-notes', + ); + assert.ok(releaseNotesPrompt, 'expected a release-notes Claude invocation'); + assertReleaseNotesPromptRequestsNestedBullets(releaseNotesPrompt.input); } finally { fs.rmSync(workspace, { recursive: true, force: true }); } @@ -706,6 +720,7 @@ test('writePrereleaseNotesForVersion prompts Claude to revise stale prerelease b prompt, /Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/, ); + assertReleaseNotesPromptRequestsNestedBullets(prompt); } finally { fs.rmSync(workspace, { recursive: true, force: true }); } diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index 523d7863..6a268993 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -260,14 +260,18 @@ You will receive a list of FRAGMENT entries below. Each fragment has metadata (t
Do not include the Internal section at all in MODE: release-notes; internal fragments will not be present in the input for that mode. -4. Each bullet should: - - Lead with a short feature/area name in title case followed by a colon, e.g. "Playlist browser:", "Windows overlay:", "Stats dashboard:". Pick the name from the fragment's bullet content, not the raw 'area:' slug. +4. Each top-level change item should: + - Lead with a short feature/area name in title case. Pick the name from the fragment's bullet content, not the raw 'area:' slug. - Be written in user-facing language. Drop implementation jargon, internal class names, file paths, and PR numbers. - Be merged with related bullets when possible. If five fragments all touch Windows overlay z-order/focus/restore, write one or two bullets that summarize the overall improvement instead of five. - Drop bullets that only describe PR housekeeping, CodeRabbit follow-ups, or test-only changes that don't affect users. - Preserve the substance of breaking changes that remain breaking after applying the Release Outcome Rules. Do not soften or omit them. -5. Do not invent features. Every bullet must be grounded in the input fragments. -6. Do not include the version heading (## v...) — that wrapper is added by the caller. +5. In MODE: changelog, each item may be a conventional single-level bullet, e.g. "- Playlist Browser: Adds faster saved-show browsing." +6. In MODE: release-notes, use short top-level change bullets with two or three nested bullets when an item needs explanation. + Nested bullets should cover the change, user benefit, and any user action or compatibility note when useful. Do not require the exact nested labels; natural phrasing is fine. Omit the action bullet when no action is needed. + Keep nested bullets short, concrete, and readable by non-technical users. Avoid paragraph-style release-note bullets. +7. Do not invent features. Every bullet must be grounded in the input fragments. +8. Do not include the version heading (## v...) — that wrapper is added by the caller. The input begins below.