agentleFS
sequant-iosequant-io/sequant/.claude/skills/release/SKILL.md

release

Skill1 starsChanged 3 months agoMITView on GitHub

Automates the full release workflow: version bump, git tag, GitHub release, and npm publish.

  • Installs packages
  • Commits and pushes

196 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use create-agent-adapter, changelog-style and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • MicrosoftSkill

    create-agent-adapter

    Technical guide for creating a new Paperclip agent adapter. Use when building a new adapter package, adding support for a new AI coding tool (e.g. a new CLI agent, API-based agent, or custom process), or when modifying the adapter system. Covers the required interfaces, module structure, registration points, and conventions derived from the existing claude-local and codex-local adapters.

    Also in 5 projects that use this

  • GrinvSkill

    changelog-style

    Turn a commit range into CHANGELOG.md entries following common-changelog style. Use when finishing a PR or cutting a release, to write/fix the [Unreleased] section's entries — moving [Unreleased] under a new dated version heading is a separate, automated step (see the release skill / scripts/sync-version.ts), not this skill's job.

    Also in 4 projects that use this

  • GrinvSkill

    docs-consistency-check

    Check README/manifest.json/server.json/CHANGELOG.md/AGENTS.md and docs/*.md for drift against the actual registered tools and source. Use after adding, renaming, or removing a tool, or as part of a live-audit pass.

    Also in 4 projects that use this

  • Microsoft AzureSkill

    component-release

    Prepares a verifiable Agent Landing Zone UI release. Use for release branches, VERSION, changelog entries, tags, release notes, and compatibility evidence; never publish without explicit human approval.

    Also in 4 projects that use this

  • Microsoft AzureSkill

    documentation-consistency

    Keeps AI Landing Zone contributor, user, and operator documentation aligned with shipped Bicep behavior. Use for parameters, defaults, outputs, topology, deployment flow, operations, or releases.

    Also in 8 projects that use this

  • rivet-devSkill

    launch-graphics

    Generate agentOS marketing/launch graphics (launch & social heroes, code-snippet cards) from the committed generators in docs-internal/graphics. Use when asked to make, update, or render an agentOS launch image, social image, "agentOS Apps" graphic, or technical code-snippet image.

    Also in 3 projects that use this

  • +3 more from Grinv

The file

Show its rules and text

We split this skill into the rules it gives an agent

30 rules. 0 are common, 9 are distinctive, and 21 are very specific to this project.

  • 0

    Common

    Most of the 186 edited versions we read keep these.

  • 9

    Distinctive

    This author's own choices. Worth a look.

  • 21

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Run ALL checks before proceeding.

    line 54
  2. Very specific:

    Orchestrator/MCP mode: when SEQUANT_ORCHESTRATOR is set, every locks checkout action is a no-op (exit 0, no file touched) and the …

    line 194
  3. Distinctive:

    If version type not provided as argument, ask the user:

    line 201
  4. Very specific:

    If CHANGELOG.md exists, use the Edit tool to:

    line 254
  5. Very specific:

    IMPORTANT: Keep plugin.json in sync with package.json.

    line 283
  6. Very specific:

    Use the Read tool to read plugin/.claude-plugin/plugin.json, then use the Edit tool to update the version field:

    line 285
  7. Very specific:

    If plugin/.claude-plugin/plugin.json does not exist, skip this step.

    line 295
  8. Very specific:

    IMPORTANT: Keep docs/internal/what-weve-built.md version references in sync and document new features.

    line 301
  9. Very specific:

    If CHANGELOG.md doesn't exist or has no [Unreleased] section, fall back to commit-based detection:

    line 392
  10. Distinctive:

    Ask the user to review what-weve-built.md before proceeding: if there are new features to document.

    line 402
  11. Very specific:

    Use the Edit tool on SECURITY.md: in the table under ## Supported versions, replace the supported row's minor (e.g.

    line 484
  12. Very specific:

    Wait for test and canary on the PR head (poll commits/<sha>/check-runs, not gh pr checks --watch, which can report the previous head), then:

    line 547
  13. Very specific:

    When the user reports done, verify before continuing: npm view sequant dist-tags must show next: <new_version> and an unchanged latest.

    line 611
  14. Very specific:

    Do NOT attempt to pass OTP codes programmatically or retry npm publish in a loop.

    line 624
  15. Very specific:

    Without --soaked: refuse: Do not run npm dist-tag add ...

    line 637
  16. Very specific:

    If npm returns EOTP, hand off to the user with npm dist-tag add sequant@${new_version} latest --otp=<code>.

    line 645
  17. Distinctive:

    Verify both platforms show the new version:

    line 651
  18. Very specific:

    When --dry-run is specified:

    line 718
  19. Distinctive:

    Run all pre-flight checks

    line 720
  20. Very specific:

    Use pre-release tags for testing: 1.4.0-beta.1

    line 812
  21. Distinctive:

    Write for users, not developers

    line 816
  22. Distinctive:

    Keep package size small (< 500KB ideal)

    line 822
  23. Very specific:

    Use files in package.json or .npmignore

    line 823
  24. Very specific:

    Test with npm pack before publishing

    line 824
  25. Very specific:

    Use --tag for pre-releases to avoid becoming "latest"

    line 825
  26. Distinctive:

    Use pre-release flag for beta/RC versions

    line 828
  27. Distinctive:

    Use consistent title format

    line 830
  28. Very specific:

    Run npm audit before releasing

    line 833
  29. Very specific:

    Don't include sensitive files (check npm pack --dry-run)

    line 834
  30. Very specific:

    Verify npm whoami shows correct account

    line 835

“Common” is measured: kept by at least 6 in 10 of the 186 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- sequant:local-override -->

Local overrides (read this first). Before following any instruction below, check whether .claude/.local/skills/release/overrides.md exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor /release without forking it — overrides.md lives under .claude/.local/, which sequant update and sync never overwrite.

Release Skill

Automates the full release workflow: version bump, git tag, GitHub release, and npm publish.

Usage

/release [patch|minor|major] [--prerelease <tag>] [--soaked] [--dry-run]
  • /release - Interactive, asks for version type
  • /release patch - Patch release (1.3.1 → 1.3.2)
  • /release minor - Minor release (1.3.1 → 1.4.0)
  • /release major - Major release (1.3.1 → 2.0.0)
  • /release minor --prerelease beta - Pre-release (1.3.1 → 1.4.0-beta.0)
  • /release --dry-run - Preview without publishing
  • /release --soaked - Promote an already-published next version to latest (Step 9b); refused without this flag

Pre-flight Checks

Run ALL checks before proceeding. STOP if any fails.

Git Checks

# 1. On main branch
[ "$(git branch --show-current)" = "main" ] || { echo "Not on main"; exit 1; }

# 2. Clean working tree
[ -z "$(git status --porcelain)" ] || { echo "Uncommitted changes"; exit 1; }

# 3. In sync with remote
git fetch origin
[ -z "$(git log HEAD..origin/main)" ] || { echo "Behind origin - pull first"; exit 1; }

# 4. The latest CI `push` run on main is green (red-main rule, below).
#    Filter to the CI workflow: without --workflow the newest push run can be
#    Scorecard or another workflow, and a red CI would read as green.
ci_run=$(gh run list --branch main --event push --workflow ci.yml --limit 1 \
  --json conclusion,url --jq '.[0] | "\(.conclusion) \(.url)"')
case "$ci_run" in success*) ;; *) echo "Red main: latest CI push run is '$ci_run' - revert first (#1093)"; exit 1;; esac

Red main — revert first, debug second (#1093). A failing push run on main is reverted first and debugged second: the next change that lands is the revert of the commit that broke it, not a forward fix and not an investigation branch, and the revert PR references the failing run by URL. Do not release off a red main, and do not release a forward fix "that will also make it green" — revert, let CI go green, then release. Investigate afterwards on a branch off a green main. This mirrors the rule in CLAUDE.md § Red main.

Quality Checks

# 4. Tests pass
npm test

# 5. Build works
npm run build

# 6. No security vulnerabilities (high/critical)
npm audit --audit-level=high || echo "Warning: audit issues found"

npm Checks

# 7. Logged into npm
npm whoami || { echo "Not logged in - run: npm login"; exit 1; }

# 8. Version doesn't already exist
pkg_name=$(node -p "require('./package.json').name")
current=$(node -p "require('./package.json').version")
npm view "${pkg_name}@${current}" version 2>/dev/null && { echo "Version already published"; exit 1; }

# 9. Preview package contents
npm pack --dry-run 2>&1 | tail -20

GitHub Checks

# 10. Authenticated with GitHub
gh auth status || { echo "Not logged in - run: gh auth login"; exit 1; }

Plugin Checks

# 11. Plugin manifest is valid (#1135). Name each manifest explicitly:
#     `claude plugin validate .` checks only marketplace.json and skips plugin.json.
#     Warnings pass (non-strict); errors, or no `claude` on PATH, stop the release.
claude plugin validate plugin/.claude-plugin/plugin.json || { echo "Plugin manifest invalid - fix plugin/.claude-plugin/plugin.json"; exit 1; }

# 12. Marketplace manifest is valid
claude plugin validate .claude-plugin/marketplace.json || { echo "Marketplace manifest invalid - fix .claude-plugin/marketplace.json"; exit 1; }

Documentation Checks

Note: Documentation freshness checks that compare against the version being released run after Step 4 (the version bump), not here in pre-flight:

  • docs/internal/what-weve-built.md title + ASCII version stamps → Step 4.63
  • CHANGELOG.md freshness (version entry present) → Step 4.65
  • README.md "What's new in <minor>" heading freshness → Step 4.66

They must compare against the new version, which doesn't exist in package.json until Step 4 runs. Running them in pre-flight would compare against the previous release — which the docs already reflect — so the gate would never fire for the release in progress (root cause class of #684 / #687).

Release Steps

Step 0: Acquire the Checkout Lock (REQUIRED)

Everything from here on mutates the main checkout: the version bump and its commit, tag, and push (Steps 4–7), plus the rollback verbs (git reset --soft, git checkout -- …) if a step fails. git commit/tag/push are not guarded by pre-tool.sh, but the git reset in rollback is — and, more importantly, holding the checkout lock (#901) is what stops a session on another issue from interleaving a git checkout between the bump and the commit and landing the release commit on the wrong HEAD. So claim the tree now, before the first interactive approval pause (Step 2's release-notes review), exactly as /fullsolve does in its Phase 0.3.

/release has no issue of its own, and the checkout lock proves ownership by a positive integer — the pre-tool.sh guard and sequant locks checkout both key on --issue=<N>. So /release claims the tree under a reserved sentinel issue id, 999999999 (a number that will never be a real issue), and uses it on the acquire and on every release below. locks list and the CLI's release-refusal render this sentinel as /release (sentinel) so it does not read as a real issue. A symbolic holder (e.g. --label=release) would avoid the reserved-number scheme entirely, but it would require changing the lock-file schema, the CLI, and the numeric-only pre-tool.sh guard — out of scope for #911 (tracked there as a follow-up).

# Claim the shared working tree before the first mutation / approval pause.
# Skip for --dry-run: it previews only and mutates nothing, so it needs no lock.
npx sequant locks checkout acquire \
    --issue=999999999 \
    --command="/release" \
    --skip-pid-check || true
export SEQUANT_ISSUE=999999999

Acquire after the read-only pre-flight checks above, not before: those checks (git status/fetch, npm test/build, npm whoami, gh auth) mutate nothing, so a pre-flight abort holds no lock and needs no release. That is why the pre-flight exit 1 branches carry no release call — you cannot release a lock you have not taken.

--issue=999999999 is mandatory on release (#906): it is what proves you are the holder once the acquiring shell's PID is gone (--skip-pid-check). Stale recovery is age-based only (the 6h SEQUANT_SKILL_LOCK_TTL_MS and the 24h SEQUANT_MAX_LOCK_AGE_MS ceiling), not same-host dead-PID recovery.

Release contract: release the checkout lock — npx sequant locks checkout release --issue=999999999 || true — on every path that exits after this acquire: happy-path completion (Post-Release Verification), any mid-release error (Error Handling), and the Rollback Procedures. Releasing an already-released lock is a harmless no-op, so a failure that both errors and rolls back may run two of these — that is safe.

Orchestrator/MCP mode: when SEQUANT_ORCHESTRATOR is set, every locks checkout action is a no-op (exit 0, no file touched) and the pre-tool.sh checkout guard stands down, so the acquire/release calls are safe to run unconditionally.

Step 1: Determine Version

If version type not provided as argument, ask the user:

TypeWhen to UseExample
patchBug fixes, maintenance, no new features1.3.1 → 1.3.2
minorNew features, backwards compatible1.3.1 → 1.4.0
majorBreaking changes1.3.1 → 2.0.0
prereleaseAlpha/beta/RC versions1.3.1 → 1.4.0-beta.0
current=$(node -p "require('./package.json').version")
echo "Current version: ${current}"

Step 2: Generate Release Notes

Gather commits since last tag:

last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$last_tag" ]; then
  git log ${last_tag}..HEAD --oneline --no-merges
fi

Categorize by conventional commit prefix:

  • feat: → Features
  • fix: → Bug Fixes
  • perf: → Performance
  • docs: → Documentation
  • refactor: → Refactoring
  • chore: → Maintenance
  • BREAKING CHANGE: → Breaking Changes (for major releases)

Release notes template:

## What's Changed

### Features
- Feature description (#PR)

### Bug Fixes
- Fix description (#PR)

### Breaking Changes
- Description of breaking change and migration path

**Full Changelog**: https://github.com/sequant-io/sequant/compare/v{old}...v{new}

Show draft to user for approval before proceeding.

Step 3: Update CHANGELOG.md (if exists)

If CHANGELOG.md exists, use the Edit tool to:

  1. Replace ## [Unreleased] with ## [{new_version}] - {YYYY-MM-DD}
  2. Insert a fresh ## [Unreleased] section above the newly stamped version:
## [Unreleased]

## [1.4.0] - 2026-01-10

### Added
- New feature X
...

This ensures the next development cycle has an [Unreleased] section ready for contributors.

Step 4: Bump Version

# For regular releases
npm version {patch|minor|major} --no-git-tag-version

# For pre-releases
npm version prerelease --preid=beta --no-git-tag-version

Step 4.5: Sync Plugin Version

IMPORTANT: Keep plugin.json in sync with package.json.

Use the Read tool to read plugin/.claude-plugin/plugin.json, then use the Edit tool to update the version field:

Read(file_path="plugin/.claude-plugin/plugin.json")

Edit(file_path="plugin/.claude-plugin/plugin.json",
     old_string="\"version\": \"<old_version>\"",
     new_string="\"version\": \"<new_version>\"")

If plugin/.claude-plugin/plugin.json does not exist, skip this step.

Why sync? Sequant is distributed as both npm package and Claude Code plugin. Both must have matching versions to avoid user confusion and ensure compatibility.

Step 4.6: Update what-weve-built.md

IMPORTANT: Keep docs/internal/what-weve-built.md version references in sync and document new features.

Get the new version, then use the Edit tool to update the file:

new_version=$(node -p "require('./package.json').version")

Then use the Edit tool (not sed) to update docs/internal/what-weve-built.md:

# Update title version - use Edit tool with replace_all=true
Edit(file_path="docs/internal/what-weve-built.md",
     old_string="Sequant v<old_version>",
     new_string="Sequant v${new_version}",
     replace_all=true)

# Update ASCII art version - use Edit tool with replace_all=true
Edit(file_path="docs/internal/what-weve-built.md",
     old_string="SEQUANT v<old_version>",
     new_string="SEQUANT v${new_version}",
     replace_all=true)

# Update "Current Version" line - use Edit tool
Edit(file_path="docs/internal/what-weve-built.md",
     old_string="**Current Version:** <old_version>",
     new_string="**Current Version:** ${new_version}")

Auto-generate feature bullets from CHANGELOG:

Extract features from the [Unreleased] section of CHANGELOG.md:

# Extract [Unreleased] entries from CHANGELOG.md
if [ -f "CHANGELOG.md" ]; then
  # Get content between [Unreleased] and next version header
  unreleased_content=$(sed -n '/^## \[Unreleased\]/,/^## \[/p' CHANGELOG.md | head -n -1)

  # Extract Added entries (these become what-weve-built features)
  added_entries=$(echo "$unreleased_content" | sed -n '/^### Added/,/^### /p' | grep -E '^\s*-' | head -n -1 || true)

  if [ -n "$added_entries" ]; then
    echo "Features to add to what-weve-built.md:"
    echo "$added_entries"
  else
    echo "No new features in [Unreleased] section"
  fi
fi

If CHANGELOG has features, use the Edit tool to update docs/internal/what-weve-built.md:

  1. Update the "Recent Additions" section header:
    Edit(file_path="docs/internal/what-weve-built.md",
         old_string="### Recent Additions (v<old_version>)",
         new_string="### Recent Additions (v${new_version})")
  2. Add feature bullets under "Recent Additions": Transform CHANGELOG entries to what-weve-built format:

    CHANGELOG Formatwhat-weve-built Format
    - Feature description (#123)- **Feature Name** - Brief description
    - Multi-line feature (#123)\n - Sub-detail- **Feature Name** - Brief description

    Example transformation:

    # From CHANGELOG:
    - CHANGELOG update step in /exec skill (#320)
      - Instructs /exec to add [Unreleased] entries during feature commits
    
    # To what-weve-built:
    - **CHANGELOG Automation** - Automatic CHANGELOG entry requirements in /exec and /qa
  3. Recount the "At a Glance" table on every minor release — incrementing by hand let it drift to a third of the real numbers (it listed 57 test files when there were 343). Run after npm run build, write the results into the table, and update its "Counted …" line:
    echo "Slash Commands:        $(ls -d templates/skills/*/ | grep -vc _shared)"
    echo "CLI Commands:          $(node dist/bin/cli.js --help | awk '/^Commands:/{f=1;next} f && /^  [a-z]/ && $(1) == "help" {next} f && /^  [a-z]/ {n++} END{print n}')"
    echo "Core Library Modules:  $(git ls-files 'src/lib/*.ts' 'src/lib/**/*.ts' | grep -vE '\.test\.ts$|__tests__|__fixtures__|\.d\.ts$' | wc -l | tr -d ' ')"
    echo "Test Files:            $(git ls-files '*.test.ts' '*.test.tsx' | wc -l | tr -d ' ')"
    echo "Documentation Files:   $(git ls-files 'docs/*.md' 'docs/**/*.md' | grep -v '^docs/internal/' | wc -l | tr -d ' ')"
    echo "Stack Configurations:  $(node --input-type=module -e 'import("./dist/src/lib/stacks.js").then(m=>console.log(Object.keys(m.STACKS).length))')"
    echo "Lines of TypeScript:   $(git ls-files 'src/*.ts' 'src/**/*.ts' 'bin/*.ts' | grep -vE '\.test\.ts$|__tests__|__fixtures__' | xargs cat | wc -l | tr -d ' ')"

Fallback to commit-based detection:

If CHANGELOG.md doesn't exist or has no [Unreleased] section, fall back to commit-based detection:

last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$last_tag" ]; then
  echo "New features since ${last_tag}:"
  git log ${last_tag}..HEAD --oneline --no-merges | grep -E "^[a-f0-9]+ feat" || echo "  (none)"
fi

Ask the user to review what-weve-built.md before proceeding if there are new features to document.

Step 4.63: Verify what-weve-built.md Version Stamps

IMPORTANT: This runs after Step 4 (version bump) and Step 4.6 (which updates the stamps), so package.json already holds the new version. Running it earlier (in pre-flight) would compare against the previous release — which the file already reflects — so the gate would never fire for the version actually being released (root cause class of #684 / #687, the same ordering bug fixed for the README check in #685).

Warn-only (like the CHANGELOG freshness check): prompt the releaser if the title or ASCII-art version stamp omits/mismatches the version being released.

# Warn if docs/internal/what-weve-built.md version stamps don't match the version being released
if [ -f "docs/internal/what-weve-built.md" ]; then
  new_version=$(node -p "require('./package.json').version")

  # Check title version
  title_version=$(grep -oE "Sequant v[0-9]+\.[0-9]+\.[0-9]+" docs/internal/what-weve-built.md | head -1 | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" || true)
  if [ "$title_version" != "$new_version" ]; then
    echo "Warning: docs/internal/what-weve-built.md title shows v${title_version}, expected v${new_version}"
  fi

  # Check ASCII art version
  ascii_version=$(grep -E "SEQUANT v[0-9]+\.[0-9]+\.[0-9]+" docs/internal/what-weve-built.md | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" || true)
  if [ "$ascii_version" != "$new_version" ]; then
    echo "Warning: docs/internal/what-weve-built.md ASCII art shows v${ascii_version}, expected v${new_version}"
  fi
fi

Step 4.65: Verify CHANGELOG Freshness

IMPORTANT: This runs after Step 4 (version bump) so package.json already holds the new version. Running it earlier (in pre-flight) would compare against the previous release, which the CHANGELOG already lists — so the gate would never fire for the version actually being released (root cause class of #684).

Note (#701): #694 moved the changelog wall out of README.md into CHANGELOG.md and removed the README "What's new" section, so this gate now checks CHANGELOG.md (the source of truth). Grepping README.md for a now-absent # What's new heading made the warn-only gate fire on every release.

Warn-only (like the what-weve-built checks): prompt the releaser to add a CHANGELOG.md entry if the new version is absent.

# Warn if CHANGELOG.md omits the version being released
if [ -f "CHANGELOG.md" ]; then
  new_version=$(node -p "require('./package.json').version")

  # Match the Keep-a-Changelog heading for this version, e.g. "## [2.4.0] - 2026-05-30"
  if ! grep -qF "[${new_version}]" CHANGELOG.md; then
    echo "Warning: CHANGELOG.md omits v${new_version} — add a \"## [${new_version}]\" entry before releasing"
  fi
fi

Step 4.66: Verify README "What's new" Freshness

IMPORTANT: This runs after Step 4 (version bump) so package.json already holds the new version. Running it earlier (in pre-flight) would compare against the previous release — which the README already reflects — so the gate would never fire for the version actually being released (root cause class of #684 / #687, the same ordering bug fixed for the README check in #685).

Note (#702 / #707): This gate owns the hand-written ### What's new in <major>.<minor> positioning section that #702 re-added to README.md — distinct from the changelog wall that #694 moved to CHANGELOG.md (covered by Step 4.65). It keys on the minor line, not a full semver: "What's new in 2.6" is correct for the entire 2.6.x line, so a patch release (2.6.0 → 2.6.1) does NOT fire against a matching heading. During v2.6.0 the heading still read "What's new in 2.5" and shipped stale (fixed manually in 18c2d42); this gate flags that case.

Warn-only (like the CHANGELOG freshness check): prompt the releaser if the README's latest "What's new" heading lags the minor line being released. No README edit is auto-applied — the section's content and positioning are an editorial decision.

# Warn if README.md's latest "What's new" heading lags the minor line being released.
# Keys on the MINOR line (e.g. "2.6"), so a patch release against a matching heading stays silent.
if [ -f "README.md" ]; then
  new_version=$(node -p "require('./package.json').version")
  new_minor=$(echo "$new_version" | grep -oE '^[0-9]+\.[0-9]+')

  # Anchor to a markdown heading (^#+) so a "What's new in X.Y" mention in prose can't be read as
  # the section, and take the numerically-highest minor (sort -t. then tail -1) so the result is
  # independent of heading order in the file rather than assuming newest-first.
  readme_minor=$(grep -oE "^#+[[:space:]]+What's new in [0-9]+\.[0-9]+" README.md \
    | grep -oE '[0-9]+\.[0-9]+' | sort -t. -k1,1n -k2,2n | tail -1 || true)

  # The -n guard is load-bearing: if the section is ever removed again (as #694 did),
  # this gate stays silent rather than firing on every release (the bug #701 fixed for the old gate).
  if [ -n "$readme_minor" ] && [ "$readme_minor" != "$new_minor" ]; then
    echo "Warning: README.md 'What's new' section is for $readme_minor, expected $new_minor — add/update the heading before releasing"
  fi
fi

Step 4.67: Update SECURITY.md Supported Versions

IMPORTANT: Runs after Step 4 so package.json holds the new version.

SECURITY.md states that only the latest minor series receives security fixes and lists it in the ## Supported versions table. That row is typed, so every minor release must move it — otherwise the public policy points at the previous line (QA on #980 flagged exactly this rot).

Use the Edit tool on SECURITY.md: in the table under ## Supported versions, replace the supported row's minor (e.g. 2.14.x) with the new minor (e.g. 2.15.x) and the < 2.14 row with < 2.15. On a patch release the minor is unchanged and this step is a no-op.

# Verify: the supported row names the minor line being released.
new_minor=$(node -p "require('./package.json').version" | grep -oE '^[0-9]+\.[0-9]+')
if [ -f SECURITY.md ] && ! grep -qE "^\| ${new_minor}\.x \|" SECURITY.md; then
  echo "SECURITY.md supported-versions table does not list ${new_minor}.x — update it before releasing"
fi

Step 4.7: Regenerate Marketplace Artifact

IMPORTANT: The marketplace plugin artifact under dist/marketplace/ is bundled into the published tgz via files: ["dist", ...] in package.json — even though dist/ is gitignored. Regenerate it from current sources before packing/publishing so the bundled README always matches; otherwise a stale artifact ships (root cause of #684 — the v2.4.0 tgz carried a "Node.js 20+" README after the floor moved to 22.12).

This is an action step, so it lives in Release Steps (not the read-only pre-flight checks). It MUST run before Step 5 (npm pack) and Step 9 (npm publish).

# Regenerate the marketplace plugin artifact from current sources
npm run prepare:marketplace

# Freshness sanity check: generated README's Node.js floor should match package.json engines.
# Warn-only (regeneration above already fixes the artifact; this is a belt-and-suspenders signal).
gen_readme="dist/marketplace/external_plugins/sequant/README.md"
if [ -f "$gen_readme" ]; then
  engines_floor=$(node -p "require('./package.json').engines.node.replace(/[^0-9.]/g,'')")  # e.g. 22.13.0
  readme_floor=$(grep -oE "Node\.js [0-9]+\.[0-9]+" "$gen_readme" | head -1 | grep -oE "[0-9]+\.[0-9]+" || true)
  if [ -n "$readme_floor" ] && [ "${engines_floor%.*}" != "$readme_floor" ]; then
    echo "Warning: generated marketplace README shows Node.js ${readme_floor}, package.json engines floor is ${engines_floor}"
  fi
fi

Step 5: Verify Package Size

# Check what will be published
npm pack --dry-run

# Verify size is reasonable (warn if > 500KB)
size=$(npm pack --dry-run 2>&1 | grep "total files" -A1 | tail -1 || true)
echo "Package size: ${size}"

Step 6: Commit and Land Through a PR

main is protected by a ruleset (#1109): it requires the test and canary checks, strict up-to-date, and rejects every direct push with GH013. The release commit lands the same way every other commit does — through a PR that passes the same checks (#1131). Do not push main; do not add a bypass.

new_version=$(node -p "require('./package.json').version")
git switch -c "chore/release-v${new_version}"
git add package.json package-lock.json CHANGELOG.md plugin/.claude-plugin/plugin.json .claude-plugin/marketplace.json docs/internal/what-weve-built.md SECURITY.md README.md
# The shipped MCP config is gitignored-but-tracked; Step 4.7 re-pinned it to this version (#988), so force-add it into the release commit.
git add -f .mcp.json
git commit -m "chore: release v${new_version}"
git push -u origin "chore/release-v${new_version}"
gh pr create --base main --head "chore/release-v${new_version}" \
  --title "chore: release v${new_version}" \
  --body "Release v${new_version}. Lands through a PR because the main ruleset rejects direct pushes (#1109, #1131)."

Wait for test and canary on the PR head (poll commits/<sha>/check-runs, not gh pr checks --watch, which can report the previous head), then:

gh pr merge --squash --delete-branch --subject "chore: release v${new_version}"
git switch main && git pull --ff-only origin main

Step 7 tags the squash commit on main; tags are outside the branch rule.

Step 7: Create and Push Tag

git tag -a "v${new_version}" -m "Release v${new_version}"
git push origin "v${new_version}"

Step 8: Create GitHub Release

# Regular release
gh release create "v${new_version}" \
  --title "v${new_version} - {title}" \
  --notes "{release_notes}"

# Pre-release (alpha/beta/rc)
gh release create "v${new_version}" \
  --title "v${new_version}" \
  --notes "{release_notes}" \
  --prerelease

For the title, use a short summary:

  • v1.4.0 - New Feature Name
  • v1.3.2 - Bug Fix Release
  • v2.0.0 - Breaking Changes

Step 9: Publish to npm

<!-- BEGIN: next-tag-publish (#1098) -->

A regular release publishes to the next dist-tag, never straight to latest (#1098). latest moves only in Step 9b, after the canary job has passed against the published tarball and a manual soak. Downstream projects run npx sequant@latest, so a defect that only shows up in a real install must be found on next first.

# Regular release: `next` first
npm publish --tag next

# Pre-release with its own tag (also never becomes "latest")
npm publish --tag beta

If npm returns EOTP (2FA required):

Non-interactive environments cannot handle the OTP prompt. Hand the user this exact line. package.json sets "publishConfig": { "tag": "next" } (#1184), so a bare npm publish also lands on next — before that, a bare publish moved latest immediately, on both 2.17.0 and 2.18.0. Keep --tag next in the handoff line anyway: it is explicit and harmless. An explicit --tag still overrides publishConfig (pre-releases use --tag beta). With npm web auth the --otp part may be omitted:

npm publish --tag next --otp=<code>

When the user reports done, verify before continuing: npm view sequant dist-tags must show next: <new_version> and an unchanged latest. If latest moved, run the Step 9b soak checklist immediately and keep npm dist-tag add sequant@<previous> latest ready as the rollback.

A publish that npm accepted can take several minutes to appear: the registry answers PUT … 202 ("being processed"), and until then npm view sequant@<new_version> returns 404 and dist-tags looks unchanged. That is lag, not failure (seen on 2.18.0). Before concluding the publish failed, read the newest ~/.npm/_logs/*-debug-0.log: find the argv (was it a bare npm publish?), the Publishing to … with tag … line, and the PUT status. Then poll the registry directly, bypassing npm's cache: curl -s https://registry.npmjs.org/sequant | jq '."dist-tags"'.

Do NOT attempt to pass OTP codes programmatically or retry npm publish in a loop. Hand off to the user and continue with post-release verification once they confirm.

Step 9b: Promote next to latest (requires --soaked)

Print this soak checklist, then stop unless --soaked was passed:

Soak checklist for {new_version} on the next tag
  [ ] CI `canary` job green against the published tarball
  [ ] npx sequant@next sync --dry-run, run in each local repo of the maintainer's own projects, lists only expected decisions
  [ ] npx sequant@next doctor exits 0 in those repos

Without --soaked: refuse. Do not run npm dist-tag add ... latest. Report "Published to next; latest unchanged. Re-run /release --soaked after the soak checklist passes." and finish. Never infer --soaked from context or pass it yourself.

With --soaked:

npm dist-tag add sequant@${new_version} latest

If npm returns EOTP, hand off to the user with npm dist-tag add sequant@${new_version} latest --otp=<code>; same no-retry rule as Step 9.

<!-- END: next-tag-publish (#1098) -->

Post-Release Verification

Verify both platforms show the new version:

# npm verification — a 202 publish can take several minutes to appear (see Step 9);
# poll the registry directly rather than trusting one `npm view`
curl -s https://registry.npmjs.org/sequant | jq '."dist-tags"'   # next = new version; latest only after Step 9b

# GitHub verification
gh release view "v${new_version}"

# Test install
npx sequant@${new_version} --version

The release is complete — hand the working tree back (#901). This is the counterpart to the acquire in Step 0.

npx sequant locks checkout release --issue=999999999 || true

Output Summary

Release v{version} Complete

  Version:  {old} → {new}
  Commit:   {hash}
  Tag:      v{new}

  GitHub:   https://github.com/sequant-io/sequant/releases/tag/v{new}
  npm:      https://www.npmjs.com/package/sequant/v/{new}
  Plugin:   Version synced in plugin/.claude-plugin/plugin.json
  Docs:     Version synced in docs/internal/what-weve-built.md

  Install (npm):
    npm install sequant@{new}
    npx sequant@{new}

  Install (plugin):
    /plugin marketplace update sequant-io/sequant   # refreshes the marketplace LISTING only
    /plugin install sequant                         # new installs

  Update an existing plugin install:
    claude plugin update sequant@sequant            # then restart Claude Code

Verification:
  [x] npm view shows correct version
  [x] GitHub release created
  [x] Tag pushed
  [x] plugin.json version synced
  [x] what-weve-built.md version synced
  [x] New features documented (if any)

Next steps:
  - Announce release (if major/minor)
  - Update dependent projects
  - Monitor for issues
  - Installed plugins do NOT pick up this release automatically: Claude Code
    pins each install at its installed version, and `/plugin marketplace
    update` refreshes only the marketplace listing. Each existing install
    updates only via `claude plugin update sequant@sequant` + restart
    (the pre-tool hook nags stale installs once a day — #784)

Dry Run Mode

When --dry-run is specified:

  1. Run all pre-flight checks
  2. Show what version would be created
  3. Show draft release notes
  4. Show npm pack --dry-run output
  5. Do NOT execute any publish commands
Dry Run Summary

  Would release: 1.3.1 → 1.4.0

  Package contents:
    - 176 files
    - 195.9 kB packed

  Release notes:
    [draft notes here]

  To execute: /release minor

Rollback Procedures

Before npm publish

# Undo version bump (if not committed)
git checkout -- package.json package-lock.json

# Undo commit (if not pushed)
git reset --soft HEAD~1

# Delete local tag
git tag -d v{version}

# Rollback aborts the release — hand the working tree back (#901/#906).
# The git reset above is a guarded verb; it runs unrefused only because Step 0
# made this session the checkout holder.
npx sequant locks checkout release --issue=999999999 || true

After git push, before npm publish

# Delete remote tag
git push origin :refs/tags/v{version}

# Delete GitHub release
gh release delete v{version} --yes

# Revert commit
git revert HEAD
git push origin main

# Rollback aborts the release — hand the working tree back (#901/#906).
npx sequant locks checkout release --issue=999999999 || true

After npm publish

Do NOT use npm unpublish - it breaks dependent projects.

Instead:

  1. Publish a patch fix: npm version patch && npm publish
  2. Deprecate bad version: npm deprecate sequant@{bad} "Use {good} instead"

Error Handling

If a release step fails and you halt without completing, release the checkout lock first (#901/#906) — Step 0 acquired it and every abort after that must hand the working tree back before stopping:

npx sequant locks checkout release --issue=999999999 || true
ErrorCauseResolution
"Not on main"Wrong branchgit checkout main
"Uncommitted changes"Dirty working treegit stash or commit
"Behind origin"Remote has new commitsgit pull origin main
"Tests failed"Failing testsFix tests first
"Version exists"Already publishedChoose different version
"Not logged in (npm)"npm auth expirednpm login
"Not logged in (gh)"GitHub auth expiredgh auth login
"OTP required"npm 2FA enabledEnter code when prompted
"Package too large"Bloated packageCheck .npmignore

Best Practices

Versioning

  • Follow Semantic Versioning
  • Use pre-release tags for testing: 1.4.0-beta.1
  • Major version 0.x.x indicates unstable API

Release Notes

  • Write for users, not developers
  • Highlight breaking changes prominently
  • Include migration guides for major releases
  • Link to relevant issues/PRs

npm

  • Keep package size small (< 500KB ideal)
  • Use files in package.json or .npmignore
  • Test with npm pack before publishing
  • Use --tag for pre-releases to avoid becoming "latest"

GitHub

  • Use pre-release flag for beta/RC versions
  • Attach relevant assets (if any)
  • Use consistent title format

Security

  • Run npm audit before releasing
  • Don't include sensitive files (check npm pack --dry-run)
  • Verify npm whoami shows correct account

Examples

Standard Patch Release

/release patch

Minor Release with Custom Notes

/release minor
# Review generated notes
# Edit if needed
# Approve to publish

Beta Pre-release

/release minor --prerelease beta

Creates: 1.4.0-beta.0

Preview Without Publishing

/release minor --dry-run

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.