Generate Final Release Notes
Merge every completed worksheet in <version>/done/ into a single <version>/final.md, verify nothing was lost, verify the most important entries actually match their PRs, and copy-edit the whole document.
Scope: only edit final.md
- Step 1 generates
<version>/final.mdfrom the worksheets. After that, all edits in this skill are made tofinal.mdonly. The<version>/done/result_*.mdworksheets are read-only for the rest of the run (they are read for verification, never modified). - Because every edit lands in
final.md, do not re-runmerge.pyafter Step 1 — regenerating would overwrite all the polish. Run the merge exactly once, at the start. final.mdis a single file, so subagents must not edit it in parallel (concurrent writes clobber each other). Use subagents for read-only work (fetching PRs, reviewing text) that returns findings/edits; the main agent applies edits tofinal.mdserially.- Use present-tense imperative verbs in entries ("Add", "Fix", "Support", "Remove"), never past tense.
Usage
/gen-final-notes <version>
Where <version> is the PyTorch release version (e.g., 2.13.0).
Instructions
Step 0: Validate inputs & environment
- If
<version>is missing, ask the user. - Confirm
<version>/done/exists and containsresult_*.mdfiles. - Ensure the
markomarkdown library is installed (merge.py needs it):python -c "import marko" 2>/dev/null || pip install marko
Step 1: Run merge.py (the only step that reads the worksheets to write output)
python merge.py -d "<version>/done" -o "<version>/final.md"
- The version in the title is inferred from the
--dirparent (<version>/done→<version>); pass-v <version>to override. - Recovery —
AssertionError: no nice name for <area> found: a worksheet has a module name not in merge.py'smodule_name_mapping. Add that area to the dict (write_outputinmerge.py) with a sensible display name, then re-run. Every release tends to add/rename a few areas. (This editsmerge.py, not a worksheet — allowed.) - Note which categories report "Found no commits … skipping" — expected for empty sections (e.g.
security).
After this step, do not run merge.py again.
Required merge.py formatting behavior (verify the output has all of this)
merge.py must produce output matching the finished style of prior releases (e.g. 2.12.0/final.md). Confirm the generated final.md has:
- Title + table of contents + Highlights placeholder at the top:
# PyTorch <version> Release Notes, the- [Highlights](#highlights)… TOC list, then a# Highlightssection containingTODOand the "For more details …" line. (Version inferred from the--dirparent, or-v.) - A blank line after every
#and##heading. - Multi-line entries indented properly. BC-breaking / deprecation entries with multi-paragraph bodies and fenced
Version X / Ycode blocks must have every continuation line indented 2 spaces so it stays inside the bullet. If indentation is missing, the#/###lines inside a code fence get parsed as top-level headings — the classic symptom is the Step 2 heading count coming out > 11. Handled by therender_commit()helper. - Loose-list spacing: when any entry in a module block spans multiple lines, a blank line separates every bullet in that block (otherwise the markdown renders incorrectly).
- Worksheet preamble filtered out: the
## 1.,## 2.,## 3.instruction headings from each worksheet must NOT appear —parse_result_mdskips level-2 headings matching^\d+\..
If any is missing, put the fix in merge.py so it's reproducible, then re-run Step 1. These enhancements may be carried as uncommitted changes from a prior release, so verify merge.py isn't in a pre-enhancement state (git status merge.py) and commit it once confirmed.
Step 2: Verify all commits moved from done/ to final.md
Reconcile PR numbers between the worksheets (read-only) and final.md, reusing merge.py's own parser so the check matches the real pipeline. Write and run this:
# /tmp/verify_final.py — run as: PYTHONPATH=. python /tmp/verify_final.py <version>
import re, glob, sys
from merge import parse_result_md
version = sys.argv[1]
CATEGORY_ORDER = {"bc breaking","deprecation","new features","improvements",
"bug fixes","performance","docs","security","devs"}
PR = re.compile(r'#(\d+)')
def prs(t): return set(PR.findall(t))
should_be, dropped = {}, {} # pr -> [(module,cat)] ; (module,cat)->count
for fn in glob.glob(f"{version}/done/result_*.md"):
with open(fn) as f: text = f.read()
module, info = parse_result_md(text)
for cat, commits in info.items():
for c in commits:
if cat in CATEGORY_ORDER:
for p in prs(c): should_be.setdefault(p, []).append((module, cat))
if cat not in CATEGORY_ORDER and cat != "not user facing":
dropped[(module, cat)] = dropped.get((module, cat), 0) + len(commits)
final_prs = set()
with open(f"{version}/final.md") as f:
for line in f:
if line.startswith("- [") and "](#" in line: # skip TOC
continue
final_prs |= prs(line)
missing = set(should_be) - final_prs
extra = final_prs - set(should_be)
print(f"user-facing PRs in worksheets: {len(should_be)}")
print(f"PRs found in final.md: {len(final_prs)}")
print(f"MISSING: {len(missing)}")
for p in sorted(missing, key=int): print(f" #{p} <- {should_be[p]}")
print(f"EXTRA: {len(extra)}")
for p in sorted(extra, key=int): print(f" #{p}")
print(f"NON-STANDARD categories dropped (NOT published): {dropped or 'none'}")
- MISSING must be 0 and EXTRA must be 0 at this point (right after generation). If not, investigate before continuing.
- NON-STANDARD categories flags commits under headings merge.py doesn't emit (e.g. a stray
Untopicedcategory). Non-zero counts mean those commits are silently dropped — surface them to the user. (Fixing this means recategorizing in the worksheet, which is outside this skill's scope; flag it rather than editing worksheets.) - Confirm the top-level heading count is exactly 11 (title + Highlights + 9 sections):
More than 11 means a multi-line entry's code fence orgrep -cE '^# ' "<version>/final.md" # expect 11#line leaked out as a heading — fix that entry's indentation directly infinal.md.
Run this reconciliation before the editing steps. After Steps 3–4 you may intentionally delete/alter entries, so a later MISSING result is expected only for those deliberate changes.
Step 3: Verify BC-breaking / deprecation / new-feature entries against their PRs
These three sections get the most reader scrutiny, so verify each entry's summary actually matches what the PR did.
- Extract every entry (and its PR number(s)) from the
# Backwards Incompatible Changes,# Deprecations, and# New Featuressections offinal.md. - Fan out to parallel subagents (Agent tool,
general-purpose) for the read-only fetch-and-compare work — this does not editfinal.md, so it parallelizes safely. Split the entries into batches; give each agent the exact summary text + PR number(s) and these instructions:- Fetch each PR from
pytorch/pytorch— use the github-query skill (.llms/skills/claude-templates/github-query/SKILL.md), orgh pr view <n> --repo pytorch/pytorch --json title,body,urlandgh pr diff <n>. (If the github-query skill lacks credentials, it can be bootstrapped from the authenticatedghtoken.) - Judge: does the summary correctly describe the actual change (API names, direction, behavior)? Are code examples and version references correct? For a 2.X release, "Version 2.(X-1) → 2.X" framing is expected; flag references to unrelated versions.
- Return per entry:
OK/MISMATCH/UNVERIFIABLEwith a one-to-three-sentence justification, and a collected MISMATCH list. Agents report; they do not edit.
- Fetch each PR from
- Common failure modes (all seen in practice): wrong warning type (
DeprecationWarningvsUserWarning), a stale version number copied from the PR body, and — most important — a wrong PR linked entirely (the summary describes something the PR didn't do). Cross-check a suspected wrong link against<version>/commitlist.csv: if the PR's row there describes something different and the PR is already used correctly by another entry, the entry is bogus. - The main agent applies fixes to
final.md(serially): correct the wording, or delete a bogus entry. Flag every change to the user with the PR number and what was wrong. For anything you can't resolve confidently (e.g. a claim that maps to no PR in the release), do not silently substitute a PR — report it and ask.
Step 4: Proofread, add code formatting, remove internal references
Copy-edit all published entries in final.md.
Because final.md is one file, do review in parallel, apply serially:
- Fan out read-only reviewer subagents, each assigned a section or line-range of
final.md(the big sections — Improvements, Bug Fixes — can be split further). Each agent reads its range and returns a list of precisebefore → afteredits (exact original substring + replacement). Agents must not edit the file. - The main agent applies those edits to
final.mdwith the Edit tool, one at a time.
Give each reviewer these rules:
Goal 1 — inline code formatting: wrap bare code identifiers in backticks (function/method names, class names, API names, argument names, dtypes, attribute names, file paths, env vars, config flags, C++/Metal symbols, decorators, dotted module paths). Signals: snake_case, CamelCase, dotted.paths, (), ::, leading underscores, known API names. Do not wrap ordinary English or acronyms/proper names (NCCL, ROCm, HIP, SDPA, DDP, etc.). Never double-wrap.
Goal 2 — grammar: fix subject-verb agreement, articles, plurals, typos, awkward/run-on phrasing. Keep present-tense imperative style. Do not change technical meaning.
Goal 3 — remove internal references: strip internal-only content from entries:
- Bracketed internal tags at the start of a title:
[Optimus],[BE],[codemod], team/infra tags, and area tags that merely duplicate the section (seecategorize-miscategorizedStep 7a for the full list). Reword so the entry describes what the change does in user-facing terms. - Internal codenames/systems (e.g. "Optimus" is the internal name for the pre-grad batch-fusion passes; "APS", "IGCTR", "AFOC", "MAST" are internal). Replace with the public concept or remove.
- Phabricator/Diff/task IDs (
D#######,P#######,T######),fbcode,Differential Revision, internal URLs.
Goal 4 — drop private-API-only entries. PRs whose subject is a private API — an op/function/attribute whose name begins with _ should generally not appear in the notes. Exceptions to KEEP:
- the entry describes a user-visible bug fix that merely mentions a private symbol as implementation detail (keep — reword to foreground the user-visible effect if needed);
- the private op backs a public feature named in the same entry (e.g.
_fused_adagrad_backingAdagrad(fused=True),_sample_dirichlet/_dirichlet_gradbackingtorch.distributions.Dirichlet); - semi-public families in common use (e.g.
_foreach_*). Present the removal candidates to the user before deleting.
Goal 5 — BC-breaking / deprecation completeness & ordering. Each BC-breaking entry needs a summary + rationale + before/after code (or clear migration); each deprecation needs before/after and should name the full replacement API path (e.g. not just "use export" but torch.onnx.export(..., dynamo=True)). Flag any bare one-liner in these two sections. Order the entries most-impactful first (within each module/section), not alphabetically — these are the highest-scrutiny sections.
Goal 6 — group related PRs & clarify scope. Combine several PRs that build out one feature/area (e.g. a family of stable-ABI ops, or one op across multiple PRs) into a single bullet with all PR links, rather than many near-duplicate lines. If a bullet's scope isn't clear from its module heading (a reviewer's "is this ONNX/export specific?"), reword so the reader can tell what it applies to.
Strict rules for every reviewer:
- Only touch bullet text under the published category sections. Never touch PR/issue links or numbers, section/module headings, the TOC, the Highlights block, or anything inside existing backticks or fenced code blocks.
- Copy-editing only — do not reword/summarize substance (except when removing an internal tag requires a light reword). When unsure, leave as-is.
- Goals 4–6 are judgment-heavy (removing/merging/reordering entries) — flag those changes to the user rather than doing them silently.
Note: most internal-looking commits already lived under
not user facingin the worksheets and never reachedfinal.md, so you generally won't see Diff IDs or[Dependabot]/[CI]bumps here. Only fix internal refs that actually appear infinal.md.
Step 5: Final integrity check (on final.md)
Check for unbalanced inline backticks introduced by editing (the awk toggles a flag on code-fence marker lines — after stripping indentation — skips fenced content, then flags any remaining line with an odd number of backticks):
awk '
{ line=$0; sub(/^[ \t]+/,"",line) }
line ~ /^```/ { infence=!infence; next }
infence { next }
{ n=gsub(/`/,"`"); if (n%2==1) print NR": "$0 }
' "<version>/final.md" # any output = a broken inline-code span to fix
Also re-check the heading count (grep -cE '^# ' "<version>/final.md" — still 11 unless you intentionally added/removed a section). Optionally re-run the Step 2 reconciliation, remembering that MISSING should now list only entries you intentionally deleted in Step 3.
Step 6: Highlights (optional, last)
# Highlights in final.md is a TODO placeholder with no worksheet source. If the user wants it filled, edit it directly in final.md. Prior releases (e.g. 2.12.0/final.md) use an HTML <table> of one-line highlights; match that style.
Step 7: Report
Tell the user:
- That
final.mdwas generated and the Step 2 reconciliation passed (X/X PRs, 0 missing, 0 extra). - Any Step 3 PR-content mismatches found and how each was resolved (fixed / deleted / needs-your-input).
- A summary of Step 4 edits (roughly how many, notable internal-reference removals).
- Do not commit unless asked.
Key references
| File | Why |
|---|---|
merge.py |
The merge script; module_name_mapping + category_order in write_output |
final_template.md |
Canonical section order and heading style for the final notes |
<version>/commitlist.csv |
Source of truth for which PRs belong to the release; use to confirm wrong-link suspicions |
.claude/skills/gen-release-notes/SKILL.md |
Entry format, category guidelines, GraphQL batch-fetch pattern |
.claude/skills/categorize-miscategorized/SKILL.md (Step 7a) |
Full list of internal title-prefix tags to strip |
A prior <version>/final.md (e.g. 2.12.0) |
Reference for finished formatting incl. Highlights table |