git-wrapup — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited git-wrapup (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
Aggregate score unchanged between these scans.
The primary manifest — the file an agent reads to learn what this artifact does.
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). It does NOT tag, push to main, or publish — release-and-publish does all three.
Common triggers:
A project can route every release through a pull request — one PR per version, for the audit trail and a stable review target. The mode is declared in the project's CLAUDE.md/AGENTS.md or in the caller's brief; when neither says anything, there is no release PR and the stack lands on main directly.
| Mode | Wrapup ends at | Then |
|---|---|---|
| (none — default) | commit stack on main, tree clean | release-and-publish tags HEAD and ships |
| gated | commit stack on release/<version>, branch pushed, PR open | a review pass on the PR (release-pr-review skill), then a separate release-and-publish run fast-forwards main, tags, and ships |
| straight-through | same as gated | the same agent continues straight into release-and-publish |
Create release/<version> as soon as the version is known, before any commit; a caller-pinned version permits branching before step 1. Every work and release commit lands on that branch. If commits were accidentally made on an unpushed main, create the release branch at HEAD and continue without moving main; never reset, force a branch ref, or rewrite history. If those commits were already pushed to main, report the state before proceeding. The commit stack, release commit, and tag format are identical in every mode — the PR adds an audit artifact.
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.
code-simplifier skill has been run across the changeslint:mcp's "or verify by hand that format() renders it") is a way to UNDERSTAND a warning, never a licence to ship it; hand-verifying it and filing a follow-up issue is still shipping dirty. Nor is a pinned version a reason to defer the real fix: when the correct fix crosses the minor floor, the VERSION yields, not the gate — take the minor and say so. Two things that look dirty but aren't: (1) `info` is not `warning` — Biome's unsafe-fix suggestions ("Skipped N suggested fixes", useLiteralKeys and friends) print at info severity with the step still ✅; not the block. (2) The Security Audit step's transitive warn — a DIRECT-dep advisory is a hard failure, an all-transitive one is ⚠️ WARNING with the run still ✅; confirm from the bun audit dependency path (pkg › child = transitive), then ship it. Never force a gate green with a resolutions/overrides hack, and never run audit:refresh to silence it (it re-resolves the ^-ranged framework pin off its hold). The exception is a deliberate, maintainer-directed overrides block as its own dependency-hygiene pass — mcp-ts-core carries one as of 0.11.0: leave it in place, pre-authorize it by name in implement/release briefs (or agents burn a phase chasing it), pin entries to the lowest patched version inside the consumer's existing major, and verify the installed tree rather than assuming a pin applied (bun skips resolutions its range can't satisfy). Read the severity label and the failing step before calling a green run dirty — filtering devcheck's output strips exactly the context that separates the tiers.bun run test:all (or bun run test), plus bun run test:package where the project defines one: it guards the public-export manifest and is not part of test:all. New tests and regression tests added as needed for the changes being shipped.bun run rebuild and field-testing. Not just written — confirmed to resolve the described behavior.If any gate is red, fix it before proceeding. This skill re-verifies build + tests in step 7, but the work is committed by then — starting wrapup on a broken tree wastes the version number and turns the fix into an extra commit on a stack that should have been green.
Understand what's about to ship before touching version numbers:
git branch --show-current # empty output = detached HEAD
git status
git log v<latest-tag>..HEAD --oneline # commits since last release
git diff HEAD --stat # every uncommitted change, staged or not
git diff HEAD # review the actual content
git ls-files --others --exclude-standard # new files, which never appear in the diffCheck the branch first. Empty output from git branch --show-current means a detached HEAD: halt. Start on main, or on release/<version> when the caller has already pinned the version and created that branch. Release PR mode must be on the release branch before step 3.
Diff against HEAD, not the index: plain git diff omits staged changes entirely, so a group already staged before wrap-up began — a git mv from a migration step, a hook's output — shows up in git status as a line to scroll past and nowhere in the diff review. Whatever is staged is part of what ships and gets grouped in step 3 like everything else.
Read every untracked file in full. New files ship through git add like any other change, but no diff shows their content, and git status collapses a new directory to a single line.
Ship only finished work no one is still writing. Uncommitted changes outside the work being wrapped are candidates, not automatic exclusions: git records no author for them, so judge each from the evidence. A complete, safe change that has sat idle for hours folds in as its own commit. A change this run didn't make, modified in the last few minutes (check mtimes) or visibly half-done, may belong to a session still writing it: leave it uncommitted and report it, since committing it would ship work in progress and leave its writer editing on top of a released commit. Anything unsafe or secret-bearing stays out regardless.
If the working tree is clean AND there are no commits since the last tag, halt — nothing to wrap up.
Read the current version from package.json. The version is the caller's to set, not this skill's to pick:
CLAUDE.md/AGENTS.md), which may classify changes differently from plain semver.| Bump | When |
|---|---|
| patch | Backward-compatible fixes with no new functionality — bug fixes, dependency updates, metadata, docs |
| minor | Backward-compatible new functionality; below 1.0, also any breaking change |
| major | A breaking change to an existing contract (tool schemas, removed tools, incompatible config), once the project is at 1.0 or later |
When the diff doesn't clearly settle which row applies, ask the caller rather than picking.
In `gated` or `straight-through` mode, create the branch now, before going on to step 3. If the caller already had you create this release branch, verify its name and skip the creation command; otherwise:
git branch --show-current # must be main
git switch -c release/<version> # uncommitted work rides alongDo not defer this to "just before the first commit". Step 3 is where committing starts, so a branch not created here is a branch created too late. If git branch --no-merged main --list 'release/*' prints a branch, a prior release PR was never merged — halt and report it rather than stacking a second release on top. A release/* branch already merged into main is a leftover from a finished release, not a blocker: delete it with git branch -d and continue.
Check the remote too, before creating the branch:
git ls-remote --heads origin release/<version> # must print nothing
gh pr list --head release/<version> --state all # must print nothingA hit means an abandoned run already claimed this version: halt and report the branch and PR. Never force-push past it.
Release PR mode: `git branch --show-current` must print `release/<version>` before every `git commit`. If it prints main, create the release branch before committing. If commits already exist on an unpushed main, branch at HEAD and continue; never reset, force a branch ref, or otherwise move existing refs to recover.
The work is committed before the version is bumped. Work concerns routinely share a file with the version — a dependency refresh edits package.json, a doc edit lands in a CLAUDE.md/AGENTS.md that pins a version string — and the file is the atomic boundary, so whichever commit comes first takes the file whole. Committing the work first leaves the version hunk (step 4) as the only thing those files carry into the release commit.
Do NOT git add -A into one commit. Group the working tree into a handful of logical commits — never one blob. 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. A dependency refresh is its own commit — chore(deps): … carrying package.json, the lockfile, and any pins it moved elsewhere. Work commits do not carry the version.
Verify each proposed group using the snapshot gate below before running git commit. Then stage that unchanged group explicitly, commit it by pathspec, and move to the next:
git add <paths-for-this-concern>
git commit --only <paths-for-this-concern> -m "<subject>" -m "<body>"
# repeat per concernCommit by pathspec, never the bare index. A bare git commit commits everything staged, not the paths just added, so anything staged before wrap-up began — a git mv left by a migration step, a concurrent stage from a second session or a hook — rides into the first concern's commit. --only takes the named paths' working-tree content and disregards the rest of the index, so a pre-staged group never rides along; it stays staged, to be committed as its own concern (chore(skills): move the skill tree to framework-skills/) or reported. Anything still staged when the release commit lands then fails step 10's clean-tree check instead of shipping silently.
The file is the atomic boundary: NEVER split a single file's working-tree changes across commits, regardless of mechanism — not git add -p, not an index-only patch (git apply --cached), not editing the file between commits to remove-then-re-add a hunk. When one file serves two concerns, it ships whole in the commit of its dominant concern; a later commit may touch the file again only for changes made AFTER the first commit (the version bump applied in step 4).
Every commit builds and passes its tests on its own — verify BEFORE committing. A changed service contract or I/O boundary ships with its consumers and tests, even when those files also carry other concerns. Test the baseline snapshot first. For each proposed group, use a temporary GIT_INDEX_FILE: git read-tree HEAD, then git add -- <whole-file-paths>. Export that candidate with git checkout-index -a --prefix=<scratch>/ under the same temporary index — not git archive, which drops every export-ignore path in .gitattributes, often tests/ and scripts/ — symlink the project's node_modules, and run the fast pair there: the build (or typecheck), then the unit test script (bun run test). Never the full lane matrix per commit — integration, worker, packaging, or coverage lanes, or an aggregate such as test:all or test:package: it already passed on the finished tree before wrapup, runs again in step 7 and in release-and-publish, and repeating it per snapshot multiplies the slowest gate by the commit count. Also run any verification command the candidate introduces or strengthens, such as coverage: it must pass in the first commit that advertises it, with its supporting tests and configuration. Wait for every command to finish successfully before committing the same unchanged paths through the real index; a running gate is not a pass. The temporary index leaves existing staging and the working tree untouched. If the candidate fails where the baseline passes, combine its dependent file groups and repeat the snapshot gate while they are still uncommitted. A failure shared with the baseline may be a snapshot-method problem — a test that resolves real paths through the symlinked node_modules, or one that needs a git repository — so copy node_modules (cp -cR clones it on APFS) or git init the scratch tree, and resolve it before judging the group. Never create a commit to discover whether its grouping passes, and never rewrite history to repair grouping.
A dependency bump lands before the commits that use it. When any later commit in the stack uses something the new versions introduce — a new framework export, a new tool() option, a changed signature — chore(deps) is the first work commit. It builds on its own: package.json, the lockfile, and any source change the upgrade itself forces (a renamed import, a removed option) ride in it, so the commits above it compile against the versions they were written for. Ordered the other way, the adopting commit and every commit up to the bump fail typecheck at their own SHA. The exception is a package.json that also carries another concern's entry pointing at files a deps-first commit would not contain, such as a new exports subpath or files entry. That snapshot fails, so the dependency changes ride the commit that adds those files, and its body says so.
Subject format: Conventional Commits, no version in the subject — feat: hosted server endpoint, fix: handle empty SPARQL result sets, feat(linter): enrichment contract rules, docs: document the enrichment block, chore(deps): refresh dev dependencies.
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:
git show --statThe 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:
-m flag only — no heredoc, no command substitution. Never put a backtick inside a double-quoted -m: the shell runs it as command substitution and the span silently disappears from a message that, once pushed, can't be rewrittenCo-authored-by or Generated with trailersgit 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, with step 8's release commit on top of it. Only when the version bump is the whole change — a republish, a metadata-only patch with nothing else in the tree — is there no work commit to make, and step 8's release commit becomes the entire stack. The failure mode to prevent is the inverse: a large, multi-layer feature crammed into one commit alongside the release artifacts.
When every concern is committed, git status is clean, apart from changes step 1 left out and reported. That clean tree is what makes everything steps 4–6 touch a release artifact and nothing else — confirm it before moving on.
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 — versionserver.json — top-level version AND every packages[].version entry. lint:mcp flags a mismatch at either levelmanifest.json (if present) — version. Packaging validation fails on a mismatch, and on a scoped name (use bls-mcp-server, not @cyanheads/bls-mcp-server).claude-plugin/plugin.json, .codex-plugin/plugin.json, and root plugin.json (if present) — version. Packaging validation fails on a mismatch; .codex-plugin/mcp.json and root mcp.json are connection config and carry noneREADME.md — version badge. Packaging validation fails on a mismatch with package.json; a literal - in a prerelease is escaped as -- (Version-0.14.0--rc.1-)CLAUDE.md / AGENTS.md — if they pin a version stringDockerfile — OCI labels if they pin the versionCatch stragglers (replace the placeholder with the actual current version string, e.g. 0.9.7):
grep -rnF "0.9.7" . --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=changelog --exclude=bun.lock-F matches the string literally; without it each . matches any character, and 0.9.7 also hits 10.9.7, 0.9.70, and every dependency version in the lockfile that happens to match.
Resolve hits case by case — historical changelog entries are correct as-is; everything else should match the new version.
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 # true if consumers must change code to upgrade
security: false # true ONLY for a security fix in this server's own source — NOT a dependency/transitive CVE bump (those go under ## Dependencies)
---Write `summary:` LAST, derived from the body you just wrote — never independently. It is the line most readers see: changelog:build copies it verbatim into the CHANGELOG.md rollup, the entry file ships inside the npm tarball (changelog/ is in package.json files; the rollup is not), and in release PR mode it opens the PR body. 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: it is one headline, 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.
Re-read the entry file after writing it, then sweep for harness markup: grep -rlF -e '</invoke>' -e '</content>' changelog/ must print nothing. The sweep covers the whole directory: a hit in an older entry gets deleted and ships in this release's commit, never left as out of scope, because every entry ships in every tarball. A stray closing tag at EOF is the authoring tool's own syntax bleeding into the file; changelog/ is in package.json files, so it ships inside the npm tarball, and changelog:check cannot catch it — the rollup drops the trailing line, so a clean CHANGELOG.md proves nothing about the entry.
bun run changelog:build # rebuilds CHANGELOG.md rollup from per-version files
bun run tree # regenerates docs/tree.md after the changelog files are createdChangelog files and directories count as structure changes even when src/ is unchanged.
The stack being shipped must pass verification. All must succeed:
bun run devcheck
bun run rebuild
bun run test:all # or `bun run test` if no test:all script exists
bun run test:package # only if the script exists — NOT part of test:allIf any fails, halt. Do not bypass verification to land the release commit. Halting means reporting back with the failing output — a failure that isn't a formatter auto-fix (a red or intermittent test, a type error) is the caller's to diagnose and fix; don't investigate it, add debug files, or edit source or tests to chase it.
The work is already committed by this point, so the fix is a new commit on top of the stack, under step 3's conventions — never git commit --amend, never a rebase, reset, or any other rewrite of a commit the stack already carries. Land the fix, then re-run this step. The same holds when the gate passes but leaves the tree dirty: devcheck auto-fixes as it runs, and a formatter fix to a file committed in step 3 is a follow-up commit of its own, not something to fold into the release commit.
Only the version bump, the changelog entry, and the regenerated artifacts may still be uncommitted when this step goes green.
One commit, on top of the work stack from step 3, carrying only what steps 4–6 produced: the version bumps (package.json, server.json, manifest.json, the plugin manifests, README badge, CLAUDE.md/AGENTS.md), the changelog entry, CHANGELOG.md, and docs/tree.md.
git add <release-artifact-paths>
git commit --only <release-artifact-paths> -m "chore(release): <version> — <theme>" -m "<body>"Subject leads with the version: chore(release): 0.2.1 — empty SPARQL result handling. Step 3's conventions carry over unchanged — pathspec staging rather than the bare index, a one- or two-line body, no closing keywords, no trailers, no marketing adjectives.
Anything else still uncommitted at this point was made after step 3 — a formatter auto-fix, a gate fix. It is committed on its own first, and the release commit goes on top of it; it is never folded in.
Skip this step entirely when the project has no release PR mode — go to step 10.
git push -u origin release/<version>
gh pr create --base main --head release/<version> --title "<release commit subject>" --assignee @me --body-file <path-to-body.md>Title: the release commit's subject, verbatim — chore(release): <version> — <theme>.
Assignee: --assignee @me on every release PR, so it lands in the maintainer's assigned queue like a filed issue. A reviewer is not set: GitHub drops a review request aimed at the PR's own author and refuses a self-approval, so on a self-authored release PR the reviewer field stays empty by design — the review record is the summary comment release-pr-review leaves.
Body — always via `--body-file`, never an inline `--body` string (backticks inside a double-quoted argument are command substitution and silently vanish). Write the file to a scratch location, not into the repo.
The body is the release digest — the ## Changes bullets and changelog link the annotated tag will carry, under a theme line and above a gates record that both stay on the PR. The digest is written here, reviewed on the PR, and copied into the tag at release time, so it is the one place the release notes get reviewed before they become permanent. Format:
<theme — the changelog entry's summary: line, plain prose, one line. It opens the PR; the tag's subject is written fresh at release time and is not this line>
## Changes
- <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)
## Gates
- `bun run devcheck` — clean
- `bun run rebuild` — ok
- `bun run test:all` — <N> passed
- `bun run test:package` — <N> passed (only where the project defines it)
[CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md)Rules:
release-and-publish step 4): flat bullets, never Keep-a-Changelog section headers; complete at headline granularity — notable changes get their own bullet, minor/internal items share ONE grouped bullet; deps one line max, naming only what earns it; no narrative, no marketing adjectives. Depth lives in the changelog entry, which is in this PR's diff and linked on the last line.Closes #N, Fixes #N); the merge would close the issue before its close-out comment lands.If the review pass changes what ships, release-pr-review updates ## Changes and ## Gates to match; release-and-publish then lifts ## Changes plus the final link into the tag verbatim.
Gated mode: halt here. Report the PR URL, the branch, and the commit stack. Do not tag, do not merge, do not touch main. The review pass and release-and-publish run as separate steps after this one.
Straight-through mode: continue directly into release-and-publish.
git log --oneline -8 # confirm the commit stack: work commits + release commit on top
git status # clean, apart from step 1's reported exclusions
git tag --points-at HEAD # must print nothing — tagging is release-and-publish's job
git branch --show-current # main, or release/<version> in release PR mode
gh pr view --json number,url,state # release PR mode: OPEN, head = the branch aboveIf the working tree holds anything beyond those exclusions, or the release commit isn't at HEAD, something went wrong — investigate before proceeding.
Do NOT tag, push `main`, or publish. This skill stops here. release-and-publish merges the release branch when there is one, creates the tag, pushes, and publishes.
git reset --hard, git restore ., git clean -f, git checkout -- ., no force-push--amend, which would rewrite the previous commit. When a hook reformats files, re-stage those paths before retryingv<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 authorizationmain or the caller-created release/<version>, not a detached HEAD; no conflicting release branch or PR existspackage.json, server.json, manifest.json, .claude-plugin/plugin.json, .codex-plugin/plugin.json, root plugin.json, README badge, CLAUDE.md/AGENTS.md if they pin a version) — devcheck flags a mismatch in server.json (both levels), manifest.json, the plugin manifests, and the README badge; step 4's straggler grep covers the docs and Dockerfile labelschangelog/<major.minor>.x/<version>.mdCHANGELOG.md rollup regenerated (bun run changelog:build)docs/tree.md regenerated after changelog creation (bun run tree)bun run devcheck passesbun run rebuild succeedsbun run test:all (or test) passesbun run test:package passes, when the project defines it — it guards the public-export manifest and test:all does not run itrelease/<version>, never on mainchore(deps) is the first work commit whenever a later commit uses what the new versions introduce, and it builds on its own — carrying package.json, the lockfile, and any source change the upgrade forces## Changes in tag rules, ## Gates, changelog link last (via --body-file, no closing keywords)--no-verifymain — release-and-publish owns both~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.