Use when generating a Warp pre-release or release-candidate audit report from Towncrier fragments and release history.
61
72%
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
Fix and improve this skill with Tessl
tessl review fix ./.claude/skills/warp-release-audit/SKILL.mdGenerates a markdown audit of the upcoming Warp release for keep/defer decisions. Runs in two modes (auto-detected): a pre-release spot-check while work is still landing on main, or a release-candidate readiness review after the release branch is cut.
Output: a single markdown report, filed according to the destination chosen in Phase 1:
gh is available and authenticated): stable filename warp-<version-string>-<prerelease|rc>-report.md, stable description Warp <version-string> <Pre-Release|Release Candidate> Report. Later runs against the same version revise the same gist in place; prior versions are preserved in the gist's git history.gh unavailable, or opt-in when gh available): dated path at $(git rev-parse --show-toplevel)/warp-<version-string>-<prerelease|rc>-report-<YYYY-MM-DD>.md. Not auto-committed; user moves, shares, or deletes as desired.Inputs inferred from repo state: target version from VERSION.md, checked against warp/config.py; base from the latest previous-minor tag; head from upstream/release-<target> when present, otherwise upstream/main.
References to load on demand: use references/destination-rules.md in Phase
1; references/classification-rules.md in Phases 3-5;
references/language-review-examples.md in Phase 5a; and
references/report-template.md plus references/render-rules.md in Phase 6b.
Read the version string from VERSION.md, Warp's canonical version source, and verify that warp/config.py declares the same value. If either value is missing or unparsable, or if they differ, show both raw values and wait for explicit user confirmation instead of selecting one. (warp/__init__.py aliases config.version and is not an independent version source.) Parse the confirmed version to extract the target minor (e.g., 1.13.0dev0 → target 1.13) and determine the report mode:
"rc" (e.g., 1.13.0rc1) → RC mode: this is a release-candidate readiness report."dev" (e.g., 1.13.0dev0) → Pre-release mode: this is an early-stage audit of unreleased work.Parse the target version into integers (major, minor) (e.g., 1.13.0dev0 → (1, 13)). Enumerate previous-minor tags:
git tag --list 'v<major>.<minor - 1>.*' --sort=-v:refnameTake the first result as the base candidate. Major-boundary fallback: if minor == 0 (e.g., target (2, 0)), there is no v2.-1.* line. In that case, enumerate tags from the previous major instead: git tag --list 'v<major - 1>.*' --sort=-v:refname, and take the highest as the base candidate. Always use integer math on the parsed (major, minor) tuple; never treat the dotted version string as a float (1.13 - 0.1 is not 1.12).
Probe for the head:
git rev-parse --verify upstream/release-<target>If this succeeds, head = upstream/release-<target> and this is also a strong signal for RC mode (the branch-cut has happened). Otherwise try origin/release-<target>; otherwise head = upstream/main (falling back to origin/main, then main) which is pre-release mode. Record whichever fallback was used for the report header.
Reconcile mode from version string and head:
Read references/destination-rules.md, probe gh, and look up matching
gists as specified there.
Present the resolved refs and destination options, then wait for explicit user confirmation. Record the selected destination token for Phase 6c and do not continue until the user confirms.
Resolve <skill-dir> to the directory containing the currently loaded SKILL.md for this skill (for example, <repo>/.claude/skills/warp-release-audit or <repo>/.codex/skills/warp-release-audit). Do not hardcode .claude or .codex; the skill content must work from either tree. Run the commit-list tool via uv run as a single command:
uv run "<skill-dir>/scripts/list_commits.py" \
--base <base-ref> \
--head <head-ref> \
--report-date "$(date +%F)" \
--main-ref <resolved-main-ref>Use $(date +%F) literally so the shell supplies today's date; do NOT substitute a date you generated yourself, since training-time bias can produce stale days_since_merge / days_in_main values. Capture stdout as the commit_list_json.
Read changelog/README.md and enumerate fragment paths from <head-ref>, not from the current working tree:
git show <head-ref>:changelog/README.md
git ls-tree -r --name-only <head-ref> -- changelogExclude changelog/README.md. Record path, identifier, category, optional counter, and full content. Numeric IDs are GitHub issues; +slug IDs are
orphans with no generated issue link.
Render those exact fragments with pinned Towncrier in a temporary detached worktree, keeping the selected ref's configuration and fragment set together:
audit_worktree=$(mktemp -d)
git worktree add --detach "$audit_worktree" <head-ref>
trap 'git worktree remove --force "$audit_worktree" 2>/dev/null || true' EXIT
draft_status=0
(cd "$audit_worktree" && uvx --from towncrier==25.8.0 towncrier build \
--draft --version <target-X.Y.Z> --date "$(date +%F)") || draft_status=$?
git worktree remove --force "$audit_worktree"
trap - EXIT
test "$draft_status" -eq 0Capture ## [<target-X.Y.Z>] as the pending release view. Always remove the temporary worktree, including after failure. A draft never modifies the ref.
Parse the six rendered subsections. For every bullet, extract:
GH-(\d+) over the bullet text. Dedup.**Breaking:** in the bullet.Read CHANGELOG.md at <head-ref> separately as historical release data.
Do not infer pending content from its newest released section.
Read docs/user_guide/compatibility.rst and design/deprecations.md at
<head-ref>. Treat them as the repository's canonical definitions of the
documented stability boundary and planned deprecation schedule. If either
file is absent, record that the policy source was unavailable and avoid
inventing stability promises from file names, symbol visibility, or presumed
downstream use.
Build the commit ↔ pending-entry join on GH-ref overlap and fragment provenance:
commit_list_json) whose gh_refs intersect.git log --reverse --follow --format='%H|%s|%cs' \
<base-ref>..<head-ref> -- <fragment-path>Do NOT build an audit trace of unmatched commits. The old "commits without changelog entries" appendix adds noise without value. Commits that do not map to a pending entry are not surfaced in the report.
Before labeling any compatibility change, record four facts:
A source or ABI difference is not automatically release risk. Raise an alarming finding only when the change affects a reachable surface covered by the compatibility policy, or when base-versus-HEAD execution confirms a supported user-visible behavior change. Direct use of internal native headers is in scope only if the current policy explicitly promises that interface.
For each pending Added entry, extract the named symbol(s) (text in backticks matching wp.*, @wp.*, or patterns like ClassName).
For EACH named symbol, check if it existed at base:
wp.X: look at git show <base>:warp/__init__.py — does it re-export X?wp.submodule.X: read the submodule source at base.@wp.kernel, @wp.struct, @wp.func decorator-style: check warp/__init__.py at base.warp/_src/builtins.py): check if the builtin was registered at base via git show <base>:warp/_src/builtins.py | grep '"<name>"'.Classification:
If an entry mixes new and pre-existing symbols, split it: genuinely new symbols go to New API and extensions go to Changes to Existing API.
For each Python-scope symbol confirmed as new:
warp/__init__.py re-exports at HEAD.ast.parse the source module; find the FunctionDef / ClassDef.ast.get_docstring(node).For each kernel-scope builtin confirmed as new:
warp/_src/builtins.py at HEAD. Find the add_builtin("<name>", ...) call.add_builtin() registration call itself. Instead, SYNTHESIZE a Python-function-style signature from its arguments:
input_types= or inputs= (or positional args) becomes a function parameter with type annotation.value_type or output arg (or return value) becomes the return annotation.tile(dtype=...), vector(length=N, dtype=...)) as they appear — this is the user-facing form.builtin_name(inout: tile(dtype=Float, shape=tuple[int, ...])) -> Noneadd_builtin("builtin_name", input_types={"inout": tile(...)}, ...)doc= parameter of add_builtin(). Render verbatim as a blockquote.<symbol> in source — verify entry names a real public symbol." Do not fabricate a header like wp.*(no symbol)*. Use the entry's natural subject as the section title.wp.* name.wp.*,
wp.(something), or another stub pattern.For each pending entry in Changed / Removed / Deprecated (plus any "capability extension" entries routed here from 4a):
diff block showing - and + lines.**Breaking:**: skip the diff block; include the backing commit's URL and the full CHANGELOG text.- line; omit +.Deprecation-window lookup for Removed entries. For every Removed entry (and every Changed entry whose prose describes a removal), search CHANGELOG.md for the matching prior Deprecated entry:
CHANGELOG.md beneath the Towncrier
insertion marker, top-down, for a ### Deprecated bullet that names the
same symbol(s) OR the same GH ref. The FIRST such entry is the latest match
because CHANGELOG.md is reverse chronological.1.11.0), (b) the full Deprecated entry text.Deprecated in X.Y.Z; removed here. Do NOT fabricate a version if no prior entry is found.Removal risk. Apply the stability and reachability gate before assigning an alarm level:
🚨 Policy: removed without prior deprecation in Breaking Changes. A
simultaneous deprecate-and-remove is the same policy violation.Include the deprecation window in every rendered removal detail and in the Changes-to-Existing-API row. For an unplanned removal that also appears in Breaking Changes, include it there as well.
Independently of CHANGELOG content, compare the committed public API at base and
HEAD. Read references/classification-rules.md for module selection, public-stub
coverage, signature compatibility, and manual fallbacks. Always include warp
and add public submodules named by pending entries:
uv run "<skill-dir>/scripts/diff_public_api.py" \
--base <base-ref> \
--head <head-ref> \
--module warp \
--module <public-submodule>Repeat --module per resolved submodule and omit the placeholder when there are
none. Capture stdout as api_diff_json.
For each api_diff_json.changes[] item:
diff and a concrete
before/after call.Removed symbols require risk classification, not a marker check. Do not
flag a Removed entry merely for lacking **Breaking:**. Classify it using the
stability, reachability, and deprecation-window rules above. Only unplanned
removals of supported stable surfaces enter the alarming Breaking Changes
count; planned and experimental removals use their respective labels.
Independently inspect deprecated compatibility paths for newly introduced exceptions. This catches breaks in old import paths or wrappers that still exist only to warn and forward callers to a replacement API.
deprecated, deprecation, compat, compatibility, backcompat, or a deprecated namespace name.Deprecated, Removed, or migration-style CHANGELOG entries.raise statements, changed exception types or messages, stricter guard conditions before forwarding/delegation, removed try/except fallbacks, and import-time errors added to a path that previously warned and delegated.semantic change and description new exception in deprecated compatibility path.Inspect every pending entry and backing commit whose diff plausibly changes supported, user-observable behavior. Use changes under code generation and native implementation paths as high-risk hints, not an exhaustive scope; Python runtime, serialization, interop, and platform-support changes may also require verification. Conversely, touching a high-risk path is not evidence that an internal symbol is public.
For each candidate:
Skip if the commit is already mapped to a pending entry carrying **Breaking:** (it's already in Breaking Changes).
Read the commit's diff: git show --stat <sha> then git show <sha> for small diffs, or read specific hunks for large ones.
Triage into one of three buckets:
Verification by running code. For ambiguous candidates, the assistant must actually run Warp at both base and HEAD and compare observable output:
uv run build_lib.py --quick (~2-4 min) if not already built. If a build is already current, skip.git worktree add is useful here to avoid disturbing HEAD. Alternatively, git-stash + checkout + build + stash-pop./tmp/ (never commit). Example for a numerical-algorithm change: a small kernel that applies the changed op to a fixed input and prints the result. Example for a codegen change: a kernel whose emitted code should differ; compare via wp.get_module().save_kernel_source(...) or equivalent introspection.Never punt with "please verify". If a candidate is ambiguous, either verify it by running code or drop it. Unverified flags do not land in the report.
For each confirmed breaking change, add an entry to the Breaking Changes section with:
Make every migration example self-contained: define the affected kernel, struct, argument, or setup needed to understand the call. Prefer a complete example from existing tests or documentation. If neither is suitable, write a minimal reproduction and verify it at base and HEAD. Do not synthesize an abstract call that omits the declaration responsible for the behavior.
Never produce an unexamined list of candidates. Every Breaking Changes entry either has explicit CHANGELOG backing, a signature-diff detected shape change, a public stub removal, a new exception in a deprecated compatibility path, or assistant-verified behavioral evidence.
Some symbols are shipped with an explicit **Experimental** marker in the CHANGELOG entry that introduced them. Changes to those symbols do NOT carry the same stability contract as changes to stable APIs: the whole point of the marker is to reserve the right to break them. The report must reflect that so the release manager does not over-weight the concern.
For each entry in Breaking Changes, Changes to Existing API, and Removed (as collected through 4a–4g), determine whether the affected symbol or feature area is currently experimental:
CHANGELOG.md beneath the Towncrier
insertion marker for bullets that both carry **Experimental** (bold, with
or without trailing colon) AND name one of the candidates from step 1. Also
match via GH ref if the current entry and a prior experimental entry share a
GH number.wp.Foo out of experimental", "Stabilize wp.Bar"), the symbol is still experimental. Record: (a) the release version that introduced the symbol as experimental, (b) the full text of that introduction bullet... experimental / Experimental: / experimental_api / @experimental annotation on the symbol's declaration. If present, treat as experimental regardless of CHANGELOG signal.Tag every matched entry internally as experimental=True. Do not alter the CHANGELOG text itself.
How the tag changes rendering:
Experimental rather than Yes,
even when the change is technically source-breaking.Experimental: without a warning glyph and state
the advertised stability level rather than leading with migration urgency.Never drop the entry. Experimental treatment changes placement and tone, not visibility. A removed or signature-changed experimental symbol still appears in Changes to Existing API.
Read references/language-review-examples.md. For EACH rendered pending entry,
apply LLM judgment and identify its source fragment path(s):
gh --version and gh auth status both succeed, run gh issue view <num> --json title,body per ref and compare issue title to entry topic. Skip silently if gh unavailable.warp._src.*), C++/CUDA type names (launch_bounds_t, tile_register_t), private identifiers.Record flagged entries. Keep the FULL entry text in the audit table — do not truncate.
Pre-release mode (resolved.head.sha == resolved.main_ref.sha). Every commit's main equivalent is itself, so days_in_main == days_since_merge and the bake distribution would just restate the age histogram. Render an "Age distribution" table from days_since_merge (same 🟢/🟡/🟠 thresholds), label the column "Days since merge", and skip both the "Bake distribution" table and the anomaly banner. There is no meaningful "didn't bake on main" condition when head IS main.
RC mode (resolved.head.sha != resolved.main_ref.sha). Partition commits by main_match_state:
state == "unique": bucket by days_in_main into 🟢 (>14 days), 🟡 (7–14 days), 🟠 (<7 days).state == "missing": subject not present on main_ref. Inspect the commit before
calling it a bake gap. Exclude expected release bookkeeping when the diff only
synchronizes release metadata or release-specific documentation and does not
change user-facing product or distribution behavior. Use repository policy,
release history, and the semantic content of the diff; subject wording and a
fixed path allowlist are hints, never the deciding rule. Count the remaining
missing commits separately and, if non-zero, fire the ⚠️ banner in the report
header. Optionally summarize excluded bookkeeping in a quiet footnote.state == "ambiguous": subject appears more than once on main_ref, as with
reverts or replayed commits. The commit IS on main; the script just could not
pick a single canonical occurrence. Render a separate row labeled
"⚪ ambiguous main match: K commits" and do not fire the banner.If resolved.empty_main_index == true, the bake table is meaningless: every commit will resolve as missing. Render only days_since_merge stats and surface the empty-main-index condition prominently in the report header (e.g., "main_ref <ref> had no commits in <base>..<main_ref> — main bake unverifiable") instead of firing the routine bake-gap banner.
Never compare a days_in_main of null (emitted for both missing and ambiguous) to the numeric thresholds.
Before filling the template, synthesize the {{HEADLINE_SUMMARY}} section. This is the only part of the report that requires qualitative judgment rather than mechanical rendering. Everything else flows from the cross-reference and classification work in Phases 3-5; this step picks what a reader should know first.
What the summary is (and isn't):
How to pick items. Select 4 to 8 bullets from the material already analyzed (New API, Breaking Changes, Changes to Existing API, Behavioral & Support, Removed). Use LLM judgment. An item belongs in the highlights if at least one of these is true:
An item does NOT belong in the highlights if any of these is true (drop even if the pending entry is present):
Aim for 4-8 bullets total. Fewer than 4 almost always means you missed a theme; more than 8 means you listed changes instead of highlights.
How to write each bullet. Each bullet leads with a bold 2-6 word headline,
then a colon, then one sentence of rationale that explains what it is and why
it matters. Prepend ⚠️ Breaking: only for alarming breaking changes, use
Experimental: for headline-worthy experimental work, and append a bake hint
(🟠 N days bake.) when the headline item's minimum bake is under 7 days.
Example:
- ⚠️ Breaking: (GH-NNN): .
Lead with the unlock, not the mechanism. State the new user capability and why it matters before listing supporting API names or implementation details. For a new artifact, format, protocol, or cross-language boundary, name it and state the workflow it enables.
GH refs MUST be hyperlinks, always. Every GH-NNNN in a highlight bullet is a markdown link to https://github.com/NVIDIA/warp/issues/NNNN. This applies even when a single bullet combines multiple GH refs. Do NOT use shortcuts like (multiple GHs), (GH-1287, GH-1298, ...) in plain text, or (see CHANGELOG). If the bullet covers six issues, render all six as individual links, either inline (([GH-1287](...), [GH-1298](...), [GH-1335](...))) or in a trailing parenthesis at the end of the headline. There is no upper limit on link count; a reader can scan links but cannot resolve plain numbers.
Experimental softening. If Phase 4h tagged an entry as experimental and it
is otherwise highlight-worthy, use Experimental: rather than a warning or
breaking prefix. Lead with what changed and its stability bar, not migration
urgency.
Open the summary with a 2-3 sentence intro paragraph that names the shape of the release in plain language. This sets the tone for everything below it. Do not stuff the intro with numbers or repeat the bake distribution.
Output style rules apply here too. No em dashes. No skill-internal terminology ("Phase 4f"). No "end of summary" markers. The summary reads as release-note input, not as an audit artifact.
Read references/report-template.md. Fill in every {{PLACEHOLDER}} marker, including the {{HEADLINE_SUMMARY}} produced in 6a.
Read references/render-rules.md and apply every rule there: URL shapes, signature + docstring code-block forms, table column specs, audit-appendix conditional, and the output-style hard constraints (no em dashes, no skill-internal terminology, no terminal markers, every GH ref hyperlinked, no Phase names).
Read the filing instructions in references/destination-rules.md and act on the
destination token confirmed in Phase 1. Return the report location, headline
counts, and revision-history note when applicable.
\bGH-(\d+) — word boundary prevents matching inside other identifiers.**Breaking:** (with the colon).^changelog/(?:\+[A-Za-z0-9][A-Za-z0-9-]*|\d+)\.(added|removed|deprecated|changed|fixed|documentation)(?:\.\d+)?\.md$.## [<target-X.Y.Z>] with optional trailing date.### Added, ### Removed, ### Deprecated, ### Changed, ### Fixed, ### Documentation.wp.X, wp.X.Y, @wp.X, or bare ClassName (capitalized identifier). The FIRST backtick-quoted symbol in the bullet is usually the primary subject.Added entry names a symbol not resolvable at HEAD: render with a ⚠️ note; do NOT emit synthetic wp.* stub names.upstream/ remote missing: substitute origin/. Note the substitution in the report header.<head-ref>." Do not substitute the newest historical release section.gh installed but not authenticated: treat as gh unavailable; skip gist matching and gist prompt; add one-line chat note.238de60
Also appears in
since Sep 11, 2026
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.