release — versioned releases with git-flow discipline
Requires: the sibling
protocolsskill (shared protocol masters); usesskills.config.jsonwhen present (releasesection — defaults apply without it). Missing protocols → tell the user to install the full supermodo package.
The message of this skill: main only ever contains released states (it is
what installers and users consume), so releasing is a deliberate, gated act.
Everything scriptable is scripted; git mutations pass a consent gate that
defaults to an explicit per-release yes (the yes IS the authorization, for
THIS release only) and can be relaxed to auto via confirmations in config.
Cross-tool note (Claude Code ↔ Codex). Written in Claude Code idioms. Under Codex: run the same script with
node, ask the questions in chat, and execute the same git sequences with your native shell tool.
Configuration
skills.config.json → release section (all optional, defaults per
../protocols/references/config.md): branches.main/branches.dev,
versionFile + versionPath (where the version lives —
package.json:version, .claude-plugin/plugin.json:version, …), changelog,
tagPrefix, tagStyle, mergeStrategy ("squash" default | "merge"),
remote, versionPattern, alphaPolicy. Nothing in this section names a
forge or an integration mode: how the main branch is written and what
publishes the release are steps of the process, and they live in the rules
file in the project's own words.
Two keys were removed and are migrated, never ignored. release.mode
named a workflow, and a workflow is a sequence, so it now lives in
.supermodo/rules/release.md frontmatter (template: light|full).
release.githubRelease hardcoded one forge into a boolean; whether and how a
release is published is the publish-release step of the rules file. A config
still carrying either is a config-check error naming
the migration; the preflight also reports them under migrations. Perform the
migration through the config skill — config --rules release to materialize
the template, and let config rewrite the config file under its own dry-run
and approval gate. release never edits skills.config.json itself; it is a
read-only preflight and stays one.
The project's process (read FIRST)
Read .supermodo/rules/release.md if it exists. It IS the release process for
this project. Absent, the process is the shipped default template
(rules-templates/light.md) — never a sequence remembered from this file. What
follows below is HOW to execute each step correctly: the exact commands, the
gates, the failure reporting. The rules file says WHICH steps, in what order,
on which branches. Then read
.supermodo/rules/INDEX.md if present and load only the cross-cutting files
naming release (typically vcs.md). Contract:
../protocols/references/rules.md.
The skill still supplies every capability the process names: establishing the version state from git, deriving the bump from the Conventional Commits in the unreleased range, building the changelog entry from fragments, generating the exact git sequence for this repository, and handing the changelog entry to whatever this project publishes with.
What NEVER comes from that file: preflight blockers halt and are never worked
around; version state is established from git refs, never from the checkout
alone; the version bump and changelog entry travel in one commit; consumed
fragments are deleted in that commit and never counted twice; the complete
command sequence is generated by the preflight and shown before anything runs;
a failure stops at the first command and reports exactly which ran; a hotfix
that leaves an open stabilization branch un-merged is incomplete, and which
branch receives it is ASKED whenever more than one is open — never picked
silently; worktree cleanup is printed, never run; gh missing is reported NOT
done, never silently skipped.
Absent file → the first-use gate at the end of Step 1, under the preflight
result, before Step 2 writes anything. Absence alone is not first run: read
.skills/supermodo/config-manifest.json and gate only when the file is absent
AND rulesDeclined does not name "release".
Step 1 — Preflight (deterministic, read-only)
Establish where the release actually got to — from git, never from memory.
A release is many steps (version bump, changelog, merge, tag, push, publish)
and any of them may already have been run by the user or by an earlier
attempt. Read HEAD, the tags (local AND remote) and the branches — the
preflight prints all three — and, when the rules file names a way to list
what this project has published, that too, before you say anything about
state. Never run a forge CLI the rules file did not name. NEVER report a step as
"still pending" or "never done" because you did not see it happen in this conversation: a proposal that went unanswered in chat is not evidence of an unchanged repository. If the tag exists, say so and continue from there — re-running a completed step is how tags get moved and releases get duplicated.
If skills.config.json exists, validate it FIRST (run the config skill's
config-check.ts or apply ../protocols/references/config.md) — never feed
unvalidated values into git commands. Then run the bundled script, resolved
RELATIVE TO THIS SKILL FOLDER:
node <dir-of-this-SKILL.md>/scripts/release-check.ts [project-root] [--hotfix]
(--hotfix: expects main or an existing hotfix/* branch instead of dev,
and the bump is patch.)
Version state comes from git first, the working tree last. The script
reads every tag matching tagPrefix, both long-lived branches, and their
remote-tracking counterparts, resolving the version file and changelog AT each
of those refs — and only then compares the checkout against them. That order is
the point: the checkout is the one place the version can be stale, so it is
never the baseline. It refreshes remote-tracking refs first (git fetch --tags --prune) — that writes no working tree, no branch and no history, and a
preflight verified against refs that went stale days ago has verified nothing.
There is no flag to skip it: the fetch is what makes every other answer
trustworthy, and a switch whose only effect is to reintroduce the staleness
this check exists to catch is a footgun, not an option. Offline is handled by
DEGRADING — the fetch fails, that becomes a warning, and the result says
plainly that only local refs were checked.
Reading refs rather than HEAD's ancestry is what catches the failures a
checkout cannot see, each reported as a blocker with its repair command beside
it: a hotfix tag published on main while you stand on dev (invisible to
git describe), a branch behind its remote, a missing back-merge, and a
version file declaring less than something already published — a release from
there rolls the published version BACKWARDS.
Version files are parsed as JSONC — comments and trailing commas are fine, so
a deno.jsonc or a tsconfig.json is readable; skills.config.json is read
the same way. A version file the project's own toolchain accepts is never
reported as "cannot read version".
It also verifies: on the expected branch, clean tree, version ↔ changelog
consistency, and that config-supplied refs/paths are safe (no option-shaped
values). It computes the suggested bump from the Conventional Commits in the
unreleased range, reading full commit bodies so BREAKING CHANGE: footers
count (!/BREAKING → major, feat → minor, others → patch; alpha policy: on
0.x, breaking demotes to minor). Under a squash workflow that range is bounded
by the last BACK-MERGE, not the last tag: a squash rewrites dev's commits, so
they never become ancestors of the tag, and a tag-based range would keep every
commit dev ever made — an already-released feat: inflating today's patch into
a minor. Two states it reports instead of guessing: a pre-bumped tree
(version + changelog already advanced past the last tag → release the DECLARED
version, never bump again on top) and no bump signal (no conventional
commits in range → the user must choose; never re-release the current
version). The final JSON line is the machine-readable result.
This section is a guide, not a script. What follows says what a release
must GUARANTEE and what to look at; it does not presume to know your steps.
The steps are yours, they live in .supermodo/rules/release.md, and the one
thing the skill will not do is invent them for you.
Process is cached; state never is. The script reads
.supermodo/rules/release.md and reports which of the two worlds you are in.
Present → that file IS the sequence, and the script's job shrinks to gathering
state and rendering this project's commands. Absent → it additionally emits a
PROCESS EVIDENCE block: the main and integration branches, tag prefix,
merge strategy, which shipped template fits, version file and changelog — each
line carrying what it was read from — followed by raw observations it does NOT
interpret: every remote URL verbatim, what the CI config actually does, whether
the merges on the main branch look like forge requests, and any release
documentation the repository already has. Those are evidence, not answers. You
read them and propose the process; a git@gitea.example.com remote means
something to you that no enum in this skill could have held.
Discovering how a repository releases is the expensive and guessy part of this
skill, and it does not change between releases, so it happens ONCE and is
written down. What is never cached and never inferred is STATE — which version
is published, whether this checkout is behind. Those differ every run, and a
remembered answer to them is the staleness bug one level up.
Blockers → report them and stop; never work around a blocker silently. If the
project has its own repo self-check (e.g. a check script), run it too — a
release never ships red. When skills.config.json defines them, also run
the configured quality tiers: commands.testAll (fallback commands.test)
and commands.lint (see ../protocols/references/tooling.md). A red tier
is a preflight blocker like any other; an absent tier is reported as "not
gated", never silently assumed green.
First run (no .supermodo/rules/release.md AND no recorded decline): under
the preflight result, before Step 2 writes anything, read the PROCESS
EVIDENCE block and then go look. Open any release documentation it names —
a RELEASING.md or the release section of a CONTRIBUTING.md is the project
telling you its process in its own words, and it beats anything inferred from
history. Read the CI config it names: a workflow that tags on merge means the
tag step is not yours to run. Then ask an ordered choice
per ../protocols/references/questions.md. Lead with the shipped starting
point the inference matches (mode in the block) and say what it was inferred
FROM; offer the other shipped point, customize, and show-full. This is the one
moment the process is worked out, so work it out properly and get it confirmed:
every later run reads the file instead, which is the whole point — the
expensive question is asked once, the cheap one (what is the state right now)
every time.
Write the rules file in the PROJECT's vocabulary, not this skill's. Name the publish command the project actually uses — whatever CLI, or a web UI step, or nothing because the tag is the release. Verify any command before you write it into the file: a command in a rules file is one a future run will print as verified, so a flag you half-remember belongs in a question to the user, not in the file. If CI already performs a step, say so there and delete the step rather than leaving two things doing it — that is how a tag gets moved.
Correct the inference against what the user says, and carry the confirmed
values — branches, tag prefix, merge strategy, version file, changelog — into
BOTH the rules file (the sequence) and
skills.config.json (the names), so neither is left guessing afterwards. Materialize through config --rules release, never
by writing the file directly — that procedure owns containment,
exclusive-temp writing, validate-before-rename, index regeneration and the
manifest record. Then continue under the new file.
confirmations.mode: "auto" does NOT skip this: it is a class-(c) preference.
A decline is recorded as rulesDeclined: ["release"] and never asked again.
Step 2 — Write the release files (automatic file edits — no git)
Every /release run gets this far without asking: the version bump and the
changelog entry are ordinary, reversible working-tree edits — consent gates
git, not file drafting. Only two preflight states interrupt: no bump
signal (the user must choose the version — never re-release the current
one) and pre-bumped (release the declared version; write nothing).
Full mode: these writes happen AFTER cutting the release/* or hotfix/*
branch, on that branch — never on dev.
Bump. Adopt the script's suggestion. Sanity-check it against squash workflows first: commits that are dev-side pre-squash duplicates of content already released under the last tag are NOT bump evidence — count only work genuinely new since that release; state it when you correct the script.
Changelog entry. Sources, in order of preference:
- Fragments first. When
changelog.fragmentsis on (default — see../protocols/references/config.md), read every file in<changelog.dir>/(defaultchanges/): each carries a bump hint, a Keep-a-Changelog section, and user-facing prose written bycommitwhile the full context was live. Group by section, use the prose near-verbatim. Fragment bump hints are evidence for step 1's bump alongside the commit scan (the higher of the two wins). - Commit subjects as fallback for commits since the last tag that
have no fragment — grouped Added / Changed / Fixed, written for users
(what changed for them), not a raw
git logdump. Non-conventional subjects still get summarized; never dropped silently.
Draft the
## [x.y.z] - YYYY-MM-DDentry from both.- Fragments first. When
Write, then show. Write the bump into
<versionFile>and the entry into<changelog>, and delete the consumed fragments — file edits only, nothing staged, no git. Then print the full entry text and the version change (old → new) so the single consent question below is asked over the real artifacts. Changelog text is an artifact: full grammar regardless of verbosity setting. The user edits or overrides at the gate (their project, their number); a decline leaves the tree pre-bumped and uncommitted — report the state and the one-line revert (git checkout -- <versionFile> <changelog> && git checkout -- <changelog.dir>/ re-run releases the declared version via the pre-bumped path).
Step 3 — Execute (consent-gated, shown-then-run)
The gate follows confirmations in config (see
../protocols/references/config.md): default ask = explicit yes required.
With confirmations.mode: "auto" (or perSkill.release: "auto") the
sequence runs without asking — every command is still printed as it
executes, and preflight blockers still halt. The only questions that
survive auto mode are the two preflight interrupts (no bump signal,
pre-bumped) — Step 2's writes are automatic in every mode.
The commands are generated; the ORDER is the project's. That split is the
rules contract (../protocols/references/rules.md): rendering a step into an
exact command is a CAPABILITY and stays here; which steps run in what order is
the project's sequence and lives in its rules file. So the script emits a vocabulary of
named steps, and is honest about which half of it it can write:
supplied |
steps | what you get |
|---|---|---|
"skill" |
bump-commit · integrate · tag · push · push-tag · push-branch · sync-main · back-merge · extract-notes · cut-stabilization · cut-hotfix · rejoin-stabilization · delete-branch |
commands, rendered from verified facts |
"project" |
open-request · await-approval · publish-release |
a title and no commands |
Everything rendered is pure git (plus a POSIX awk that extracts the
changelog entry into $NOTES for whatever publishes it). That is not a
limitation, it is the line where guessing starts. The set of forges is not
enumerable — GitHub, GitLab, Bitbucket, Gitea, Forgejo, Codeberg, Azure
DevOps, Gerrit, a plain remote with no forge at all — and neither are their
CLIs, their flags, or what a given team calls the thing you open against a
protected branch. A GitHub-only publish command emitted at a Gitea project, or a flag
recalled from training data, is a command that LOOKS verified and is not, which is worse than none: it reads as finished right up until it fails.
So the skill renders what is universal and refuses the rest by name. A
"project" step says the step exists, that this project must supply it, and
sends you to the evidence to find out how. Write it into the rules file once,
in the project's own words, and it stops being a gap.
Never reconstruct commands from this file, from a previous project, or from
memory: a remembered sequence names main and dev in a project that renamed
them, and pushes a tag that does not exist. The steps rendered suit the shape
the process actually has — the rules file's template: decides whether
stabilization branches exist, and its open-request and publish-release
steps — written in the project's own words — decide how the main branch is
written and what publishes the release;
--hotfix renders the hotfix flow.
The plan is emitted ONLY when the preflight is clean, because a sequence built
on an unresolved blocker is wrong somewhere the user cannot see. With blockers
the script prints REPAIR FIRST instead — the commands that bring the checkout
back into sync (fetch, fast-forward onto the remote, back-merge, switch
branch). Those are proposals under the same gate as everything else: show them,
let the user run them or authorize them, then re-run the preflight. Never
release past a blocker.
Show the complete sequence first (ALWAYS — even when the user will decline; the printed plan is part of every run's deliverable, like commit's command plan), then run it command by command, stopping at the first failure and reporting exact state (which commands ran, which didn't — half-done releases must be visible, never papered over).
Read the generated sequence before running it and stop if any of these is violated — they are what the generator guarantees, and the only way to notice it did not is to look:
the version bump, the changelog entry and the consumed fragments are staged together and land in one commit;
the release commit is created on the main branch by the configured merge strategy (a squash stages, so a commit follows it;
--no-ffmakes the commit itself and must NOT be followed by one — a second commit would find nothing to commit and abort the sequence);the push carries the branch and exactly ONE tag, this release's — never every tag at once;
the changelog entry is extracted into a fresh
mktempfile, never a fixed path, for whatever publishes the release — the project's ownpublish-releasecommand, never a CLI this skill guessed;the back-merge into the dev branch closes the release — it is part of the release, not optional cleanup, and skipping it breaks the next cycle;
every
-mmessage is SINGLE-quoted (an interactive shell reads!inside double quotes as a history expansion and refuses the line — a plan the user cannot paste is not a plan). Same rule ascommit.
Step 4 — worktree cleanup (suggested, never run)
Supermodo's worktree-per-task mode (work/flow --worktree, config
workspace.worktree) leaves one worktree + branch per task. After the
release sequence, run git worktree list --porcelain read-only; for every
worktree other than the main checkout whose branch is now fully merged into
<dev> or <main> (git branch --merged), PRINT — never run — the cleanup
pair, AFTER the merge/tag/push commands:
git worktree remove <path>
git branch -d <branch> # -d refuses if not merged; never -D
Removing a worktree and deleting a branch are state the user owns (like merge/push): release only ever suggests them. A worktree whose branch is NOT yet merged is listed as "still open — not cleaned up", never force-removed.
Hard rules
In
askmode (default) NEVER run any of this without the explicit per-release yes; decline = report the preflight + proposal and stop. Inautomode, transparency replaces consent — print everything, halt on any blocker or failure.Never
push --force, never rebase published branches, never amend published commits, never delete tags.Version bump and changelog entry always travel in the same commit — a release where they disagree must be impossible (the preflight enforces it).
Consumed changelog fragments are deleted in that same release commit — a fragment is never counted into two releases. Fragments are read-only input until then; release never edits or rewrites them in place.
The back-merge into dev after a squash release is part of the release, not optional cleanup.
the project's publish command absent, failing or unauthenticated → do everything up to the tag push, then report the publish step as NOT done with the ready-to-run command. Never silently skip it. A rules file that names no publish command is reported the same way — the tag is pushed and the publish step is listed as this project's to supply — never pretended to have succeeded.
Persist the outcome per
../protocols/references/reports.md(standalone location), in these sections every run: Preflight · Version (old → new, suggested vs chosen, and why they differ) · Changelog entry (the text verbatim) · Commands (each one, with ran / not-run beside it) · Not gated (tiers absent from config — named, never silently assumed green). A half-done or declined release must be reconstructable from this file alone.status:okwhen the sequence completed,failedwhen it stopped part-way (the Commands section then shows exactly where),skippedwhen the user declined the gate — a decline is the system working, never a failure — andneeds-inputfor the two preflight interrupts, with the question inquestions. Then publish it per the reports protocol — render it withnode <skills>/reports/scripts/render.ts --root <project-root> --report <that path>and name the page in your final message (standalone runs only; insideflowthe orchestrator renders the run page).
What this skill is not
Not a commit generator (that's commit), not CI, and not a substitute for
the project's own checks — it sequences and gates; the project's gates still
gate.