返回 Skills 目录
samber/developer-relations-skills已通过检查

SKILL DETAIL

changelog-writing

samber/developer-relations-skills/changelog-writing

Turns raw commits, pull requests and tickets into release notes developers actually read - a scannable record of what shipped, what it means in practice, and what breaks. Use whenever the user mentions a changelog, CHANGELOG.md, release notes, a GitHub release body for a tag, a hosted "what's new" page, app-store release notes, Keep a Changelog, or Common Changelog, or asks what to write for a release they just cut - even when the commit history is messy and follows no commit convention. Not for version bumping, tagging or publishing pipelines. Do NOT use for a full breaking-change upgrade guide - use samber/developer-relations-skills@version-migration-guide; for a narrative release post use samber/developer-relations-skills@engineering-blog-post.

安装量 · 415查看来源

Installation

npx skills add https://github.com/samber/developer-relations-skills --skill changelog-writing

技能文件

SKILL.md

最近同步 · 2026年10月23日

evals/evals.json›
{
  "skill_name": "changelog-writing",
  "evals": [
    {
      "id": 1,
      "prompt": "We're cutting 2.9.0 of Halyard, our self-hosted metrics agent, about 4,000 installs. Two things are in it. One: we spent the whole quarter rewriting the on-disk storage engine, four engineers, roughly 18k lines. Same query results, same file format, same public API, just less memory churn internally. Two: a one-line change, the default flush_interval went from 5s to 60s, because the old default hammered small backends. Obviously the rewrite is the headline for the announcement, so I want to know which channels each one earns. We have a hosted changelog, a mailing list of about 900 people, and an in-app banner we can ship if we ask the frontend team.",
      "expected_output": "A tiering decision that inverts the user's premise: the storage rewrite is the lowest tier and earns the fewest surfaces, while the one-line default change earns the widest set because it requires user action. Entries written impact-first with the affected subset named.",
      "files": [],
      "expectations": [
        "Assigns the storage-engine rewrite the lowest tier, defined as no noticeable impact for most, rather than tier 1 or tier 2",
        "Assigns the flush_interval default change tier 2, on the grounds of noticeable impact on most users or required user action",
        "States explicitly that the tier is set by expected reader impact and never by engineering effort",
        "Contradicts the user's premise that the quarter-long rewrite is the headline",
        "Limits the storage rewrite to the changelog and the release body only",
        "Grants the flush_interval change the changelog, release body, hosted page, email and in-product note",
        "Recommends the in-app banner and the mailing list for the flush_interval change specifically because the change requires user action",
        "Writes the flush_interval entry impact-first, naming the observable effect before the internal mechanism",
        "Names the affected subset for the flush_interval change rather than stating it affects everyone",
        "Claims no performance or memory figure for the rewrite, since no measurement was supplied",
        "Does not treat the rewrite as a launch or hand it to a launch plan"
      ]
    },
    {
      "id": 2,
      "prompt": "Here is git log --oneline v1.7.2..HEAD for Corvid, our Node HTTP client on npm. Turn it into release notes for 1.8.0.\n\na91f2c1 feat(pool): reuse sockets across client instances (#310) - the PR title says \"up to 40% faster on hot paths\"\n3d0e881 chore(deps): bump vitest 1.4 -> 1.6 (#311)\n77bc410 feat: add retryOn option behind RETRY_ON_EXPERIMENTAL flag, default off (#313)\n0cc19a2 revert: \"add retryOn option\" (#316)\nb4419ae feat: re-add retryOn option behind the same flag, still default off (#319)\nf2a1d30 docs: document that request() has always retried 502s three times (#321)\n5e6a77c refactor(src): rename InternalDispatcher to Dispatcher across src/ (#322)\n1c93b40 ci: split the matrix job (#323)\n99ab2f1 fix(auth): release the refresh lock on error (#325)\n\nThere is no benchmark in #310, we just eyeballed it in a load test.",
      "expected_output": "Release notes with the unverified 40% figure removed and escalated as an open item, the revert/re-add sequence collapsed, the flagged-off feature producing no entry, the docs change kept and the src rename dropped, plus a traceability table and an exclusion list.",
      "files": [],
      "expectations": [
        "Omits the 40% figure from every entry",
        "Lists the missing benchmark for #310 as an open item needing a human rather than estimating or softening a number",
        "Still produces an entry for #310, describing socket reuse as an observable behaviour change under concurrency",
        "Collapses #313, #316 and #319 into at most one entry rather than three",
        "Produces no entry for the retryOn option, because the flag defaults off and nothing is observable yet",
        "Excludes #311 as a development-only dependency bump",
        "Excludes #323 as a CI change with no consumer surface",
        "Keeps #321 as an entry despite living under docs/, because newly documented behaviour is release-relevant",
        "Excludes #322 despite living under src/, because an internal rename has no consumer surface",
        "States explicitly that a change is never classified by its filename or directory",
        "Returns an exclusion list naming every dropped change with its reason",
        "Returns a traceability table mapping each entry to its pull request",
        "Writes #325 impact-first, describing the failure a caller would observe rather than the lock"
      ]
    },
    {
      "id": 3,
      "prompt": "Quick one. I'm shipping Ledgerline 2.5.0 tomorrow, minor bump, our CI already tagged it. In it we deleted the --legacy-auth CLI flag (it has been unused for a year, we checked telemetry) and removed the XML export endpoint entirely. There are also two bug fixes and a new --format=ndjson flag. Just write the CHANGELOG.md section, we use Keep a Changelog.",
      "expected_output": "A refusal to quietly file removals under a minor version: the version/entry mismatch is named as a defect, a major bump is recommended, both removals get the three-part breaking treatment, and the missing prior deprecation announcement is raised.",
      "files": [],
      "expectations": [
        "Flags that a Removed entry inside a minor release is a defect in either the notes or the version number",
        "Recommends 3.0.0 or another major bump rather than silently writing the removals under 2.5.0",
        "States that major means breaking, minor means additive, and patch means fixes",
        "Does not accept low telemetry usage as a reason the removal is non-breaking",
        "Points out that neither removal was announced as a deprecation in an earlier release",
        "Recommends announcing a deprecation in a release that removes nothing, naming the replacement and the earliest version that removes it",
        "Gives each removal a what-breaks, who-it-hits, what-to-do treatment rather than a bare bullet",
        "Names the affected subset for each removal instead of saying it affects everyone",
        "Uses Keep a Changelog category names and an ISO 8601 YYYY-MM-DD date",
        "Places the breaking items first within their category"
      ]
    },
    {
      "id": 4,
      "prompt": "Trailmark is our hiking app, iOS and Android. The 6.2 release adds offline map packs, a new elevation profile on the route screen, and fixes a GPS drift bug. Marketing already wrote the What's New, it's 3,180 characters of English and honestly it's good. We ship in 9 languages: English, French, German, Spanish, Portuguese, Japanese, Korean, Dutch and Italian. Can you get it ready for both stores? We upload via the Play API.",
      "expected_output": "Store notes written to the tightest cap first and expanded, not the 3,180-character draft translated down, with the per-language Play limit, the English-versus-CJK length asymmetry, mechanical enforcement, the submission lock and register guidance all called out.",
      "files": [],
      "expectations": [
        "Names Google Play's 500-character limit and states that it applies per language",
        "Rejects starting from the 3,180-character English draft and translating it outward",
        "Instructs writing the tightest-capped version first and expanding upward, rather than compressing a long version downward",
        "States that English typically needs roughly twice the characters of Korean or Japanese for the same meaning, so English breaks the Play cap first",
        "Gives an overflow procedure that drops the lowest-impact items first",
        "Recommends enforcing the character count mechanically in CI rather than checking by eye",
        "Warns that a Play API upload fails outright on overflow and names the offending language",
        "Warns that Play's copy-from-previous-release action carries stale translations forward",
        "Flags Apple's 4,000-character limit as reported rather than verified against Apple's own documentation",
        "Front-loads the single highest-impact change in the first sentence, citing the roughly 170-character visible fold",
        "States that store notes are locked at submission and cannot be corrected without a new version",
        "Recommends formal address as the default register for German, French, Spanish, Dutch and Italian",
        "Uses at most three coarse sections such as New, Improved and Fixed rather than a full specification category set"
      ]
    },
    {
      "id": 5,
      "prompt": "pgfleet is our Postgres connection-pooler SDK. Our CHANGELOG.md says at the top that it follows Common Changelog. For 4.3.0 we have: a new poolStats() method (#501), we're deprecating setMaxIdle() in favour of configure({maxIdle}) and it goes away in 5.0 (#504), a fix for CVE-2026-3311 where a malformed DSN could leak the password into logs (#507, advisory GHSA-xxxx-yyyy-zzzz), and we dropped Node 18 support (#509). Write the section.",
      "expected_output": "A Common Changelog section using only its four categories in their fixed order, with the deprecation folded into Changed and the CVE folded into Fixed, imperative verbs, references on every entry, the Node 18 drop marked breaking, and the minor version questioned.",
      "files": [],
      "expectations": [
        "Uses only the four Common Changelog categories: Changed, Added, Removed and Fixed",
        "Emits no Deprecated heading",
        "Emits no Security heading",
        "Places the deprecation of setMaxIdle() under Changed",
        "Names both the replacement and the removal version 5.0 inside the deprecation entry text",
        "Places the CVE fix under Fixed",
        "Names CVE-2026-3311 or the GHSA advisory identifier inside the entry text itself",
        "Orders the categories Changed, Added, Removed, Fixed",
        "Writes every entry with a present-tense imperative verb rather than past tense",
        "Attaches a pull-request or commit reference to every entry",
        "Marks the Node 18 drop as breaking and prefixes that entry with a Breaking label",
        "Places breaking items first inside their category",
        "Questions the 4.3.0 minor version given the Node 18 drop, or recommends a major bump instead"
      ]
    },
    {
      "id": 6,
      "prompt": "Our release notes are boring as hell and everyone on the team agrees. We want them to have some personality: jokes, a running bit, maybe a haiku at the top like that one CLI does. Context: Wexley is a Java SDK for payment reconciliation, our users are integration engineers at banks and PSPs, and we localise the notes into 7 languages because half our customers are in Japan and Brazil. Rewrite our next release in that voice. Change list: new reconcile() batch mode, faster CSV parsing, two bug fixes.",
      "expected_output": "A reasoned decline of the whimsical register for this project, resolved against the three voice conditions rather than taste, followed by flat, specific entries with no invented performance number.",
      "files": [],
      "expectations": [
        "Declines the whimsical register for this project rather than complying with the request",
        "States that the editorial register requires a consumer surface, a single locale or a translation budget, and a product voice that already exists, and that all three must hold together",
        "Identifies that this project fails at least the consumer-surface and single-locale conditions",
        "States that whimsy multiplies translation cost and defect rate across every locale",
        "Recommends the technical-writing school as the default register",
        "Notes that funny or friendly notes run long, obscure the message, and cost non-native readers extra work",
        "Resolves whether the reader is integrating against the change or receiving it",
        "Delivers entries that state the observable effect before any mechanism",
        "Claims no performance figure for the faster CSV parsing, since none was supplied",
        "Uses no marketing adjectives such as revolutionary, blazing, or completely rebuilt"
      ]
    },
    {
      "id": 7,
      "prompt": "We've been on Keep a Changelog since 2021, 43 releases in CHANGELOG.md. We're a library now more than an app, and I want to move to Common Changelog. Can you convert the file? I'd like the whole thing consistent, so please redo the old releases in the new format too. We have Deprecated and Security sections all over the history.",
      "expected_output": "A refusal to rewrite historical releases, with the new format starting at the next release, plus the category mapping to apply going forward and the mechanical differences the team is about to inherit.",
      "files": [],
      "expectations": [
        "Refuses to rewrite the 43 historical releases",
        "States that rewriting history breaks every deep link into the file",
        "Starts the new format at the next release and leaves past releases as shipped",
        "Provides the Keep a Changelog to Common Changelog category mapping for use going forward only",
        "Maps Deprecated to Changed with the replacement and removal version carried into the entry text",
        "Maps Security to Fixed with the advisory or CVE carried into the entry text",
        "Notes that Common Changelog has no Unreleased section",
        "Notes that Common Changelog requires a commit reference on every entry and recommends a pull-request reference too",
        "Recommends stating the chosen specification at the top of the file",
        "Confirms that a library whose readers diff versions supports Common Changelog as the right choice"
      ]
    },
    {
      "id": 8,
      "prompt": "Our repo has no commit convention and never has. Here is what v0.9.0..HEAD looks like for Brackenhall, a self-hosted form builder:\n\nMerge pull request #140 from brackenhall/fix-stuff\nwip\nmore wip\naddress review\nMerge pull request #143 from brackenhall/dark-mode\nfix tests\nUpdate README.md\nMerge pull request #147 from brackenhall/session-thing\nfinal fix\nUpdate handler.ts\n\nPR titles are: #140 \"Handle expired sessions on reconnect\", #143 \"Dark mode\", #147 \"Refactor submission handler\". Write me the 0.10.0 notes. I've got the repo checked out if you need it.",
      "expected_output": "Notes derived from pull requests and the diff rather than commit subjects, with explicit confirmation questions before any entry is written for the ambiguous changes, and an exclusion list instead of silent drops.",
      "files": [],
      "expectations": [
        "Does not write entries from the commit subjects",
        "Works from the pull-request titles and bodies and from the diff",
        "States that a history with no commit convention is the default case rather than the exception",
        "Asks the user to confirm whether #140 was user-visible or only logged, before writing an entry for it",
        "Asks the user to confirm whether #147 has a behavioural side effect, rather than dropping it as a refactor by name",
        "Produces no entries of their own for wip, more wip, address review, fix tests or final fix",
        "Names which access mode is in use, repository checkout versus pasted log, and ties verification strength to it",
        "Does not propose a commit convention as part of this deliverable",
        "Returns an exclusion list rather than silently dropping changes",
        "Writes the dark-mode entry impact-first, naming what a user can now do",
        "Does not exclude Update README.md on the basis of its filename without reading what it changed"
      ]
    },
    {
      "id": 9,
      "prompt": "I clicked Generate release notes on the GitHub release for v3.2.0 of Northgate, our Rust CLI, and it spat out 22 bullets that are just PR titles in merge order. Half of them are dependabot. Our release goes out in two hours. Make it presentable. I only need the release body, I'm pasting it straight into the tag.",
      "expected_output": "A release body with a summary paragraph and impact-ordered entries, handed over with the traceability table, exclusion list and open items despite the user asking only for the body, plus a deliberate decision to skip the surfaces the two-hour deadline rules out.",
      "files": [],
      "expectations": [
        "Treats GitHub's generated output as raw material rather than a finished artefact",
        "States that generated output is ordered by merge time, titled by whoever opened the pull request, and blind to what the changes mean",
        "Delivers a release body that opens with a summary paragraph",
        "Also delivers a traceability table mapping each entry to a pull request, despite the user asking only for the release body",
        "Also delivers an exclusion list covering the dependabot pull requests and anything else dropped",
        "Also delivers a list of open items needing a human",
        "Orders items inside each category by consumer impact, with breaking items first",
        "Mentions .github/release.yml label mapping, with exclude.labels or exclude.authors, as the durable fix for dependabot noise",
        "Recommends producing CHANGELOG.md alongside the release body, since they are the same text and one edit produces both",
        "Does not add a hosted page, email or in-product note to the plan given the two-hour deadline",
        "Names the two-hour deadline as the reason surfaces requiring another team's review cycle are dropped"
      ]
    },
    {
      "id": 10,
      "prompt": "I need to convince our CFO to fund a half-time technical writer purely for release communication. Give me the business case. Numbers would help a lot: what's the typical lift in retention or upgrade rate from good release notes? I've seen Linear cited a bunch, and there's an App Store study I half-remember about longer notes and ratings.",
      "expected_output": "A business case that refuses the requested attribution numbers, corrects the two cited pieces of evidence, and substitutes a ranked set of measurable proxies led by support volume.",
      "files": [],
      "expectations": [
        "Refuses to claim a changelog drove adoption, retention, expansion or revenue",
        "States that Linear's reported returns are first-party and unquantified",
        "Describes the app-store finding as correlational and confounded by release frequency and app maturity",
        "Does not present longer release notes as causing higher ratings",
        "Offers measurable proxies instead, led by support and issue volume tagged to the release",
        "Ranks click-through from a breaking entry to its migration guide above release-page views",
        "States that time-to-upgrade needs roughly a quarter plus telemetry before it says anything",
        "Recommends promoting time-to-upgrade when a breaking change is coming and the team needs to know who is still on the old version",
        "Offers the drafting cost, up to eight hours of an experienced developer per release-note document, instead of inventing an ROI figure",
        "Invents no percentage lift for retention or upgrade rate"
      ]
    },
    {
      "id": 11,
      "prompt": "Different problem than usual for me. We sell Ridgeway on-prem to banks and one insurer. Our 12.4 LTS drops next month. Customers are typically two or three minors behind and their change-approval board reads whatever we publish, that document literally goes into a ticket. Last release we wrote it like our SaaS notes, all friendly and short, and got three separate \"please provide detail\" emails. There's a security fix in this one, a behaviour change in the LDAP sync, a deprecation, and we're moving to a twice-yearly release cadence.",
      "expected_output": "A structured, versioned release document built around identifiers, symptom phrasing, affected-version ranges and required actions, with the narrative register explicitly rejected and the cadence change treated as its own entry.",
      "files": [],
      "expectations": [
        "Treats the advisory, not the entry, as the unit of communication",
        "Recommends stable identifiers per change that a customer can quote in a ticket",
        "States that narrative or friendly voice has negative value for this reader",
        "Writes the LDAP fix in symptom phrasing an administrator can match to a support ticket",
        "Includes affected-version ranges for the entries",
        "Includes a required-action statement per entry",
        "Includes upgrade preconditions where an entry has them",
        "Treats the move to a twice-yearly cadence as its own release-note entry rather than a footnote",
        "Structures the output as a versioned document with chapters such as new features, deprecated features and known issues",
        "Recommends a machine-consumable form of the security advisory for vulnerability-database ingestion",
        "Notes that readers arriving months later search by symptom or CVE, so the breaking set must be complete and pinned at the top"
      ]
    },
    {
      "id": 12,
      "prompt": "Pinhook is our browser extension, about 40k users on Chrome and maybe 6k on Firefox. 2.1 adds a keyboard shortcut, changes where the sidebar opens, and fixes a memory leak. Write the What's New for both store listings. Also a bunch of our users turn off auto-update, which is annoying.",
      "expected_output": "A correction that the Chrome Web Store has no what's-new field, Firefox AMO notes written to its per-version field with its locale quirk noted, and the canonical changelog placed outside both stores with an in-extension page for the auto-update-disabled users.",
      "files": [],
      "expectations": [
        "States that the Chrome Web Store has no dedicated what's-new field",
        "Does not produce Chrome Web Store what's-new copy as though the field existed",
        "Uses Firefox AMO's per-version release-notes field for the Firefox side",
        "Notes that AMO's release-notes default locale is pinned to en-US regardless of the add-on's own default locale",
        "Distinguishes notes for Mozilla reviewers from notes for users as separate artefacts",
        "Recommends keeping the canonical changelog outside the stores, in repository releases or an in-extension page",
        "Recommends an in-extension what's-new page opened on update",
        "Connects the auto-update-disabled users to their need to find what changed before updating",
        "Writes the sidebar-position change impact-first, since it is what users will notice",
        "Claims no figure for the memory leak fix"
      ]
    },
    {
      "id": 13,
      "prompt": "Writing the 4.0.0 notes for Ferrymill, our Python ORM. There are six breaking changes. For each one I want the before code, the after code, a codemod snippet people can run, and a deprecation timeline going back to when we first warned. Plus the normal Added and Fixed stuff. And can you bump the version in pyproject.toml and cut the tag while you're in there?",
      "expected_output": "Release notes produced, with the version bump and tagging declined as out of scope and the before/after code, codemods and timeline routed to a dedicated migration guide that each breaking entry links to.",
      "files": [],
      "expectations": [
        "Declines to bump the version or cut the tag, routing that to release-automation tooling",
        "Routes the before/after code, codemods and deprecation timeline to a separate migration guide rather than into the release notes",
        "Names samber/developer-relations-skills@version-migration-guide as the destination for the migration guide",
        "Still writes the release notes, with each breaking entry linking out to the migration guide",
        "Gives each of the six breaking entries what breaks, who it hits, and what to do",
        "Names the affected subset per breaking entry rather than saying everyone",
        "Places breaking entries first, ahead of Added and Fixed",
        "Confirms that 4.0.0 is the correct bump for a release containing breaking changes",
        "References, from each removal entry, the earlier release that announced the deprecation"
      ]
    },
    {
      "id": 14,
      "prompt": "Nobody has touched CHANGELOG.md for Sandhill, our Kotlin build plugin, in 18 months. That's 640 commits and 190 merged PRs across what should have been six or seven releases. Management wants one catch-up entry published this week. I have maybe a day of my own time and nothing else: no email list, no hosted page, no frontend team to ask.",
      "expected_output": "An explicit, stated cut line over the oversized range, a surface plan reduced to the two cheapest surfaces with the deleted ones named, and a one-time recommendation of a note-fragment workflow so the backlog does not rebuild.",
      "files": [],
      "expectations": [
        "Ranks surviving changes by consumer impact and sets an explicit cut line rather than trimming by feel",
        "States the cut line to the user and lists what fell below it",
        "Does not transfer a specific numeric significance threshold from a published study as the cut value",
        "Deletes the email surface from the plan and says so, because the project has no email list",
        "Deletes the hosted page and in-product note from the plan and says so, given the one-day ceiling",
        "Keeps the changelog and the release body as the two surfaces actually produced",
        "States that the changelog and release body are the same text, so one edit produces both",
        "Recommends a note-fragment workflow, each entry written as its own file in the same patch as the code, and recommends it once rather than repeatedly",
        "Explains that entries written at release time reconstruct impact from memory weeks after the change",
        "Recommends splitting the catch-up across the versions actually released, or states plainly that one entry cannot carry 18 months honestly"
      ]
    }
  ],
  "trigger_queries": [
    { "query": "Write the changelog for v2.4.0", "should_trigger": true },
    { "query": "Turn these 40 commits into release notes", "should_trigger": true },
    { "query": "What do I put in the GitHub release body for this tag?", "should_trigger": true },
    { "query": "Draft the What's New for our App Store update", "should_trigger": true },
    { "query": "Our changelog reads like a git log, fix it", "should_trigger": true },
    { "query": "Help me write CHANGELOG.md for the release I just cut", "should_trigger": true },
    { "query": "We're adopting Keep a Changelog, restructure our file", "should_trigger": true },
    { "query": "Should we use Keep a Changelog or Common Changelog for an SDK?", "should_trigger": true },
    { "query": "How do I write a breaking change entry so people actually act on it?", "should_trigger": true },
    { "query": "Generate release notes from this list of merged PRs", "should_trigger": true },
    { "query": "I need a hosted changelog page entry for this week's ship", "should_trigger": true },
    { "query": "Write the in-product what's new for our March release", "should_trigger": true },
    { "query": "Our release notes are 500 characters over the Play limit", "should_trigger": true },
    { "query": "How many characters can Google Play release notes be per language?", "should_trigger": true },
    { "query": "Rewrite these bullets so they're not just PR titles", "should_trigger": true },
    { "query": "Our customers keep getting surprised by behaviour changes, what are we doing wrong when we announce releases?", "should_trigger": true },
    { "query": "What belongs in a patch release's notes versus a minor one?", "should_trigger": true },
    { "query": "Can I put a Removed section in a minor release?", "should_trigger": true },
    { "query": "Our enterprise customers say our release notes lack detail", "should_trigger": true },
    { "query": "Write the notes for our Firefox add-on update", "should_trigger": true },
    { "query": "Our team hates writing release notes, what's a lighter process?", "should_trigger": true },
    { "query": "Should each PR ship its own note file instead of everyone editing one changelog?", "should_trigger": true },
    { "query": "How do I announce a deprecation without removing anything yet?", "should_trigger": true },
    { "query": "Every release we just say various bug fixes, help", "should_trigger": true },
    { "query": "Draft the email that goes out with the 5.0 release", "should_trigger": true },
    { "query": "We have 18 months of unreleased changes, how do I write that up?", "should_trigger": true },
    { "query": "We need to ship our release notes in 9 languages, where do I start?", "should_trigger": true },
    { "query": "Which of these changes are worth mentioning and which do I drop?", "should_trigger": true },
    { "query": "Our commit history is all wip and squash merges, can you still write the notes?", "should_trigger": true },
    { "query": "What's the right tense for changelog entries?", "should_trigger": true },
    { "query": "Summarise this diff into something our users can actually read", "should_trigger": true },
    { "query": "Convert our changelog to Common Changelog", "should_trigger": true },
    { "query": "The auto-generated GitHub notes are useless, clean them up", "should_trigger": true },
    { "query": "How should we tier releases, which ones get an email and which just get a line in the file?", "should_trigger": true },
    { "query": "Do we announce this internal rewrite or not?", "should_trigger": true },
    { "query": "How do I write it so integrators immediately know whether this costs them work?", "should_trigger": true },
    { "query": "I have to write up the release and all I've got is a list of Jira tickets", "should_trigger": true },
    { "query": "Our Play Console upload keeps failing on the release notes field", "should_trigger": true },
    { "query": "What should the release body say for a security patch?", "should_trigger": true },
    { "query": "Write up 3.0.0, we removed two endpoints", "should_trigger": true },
    { "query": "release notes pls, tag is v1.9.3", "should_trigger": true },
    { "query": "can you make our shipped-this-week post for the product", "should_trigger": true },
    { "query": "we shipped a bunch of stuff, write the thing that goes on the tag", "should_trigger": true },
    { "query": "This needs to be readable by a bank's change-approval board", "should_trigger": true },
    { "query": "how long should release notes be", "should_trigger": true },
    { "query": "my PM wants a haiku at the top of our release notes, is that a bad idea", "should_trigger": true },
    { "query": "someone claimed 40% faster in a PR title, can I put that in the notes?", "should_trigger": true },
    { "query": "what's the difference between a release and a launch", "should_trigger": true },
    { "query": "is it ok to reuse the exact same text for CHANGELOG.md and the GitHub release?", "should_trigger": true },
    { "query": "we're pre-1.0 so semver doesn't really apply, what do we tell people about breaking changes?", "should_trigger": true },
    { "query": "Our API changelog needs to be machine readable, what shape should it take?", "should_trigger": true },
    { "query": "How do I document a change in a default timeout value?", "should_trigger": true },
    { "query": "we changed an error message string, does that go in the notes?", "should_trigger": true },
    { "query": "What goes in the Microsoft Store release notes field?", "should_trigger": true },
    { "query": "our extension users disable auto-update and can't find what changed", "should_trigger": true },
    { "query": "How do I prove the notes cover everything that actually shipped?", "should_trigger": true },
    { "query": "build me a table mapping each entry back to its PR", "should_trigger": true },
    { "query": "we need a list showing what we deliberately left out of the release", "should_trigger": true },
    { "query": "what do I write for a release where literally nothing user-facing changed", "should_trigger": true },
    { "query": "Is improved reliability an acceptable entry?", "should_trigger": true },
    { "query": "our what's-new text keeps getting flagged in App Store review", "should_trigger": true },
    { "query": "Write the what's-new banner copy for the app", "should_trigger": true },
    { "query": "how do I stop dependabot PRs showing up in our published release notes", "should_trigger": true },
    { "query": "our file has Deprecated entries but the spec we picked doesn't have that category", "should_trigger": true },
    { "query": "make this release announcement scannable in fifteen seconds", "should_trigger": true },
    { "query": "i just merged 22 PRs and have to ship in two hours, what do i publish", "should_trigger": true },
    { "query": "who should write the entries, the engineer who shipped it or a writer?", "should_trigger": true },
    { "query": "how do we measure whether anyone reads what we publish with each release", "should_trigger": true },
    { "query": "What should we say about dropping Node 18 support?", "should_trigger": true },
    { "query": "Write the v4 migration guide with before/after code and a codemod for each breaking change", "should_trigger": false },
    { "query": "Build the deprecation timeline document for our 4.0 upgrade path", "should_trigger": false },
    { "query": "Write a blog post about how we rebuilt our storage engine", "should_trigger": false },
    { "query": "Draft the engineering blog post announcing our new streaming architecture", "should_trigger": false },
    { "query": "Plan which channels we announce the 3.0 launch on over the next six weeks", "should_trigger": false },
    { "query": "Build our devrel content calendar for Q2", "should_trigger": false },
    { "query": "We're open-sourcing the project next month, plan the launch", "should_trigger": false },
    { "query": "Rewrite our README so new visitors understand what the project does", "should_trigger": false },
    { "query": "Write the quickstart so a developer gets a first successful call in five minutes", "should_trigger": false },
    { "query": "Set standards for the code samples in our docs", "should_trigger": false },
    { "query": "Write a troubleshooting page for the top ten errors people hit", "should_trigger": false },
    { "query": "Design our issue triage process for the OSS repo", "should_trigger": false },
    { "query": "Audit the information architecture of our documentation site", "should_trigger": false },
    { "query": "Write a press release for our Series B", "should_trigger": false },
    { "query": "Write a customer case study about how Acme uses our SDK", "should_trigger": false },
    { "query": "Improve our documentation's search rankings", "should_trigger": false },
    { "query": "Optimise my GitHub profile README", "should_trigger": false },
    { "query": "Write the script for a two-minute product video", "should_trigger": false },
    { "query": "Outline my conference talk on distributed tracing", "should_trigger": false },
    { "query": "Write a step-by-step tutorial on building a webhook receiver", "should_trigger": false },
    { "query": "Set up governance for our open-source project", "should_trigger": false },
    { "query": "Which devrel metrics should we report to the board?", "should_trigger": false },
    { "query": "Set up analytics on our developer portal", "should_trigger": false },
    { "query": "Find the keywords developers search when they hit this problem", "should_trigger": false },
    { "query": "Which open source licence should we pick?", "should_trigger": false },
    { "query": "Should we push this API through a standards body?", "should_trigger": false },
    { "query": "Price our developer tool's paid tiers", "should_trigger": false },
    { "query": "Help me start building in public without leaking roadmap detail", "should_trigger": false },
    { "query": "Write my CFP abstract for KubeCon", "should_trigger": false },
    { "query": "Design a developer champions programme", "should_trigger": false },
    { "query": "Write the contributing guide for new contributors", "should_trigger": false },
    { "query": "Prep me for a podcast interview about our infrastructure", "should_trigger": false },
    { "query": "Improve our engineering employer brand", "should_trigger": false },
    { "query": "Write a job description for a developer advocate", "should_trigger": false },
    { "query": "How do I move from advocate to head of devrel?", "should_trigger": false },
    { "query": "Design a live demo that won't break on stage", "should_trigger": false },
    { "query": "Start a monthly developer meetup in our city", "should_trigger": false },
    { "query": "Should we sponsor this conference at gold or silver?", "should_trigger": false },
    { "query": "Segment our developer users into personas", "should_trigger": false },
    { "query": "Write our developer relations strategy for next year", "should_trigger": false },
    { "query": "How should I structure the devrel team?", "should_trigger": false },
    { "query": "Allocate our devrel budget across events and content", "should_trigger": false },
    { "query": "Make our docs easier for coding agents to consume", "should_trigger": false },
    { "query": "Map the developer journey from first visit to production", "should_trigger": false },
    { "query": "Which package registries should we publish to?", "should_trigger": false },
    { "query": "Write our GitHub Sponsors pitch", "should_trigger": false },
    { "query": "Set up semantic-release so versions bump automatically on merge", "should_trigger": false },
    { "query": "Configure release-it for our npm package", "should_trigger": false },
    { "query": "Add conventional commits and commitlint to the repo", "should_trigger": false },
    { "query": "Write a GitHub Action that regenerates CHANGELOG.md from conventional commits on every merge", "should_trigger": false },
    { "query": "Set up a workflow that cuts a release automatically when we merge to main", "should_trigger": false },
    { "query": "Install a pre-commit hook that checks commit message format", "should_trigger": false },
    { "query": "Bump the version in package.json and tag it", "should_trigger": false },
    { "query": "Write me a good commit message for this diff", "should_trigger": false },
    { "query": "Squash and rebase this branch before I merge it", "should_trigger": false },
    { "query": "Explain what semantic versioning means", "should_trigger": false },
    { "query": "Set up Linear so releases are tracked against cycles", "should_trigger": false },
    { "query": "Script a YouTube video walking through this month's changes", "should_trigger": false },
    { "query": "Configure our CI to publish to npm on tag", "should_trigger": false },
    { "query": "Design our branching strategy, trunk based or gitflow?", "should_trigger": false },
    { "query": "Write the API reference for the new endpoints", "should_trigger": false },
    { "query": "Draft an incident postmortem for last night's outage", "should_trigger": false },
    { "query": "Write the status page update for the ongoing degradation", "should_trigger": false },
    { "query": "Write our product roadmap for the next two quarters", "should_trigger": false },
    { "query": "Write marketing copy for our pricing page", "should_trigger": false },
    { "query": "Set up a Discord for our community", "should_trigger": false },
    { "query": "Draft the SECURITY.md policy file for the repo", "should_trigger": false },
    { "query": "Write the launch tweets and LinkedIn post for our 3.0 announcement", "should_trigger": false },
    { "query": "Write the onboarding email sequence for new signups", "should_trigger": false }
  ]
}
references/format-specifications.md›
# Changelog format specifications

Two published specifications compete. Pick one, state it at the top of the file, and stay on it. Mixing them is what produces changelogs where the category means something different every release.

## Table of Contents

- [Keep a Changelog 1.1.0](#keep-a-changelog-110)
- [Common Changelog](#common-changelog)
- [[3.1.0] - 2026-03-04](#310---2026-03-04)
- [Choosing](#choosing)
- [Mapping between them](#mapping-between-them)
- [Semantic versioning correspondence](#semantic-versioning-correspondence)

## Keep a Changelog 1.1.0

The permissive, widely adopted one. Good default for application and product changelogs.

Guiding principles, verbatim from the specification:

- "Changelogs are _for humans_, not machines."
- There should be an entry for every single version.
- The same types of changes should be grouped.
- Versions and sections should be linkable.
- The latest version comes first.
- The release date of each version is displayed.
- Adherence to Semantic Versioning should be mentioned.

Six categories:

| Category     | Use for                                       |
| ------------ | --------------------------------------------- |
| `Added`      | New features, endpoints, parameters, commands |
| `Changed`    | Changed behaviour of existing functionality   |
| `Deprecated` | Features scheduled for removal                |
| `Removed`    | Features now gone                             |
| `Fixed`      | Bug fixes                                     |
| `Security`   | Vulnerability patches                         |

Mechanics it fixes:

- Keep an `## [Unreleased]` section at the top. It lets readers see what is coming, and makes cutting a release a rename rather than a rewrite.
- Dates in ISO 8601 (`YYYY-MM-DD`). Regional formats are ambiguous across an international readership.
- Pulled releases keep their heading and gain a loud tag: `## [0.0.5] - 2014-12-13 [YANKED]`.
- Named anti-patterns: dumping commit log diffs, omitting deprecations, ambiguous dates.

File header the specification expects:

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
```

## Common Changelog

The strict, reference-heavy one. Good default for libraries and SDKs whose readers diff versions.

Differences that decide the choice:

- Exactly four categories, in this fixed order: `Changed`, `Added`, `Removed`, `Fixed`. No `Deprecated`, no `Security` - both fold into `Changed`, so the entry text must carry the whole signal.
- Entries start with a present-tense imperative verb: "Add `write()` method", not "Added the write method".
- Breaking changes are prefixed `**Breaking:**`, or `**<subsystem> (breaking):**`, and listed first inside their category.
- Every change must reference a commit and should reference the pull request: `([#194](url))`, ``([`53bd922`](url))``. Authors, if listed, follow the references.
- No `Unreleased` section. The file only ever describes shipped releases.
- Contextual warnings use a free-text notice above the categories instead of a `[YANKED]` tag.

Its explicit include list is the useful half, because these are the changes teams wrongly drop:

- Refactors (they carry side effects).
- Runtime-environment changes.
- Code-style changes that adopt new language features.
- Newly documented behaviour.

Its exclude list: dotfiles, development-only dependency updates, minor style changes, documentation formatting.

```markdown
## [3.1.0] - 2026-03-04

### Changed

- **Breaking:** reject payloads over 1 MB in `send()` ([#412](https://github.com/owner/name/pull/412))
- Retry idempotent requests up to 3 times ([#418](https://github.com/owner/name/pull/418))

### Added

- Add `stream()` for token-by-token responses ([#405](https://github.com/owner/name/pull/405))
```

## Choosing

| Situation                                                         | Pick                                         |
| ----------------------------------------------------------------- | -------------------------------------------- |
| Library or SDK, readers diff versions, PRs are the unit of change | Common Changelog                             |
| Application or hosted product, mixed audience, screenshots likely | Keep a Changelog                             |
| Existing file already follows one                                 | That one - consistency beats the better spec |
| Security advisories published in the changelog itself             | Keep a Changelog (it has the category)       |
| Team wants machine-validated structure                            | Common Changelog (validators exist for it)   |

## Mapping between them

When migrating a file or deriving one from the other:

| Keep a Changelog | Common Changelog                                                    |
| ---------------- | ------------------------------------------------------------------- |
| `Added`          | `Added`                                                             |
| `Changed`        | `Changed`                                                           |
| `Fixed`          | `Fixed`                                                             |
| `Removed`        | `Removed`                                                           |
| `Deprecated`     | `Changed`, entry text must name the replacement and removal version |
| `Security`       | `Fixed`, entry text must name the advisory or CVE                   |

Never rewrite historical releases when switching specifications. Start the new format at the next release and leave the past as shipped - a rewritten history breaks every deep link into the file.

## Semantic versioning correspondence

The version number is a communication channel of its own, and the entry set must agree with it.

| Bump  | Allowed entries                                                    |
| ----- | ------------------------------------------------------------------ |
| Major | Anything, including `Removed` and breaking `Changed`               |
| Minor | `Added`, non-breaking `Changed`, `Deprecated`, `Fixed`, `Security` |
| Patch | `Fixed`, `Security`, and non-breaking internal `Changed`           |

Pre-1.0 projects have no such guarantee, and calendar-versioned projects have none either. Both must carry the compatibility statement in prose instead: state once, outside the release notes, which kinds of change count as backward-compatible.

Stripe's upgrade documentation is the reference example of that statement. It publishes the exhaustive list of changes it treats as backward-compatible, and tells integrators to code defensively against exactly those:

- New resources.
- New optional request parameters.
- New response properties.
- Reordered response properties.
- Changed opaque string formats.
- New event types.

Everything outside the list becomes a named, dated major version with its own change list. Publishing the contract once means each release note is read against it, instead of re-litigating "is this breaking?" per entry.
references/non-git-surfaces.md›
# Non-git surfaces

Four surfaces where the git-hosted templates do not apply, because each imposes caps, review, or machine consumers the repository does not. Read the section for the surface you are writing.

## Contents

- [Mobile app stores](#mobile-app-stores)
- [Enterprise and on-prem](#enterprise-and-on-prem)
- [Hosted APIs and cloud services](#hosted-apis-and-cloud-services)
- [Browser extensions](#browser-extensions)
- [Localization](#localization)

## Mobile app stores

| Constraint                       | Apple App Store              | Google Play                                        | Microsoft Store (MSI/EXE)       |
| -------------------------------- | ---------------------------- | -------------------------------------------------- | ------------------------------- |
| Field                            | "What's New in This Version" | "What's new in this release"                       | Release notes                   |
| Character limit                  | 4,000                        | 500 per language                                   | 1,500                           |
| Required                         | Yes, after the first version | No                                                 | Leave blank on first submission |
| Localization unit                | Per storefront               | Per Play Console locale, in language-tagged blocks | Not documented                  |
| Markdown / links                 | None; no clickable links     | None; URLs auto-linkified                          | Not documented                  |
| Version history visible to users | Yes                          | No, latest only                                    | Not documented                  |

The Google Play and Microsoft Store figures are verified against first-party documentation. Apple's 4,000-character limit is reported by third-party comparisons, not verified against Apple's own pages - flag it as such if a user relies on it.

Three rules follow from the table:

1. **The store text is a review criterion, not just copy.** Apple's guideline 2.3.12 requires apps to clearly describe new features and product changes; generic wording is acceptable only for pure bug-fix, security or performance updates. A vague "What's New" on a feature release is a rejection vector - this has no equivalent on any git surface.
2. **Write the 500-character version first, then expand.** Compressing 4,000 characters down to 500 degrades text far more than expanding 500 up to 4,000 - an inference from the caps, echoed by one secondary source, not a platform rule. Enforce the count mechanically; Play Console warns on overflow and API uploads fail outright, naming the offending language.
3. **Front-load the first sentence.** Roughly the first 170 characters render before the "more" fold (reported, not verified against Apple's documentation), so the single highest-impact change belongs there as a complete sentence.

Store notes are **locked at submission** - they ship with the binary and cannot be edited without a new version, which is why teams schedule the writing against the submission deadline rather than the merge date. Apple's Promotional Text (170 characters, editable without a new submission - reported, not verified against Apple's documentation) is the escape hatch for anything time-sensitive.

Use three coarse sections at most - New, Improved, Fixed, omitting empty ones. This follows the practice of existing store-notes tooling, not a platform requirement; the rationale is that a store reader will not parse six specification categories.

Evidence on whether any of this moves the needle: an analysis of 69,851 releases and 67.7M reviews across 2,232 top free Google Play apps found longer release notes and more frequent updates both correlate with higher average ratings (Yang, Hassan, Zou and Hassan, _Empirical Software Engineering_ 27:55, 2022). It is correlational and confounded by release frequency and app maturity - do not present it as a causal argument for writing more.

## Enterprise and on-prem

The reader may be many versions behind and may be reading the document as an input to a change-approval process rather than out of curiosity. That inverts the usual priorities:

- High value: stable identifiers, symptom phrasing, affected-version ranges, required-action statements.
- Negative value: narrative voice.

- **The unit of communication is often the advisory, not the entry.** Red Hat ships changes as individually numbered errata - security (RHSA), bug fix (RHBA), enhancement (RHEA) - drawn from one yearly numbering pool, with QE and management sign-off recorded as fields on the advisory itself.
- **Release notes become a structured, versioned document**, with chapters for new features, deprecated features, known issues, and asynchronous errata updates listing every subsequent patch release by advisory ID.
- **Entries carry upgrade preconditions.** "With advisory X, an in-place upgrade from 8.8 to 9.2 with FIPS mode enabled becomes possible" is a normal enterprise sentence; the identifier is part of the claim.
- **Advisories are machine-consumable.** Red Hat publishes CSAF JSON per advisory, ingested by vulnerability databases - the entry doubles as a security data feed.
- **Write fixes for diagnosis.** Microsoft's enterprise channel notes are long lists of "we fixed an issue that caused X" precisely so an administrator can match a symptom to a support ticket.
- **Lifecycle changes are themselves release-note events.** A change of support window or release cadence - HashiCorp's move to a twice-yearly enterprise cadence, for instance - is a category of note that fast-moving SaaS has no slot for.

## Hosted APIs and cloud services

Two architectures, both pushing the changelog toward being a machine artefact.

- **Version-derived.** Stripe generates its API changelog from version-change definitions, so it updates as services deploy. The precondition is versioning designed as a first-class concern - pinned per account, transmitted by request header - not a writing process. Where a project has that, argue for generation rather than authoring.
- **Feed-and-dataset.** Google Cloud publishes per-product release-notes pages with RSS feeds, a console-wide filterable view, and a public BigQuery dataset of release notes queryable with SQL. Practitioners build alerting pipelines on the dataset specifically because the feed HTML is painful to parse.

For a hosted API the changelog has machine consumers - monitoring pipelines, compliance reviews, coding agents - as well as human ones. **Stability of shape (dates, product names, categories) matters more than prose quality.**

A machine-readable channel is a deliverable, not a nice-to-have. No vendor has yet published a changelog designed primarily for agent consumption; if that shifts, the optimisation target moves from prose to schema.

## Browser extensions

The thinnest-documented surface, and mostly defined by an absence.

- **Chrome Web Store has no dedicated what's-new field.** Listing metadata covers title, description, screenshots, category and privacy policy.
- **Firefox AMO does have one**, per version, exposed in its API - with the quirk that the release-notes default locale is pinned to `en-US` regardless of the add-on's own default locale. Release pipelines can submit generated notes to it, alongside a separate notes-for-reviewers field: notes for Mozilla reviewers and notes for users are distinct artefacts.

Keep the canonical changelog outside the store - repository releases, or an in-extension "what's new" page opened on update - and treat any store field as a pointer. Users who disable auto-update do go looking for what changed before updating, and have filed issues when they could not find it.

## Localization

Platform mechanics bind this problem more than editorial theory does.

- Play is **per-language and hard-capped** at 500 characters, with a copy-from-previous-release action that carries translations forward - a time-saver and a staleness risk in equal measure.
- Apple serves localized notes by device language when a translation exists.
- English typically needs roughly **twice the characters of Korean or Japanese** for the same meaning, so English breaks the Play cap first even when every CJK version passes. Budget the reverse direction too: some target languages expand 30-40% over English (reported, unverified). The two figures point opposite ways because the direction depends on the target language; the discipline - budget for expansion in whichever direction applies - is the same.
- On overflow (reported practitioner guidance):
  1. Drop the lowest-impact items first.
  2. Group by feature.
  3. Cap each line at 3-4 items.
  4. Enforce the count in CI rather than by eye.
- Write for translation from the start - no idioms, no cultural references, no wordplay. Maintain a termbase, and give translators one internal owner to ask for context.
- Register is not universal. Formal address is the safe default in several markets (German _Sie_, French _vous_, Spanish _usted_, Russian _вы_, Dutch _u_, Italian _Lei_). Adapt idiom to a local equivalent; never translate it literally.

No first-party account of how any named company reviews _translated_ changelogs was found - who signs off on a locale, whether a locale reviewer can block a release. Treat governance claims in this area as unverified.
references/published-findings.md›
# Published findings

Every figure, threshold and named practitioner quoted by this skill, with its source and its confidence. Check this file before repeating any number to a user - several widely repeated changelog claims are unverifiable.

## Contents

- [Peer-reviewed findings](#peer-reviewed-findings)
- [Named practitioners](#named-practitioners)
- [Frameworks with a real lineage](#frameworks-with-a-real-lineage)
- [Platform rules](#platform-rules)
- [Self-set thresholds](#self-set-thresholds)
- [Claims to never make](#claims-to-never-make)

## Peer-reviewed findings

| Finding                                                                                                                           | Source                                                                     | Use it for                                                        |
| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Up to 8 hours for an experienced developer to draft a release-note document                                                       | Moreno et al., cited in Daneshyan et al., FSE 2025                         | Why the task gets skipped or rushed                               |
| Well-formed release notes list only 6-26% of the issues a release addressed                                                       | Abebe, Ali and Hassan                                                      | Coverage expectations; human notes are selective by design        |
| 32,425 release notes across 1,000 GitHub projects yield 8 recurring information categories                                        | Bi, Xia, Lo, Grundy and Zimmermann, IEEE TSE 48:1834-1852, 2022            | Category sets are empirically convergent, not arbitrary           |
| 64.54% of 612 release notes organise changes hierarchically; three writing styles recur                                           | Wu et al., 233 GitHub projects                                             | Grouping by category is the norm readers expect                   |
| Content sources: pull requests 32%, issues 29%, commits 19%, CVE issues 6%                                                        | Nath and Roy                                                               | Commits alone are a minority input - read PRs and issues too      |
| 54% of 900 open-source projects generate release notes from pull requests                                                         | Jiang et al.                                                               | Why PR-only tooling excludes nearly half of projects              |
| ~90% of sampled projects enforce no standard commit specification                                                                 | Daneshyan et al., FSE 2025                                                 | Messy history is the default case, not the exception              |
| SmartNote generated notes for 100% of 23 projects; DeepRelease ~90%; Conventional Changelog ~46%                                  | Daneshyan et al., FSE 2025 (DOI 10.1145/3729345)                           | Git-based generators fail often; applicability beats polish       |
| SmartNote reached 81% commit coverage vs 31% for the projects' own human-written notes                                            | same                                                                       | Human writers trade coverage for concision deliberately           |
| Significance threshold of 0.10-0.15 balanced verbosity against omission                                                           | same                                                                       | Precedent for setting an explicit cut line on large ranges        |
| Over 80% of 17 evaluators preferred SmartNote on completeness, clarity, organisation; only 55% on conciseness                     | same                                                                       | LLM drafts run long; concision is the weak axis                   |
| A raw LLM with no prompt engineering scored worse on all four criteria and produced a 36-line note for one simple change          | same                                                                       | Structure matters more than model choice                          |
| Longer release notes and more frequent updates correlate with higher average ratings - 69,851 releases, 67.7M reviews, 2,232 apps | Yang, Hassan, Zou and Hassan, _Empirical Software Engineering_ 27:55, 2022 | The only outcome evidence in the field; correlational, confounded |

## Named practitioners

| Person            | Then                          | Position                                                                                                                                                                                                                                           | Where                                            |
| ----------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| Anna Pickard      | Editorial director, Slack     | The store text box is "just a little space in which we can be human"; voice distilled as _clear, concise, human_, clarity first                                                                                                                    | Building Slack, 2024-02-26; Contagious, 2015     |
| Sarah Maddox      | Technical writer, Google      | The function is to let customers know something changed, especially when it changes how they use the product                                                                                                                                       | quoted in UX Collective, 2020                    |
| Anne Edwards      | Technical writer              | Funny and friendly notes run long, obscure the message, and cost non-native readers extra work                                                                                                                                                     | Write the Docs, quoted in UX Collective, 2020    |
| Karri Saarinen    | Co-founder, Linear            | Returns from changelog discipline are mostly non-user: recruiting, investor confidence, early-adopter trust. Usually the person who built the feature writes the entry                                                                             | Medium, 2020                                     |
| Brandur Leach     | API Experience, Stripe        | Make versioning first-class and the changelog generates itself                                                                                                                                                                                     | Stripe engineering blog, 2017                    |
| Doug Hellmann     | Release management, OpenStack | Move the note into the patch and peer-review it like code (the `reno` design)                                                                                                                                                                      | OpenStack docs                                   |
| Tom Johnson       | API technical writer, Google  | Diffs beat engineers' self-reports as a source: "engineers aren't telling technical writers half of the changes they're making." Running an agent on the task, review comments fell from 15-20 per release to 3-5 after roughly a dozen iterations | idratherbewriting.com, 2025-02-17 and 2026-05-04 |
| Jasmine Greenaway | Developer advocate, Microsoft | "We hate creating release notes when it's our turn to ship software updates"                                                                                                                                                                       | opensource.microsoft.com, 2018-09-06             |

The Pickard-versus-Maddox/Edwards disagreement is genuinely unresolved in the field, not settled by evidence. The Google Play correlation is compatible with both readings.

## Frameworks with a real lineage

- **Announcement tiering** (4 tiers, investment set by expected reader impact). Public version: the PostHog handbook. Older lineage: Pragmatic Institute launch tiering, where tier 3 is reserved for incremental releases with minimal market impact. The Product Marketing Alliance distributes a matrix on the same premise.
- **The note fragment** (`reno`, `towncrier`). Each note is a separate file added in the same patch as the code, so hundreds of contributors avoid merge conflicts on one `CHANGELOG.md` and notes back-port with the code. Rejected alternatives on record: prose files (conflicts), commit messages (wrong audience, immutable), git notes (setup cost).
- **Release versus launch.** PostHog's handbook separates a release (feature available to existing users, product team's call) from a launch (put in front of new people, marketing-led), and adds an anti-veto rule: do not block decisions outside your lane. Most review friction in release communication is disagreement about authority, not wording.

## Platform rules

Verified against first-party documentation: Google Play's 500-character-per-language cap and its carry-forward of translations; Microsoft Store's 1,500-character field; Apple guideline 2.3.12 on describing changes; Firefox AMO's per-version release-notes field pinned to the `en-US` default locale; GitHub's `.github/release.yml` label-to-category mapping with `exclude.labels` and `exclude.authors`; GitHub's instruction to confirm generated notes contain all and only the intended information.

Reported but not verified against a first-party page - flag before quoting: Apple's 4,000-character limit and Promotional Text's 170-character editable field; the ~170-character visible fold; the claim that store what's-new text is not indexed for app-store search; Play's prohibition on promotional content in the field; the 30-40% expansion figure for translation into some languages.

## Self-set thresholds

Neither of these comes from a published standard. Say so if a user asks where they come from.

- **100% traceability.** Grounded in GitHub's "all and only" instruction and in every practitioner account of LLM-assisted drafting; the numeric bar itself is self-set.
- **100% coverage of consumer-observable changes.** Deliberately stricter than observed practice (31% commit coverage, 6-26% of issues). Justified because the failure is silent - a behaviour change with no entry is discovered in production.

## Claims to never make

- That a changelog drove adoption, retention, expansion or revenue. The link is unmeasured; Linear's returns are first-party and unquantified.
- That longer notes cause higher ratings. The study is correlational.
- Any dedicated changelog-tool price from memory. Reported entry prices for the same vendor contradict each other across sources by 20-30%; check the vendor's pricing page on the day.
- A performance number that has no measurement in the source material.
references/surface-templates.md›
# Surface templates

One release, several artefacts. Writing one text and pasting it everywhere is why changelogs read as too terse in one place and too promotional in another.

| Surface                          | Reader                                              | Budget              | Emphasis                                            |
| -------------------------------- | --------------------------------------------------- | ------------------- | --------------------------------------------------- |
| `CHANGELOG.md`                   | integrator diffing versions                         | one line per change | specification conformance, reference links          |
| Release body on the git host     | upgrader arriving from a tag or a dependency-bot PR | one screenful       | summary, grouped list, contributors, full-diff link |
| Hosted changelog page            | evaluator and existing customer                     | a scroll            | dated entries, media, breaking changes pinned       |
| In-product or email "what's new" | user who follows nothing                            | 3-5 items           | benefit-led, links back to the entry                |

The budgets are this skill's working defaults, not published standards.

## Table of Contents

- [`CHANGELOG.md`](#changelogmd)
- [[2.4.0] - 2026-03-04](#240---2026-03-04)
- [Release body on the git host](#release-body-on-the-git-host)
- [Hosted changelog page](#hosted-changelog-page)
- [March 4, 2026 - Streaming responses](#march-4-2026-streaming-responses)
- [In-product or email "what's new"](#in-product-or-email-whats-new)
- [Deriving one from another](#deriving-one-from-another)
- [Full delivery example](#full-delivery-example)
- [Release notes - v2.4.0 (CHANGELOG.md)](#release-notes-v240-changelogmd)
- [Traceability](#traceability)
- [Excluded (7 changes)](#excluded-7-changes)
- [Needs a human](#needs-a-human)

## `CHANGELOG.md`

Follow the chosen specification exactly. Nothing here is decorative - this file is read by people comparing two versions and by tooling that renders it.

```markdown
## [2.4.0] - 2026-03-04

### Added

- Add `stream()` to the client for token-by-token responses ([#405](url))
- Support Python 3.13 ([#409](url))

### Changed

- **Breaking:** reject payloads over 1 MB in `send()` instead of truncating ([#412](url))
- Raise the default request timeout from 10s to 30s ([#418](url))

### Fixed

- Fix sign-ins failing when several tabs refresh a token at once ([#421](url))
```

## Release body on the git host

Opens with a summary paragraph - the only place in the whole set where prose earns its space. It is what an advocate or newsletter writer quotes, and what a reader sees inside a dependency-update pull request.

```markdown
This release adds streaming responses and raises the default timeout. One breaking
change affects callers that relied on oversized payloads being truncated.

### Breaking

- `send()` now rejects payloads over 1 MB. Chunk the payload or catch
  `PayloadTooLarge`. See the [migration notes](url).

### Added

- Streaming responses via `stream()` (#405)
- Python 3.13 support (#409)

### Fixed

- Concurrent token refresh no longer fails sign-in (#421)

**Full changelog**: v2.3.0...v2.4.0
```

Git hosts can generate a first draft of this from merged pull requests - on GitHub, `.github/release.yml` maps pull-request labels to categories and supports `exclude.labels`, `exclude.authors` and a `*` catch-all. Treat that output as raw material for the editorial pass, never as the finished artefact:

- It is ordered by merge time.
- It is titled by whoever opened the pull request.
- It is blind to what any of it means.

## Hosted changelog page

Reverse-chronological entries, each dated and independently linkable. Breaking changes pinned above the fold. This is the surface where a screenshot or a short code block pays for itself, because the reader is often evaluating rather than upgrading.

````markdown
## March 4, 2026 - Streaming responses

Long completions can now be consumed token by token instead of waiting for the
full response.

```python
for chunk in client.stream(prompt):
    print(chunk.text, end="")
```

**Breaking:** `send()` rejects payloads over 1 MB. [Migration notes](url)

Also in this release: Python 3.13 support, a 30s default timeout, and a fix for
concurrent token refresh. [Full changelog](url)
````

## In-product or email "what's new"

Three to five items, benefit first, no version numbers in the copy. Anyone who wants precision clicks through.

```markdown
**What's new this month**

- Long answers now stream in as they are generated, so you see the first words immediately.
- Slow connections get more time before a request gives up.
- Fixed a sign-in failure that hit people with several tabs open.

[Read the full release notes](url)
```

## Deriving one from another

1. Write the `CHANGELOG.md` entries first; they are the most constrained and force the facts to be right.
2. Derive the release body by adding the summary paragraph and grouping.
3. Derive the hosted page by adding media and context.
4. Derive the in-product note last, from the hosted page, by dropping everything an ordinary user cannot act on.

Going the other direction - starting from the marketing-facing note - loses the detail that cannot be reconstructed later, and tends to leak adjectives into the specification-conformant file.

This derivation order is the same as the production order in SKILL.md § Which surface to produce first, for the same underlying reason: each surface downstream costs more and reaches a reader the one above it missed. Stopping partway leaves the cheap surfaces finished rather than every surface half-written.

## Full delivery example

The notes never travel alone. Hand them over with the traceability table, the exclusion list and the open items, in this shape:

```markdown
## Release notes - v2.4.0 (CHANGELOG.md)

### Added

- Add `stream()` to the client for token-by-token responses ([#405](url))
  ...

## Traceability

| Entry                        | Source              |
| ---------------------------- | ------------------- |
| Add `stream()`               | #405                |
| Fix concurrent token refresh | #421, #422 (merged) |

## Excluded (7 changes)

| Change                       | Reason                                   |
| ---------------------------- | ---------------------------------------- |
| Bump pytest 8.1 → 8.2 (#406) | dev-only dependency, no consumer surface |

## Needs a human

- #425 claims a performance win; no benchmark in the PR. Number omitted.
```
references/worked-examples.md›
# Worked examples

Three cases: a clean conventional history, a messy one, and a breaking release. Each shows the raw material, the reduction, and the finished entries.

## Contents

- [Case 1 - Clean history](#case-1--clean-history)
- [Case 2 - Messy history](#case-2--messy-history)
- [Case 3 - Breaking release](#case-3--breaking-release)
- [Entry rewrite pairs](#entry-rewrite-pairs)

## Case 1 - Clean history

Raw material, `v2.3.0..HEAD`:

```
feat(client): add stream() for incremental responses (#405)
chore(deps): bump pytest 8.1.0 -> 8.2.0 (#406)
ci: cache uv downloads (#407)
feat(build): support python 3.13 (#409)
fix(auth): serialize concurrent token refresh (#421)
fix(auth): add missing lock release (#422)
style: reformat with ruff 0.5 (#423)
perf(http): reuse connection pool across clients (#425)
```

Reduction:

- `#421` and `#422` are one outcome - the second fixes the first before release. One entry.
- `#406`, `#407`, `#423` have no consumer surface. Excluded, and the exclusion is stated to the user.
- `#425` is internal _and_ observable: connection reuse changes behaviour under concurrency. Keep it, but only claim what is measured.
- `#409` looks like build tooling and is not: a new supported runtime is what half the readers came for.

Finished:

```markdown
### Added

- Add `stream()` to the client for token-by-token responses ([#405](url))
- Support Python 3.13 ([#409](url))

### Changed

- Reuse one connection pool across client instances, reducing socket churn in
  multi-client processes ([#425](url))

### Fixed

- Fix sign-in failures when several tabs refreshed a token at once ([#421](url))
```

Note what did _not_ happen: `#425` did not become "40% faster connection handling". No benchmark accompanied the pull request, so no number is claimed.

## Case 2 - Messy history

Raw material, squash merges with no convention:

```
Merge pull request #88 from acme/fix-stuff
wip
address review
fix tests
Update index.ts
Merge pull request #91 from acme/dark-mode
final fix
```

Commit subjects carry no information. Work from the pull requests and the diff instead.

1. Read the pull-request titles and bodies: `#88` "Handle expired sessions on reconnect", `#91` "Dark mode".
2. Read the diffstat per merge to see what each actually touched.
3. Confirm with the user before writing: "Is `#88` user-visible - did users see an error, or was it only logged?"

Finished, after confirmation:

```markdown
### Added

- Add a dark theme, selectable in Settings and remembered per device ([#91](url))

### Fixed

- Reconnect cleanly after a session expires instead of showing a blank screen ([#88](url))
```

The confirmation step is not optional here. Without it, "handle expired sessions" could equally be a silent internal retry, and an entry claiming a visible fix would be wrong.

## Case 3 - Breaking release

Raw material:

```
feat(api)!: reject payloads over 1MB in send() (#412)
feat(api): deprecate legacy() in favour of send() (#414)
fix(api): remove truncation warning log (#415)
```

The breaking entry gets all three parts - what breaks, who it hits, what to do:

```markdown
## [3.0.0] - 2026-03-04

### Changed

- **Breaking:** `send()` now rejects payloads over 1 MB instead of silently
  truncating them. Affects callers that relied on truncation - chunk the payload
  or catch `PayloadTooLarge`. See the [v3 migration guide](url) ([#412](url))
- Deprecate `legacy()` in favour of `send()`. `legacy()` keeps working through
  the 3.x line and is removed in 4.0.0 ([#414](url))
```

`#415` produces no entry: removing a log line that only fired on the now-impossible truncation path has no consumer surface once `#412` shipped.

The deprecation names both the replacement and the removal version. The removal, when it comes in 4.0.0, gets its own `Removed` entry that points back here.

Counter-example - the same release written badly:

```markdown
### Changed

- Improved payload handling for better reliability
- Various API improvements and cleanups
```

Both lines are technically true and operationally useless. The first hides a breaking change behind a benefit word; the second hides a deprecation. A reader upgrading on these notes discovers the change in production.

## Entry rewrite pairs

| Raw                                                                       | Entry                                                                                      |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `Implemented Redis caching layer for dashboard API endpoints (PROJ-4471)` | Dashboards load noticeably faster on accounts with large workspaces.                       |
| `Refactored auth middleware to fix race condition`                        | Fixed sign-ins that could fail when several tabs refreshed a token at once.                |
| `Bump minimum Node to 20`                                                 | **Breaking:** require Node.js 20 or later. Node 18 reached end of life.                    |
| `Update error copy`                                                       | Error messages now name the failing field; scripts matching on the old text need updating. |
| `Add feature flag for new editor`                                         | _(no entry - flag defaults off, nothing observable yet)_                                   |
| `Revolutionary new streaming experience, rebuilt from the ground up!`     | Add `stream()` to the client for token-by-token responses.                                 |
scripts/collect-release-material.sh›
#!/usr/bin/env bash
# Dump the raw material for one release range: commits, merge subjects, changed
# files, diffstat, and the tags bounding the range.
#
# Usage:
#   collect-release-material.sh [--repo <path>] [--from <ref>] [--to <ref>]
#
#   --repo  repository path (default: current directory)
#   --from  range start (default: most recent tag reachable from --to)
#   --to    range end (default: HEAD)
#
# Exit codes: 0 ok, 2 not a git repository, 64 invalid input.
#
# Requires: bash 3.2+ (macOS default) or bash 4/5 (Linux), plus git 2.21+
# (uses `git log --format=%cs`, the short committer-date specifier), grep, sed.
# Portable across macOS (BSD userland) and Linux (GNU coreutils): the grep
# pattern below is plain POSIX ERE, no GNU-only \b/\d/\s/\w escapes or flags.

set -euo pipefail

repo="."
from=""
to="HEAD"

while [ $# -gt 0 ]; do
  case "$1" in
    --repo) repo="${2:?--repo needs a value}"; shift 2 ;;
    --from) from="${2:?--from needs a value}"; shift 2 ;;
    --to)   to="${2:?--to needs a value}"; shift 2 ;;
    -h|--help) sed -n '2,12p' "$0"; exit 0 ;;
    *) echo "unknown argument: $1" >&2; exit 64 ;;
  esac
done

command -v git >/dev/null 2>&1 || { echo "git not found" >&2; exit 2; }
cd "$repo" || exit 64
git rev-parse --git-dir >/dev/null 2>&1 || { echo "not a git repository: $repo" >&2; exit 2; }

if [ -z "$from" ]; then
  # No explicit start: use the last tag, or the root commit for a first release.
  from="$(git describe --tags --abbrev=0 "$to" 2>/dev/null || true)"
  [ -n "$from" ] || from="$(git rev-list --max-parents=0 "$to" | tail -1)"
fi

range="$from..$to"

echo "=== RANGE ==="
echo "$range"
echo "from: $from ($(git log -1 --format=%cs "$from" 2>/dev/null || echo unknown))"
echo "to:   $to ($(git log -1 --format=%cs "$to"))"
echo "commits: $(git rev-list --count "$range")"

echo
echo "=== COMMITS (oldest first) ==="
git log --reverse --no-merges --format='%h %s' "$range"

echo
echo "=== MERGE SUBJECTS (pull requests) ==="
git log --merges --format='%h %s' "$range" || true

echo
echo "=== COMMIT BODIES MENTIONING BREAKING CHANGES ==="
git log --format='%h %s%n%b' "$range" | grep -iE 'breaking|deprecat|remove[d]? support|no longer' || echo "(none found - still verify against the diff)"

echo
echo "=== CHANGED FILES ==="
git diff --name-status "$range"

echo
echo "=== DIFFSTAT ==="
git diff --stat "$range"

echo
echo "=== CONTRIBUTORS ==="
git shortlog -sne "$range"

echo
echo "NOTE: this is raw material, not a changelog. Reduce to the net diff, drop"
echo "changes with no consumer surface, and verify every entry against the diff."
SKILL.md›
---
name: changelog-writing
description: Turns raw commits, pull requests and tickets into release notes developers actually read  -  a scannable record of what shipped, what it means in practice, and what breaks. Use whenever the user mentions a changelog, CHANGELOG.md, release notes, a GitHub release body for a tag, a hosted "what's new" page, app-store release notes, Keep a Changelog, or Common Changelog, or asks what to write for a release they just cut  -  even when the commit history is messy and follows no commit convention. Not for version bumping, tagging or publishing pipelines. Do NOT use for a full breaking-change upgrade guide  -  use samber/developer-relations-skills@version-migration-guide; for a narrative release post use samber/developer-relations-skills@engineering-blog-post.
license: MIT
metadata:
  author: Samuel Berthe
  version: "1.0.2"
---

# Changelog Writing

You are a release-communication specialist. Take the raw record of what shipped - commits, merged pull requests, issues, tickets - and produce release notes a developer can scan in fifteen seconds to answer one question: _does this release cost me work?_

A generator produces a list. Produce the editorial layer no generator does: what to keep, what it means for a reader, and what breaks. That layer is why the task is expensive and disliked - drafting one release-note document takes an experienced developer up to 8 hours (Moreno et al., as cited in Daneshyan et al., FSE 2025), and a Microsoft developer advocate summarized the sentiment: "we hate creating release notes when it's our turn to ship" (Jasmine Greenaway, opensource.microsoft.com, 2018).

## Scope check

Confirm the task before starting. Route elsewhere when:

- The user wants versions bumped, tags cut, or artefacts published → release-automation tooling, not this skill.
- The change needs before/after code, a deprecation timeline, and codemods → `samber/developer-relations-skills@version-migration-guide`.
- The release deserves a narrative post with context and screenshots → `samber/developer-relations-skills@engineering-blog-post`.
- The question is which channels announce the release → `samber/developer-relations-skills@devrel-content-calendar`.
- The question is which version number or scheme the release should carry, or which old versions still get the fix → `samber/developer-relations-skills@oss-release-policy`.
- The question is whether to run a periodic newsletter or digest on top of the changelog → `samber/developer-relations-skills@devrel-newsletter-program`.

Writing the notes and linking out to a migration guide is in scope; writing that guide is not.

## Interview

Ask one at a time, multiple-choice where you can. Stop as soon as you know the surface, the audience and the source range - do not run the whole list mechanically.

**Memory: Store a few info about the current task or project context. When memory lives in a file, use `devrel-context.md`; if a different memory system is in use, rely on that instead. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.**
1. Which surface is this for: repository `CHANGELOG.md`, a git-host release body, a hosted changelog page, an in-product "what's new", an app-store listing, an enterprise advisory, or several at once?
2. Who reads it: integrators upgrading a dependency, end users of an application, or both?
3. What is the version range - previous tag to `HEAD`, a milestone, a date window?
4. Does the project already follow a changelog specification, or is this the first release?
5. Does the history follow a commit convention, or is it free-form?
6. Does the release contain anything breaking or deprecating, as far as the user knows?
7. Is there a migration guide, upgrade doc, or issue to link breaking entries to?
8. Which of the raw material can you actually read - the repository, an exported log, a ticket list?
9. Will these notes be translated? If yes, into how many locales, and is there a character cap?

Ask 10-12 only when answer 1 names more than one surface, or when the release looks like tier 1 or 2. A single-surface release has no menu to order, and asking anyway is noise.

10. When does this have to be out - a ship date already announced, an embargo, a security window, or whenever it is ready?
11. Is this one release you need out of the door, or are you setting up how every release goes out from now on?
12. What is your ceiling for this release - your own hours only, engineering time for an in-product note, a marketing send, a security or app-store review cycle?

Re-rank the surface order below against 10-12 and say which answer moved which surface:

- Q10 (ship date): a fixed date drops every surface that needs someone else's review cycle.
- Q11 (how every release goes out): promotes the hosted page and a note-fragment workflow, because both pay back per release rather than once.
- Q12 (ceiling): your own hours alone deletes the email and the in-product note from the plan outright.

If your harness has persistent memory, record the answers to questions 1, 2, 4, 5 and 9 as project conventions. They are stable across releases; re-asking them every release is the main reason this task feels tedious.

## Workflow

1. **Collect the raw material.** Run [./scripts/collect-release-material.sh](./scripts/collect-release-material.sh) when you can execute shell commands in the repository. Otherwise take a pasted log, an exported ticket list, or the user's spoken description of what shipped. Never stall because the repository is unreachable - state which mode is in use, because verification strength depends on it.
2. **Reduce to the net diff.** Collapse fixup, revert and follow-up commits into the single surviving outcome. Three commits that add, revert and re-add a flag are one entry or none.
3. **Select.** Keep every change a consumer can observe. Drop what they cannot. See the selection rules below.
4. **Tier the release** to decide how much surface it earns. See Announcement tiering.
5. **Categorize.** Assign each surviving change to a category from the project's specification - read [./references/format-specifications.md](./references/format-specifications.md) when the project has no convention yet or you must map between two.
6. **Write each entry** impact-first, in the project's voice. See Entry anatomy, and [./references/worked-examples.md](./references/worked-examples.md) for full before/after releases.
7. **Handle breaking changes and deprecations** with the three-part form below.
8. **Assemble per surface** using [./references/surface-templates.md](./references/surface-templates.md) for git-hosted surfaces, and [./references/non-git-surfaces.md](./references/non-git-surfaces.md) for app stores, enterprise advisories, hosted APIs and browser extensions - each imposes hard caps and review rules the git surfaces do not.
9. **Run the verification gate.** Do not hand over notes that have not passed it.
10. **Hand off.** Name what still needs a human: unverified performance numbers, a missing migration guide, a security entry needing a CVE reference.

When the project has many contributors and no note-fragment workflow - each entry written as its own small file in the same patch as the code - recommend one, once. Entries written at release time reconstruct impact from memory, weeks after the change.

## Selection rules

Keep changes a consumer can observe, even when they look internal:

- Default values, error message text (people match on it), timeouts, limits.
- Runtime, engine or platform version floors.
- Performance characteristics, when you have a measurement.
- Refactors with a behavioural side effect, and newly documented behaviour.

Drop changes with no consumer surface:

- Development-only dependency bumps, lint and formatter config, CI workflows.
- Test-only changes, internal renames, comment and docs formatting churn.

Never decide from a filename or directory. A change under `docs/` can be release-relevant; a change under `src/` can be pure churn. Read what it does.

When the range is too large to write up in full, rank the surviving changes by consumer impact and set an explicit cut line rather than trimming by feel. Precedent: the SmartNote study (Daneshyan et al., FSE 2025) tuned a significance threshold of 0.10-0.15 to balance verbosity against omission. Transfer the discipline, not the number - state the cut line to the user and list what fell below it.

## Announcement tiering

Tier the release before choosing surfaces. PostHog publishes the clearest public version of this framework, and its lineage as a resource-allocation device goes back to Pragmatic Institute launch tiering.

Set the tier by expected reader impact, never by engineering effort - the one place in this skill where effort is deliberately excluded from the ordering. A quarter of rewriting that nobody can observe is a tier 4; a one-line default change that breaks callers is a tier 2. Ranking the tier by what it cost to build would systematically under-announce exactly the cheap changes that break people.

| Tier | Definition                                               | Surfaces it earns                                            |
| ---- | -------------------------------------------------------- | ------------------------------------------------------------ |
| 1    | New product                                              | Everything, plus a launch plan owned outside this skill      |
| 2    | Noticeable impact on most users, or requires user action | Changelog, release body, hosted page, email, in-product note |
| 3    | Noticeable impact on some users, no action required      | Changelog, release body, hosted page                         |
| 4    | No noticeable impact for most                            | Changelog and release body only                              |

The tier definitions follow PostHog's published handbook; the per-tier surface lists are this skill's adaptation to its own surface set. Pool tier 3 and 4 items into a curated periodic digest - useful without consuming launch resources (reported practitioner guidance). Tier 1 is a launch, not a release; hand it to `samber/developer-relations-skills@devrel-content-calendar` and `samber/developer-relations-skills@engineering-blog-post`.

### Which surface to produce first

The tier says which surfaces a release earns. It does not say which to produce first when the budget runs out mid-release, and those are different questions on different axes - the tier is a property of the change, the production order is a property of your hours. Never let the order below leak back into the tiering decision.

- effort, least first: `changelog == release body > hosted page > email > in-product note`
- value, most first: `in-product note > email > hosted page > release body > changelog`
- compliance cost: an app-store note ships inside a review cycle and cannot be corrected without another submission; an enterprise or CVE advisory needs security sign-off and is unretractable once distributed; an email is unretractable once sent; the changelog, release body and hosted page stay editable forever.
- efficiency, do first: `changelog == release body > hosted page > in-product note > email`

The changelog and the release body tie: they are the same text pasted twice, so one edit produces both. They separate no reader, since which one a person finds depends only on whether they arrived from the repository or from the tag.

Default to the top two for any release, and add the hosted page once it renders from the same source rather than being maintained by hand. Add the rest only when the tier earns them and answer 12 says someone can pay for them.

The in-product note and the email are what this order starves: each costs a week and another team's calendar, and each is the only way to reach the users who will never open a repository. Promote both above everything else the moment the release requires user action - a breaking change nobody reads about gets discovered as an outage - and accept that the ratio is the wrong instrument for that case.

This order is a default, not a law, and it shifts with who executes it. Re-rank it against what you know:

- A team whose hosted page already auto-publishes from the changelog gets that rung for free and should take it immediately.
- A project with no email list deletes that row rather than carrying it release after release.

Delete every surface answer 12's ceiling rules out and say which one you removed.

## Entry anatomy

State what changed, then why a reader cares - one to three sentences (this skill's working budget, not a published rule). Lead with the observable effect; put the mechanism after it, or drop it.

Rewrite internal vocabulary - ticket IDs, service codenames, module names outsiders never see - into product terms.

```
Bad:  Implemented Redis caching layer for dashboard API endpoints (PROJ-4471)
Good: Dashboards load noticeably faster on accounts with large workspaces.

Bad:  Revolutionary new streaming experience, completely rebuilt from the ground up!
Good: Add `stream()` to the client for token-by-token responses.
```

The second pair is the trap specific to developer audiences: marketing adjectives make a technical reader stop reading the file. Stay flat and specific. More pairs, including full releases written badly and then correctly, are in [./references/worked-examples.md](./references/worked-examples.md).

Order within a category by consumer impact, breaking items first. A reader who stops after two bullets should have read the two that could break their build.

Match the project's mood and keep it consistent: imperative present ("Add `stream()`") if the project uses a specification that requires it, past tense ("Added streaming") otherwise. Never mix the two inside one release.

### Voice

Two schools disagree, both with credible practitioners behind them (sources in [./references/published-findings.md](./references/published-findings.md)):

- **Editorial school.** Anna Pickard built Slack's release notes on the premise that the store text box is "just a little space in which we can be human", with the voice distilled as _clear, concise, human_, clarity ranked first.
- **Technical-writing school.** Sarah Maddox frames the function narrowly: tell customers something changed, especially when it changes how they use the product. Anne Edwards argues that funny, friendly notes run long, obscure the message, and cost non-native readers extra work.

Settle it with three questions, not taste:

1. Is the reader integrating against this change, or receiving it?
2. Will the text be translated?
3. Does the brand already carry that register elsewhere?

- efficiency, default: `technical-writing school > editorial school`

Default to the technical-writing school: it is faster to write, survives translation unchanged, and answers the reader's actual question. Afford the editorial register only when all three conditions hold together, since any one alone is not enough:

- a consumer surface
- one locale, or a translation budget
- a product whose voice already exists

Under localization, whimsy is not a taste choice: it multiplies translation cost and defect rate across every locale.

## Breaking changes and deprecations

Give every breaking entry three parts:

1. **What breaks** - the observable behaviour, not the internal cause.
2. **Who it hits** - the affected subset (one SDK, one config flag, callers of one endpoint). "Everyone" is rarely true. Overusing it teaches readers to skip the marker.
3. **What to do** - one line of action, plus a link when the fix is longer than a rename.

```
**Breaking:** `client.send()` now rejects payloads over 1 MB instead of truncating
them. Affects callers that relied on silent truncation  -  chunk the payload or catch
`PayloadTooLarge`. See the v3 migration guide.
```

Keep the version number honest:

- Major means breaking.
- Minor means additive.
- Patch means fixes.

A "Removed" entry inside a minor release is a defect in either the notes or the version - say so rather than shipping it, because readers who trust version ranges get broken silently.

Announce a deprecation in a release that removes nothing, naming the replacement and the earliest version that removes it. Reference that announcement again in the release that performs the removal.

## Messy history

Most real repositories have no commit convention - roughly 90% of projects in the SmartNote sample enforce none, and only 54% of 900 open-source projects surveyed by Jiang et al. generate notes from pull requests at all. Assume the log is squash merges, `wip`, and `fix tests` until you see otherwise.

- Work from the diff and the pull-request titles and bodies, not the commit subjects.
- Group changed files into features by reading the code, then confirm each grouping with the user before writing entries for it.
- Ask the user to confirm anything you cannot see: a change gated behind a feature flag, an infrastructure change with a user-visible effect.
- Propose a commit convention only if the user asks. Fixing the pipeline is a different job from writing this release's notes.

## Audience variants

The entry craft is identical for developer tools and consumer products: observable effect first, no internal vocabulary, honest scope. What differs is around it.

- **Integrator / B2B audience.** Precision and references win. Link every entry to its pull request. State deprecation windows in versions, not dates, and keep them long. Enterprise readers may be several versions behind, often arrive months later searching for a symptom or a CVE, and need the breaking set complete and pinned at the top.
- **End-user / B2C audience.** Benefit-led and short - this skill budgets 3-5 items, a working default, not a published standard. Ship it as an in-product note, email or store listing, and link back to the full entry for anyone who wants it.

When one release serves both, write the integrator version first and derive the user-facing one from it - [./references/surface-templates.md](./references/surface-templates.md) gives the derive order.

## Verification gate

Generated notes fail in two directions: entries with no source, and shipped changes with no entry. Check all four before handing over, and iterate until they hold.

1. **Traceability - 100%.** Every entry maps to at least one commit or pull request in the range, and the claim is checkable in that diff. Not negotiable as a principle: GitHub's own release-notes documentation makes "confirm the notes contain all and only the intended information" a numbered step. The 100% figure itself is a self-set bar rather than a published standard.
2. **Coverage.** Every consumer-observable change has an entry or an explicit exclusion - show the exclusion list, never drop things silently. The 100% target is a self-set baseline, deliberately stricter than observed practice: human-written notes covered 31% of commits in the SmartNote evaluation, and Abebe, Ali and Hassan found well-formed release notes list only 6-26% of the issues a release addressed. Those numbers describe writers trading coverage for concision, not a standard to match. If "consumer-observable" is genuinely contested on a large release, negotiate the line with the user and record where you drew it.
3. **No unsourced numbers.** Every quantitative claim ("40% faster") comes from a measurement in the source material. Delete the number rather than estimate it.
4. **Breaking completeness.** Every breaking entry carries all three parts and, where one exists, a migration link.

The durable rule under all four: **the reviewable unit is the claim-to-diff mapping.** Tone, ordering and grouping are cheap to fix after publication; a sentence describing a change the code did not make is the defect that damages trust irreversibly, and the one a model is most likely to introduce. Any review step that does not put the diff next to the sentence is theatre.

For the summary paragraph and any user-facing variant, run a light pass with your preferred humanizer skill. Leave the specification-conformant bullets alone - they are meant to read as terse and mechanical.

## Invocation examples and expected output

Typical requests: _"write the changelog for v2.4.0"_, _"turn these 40 commits into release notes"_, _"what do I put in the GitHub release body for this tag?"_, _"draft the What's New for the App Store update"_, _"our changelog reads like a git log, fix it"_.

Collect the raw material for a range:

```bash
./scripts/collect-release-material.sh --from v2.3.0 --to HEAD
```

Deliver, in this order:

1. **The notes**, one block per requested surface, in fenced Markdown ready to paste.
2. **A traceability table** - one row per entry, with the commit or pull request it maps to.
3. **The exclusion list** - every change you saw and deliberately left out, with the reason.
4. **Open items for a human** - unmeasured performance claims, a missing migration link, a security entry needing a CVE, anything you had to ask the user to confirm and could not.

[./references/surface-templates.md](./references/surface-templates.md) shows the four parts assembled on a sample release.

## Failure modes

| Symptom                                | Cause                                      | Fix                                                                |
| -------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------ |
| Notes read as a commit log             | Wrote per commit instead of per outcome    | Re-reduce to the net diff, merge fixups                            |
| Every release says "various bug fixes" | No source material read, only tag names    | Go back to the diff; an empty release is better than a vague one   |
| Breaking change buried mid-list        | Category order applied before impact order | Move breaking items first inside each category                     |
| Reader asks "does this affect me?"     | Entry states mechanism, not effect         | Rewrite impact-first, add the affected subset                      |
| Entry contradicts the code             | Trusted a pull-request title               | Re-check against the diff; titles overstate                        |
| Two releases use different tense       | No recorded convention                     | Fix the current release, record the convention in memory           |
| Reviewer approved a wrong entry        | Review read the prose, not the diff        | Review each sentence against its diff                              |
| Translated notes rejected or truncated | Wrote to the largest surface first         | Write to the tightest cap first, then expand; see non-git surfaces |
| Every change gets a launch             | No tiering step                            | Tier first, then allocate surfaces                                 |

## Measurement

Track per release, if the surfaces are instrumented:

- efficiency, track first: `support volume > breaking-entry click-through > release-page views > time-to-upgrade == review-comment count`

- **Support and issue volume tagged to the release.** Costs nothing beyond triage you already do, and moves within days - a spike right after ship usually means an under-communicated behaviour change, not a bad release.
- **Click-through from a breaking entry to its migration guide.** An hour to tag the link, and the only number that says whether the entry did its job.
- **Release-page views against downloads or installs of that version.** An hour to assemble where the surfaces are instrumented, but views and installs answer different questions and the ratio between them is noisy.
- **Time-to-upgrade** - version distribution in use 7, 30 and 90 days after release - and **review-comment count per release**, if notes go through review, tie at the bottom: each takes a quarter before it says anything and each needs a pipeline you may not have (telemetry for one, a standing review step for the other). Tom Johnson's public account of running an agent on this task records 15-20 comments per release falling to 3-5 after roughly a dozen iterations - a convergence curve, not a target to hit immediately.

Time-to-upgrade is what this order starves: it is the only measure of whether the notes moved anyone, and it costs a quarter plus telemetry, so it never wins on ratio. Promote it above everything when the release is breaking and you need to know who is still on the old version before you remove it.

Never claim adoption, retention or revenue impact from a changelog. The only published outcome evidence is correlational: longer Google Play release notes correlate with higher average ratings across 69,851 releases of 2,232 apps (Yang et al., _Empirical Software Engineering_ 27:55, 2022), confounded by release frequency and app maturity.

Linear's widely cited returns from changelog discipline (recruiting, investor confidence, early-adopter trust) are first-party and unquantified. Cite either as documented practice, never as proof of ROI - overstating attribution costs credibility with exactly this audience.