release-cut — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited release-cut (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.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are a release engineer running the full release pipeline as a single atomic flow. Sequence the release phases (bump -> verify -> ship -> tag -> refresh-plugin) with shared error handling and checkpointed step output, so a mid-flow failure re-runs from the last successful checkpoint rather than restarting from the top.
The server ships on two install surfaces and Step 6 refreshes both:
| Layer | Surface | Refreshed by |
|---|---|---|
| Layer 1 | standalone global .NET tool darylmcd.roslynmcp (command roslynmcp) | just tool-update |
| Layer 2 | Claude Code plugin — marketplace cache, skills, hooks, exact-version dnx pin | maintainer-local /update skill |
Neither layer is optional. They drift apart silently and in both directions: v1.29.0 and v1.34.2 updated Layer 1 and left Layer 2 stale in the plugin cache; v4.1.2 refreshed Layer 2 and left Layer 1 a release behind because the global-tool update was documented as optional. eng/verify-install-layers.ps1 is the check that closes Step 6 — a release is not refreshed until it exits 0.
The $ARGUMENTS parameter is the bump type: patch, minor, or major. If not provided, ask the user which type of bump to perform with a brief explanation:
Refuse and report cleanly — do NOT produce a partial release — if any of the following hold:
git status --porcelain non-empty). Message: "Refusing: working tree must be clean before a release cut. Commit or stash first."main. Message: "Refusing: release cuts run from main. Current branch: <branch>."main is not up-to-date with origin/main (after git fetch). Message: "Refusing: local main is behind origin/main. Run git pull --ff-only first."$ARGUMENTS is not one of patch / minor / major. Message: "Refusing: bump type must be 'patch', 'minor', or 'major'. Got: <value>."pwsh -NoProfile -File eng/verify-surface-snapshot-freshness.ps1 exits non-zero. Message: "Refusing: the .ai-doc-audit.md Live Surface snapshot has drifted from the live surface. Run /surface-audit, refresh the table, and re-date the heading first." The script prints the per-row delta; re-measure rather than hand-editing the numbers to green.All five checks run BEFORE Step 2. Emit the refusal and stop — do not proceed to any delegated skill.
Each step records its result (stdout + exit code + any derived values like the new version string) so a re-invocation can detect completed work and skip forward. The implicit checkpoint tests per step:
Directory.Build.props; if <Version> already equals the computed new version AND CHANGELOG.md has a ## [X.Y.Z] - YYYY-MM-DD header for that version, skip.artifacts/verify-release.log exists and its mtime is newer than the newest commit, skip. Otherwise re-run.gh pr list --state merged --head release/vX.Y.Z --limit 1; if a merged PR exists for the bump branch, skip.git tag -l vX.Y.Z; if the tag exists locally AND on origin (git ls-remote --tags origin refs/tags/vX.Y.Z), skip.pwsh -NoProfile -File eng/verify-install-layers.ps1; exit 0 means both layers already track the new version, so skip. A non-zero exit names the stale layer — re-run only that layer's refresh (6a for Layer 2, 6b for Layer 1), then re-verify.The probes are best-effort; when uncertain, re-run the step. Re-running /bump on an already-bumped repo is a no-op when all 7 files agree (verify-version-drift.ps1 short-circuits).
Run the status and branch reads in parallel, then fetch before measuring divergence:
git status --porcelain
git rev-parse --abbrev-ref HEAD
git fetch origin
git rev-list --left-right --count origin/main...HEAD
pwsh -NoProfile -File eng/verify-surface-snapshot-freshness.ps1The final command is ordered after git fetch; never use a pre-fetch remote-tracking ref as the up-to-date proof.
The snapshot gate is the only non-git preflight check. It compares the .ai-doc-audit.md Live Surface table against the README live-surface paragraph (CI-guarded by ReadmeSurfaceCountTests against ServerSurfaceCatalog) and the shipped skills/ directory, so it needs no Roslyn workspace load. It exists because that snapshot drifted across twelve tagged releases while its backlog row sat unpicked at Low — a prose reminder did not hold.
If any refusal condition fires, emit the refusal and STOP.
Record the current version by reading Directory.Build.props (extract the <Version> value). Compute the new version from the bump type:
patch: increment patch (1.27.0 -> 1.27.1)minor: increment minor, reset patch (1.27.0 -> 1.28.0)major: increment major, reset minor and patch (1.27.0 -> 2.0.0)Display current -> new and the planned flow (Bump, Verify, Ship, Tag, Reinstall). No confirmation prompt — the user invoked the skill with an explicit bump type.
/bump)Invoke the /bump skill with the same $ARGUMENTS value. That skill:
Directory.Build.props, .claude-plugin/plugin.json, .claude-plugin/marketplace.json, manifest.json, .claude-plugin/mcp.json, .claude-plugin/server.json, CHANGELOG.md).## [X.Y.Z] - YYYY-MM-DD header to CHANGELOG.md.eng/verify-version-drift.ps1 to confirm all 7 files agree.After /bump returns, verify:
eng/verify-version-drift.ps1 exit 0.git diff --name-only shows the 7 version-file paths plus the consumed changelog.d/*.md fragment deletions.If the CHANGELOG.md ## [Unreleased] section had content, /bump moved it under the new version header. If the section was empty, remind the user: "CHANGELOG.md section is empty — fill in before Ship, or stop and explicitly revert the reviewed version-file edits." Pause for the user to review/edit before Step 3. Never discard a working tree with git reset --hard.
Run the cheap environment precheck before the publish gate:
pwsh -NoProfile -File eng/verify-release-environment.ps1The check fails closed unless all of these are true:
| Signal | Required state |
|---|---|
| Available physical memory | At least 4 GiB and at least 20% of installed memory |
Pre-existing testhost / MSBuild / VBCSCompiler processes | Zero |
Pre-existing dotnet processes | At most 8 |
Exit 2 is an environment refusal, not a product failure. Close the owning build/test sessions, run dotnet build-server shutdown, terminate only processes whose ownership you verified, and re-run the precheck. Never use image-wide taskkill or killall commands. Exit 1 means the machine state could not be inspected; stop rather than treating the publish gate as trustworthy.
Run the publish-gate invocation locally — the same entry point and flags publish-nuget.yml runs at tag time:
pwsh -NoProfile -File eng/verify-release.ps1 -Configuration Release -NoCoverage -RequireConsumedFragmentsThe flags are load-bearing — do NOT drop them to the bare form.
| Flag | Why |
|---|---|
-NoCoverage | Coverage is informational per CI_POLICY.md and gates nothing. The bare form is the dispatch/schedule coverage path, and it trips the known Coverlet .NET 10 teardown crash (coverlet-net10-session-end-crash-upgrade) — an AccessViolationException in CoverletInProcDataCollector.TestSessionEnd that aborts the run and returns exit 1 after every test has already passed. That is a false red on an otherwise green release. |
-RequireConsumedFragments | Enforces that a consumed breaking section advances the major. Step 3 is the only local gate for the breaking→major rule; without this flag a mis-typed bump reaches Ship unchecked. |
Reference invocations (keep this table in sync with the workflows):
| Gate | Invocation |
|---|---|
PR merge gate (ci.yml) | -Configuration Release -NoCoverage -ExcludeNetworkTests |
Publish gate (publish-nuget.yml) | -Configuration Release -NoCoverage -RequireConsumedFragments |
| dispatch / schedule (informational only) | -Configuration Release |
Save stdout+stderr to artifacts/verify-release.log (tee pattern) so Step 4 Ship has evidence and re-runs can probe the log mtime. If exit code is non-zero: STOP and report the failure. Run dotnet build-server shutdown even after a killed or timed-out invocation, then re-run eng/verify-release-environment.ps1 before retrying. Do NOT proceed to Ship with a red verify.
If the run reports Failed: 0 yet still exits non-zero, check the tail for a collector crash before treating it as a product failure — see the -NoCoverage row above.
For every red verify, record the precheck snapshot and re-check current machine state before classifying the result. Measured pressure is a separate signal, not proof that a product crash is environmental; reproduce on a clean runner before closing a product investigation.
Also run pwsh -NoProfile -File eng/verify-ai-docs.ps1 (fast; covers shipped-skill generality + link check).
/ship)Contract assumed from `/ship` (user-global skill at `~/.claude/skills/ship/SKILL.md`, not version-pinned to this repo):
release/vX.Y.Z) if the working tree has uncommitted version-file edits.changelog.d/*.md fragment deletions with message release: vX.Y.Z (exact casing per precedent).-u origin.release: vX.Y.Z.--delete-branch.Worktree teardown discipline (Windows). If this release cut runs from a worktree, and the release workspace was loaded into the Roslyn MCP server during the cut, call workspace_close(workspaceId: <id>, drainProcesses: true) BEFORE any git worktree remove call. The drainProcesses: true flag runs dotnet build-server shutdown after session disposal, releasing VBCSCompiler.exe / MSBuild.exe file-system locks that would otherwise cause Permission denied on git worktree remove. Omit this step only if you are certain no MCP workspace was loaded from the worktree path.
If /ship is unavailable or its contract has drifted, fall back to the manual sequence (run from the primary repo root, not a worktree):
git checkout -b release/vX.Y.Z
# /bump already produced exactly the change set to commit: the 7 version files
# (incl. .claude-plugin/mcp.json and .claude-plugin/server.json) modified + the consumed changelog.d/*.md
# fragment deletions. The tree was clean before the cut (refusal gate), so
# `git add -A` stages precisely those paths — no hand-maintained list to drift.
git add -A
git commit -m "release: vX.Y.Z"
git push -u origin release/vX.Y.Z
gh pr create --title "release: vX.Y.Z" --body "Version bump to X.Y.Z. See CHANGELOG.md for details."
gh pr merge <n> --squash --delete-branch
git checkout main
git fetch origin
git pull --ff-onlyAfter Ship completes: git log -1 --format=%s on main should show release: vX.Y.Z (#<n>).
From the primary repo root (not a worktree), on main synced to origin/main:
git fetch origin
git checkout main
git pull --ff-only
git tag -a vX.Y.Z -m "Release X.Y.Z"
git push origin vX.Y.ZVerify the tag is on origin:
git ls-remote --tags origin refs/tags/vX.Y.ZIf the tag already exists on origin (prior release-cut attempt got this far), skip. If the local and remote SHAs disagree, STOP and report — do not force-push a tag.
/update, NOT the shipped /roslyn-mcp:update)release-cut runs from inside the checkout, where .claude/skills/update/SKILL.md refreshes the plugin cache agent-executably via eng/update-claude-plugin.ps1. The plugin-namespaced shipped form can only instruct the user to run client-side /plugin commands. Do not invoke the namespaced form here.
Both layers are refreshed here. Neither is optional — see the two-layer table at the top of this skill for why.
6a — Layer 2 (plugin). Invoke the bare-name form: Skill: update (or /update interactively). It refreshes the marketplace clone and plugin cache, including the exact [email protected] launch pin. Inspect the cache and confirm only the new-version directory remains.
6b — Layer 1 (global tool). After NuGet publication and indexing complete:
just tool-updateThat resolves Darylmcd.RoslynMcp from NuGet.org, so it can only run once the v X.Y.Z tag's publish workflow has finished and the package is resolvable. A dotnet tool update that reports the old version means indexing has not caught up — wait and retry rather than moving on.
Two failure modes to expect here, both covered in /update Step 3: NuGet indexing lag, and a Windows file lock when a Layer 1 process is running (Access to the path '...\.store\darylmcd.roslynmcp\<old>' is denied). For the lock, identify the holder by image path under ~/.dotnet/tools/ — never by the roslynmcp.exe image name alone, since the plugin's dnx-launched Layer 2 server shares that name.
Identify the lock holder by parentage, not by assumption. Enumerate the holders and read each one's parent process before deciding anything:
Get-CimInstance Win32_Process -Filter "Name='roslynmcp.exe'" |
Select-Object ProcessId, ExecutablePath, ParentProcessIdThe holder is whichever agent launched roslynmcp from the Layer 1 shim, and that is not necessarily this session. Two observed cases:
| Parent | What it means | Resolution |
|---|---|---|
| Claude Code | This session's server came from the shim, not the dnx pin (server_info's stdioPid matches). Observed on the v4.1.2 cut. | Restart Claude Code (Step 7 asks for that anyway), or stop that PID mid-flow and accept losing Roslyn MCP tools for the rest of the run — nothing after 6b needs them. |
codex.exe (or any other agent) | Another tool holds the store. Observed on the v4.2.0 cut: 3 holders, all parented to one codex.exe. | Restarting Claude Code releases nothing — its server runs from the Layer 2 dnx pin, a different image path that never touches the tool store. Ask the operator before stopping another agent's processes; they respawn on that agent's next use. |
Never assume the Claude Code case. When the parent is another agent, restarting Claude Code is a no-op that wastes a cycle and leaves Layer 1 stale.
Respawn is a race. An agent whose server you stop may relaunch it within seconds. Stop the holders and run dotnet tool update in a single scripted pass rather than as separate calls, or the update finds a fresh holder and fails again.
eng/stop-owned-tool-store-process.ps1 is the sanctioned stop: it verifies image name, tool-store image path, and start time (PID-reuse guard), stops exactly one attributable PID, and fails closed naming any holder it cannot attribute. Dot-source it to stop several in one pass. Never stop a PID you have not matched to the tool-store image path, and never use an image-wide taskkill.
6c — Prove both layers. This is the step that closes Step 6:
pwsh -NoProfile -File eng/verify-install-layers.ps1It reads Layer 1 from dotnet tool list --global and Layer 2 from the plugin cache (plus the cached plugin.json), and fails naming whichever layer is stale. Do not report the release as refreshed until it exits 0.
6d — Server canary. Run the pinned dnx command from .claude-plugin/mcp.json and confirm the startup surface reports the new version.
If the cache check, the layer verifier, or the pinned-package canary fails, STOP and report it; NuGet indexing may require a retry after a short delay.
Display a summary:
Release vX.Y.Z cut complete.
Step 1 Preflight ok (clean tree, on main, up-to-date)
Step 2 Bump ok (7 files -> X.Y.Z)
Step 3 Verify ok (N tests, artifacts/verify-release.log)
Step 4 Ship ok (PR #<n>, merged at <time>)
Step 5 Tag ok (vX.Y.Z on origin)
Step 6 Refresh ok (Layer 1 global tool + Layer 2 plugin cache both X.Y.Z; verify-install-layers exit 0; pinned dnx server reports X.Y.Z)
Reminder: restart Claude Code to load the updated server pin, skills, and hooks.
Next: monitor the publish-nuget workflow triggered by the vX.Y.Z tag (if configured).If any step is in a skipped state (re-run from mid-flow failure), annotate it skipped (already done) instead of ok.
Input: /release-cut patch
What you see:
Current: 1.27.0
New: 1.27.1 (patch)
Plan: Bump -> Verify -> Ship -> Tag -> Reinstall
Step 1 Preflight... ok (clean tree, on main, up-to-date with origin/main)
Step 2 Bump ... ok (Directory.Build.props, plugin.json, marketplace.json, manifest.json, mcp.json, server.json, CHANGELOG.md -> 1.27.1)
[CHANGELOG.md ## [1.27.1] - 2026-04-22 header inserted. Review section content.]
Step 3 Verify ... ok (N tests pass, artifacts/verify-release.log written)
Step 4 Ship ... ok (PR #<n> opened, checks green, squash-merged)
Step 5 Tag ... ok (v1.27.1 pushed to origin)
Step 6 Refresh ... ok (Layer 2 cache shows only 1.27.1; Layer 1 global tool 1.27.1; verify-install-layers exit 0; pinned dnx server 1.27.1)
Release v1.27.1 cut complete.
Reminder: restart Claude Code to load the refreshed pinned server, skills, and hooks.v* tag push triggers the publish-nuget workflow (or manual eng/publish-nuget.ps1). This skill stops after the tag.gh pr merge fails inside a worktree (the branch is already used by the worktree). Run ship-related merges from $(git rev-parse --git-common-dir)/.. (see ai_docs/workflow.md Worktree + gh pr merge discipline)./publish-preflight ad-hoc to gate readiness; /release-cut assumes it has already passed (or runs the superset via verify-release.ps1).pwsh eng/update-claude-plugin.ps1, plus the required Layer 1 global-tool update. This is what Step 6 invokes./plugin marketplace update + /plugin install flow and optional global-tool path.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.