| name | git-wrapup |
| description | Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts) and an annotated tag. Verify, commit, tag. Stops at "committed and tagged locally" — no push, no publish. The release-and-publish skill picks up from here. Distilled from the git_wrapup_instructions protocol.
|
| metadata | {"author":"cyanheads","version":"1.11","audience":"external","type":"workflow"} |
When to use
Working-tree or staged changes are ready to ship as a new version. This skill lands them as a stack of logical commits — the work grouped by concern, topped by a release commit (version + changelog + tree) — with an annotated tag. It does NOT push or publish — that's a separate step (release-and-publish).
Common triggers:
- Feature work, bug fixes, or dependency updates are done and tested
- A maintenance or polish pass left changes in the working tree
- An orchestrator says "wrapup this project"
Pre-wrapup gate checklist
Every item must be true before starting wrapup. Committing means releasing — a commit only happens when the work is ready to ship, not just "the edits are done." Each item is a goal to verify.
If any gate is red, fix it before proceeding. This skill re-verifies build + tests in step 6, but starting wrapup on a broken tree wastes the version number and creates a revert-or-amend situation.
Steps
1. Review the diff
Understand what's about to ship before touching version numbers:
git status
git log v<latest-tag>..HEAD --oneline
git diff --stat
git diff
If the working tree is clean AND there are no commits since the last tag, halt — nothing to wrap up.
2. Determine the new version
Read the current version from package.json. Apply the intended bump:
| Bump | When |
|---|
| patch | Bug fixes, dependency updates, metadata changes, docs |
| minor | New tools, new features, new env vars, behavioral changes |
| major | Breaking changes to tool schemas, removed tools, incompatible config |
Default to patch unless the diff clearly warrants minor or major.
3. Bump version everywhere
Every file that declares a version must be updated. Skip any file that doesn't exist in the project. For @cyanheads/mcp-ts-core projects:
package.json — version
server.json — top-level version AND every packages[].version entry
manifest.json (if present) — version. Verify name is the bare package name (e.g. bls-mcp-server, not @cyanheads/bls-mcp-server)
README.md — version badge
CLAUDE.md / AGENTS.md — if they pin a version string
Dockerfile — OCI labels if they pin the version
Catch stragglers (replace the placeholder with the actual current version string, e.g. 0.9.7):
grep -rn "0.9.7" . --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=changelog
Resolve hits case by case — historical changelog entries are correct as-is; everything else should match the new version.
4. Author the changelog
Create changelog/<major.minor>.x/<version>.md. Use changelog/template.md as the format reference — never edit, rename, or move that file.
Frontmatter (required):
---
summary: "<one-line headline, ≤350 chars, no markdown>"
breaking: false
security: false
---
Write summary: LAST, derived from the body you just wrote — never independently. It is the line most readers see, and it propagates unedited to three further surfaces: the CHANGELOG.md rollup, the GitHub Release body, and the annotated tag (which cannot be edited once pushed). Written from recollection rather than from the body, it reliably names a mechanism that was never built or a target that was never fixed, while the body beside it stays correct. After writing it, re-read the body and confirm every claim in the summary appears there. Derived-from-the-body means the facts come from the body — not that every body item appears: the summary is the tag's theme line, and comma-stitching every change into an inventory near the 350-char cap is the failure mode.
security: is a source-code signal — not a dependency-CVE signal. Set security: true only when this release fixes a vulnerability or adds hardening in code this server ships. A dependency or transitive CVE bump — even one that clears an advisory (bun audit going 1 → 0) — is routine maintenance: record it under ## Dependencies with the advisory ID and leave the flag false. The 🛡️ Security badge answers "does the server itself have a vuln"; a dep bump must not trip it.
Body: Section order follows Keep a Changelog — Added / Changed / Deprecated / Removed / Fixed / Security. Include only sections with entries. Delete empty sections.
Tone: Terse, fact-dense. Bullet = symbol + what changed + at most one consumer-facing caveat; one sentence by default, two max — a bullet past ~40 words or three sentences is wrong. The linked issue carries the why and the commit diff the how; the changelog names what changed and what a consumer does about it. Cut: history/justification narration, design-rationale defense, "X unchanged" clauses (short parenthetical only where a misread is likely), edge-case inventories. Verified ≠ included — the diff-is-source-of-truth rule bounds the truth of what you write, never the amount. Model length on changelog/template.md's authoring guide, never on the previous entry (entries modeled on entries compound). agent-notes carries adoption steps only, never a second rendering of the body; a consequence shared by many bullets is stated once, not per bullet. Full conventions: the authoring guide in changelog/template.md.
5. Regenerate derived artifacts
bun run changelog:build
bun run tree
Both scripts are idempotent — safe to run even if nothing changed.
6. Run the verification gate
The tree being committed must pass verification. Both must succeed:
bun run devcheck
bun run test:all
bun run test:package
If either fails, halt. Do not bypass verification to land the commit. Fix the issue first, then re-run from step 6.
7. Commit — group by concern, release artifacts on top
Do NOT git add -A into one commit. Group the working tree into a handful of logical commits — never one blob:
- The work — one commit per concern. A feature spanning multiple layers splits by layer: runtime/logic, linter/tooling, docs/skills. Unrelated changes (two separate fixes, an incidental doc tweak) are their own commits. Work commits do not carry the version.
- The release commit — last, on top. Version bumps (
package.json, server.json, README badge, CLAUDE.md/AGENTS.md), the changelog entry, CHANGELOG.md, and docs/tree.md go in a single final commit that sits on top of the work stack — never mixed into a feature commit.
Stage each group explicitly, commit it, then move to the next — the release commit goes last:
git add <paths-for-this-concern>
git commit -m "<subject>"
The file is the atomic boundary: NEVER split a single file's changes across commits. When one file serves two concerns, it ships whole in the commit of its dominant concern.
Subject format: Conventional Commits.
- Work commits (no version):
feat: hosted server endpoint, fix: handle empty SPARQL result sets, feat(linter): enrichment contract rules, docs: document the enrichment block
- Release commit (subject leads with the version):
chore(release): 0.2.1 — empty SPARQL result handling
Body: every commit has one, and it is one or two lines. Uniform across the stack — no commit ships subject-only, none ships a paragraph. One sentence stating the why or the load-bearing constraint, a second only if the first genuinely cannot carry it. Two lines is the hard ceiling.
fix: handle empty SPARQL result sets
Upstream returns 200 with an empty bindings array rather than 404.
Never put a closing keyword in a commit body. Fixes #N, Closes #N, Resolves #N and friends close the issue the moment the commit is pushed — before the close-out comment recording what shipped, so the issue closes with no account of the fix. Reference issues as bare (#N) backlinks in the subject or body; closing is a deliberate later step.
A body is too long the moment it:
- enumerates the files, subsystems, or symbols touched — that is
git show --stat
- walks through how the implementation works — that is the code
- narrates a fix's mechanism across multiple sentences — that is the changelog entry
- runs to a second paragraph, ever
The changelog carries the depth, the tag carries the headline, the commit carries one line of why. When the body wants to grow, that pressure is telling you the content belongs in the changelog entry.
Rules:
- Plain
-m flag only — no heredoc, no command substitution
- No
Co-authored-by or Generated with trailers
- No marketing adjectives ("comprehensive", "robust", "enhanced", "seamless", "improved")
- Each commit message stands alone for someone reading
git log — no chat context, option numbers, or "as discussed"
Right-size it. "Group by concern" is not "always split." A genuinely single-concern change — one fix, a dependency bump, a small doc edit — is one work commit plus the release commit; when the change and its version bump are inseparable for a tiny patch, a single commit whose subject leads with the version is fine. The failure mode to prevent is the inverse: a large, multi-layer feature crammed into one commit alongside the release artifacts.
8. Create an annotated tag
git tag -a v<version> --cleanup=whitespace -m "<tag message with embedded newlines>"
Use -m with embedded newlines in the string (the commit -m-only constraint applies here too — no heredoc). The tag message renders as the GitHub Release body via --notes-from-tag. It must be structured markdown, not a flat string.
--cleanup=whitespace is load-bearing. The default cleanup (strip) deletes #-leading lines as comments, so markdown headers silently vanish from the tag body. --cleanup=verbatim is worse: it skips end-of-message normalization, so with tag signing enabled the signature is appended flush against the message's last character — git then can't parse its own signature (the tag reads as unsigned) and the whole -----BEGIN SSH SIGNATURE----- block publishes verbatim into the GitHub Release body.
Format — a headline digest, never a section-by-section changelog mirror:
<theme — omit version number, GitHub prepends v<VERSION>:>
- <notable user-facing change> (#N)
- <notable user-facing change> (#N)
- <ONE compact grouped line for the minor/internal changes — build config, repo hygiene, metadata>
- deps: `@cyanheads/mcp-ts-core` ^0.10.6 → ^0.10.14 (+ dev-dep bumps)
[CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md)
Rules:
- Subject line omits the version number (GitHub prepends
v<VERSION>: to the release title)
- Flat bullets only — never Keep-a-Changelog section headers.
Added:/Changed:/Fixed:/Dependency bumps: belong in the changelog file; a tag that mirrors the changelog's structure is wrong even when every line is accurate
- Complete at headline granularity — every changelog-worthy change stays visible: notable changes get their own bullet, minor/internal items (build config, repo hygiene, metadata) share ONE grouped compact bullet. Nothing silently dropped, nothing expanded — the changelog carries the depth, the tag carries the existence
- Deps: one line max, naming only what earns it (the framework bump, a major); per-package arrows for the rest live in the changelog entry only
- No gates line — test counts and devcheck status are changelog detail, not release-body material
- No narrative preamble — bullets under the subject, no paragraph blocks
- No marketing adjectives
- Length is earned — a subject + two bullets + changelog link is a fine tag for a small patch
- Issue backlinks: when changes address GitHub issues, include
(#N) references in the relevant bullets — same as the changelog entry. The backlinks render as clickable links in the GitHub Release body.
- Changelog link (final line): end the tag body with a Markdown link to this version's changelog file, so the GitHub Release offers a one-click jump to the full entry —
[CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md). Derive <OWNER>/<REPO> from the origin remote; the path mirrors the file authored in step 4 (e.g. changelog/0.10.x/0.10.12.md). Keep the blank line above it so it renders as its own paragraph, not appended to the gates line.
9. Verify end state
git log --oneline -8
git show v<version> --stat | head -20
git status
git tag -l v<version> --format='%(if)%(contents:signature)%(then)signed%(else)unsigned%(end)'
If the working tree isn't clean or the tag doesn't point at HEAD, something went wrong — investigate before proceeding. unsigned under enabled tag signing means the signature didn't parse (see step 8's cleanup note) — delete and recreate the tag before it leaks the signature block into the GitHub Release body.
Do NOT push. This skill stops here. Use the release-and-publish skill for the push + publish workflow.
Constraints
- Local only. No
git push, no remote operations
- Never stash. Not for quick checks, not for testing, not for any reason
- Never destructive. No
git reset --hard, git restore ., git clean -f, git checkout -- .
- Bash git only. Drive every git operation through the shell
- If
v<version> already exists as a tag, halt and report the conflict — include the version string, existing tag SHA, and current HEAD SHA so the caller can resolve it. Do not delete or move tags without explicit authorization
Checklist