Release ArcKit
Drive the ArcKit release flow end to end. This skill is the executable companion to
docs/RELEASING.md — that doc is the source of truth; if the two
disagree, trust RELEASING.md and update this skill.
The target version is $ARGUMENTS (e.g. /release 5.10.0). If no version was given, ask
for it before doing anything — never guess a version.
Preconditions
Confirm all of these before touching version files:
- The feature PR is merged to
main — releases are cut from main, never from a branch.
- Working tree is on
main and clean: git checkout main && git pull && git status.
- The version
$ARGUMENTS is valid semver (X.Y.Z) and greater than the current
plugin version (cat plugins/arckit-claude/VERSION) and CLI version (cat VERSION).
GH_TOKEN is set in the environment (needed by push-extensions.sh).
If any precondition fails, stop and surface it. Do not work around it.
Release flow
Run these in order. Each script is idempotent or safe to re-run except the git tag/git push
steps. Pause after step 6 (the commit) and after step 8 before tagging to let the user confirm.
# 1. Edit CHANGELOGs by hand — both of them:
# CHANGELOG.md (CLI) and plugins/arckit-claude/CHANGELOG.md (plugin).
# Preview what shipped since the last tag to seed the entries:
./scripts/generate-release-notes.sh
# 2. Bump every version file in lockstep (15 locations: VERSION files,
# manifests, README badges, docs, plugin.json):
./scripts/bump-version.sh X.Y.Z
# 3. Regenerate Codex/OpenCode/Gemini/Copilot/Paperclip formats from the
# plugin source — MUST run after the bump so the extensions carry the
# new version:
python scripts/converter.py
# 4. Validate generated extension outputs before committing:
pytest tests/codex/test_codex_extension.py \
tests/gemini tests/opencode tests/copilot \
tests/vibe/test_vibe_extension.py \
tests/paperclip/test_commands_json.py \
tests/plugin/test_release_process.py
# 5. Sanity-check the tree (no stray edits, counts/versions consistent):
git status && git diff --stat
# 6. Commit the bump — a clean tree is required for `claude plugin tag`:
git add -A && git commit -m "chore: bump version to X.Y.Z"
# 7. Validate EVERY plugin manifest against the marketplace entry.
# Discover plugins dynamically — do NOT hardcode the list (it grows).
# Search from `plugins`, not `.`: from the repo root the manifests sit at
# depth 4, so `find . -maxdepth 3` matches nothing and the loop below
# "passes" having validated not one plugin.
mapfile -t manifests < <(find plugins -maxdepth 3 -path '*/.claude-plugin/plugin.json' | sort)
(( ${#manifests[@]} > 0 )) || { echo "No plugin manifests found — check the find path"; exit 1; }
echo "Validating ${#manifests[@]} plugin manifests..."
for manifest in "${manifests[@]}"; do
dir=${manifest%/.claude-plugin/plugin.json} # PATH — `claude plugin tag` rejects the name
claude plugin tag "$dir" --dry-run || { echo "VERSION DRIFT: $dir"; exit 1; }
done
# 8. (optional) Prune orphaned plugin deps:
claude plugin prune --dry-run
# 9. Tag the umbrella release and push — this triggers
# .github/workflows/release.yml, which creates the GitHub Release.
# Push the tag BY NAME: `git push --tags` pushes every local tag, and
# published a stray `pre-rebase-*` backup tag during v6.8.0.
git tag -a vX.Y.Z -m "vX.Y.Z"
git push && git push origin vX.Y.Z
# 10. Create native per-plugin tags (arckit--vX.Y.Z, arckit-uae--vX.Y.Z, …).
# Auto-discovers plugins; idempotent (skips existing tags):
./scripts/tag-plugins.sh X.Y.Z
# 11. Push each distribution to its standalone GitHub repo
# (tractorjuice/arckit-claude, arckit-gemini, arckit-codex, …).
# The claude target publishes the full Claude marketplace repo: core at
# the repo root, overlays under plugin/... paths. This also creates or
# preserves each repo's vX.Y.Z tag and GitHub Release:
./scripts/push-extensions.sh
After step 11, confirm the GitHub Release was created (the release.yml workflow runs on the
vX.Y.Z tag push). Also confirm every standalone repo has a vX.Y.Z tag and GitHub
Release, then report the release URLs and which standalone repos were pushed.
Then write the release article for the major features — docs/articles/YYYY-MM-DD-<slug>.md
plus a hero generator script, force-added past the drafts-folder gitignore. No Markdown
tables — LinkedIn and Medium do not render them; write comparisons as prose. See
"Release article" in RELEASING.md for the full conventions (voice, hero style, community
block, contributor credits).
Common Gotchas
The highest-signal failures — collected from real releases. Read these before running anything.
- Releasing from a feature branch. Releases are cut from
main after the PR is merged.
Tagging a branch ships an unmerged tree. Always git checkout main && git pull first.
- Forgetting the converter (step 3).
bump-version.sh updates the plugin source, but the
Codex/OpenCode/Gemini/Copilot/Paperclip copies are generated. Skip converter.py and the
extensions ship the old version. The converter must run after the bump and before the
commit, so the regenerated files are included.
- Skipping extension tests. Run the step 4 extension suite after
converter.py. It validates
Codex, Gemini, OpenCode, Copilot, Vibe, Paperclip, release inventory, version alignment, and
platform-specific command rewrites before anything is tagged.
- Releasing from a Mac.
bump-version.sh runs on stock macOS (BSD sed, bash 3.2) and asserts
every substitution lands — a does not contain "…" after edit error is pattern drift, not a
sed-flavour problem. tag-plugins.sh and push-extensions.sh still need bash ≥ 4:
brew install bash, then /opt/homebrew/bin/bash scripts/…. Full macOS notes (uv commands for
the converter and tests, the http.postBuffer fix for protocol error: bad line length pushes,
GH_TOKEN via gh auth token) live in RELEASING.md's "Releasing from macOS" section.
- Hardcoding the plugin list. The Claude marketplace now ships 16 plugins (core plus
regional, sector, method, agent-architecture, tooling, and supplier overlays) and keeps growing. Older examples listed
only 7, silently skipping newer plugins. Discover plugins dynamically (step 7) -- this is the
exact bug that shipped
arckit-uk-nhs untagged mid-v5.4.0, which is why tag-plugins.sh
now auto-discovers. Never copy a static plugin array.
- A
for loop over an empty glob exits 0. Step 7's discovery searched from . while the
manifests sit at depth 4, so -maxdepth 3 matched nothing and the validation reported a
clean pass having checked not one plugin -- through every release from v6.0.0 to v6.7.4.
Dynamic discovery only helps if you assert it found something, hence the count check and the
Validating N plugin manifests... line. Treat any loop whose body might never run as
unvalidated until it prints what it covered.
claude plugin tag takes a PATH, not a plugin name. Passing the name from plugin.json
(arckit) resolves relative to the repo root and fails ✘ Path not found: /…/arckit. Use the
plugin directory: plugins/arckit-claude. This compounded the bug above -- had the glob ever
matched, every iteration would have failed anyway.
- Validate before
tag-plugins.sh, never after. Once the native name--vX.Y.Z tags exist,
the dry-run fails with Tag "…" already exists locally -- correct behaviour, not version drift,
but it will send you hunting for a problem that isn't there.
claude plugin tag needs a clean tree. Run it after the commit (step 6), not before, or
it errors on the dirty working tree.
claude plugin tag is --dry-run only here. It creates name--vX.Y.Z style tags that do
not match release.yml's v[0-9]+.[0-9]+.[0-9]+ trigger. We use it solely for its
validation side effect (cross-checking plugin.json vs marketplace.json). The real release
tag is the git tag -a vX.Y.Z in step 8.
- Two CHANGELOGs, not one.
CHANGELOG.md is the CLI changelog; plugins/arckit-claude/CHANGELOG.md
is the plugin changelog. Both need an entry. They are human-authored — generate-release-notes.sh
only previews what changed, it does not write them.
- CLI and plugin versions are independent but bumped together.
bump-version.sh moves both to
the same number by design; don't try to skew them.
push-extensions.sh needs GH_TOKEN and skips repos that don't yet exist on GitHub — a
"skipped" line is not an error for a brand-new extension, but double-check it's not skipping a
repo that should exist. The claude target writes the full arckit-claude marketplace repo,
including the public-but-proprietary plugin/uk/gcloud/ overlay and its license exception. It
now creates/preserves standalone repo vX.Y.Z tags and GitHub Releases; use
ARCKIT_SKIP_EXTENSION_RELEASES=1 only when intentionally doing a commit-only sync.
- Tables in release articles. LinkedIn and Medium do not render Markdown tables — a
comparison pasted there lands as a wall of pipes. Write it as prose. Articles live in the
gitignored
docs/articles/ drafts folder; force-add the keepers (git add -f).
- Articles are for users, not maintainers. Lead each section with what changed for the
reader and why it matters; no file names, hook names, schema or regex talk in the body — say
what a mechanism does, not what it is called. Hero labels follow the same rule. The first draft
of the commerce-skills article (#845) had to be rewritten for this; the maintainer detail lives
in the CHANGELOG and the PR body, which the article can link to.
- Do not put release numbers in extension READMEs. Extension release identity lives in
VERSION files, manifests, Git tags, and GitHub Releases. README-pinned versions drift and
are blocked by tests/plugin/test_release_process.py.
- Order is load-bearing. bump → convert → extension tests → commit → validate → tag → tag-plugins → push-extensions.
Re-running an earlier step after a later one (e.g. editing files after the commit) means the tag
no longer points at the released tree. If you edit after committing, redo from the commit.
Reference
docs/RELEASING.md — full release documentation and rationale
scripts/bump-version.sh — version bump across all files
scripts/generate-release-notes.sh — changelog preview from git log
scripts/tag-plugins.sh — native per-plugin tags (auto-discovers)
scripts/push-extensions.sh — pushes distribution dirs to standalone repos
.github/workflows/release.yml — creates the GitHub Release on v* tag push
1---2name: release3description: Cut a new ArcKit release — bump versions in lockstep, regenerate non-Claude formats, validate plugin/marketplace agreement, tag, and push to standalone repos. Use when the user says 'cut a release', 'release ArcKit', 'ship vX.Y.Z', 'bump the version and release', 'do the release flow', 'tag and publish', or 'push the extensions'. This is a manual, high-consequence workflow: it never runs automatically.4---56# Release ArcKit78Drive the ArcKit release flow end to end. This skill is the executable companion to9[`docs/RELEASING.md`](../../../docs/RELEASING.md) — that doc is the source of truth; if the two10disagree, trust `RELEASING.md` and update this skill.1112The target version is `$ARGUMENTS` (e.g. `/release 5.10.0`). If no version was given, ask13for it before doing anything — never guess a version.1415<HARD-GATE>16This workflow tags and publishes to public GitHub repos. Do NOT run any step that pushes,17tags, or publishes without the user confirming the version and that the PR is already merged18to `main`. Show the plan and the computed version first, then proceed step by step.19</HARD-GATE>2021## Preconditions2223Confirm all of these before touching version files:24251. The feature PR is **merged to `main`** — releases are cut from `main`, never from a branch.262. Working tree is on `main` and clean: `git checkout main && git pull && git status`.273. The version `$ARGUMENTS` is valid semver (`X.Y.Z`) and **greater** than the current28 plugin version (`cat plugins/arckit-claude/VERSION`) and CLI version (`cat VERSION`).294. `GH_TOKEN` is set in the environment (needed by `push-extensions.sh`).3031If any precondition fails, stop and surface it. Do not work around it.3233## Release flow3435Run these in order. Each script is idempotent or safe to re-run except the `git tag`/`git push`36steps. Pause after step 6 (the commit) and after step 8 before tagging to let the user confirm.3738```bash39# 1. Edit CHANGELOGs by hand — both of them:40# CHANGELOG.md (CLI) and plugins/arckit-claude/CHANGELOG.md (plugin).41# Preview what shipped since the last tag to seed the entries:42./scripts/generate-release-notes.sh4344# 2. Bump every version file in lockstep (15 locations: VERSION files,45# manifests, README badges, docs, plugin.json):46./scripts/bump-version.sh X.Y.Z4748# 3. Regenerate Codex/OpenCode/Gemini/Copilot/Paperclip formats from the49# plugin source — MUST run after the bump so the extensions carry the50# new version:51python scripts/converter.py5253# 4. Validate generated extension outputs before committing:54pytest tests/codex/test_codex_extension.py \55 tests/gemini tests/opencode tests/copilot \56 tests/vibe/test_vibe_extension.py \57 tests/paperclip/test_commands_json.py \58 tests/plugin/test_release_process.py5960# 5. Sanity-check the tree (no stray edits, counts/versions consistent):61git status && git diff --stat6263# 6. Commit the bump — a clean tree is required for `claude plugin tag`:64git add -A && git commit -m "chore: bump version to X.Y.Z"6566# 7. Validate EVERY plugin manifest against the marketplace entry.67# Discover plugins dynamically — do NOT hardcode the list (it grows).68# Search from `plugins`, not `.`: from the repo root the manifests sit at69# depth 4, so `find . -maxdepth 3` matches nothing and the loop below70# "passes" having validated not one plugin.71mapfile -t manifests < <(find plugins -maxdepth 3 -path '*/.claude-plugin/plugin.json' | sort)7273(( ${#manifests[@]} > 0 )) || { echo "No plugin manifests found — check the find path"; exit 1; }74echo "Validating ${#manifests[@]} plugin manifests..."7576for manifest in "${manifests[@]}"; do77 dir=${manifest%/.claude-plugin/plugin.json} # PATH — `claude plugin tag` rejects the name78 claude plugin tag "$dir" --dry-run || { echo "VERSION DRIFT: $dir"; exit 1; }79done8081# 8. (optional) Prune orphaned plugin deps:82claude plugin prune --dry-run8384# 9. Tag the umbrella release and push — this triggers85# .github/workflows/release.yml, which creates the GitHub Release.86# Push the tag BY NAME: `git push --tags` pushes every local tag, and87# published a stray `pre-rebase-*` backup tag during v6.8.0.88git tag -a vX.Y.Z -m "vX.Y.Z"89git push && git push origin vX.Y.Z9091# 10. Create native per-plugin tags (arckit--vX.Y.Z, arckit-uae--vX.Y.Z, …).92# Auto-discovers plugins; idempotent (skips existing tags):93./scripts/tag-plugins.sh X.Y.Z9495# 11. Push each distribution to its standalone GitHub repo96# (tractorjuice/arckit-claude, arckit-gemini, arckit-codex, …).97# The claude target publishes the full Claude marketplace repo: core at98# the repo root, overlays under plugin/... paths. This also creates or99# preserves each repo's vX.Y.Z tag and GitHub Release:100./scripts/push-extensions.sh101```102103After step 11, confirm the GitHub Release was created (the `release.yml` workflow runs on the104`vX.Y.Z` tag push). Also confirm every standalone repo has a `vX.Y.Z` tag and GitHub105Release, then report the release URLs and which standalone repos were pushed.106107Then write the release article for the major features — `docs/articles/YYYY-MM-DD-<slug>.md`108plus a hero generator script, force-added past the drafts-folder gitignore. **No Markdown109tables** — LinkedIn and Medium do not render them; write comparisons as prose. See110"Release article" in RELEASING.md for the full conventions (voice, hero style, community111block, contributor credits).112113## Common Gotchas114115The highest-signal failures — collected from real releases. Read these before running anything.116117- **Releasing from a feature branch.** Releases are cut from `main` after the PR is merged.118 Tagging a branch ships an unmerged tree. Always `git checkout main && git pull` first.119- **Forgetting the converter (step 3).** `bump-version.sh` updates the plugin source, but the120 Codex/OpenCode/Gemini/Copilot/Paperclip copies are *generated*. Skip `converter.py` and the121 extensions ship the **old** version. The converter must run *after* the bump and *before* the122 commit, so the regenerated files are included.123- **Skipping extension tests.** Run the step 4 extension suite after `converter.py`. It validates124 Codex, Gemini, OpenCode, Copilot, Vibe, Paperclip, release inventory, version alignment, and125 platform-specific command rewrites before anything is tagged.126- **Releasing from a Mac.** `bump-version.sh` runs on stock macOS (BSD sed, bash 3.2) and asserts127 every substitution lands — a `does not contain "…" after edit` error is pattern drift, not a128 sed-flavour problem. `tag-plugins.sh` and `push-extensions.sh` still need bash ≥ 4:129 `brew install bash`, then `/opt/homebrew/bin/bash scripts/…`. Full macOS notes (uv commands for130 the converter and tests, the `http.postBuffer` fix for `protocol error: bad line length` pushes,131 `GH_TOKEN` via `gh auth token`) live in RELEASING.md's "Releasing from macOS" section.132- **Hardcoding the plugin list.** The Claude marketplace now ships 16 plugins (core plus133 regional, sector, method, agent-architecture, tooling, and supplier overlays) and keeps growing. Older examples listed134 only 7, silently skipping newer plugins. Discover plugins dynamically (step 7) -- this is the135 exact bug that shipped `arckit-uk-nhs` untagged mid-v5.4.0, which is why `tag-plugins.sh`136 now auto-discovers. Never copy a static plugin array.137- **A `for` loop over an empty glob exits 0.** Step 7's discovery searched from `.` while the138 manifests sit at depth 4, so `-maxdepth 3` matched nothing and the validation reported a139 clean pass having checked not one plugin -- through every release from v6.0.0 to v6.7.4.140 Dynamic discovery only helps if you assert it found something, hence the count check and the141 `Validating N plugin manifests...` line. Treat any loop whose body might never run as142 unvalidated until it prints what it covered.143- **`claude plugin tag` takes a PATH, not a plugin name.** Passing the `name` from `plugin.json`144 (`arckit`) resolves relative to the repo root and fails `✘ Path not found: /…/arckit`. Use the145 plugin directory: `plugins/arckit-claude`. This compounded the bug above -- had the glob ever146 matched, every iteration would have failed anyway.147- **Validate before `tag-plugins.sh`, never after.** Once the native `name--vX.Y.Z` tags exist,148 the dry-run fails with `Tag "…" already exists locally` -- correct behaviour, not version drift,149 but it will send you hunting for a problem that isn't there.150- **`claude plugin tag` needs a clean tree.** Run it *after* the commit (step 6), not before, or151 it errors on the dirty working tree.152- **`claude plugin tag` is `--dry-run` only here.** It creates `name--vX.Y.Z` style tags that do153 **not** match `release.yml`'s `v[0-9]+.[0-9]+.[0-9]+` trigger. We use it solely for its154 validation side effect (cross-checking `plugin.json` vs `marketplace.json`). The real release155 tag is the `git tag -a vX.Y.Z` in step 8.156- **Two CHANGELOGs, not one.** `CHANGELOG.md` is the CLI changelog; `plugins/arckit-claude/CHANGELOG.md`157 is the plugin changelog. Both need an entry. They are human-authored — `generate-release-notes.sh`158 only *previews* what changed, it does not write them.159- **CLI and plugin versions are independent but bumped together.** `bump-version.sh` moves both to160 the same number by design; don't try to skew them.161- **`push-extensions.sh` needs `GH_TOKEN`** and skips repos that don't yet exist on GitHub — a162 "skipped" line is not an error for a brand-new extension, but double-check it's not skipping a163 repo that *should* exist. The `claude` target writes the full `arckit-claude` marketplace repo,164 including the public-but-proprietary `plugin/uk/gcloud/` overlay and its license exception. It165 now creates/preserves standalone repo `vX.Y.Z` tags and GitHub Releases; use166 `ARCKIT_SKIP_EXTENSION_RELEASES=1` only when intentionally doing a commit-only sync.167- **Tables in release articles.** LinkedIn and Medium do not render Markdown tables — a168 comparison pasted there lands as a wall of pipes. Write it as prose. Articles live in the169 gitignored `docs/articles/` drafts folder; force-add the keepers (`git add -f`).170- **Articles are for users, not maintainers.** Lead each section with what changed for the171 reader and why it matters; no file names, hook names, schema or regex talk in the body — say172 what a mechanism does, not what it is called. Hero labels follow the same rule. The first draft173 of the commerce-skills article (#845) had to be rewritten for this; the maintainer detail lives174 in the CHANGELOG and the PR body, which the article can link to.175- **Do not put release numbers in extension READMEs.** Extension release identity lives in176 `VERSION` files, manifests, Git tags, and GitHub Releases. README-pinned versions drift and177 are blocked by `tests/plugin/test_release_process.py`.178- **Order is load-bearing.** bump → convert → extension tests → commit → validate → tag → tag-plugins → push-extensions.179 Re-running an earlier step after a later one (e.g. editing files after the commit) means the tag180 no longer points at the released tree. If you edit after committing, redo from the commit.181182## Reference183184- [`docs/RELEASING.md`](../../../docs/RELEASING.md) — full release documentation and rationale185- `scripts/bump-version.sh` — version bump across all files186- `scripts/generate-release-notes.sh` — changelog preview from git log187- `scripts/tag-plugins.sh` — native per-plugin tags (auto-discovers)188- `scripts/push-extensions.sh` — pushes distribution dirs to standalone repos189- `.github/workflows/release.yml` — creates the GitHub Release on `v*` tag push