Post a "what's changed since the last release" recap to the release Slack channel for final QA. Groups merged PRs by author and classifies each as feature / bug fix / task. Use when the user wants a release recap, a changelog since the last tag, or to prep QA before cutting a release from main.
76
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Build a human-readable recap of everything merged to main that is not yet in the
latest release tag, and post it to the team's release Slack channel for final QA.
The recap is grouped by author and each change is classified as feature, bug fix, or task. This is on-demand: run it when someone asks for a release recap or wants to QA before cutting a release.
Linear ticket tracking ("which tickets are targeted for this release") is out of scope for now — punted to a later iteration. This skill only covers what's changed.
Run the gather script from the repo root. It fetches tags + origin/main, finds the
latest tag, and lists every merged PR in the range <last_tag>..origin/main:
python3 .agents/skills/release-digest/scripts/gather_changes.py > /tmp/release_digest.jsonKey design point: it uses the git commit range as the source of truth, not merge dates. A PR merged the same day a tag was cut can already be in that release, so a date filter would over-report. Trust the range.
The JSON contains:
last_tag — the most recent release (e.g. v1.0.3)head_ref — origin/main (or main if no remote ref)pr_count, commit_countprs[] — {number, title, author_name, author_login, url, labels, additions, deletions, body}direct_commits[] — teammate commits pushed straight to main, no PR — {sha, title, author_login, author_name}excluded_prs[] — PRs/commits from non-teammates, dropped from the recap. PR entries
have a number; direct-commit entries have number: null and rely on title.unresolved_prs[] — PRs whose gh pr view failed ({number, error}). If non-empty,
the change set is partial — tell the user which PRs couldn't load before posting.Only the five teammates in the script's TEAM map (Sam, Leonard, Daniel, Mike,
Steve) are included in the recap. PRs from anyone else are filtered into
excluded_prs rather than itemized. To add a teammate, edit TEAM in the script.
If pr_count is 0 AND direct_commits is empty: tell the user main is even with
<last_tag> — nothing to QA or release yet. Do not post to Slack. Stop here. (Direct
commits alone, with no PRs, still count as unreleased changes worth a digest.)
Read /tmp/release_digest.json. Treat every item in prs[] and direct_commits[]
as a change to recap (direct commits are real work that shipped — just with no PR
number). Classify each into one of three buckets:
Adding models is a Task, not a Feature. Kiln ships new model and provider entries
constantly — "Add to model list", "Add for ", backfilling a
model on a provider, re-enabling/un-recommending a model, version bumps to a model
entry. These are catalog/config maintenance, so they go in Task even though the
title starts with Add/Enable. Only promote to Feature if the PR adds a genuinely
new capability around models (e.g. a new thinking-level control, a new extraction
modality, a new provider integration type) rather than just registering a model_id.
Decision order (use the first signal that's decisive):
bug/fix → Bug fix; feature/enhancement → Feature;
chore/docs/refactor/ci → Task. (Kiln PRs are usually unlabeled, so this
rarely fires — fall through to the title.)Add/Enable verb.fix:/Fix → Bug fix;
feat:/Add /Enable (a new capability) → Feature; chore:/refactor:/ci:/
docs:/Bump /Remove /Deprecate /Version bump → Task.body if still unclear) — ask "does
this give the user a genuinely new capability (Feature), fix something that was
broken (Bug fix), or is it routine/maintenance (Task)?""Task" is the residual bucket, not a guess: a change lands there only because it
is positively not a new capability and not a fix — e.g. Deprecate Models,
min age for dependencies, Version bump, new draft readme, fix typos. When a
change is genuinely ambiguous between Feature and Bug fix, prefer the one the title
leans toward; only fall to Task when it's truly neither. This is a judgment call made
per item — surface it to the user before posting so they can re-bucket anything.
Then group by author (author_name). Within each author, list changes by PR
number, newest first; put any direct commits (no PR number) last. The feature / fix /
task split is not shown per line — it only feeds the tally at the bottom.
First, ask the user for the new release name — the version being cut (e.g.
v1.0.4). This is the release these changes are going into; it isn't tagged yet, so
it can't be derived from git. Use their answer as <new_release> in the title.
Use this shape (Slack mrkdwn — *bold*, not **bold**):
:rocket: *v1.0.4 Release Status* :rocket:
*12 changes since v1.0.3*
_<one-to-two sentence plain-English recap of the headline changes>_
*Daniel Chiang*
• Add deprecated flag to embedding models (#1466)
• Add deprecated flag to reranker models (#1465)
*Sam Fierro*
• Dedup frozen prompts and bake type into their name (#1501)
*Steve Cosman*
• Add Jinja2 input transform on RunConfigs (#1433)
• Version bump in lock and desktop app version check
:bar_chart: *7 features · 3 fixes · 2 tasks*
:information_source: _1 PR from a non-teammate: #1456 (ianjamesburke)._Rules:
:rocket: *<new_release> Release Status* :rocket:, then a blank line.*<N> changes since <last_tag>*._..._ around the
whole paragraph) — the "what did we do" summary in plain English, not just a list.
Compose it so the italics survive: write it as a single line with no hard line
breaks (wrap is fine, but don't insert \n mid-paragraph — newlines break the
italic span in Slack), and use no literal _ anywhere inside (an underscore
would close the italics mid-paragraph — rewrite e.g. snake_case/model_id or any
underscored identifier without it).• <title> (#<number>). Keep the
title; trim trailing noise.• <title> with nothing in
parentheses (e.g. • Version bump). List them under their author, after the PRs.:bar_chart: tally line: :bar_chart: *<X> features · <Y> fixes · <Z> tasks*.excluded_prs is non-empty, an italic :information_source: line noting the
excluded non-teammate changes (so the omission is visible, not silent). Reference a
PR entry by #<num> and a direct-commit entry (where number is null) by its
title — never print #None. E.g.
:information_source: _<N> change(s) from non-teammates excluded: #1456 (ianjamesburke)._<url|#1501>), but plain #1501 is fine — keep it readable.Show the composed message to the user and get confirmation before posting (it's an outward-facing post to a shared channel).
Post to #release (channel ID C0BDGTGBL4Q) — the team's release channel. This is
the default target; only post elsewhere if the user explicitly names a different
channel. (If the ID ever fails to resolve, fall back to slack_search_channels with
query release to re-find it.)
Only post after the user has confirmed the composed message in Phase 3. Post with
mcp__claude_ai_Slack__slack_send_message. After posting, give the user the channel
name and a link/confirmation.
/tmp/release_digest.json writtenpr_count is 0, reported "nothing unreleased" and stoppedff39535
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.