# Release

> Run a versioned release with git-flow discipline: deterministic preflight that reads version state from git (every tag, both branches, their remotes) rather than from the checkout, derives the semver bump from Conventional Commits, writes the changelog entry from fragments, and prints the exact command sequence for THIS repository — consent-gated by default (configurable auto mode). The workflow itself lives in .supermodo/rules/release.md, materialized once from a shipped template (light dev→main, full git-flow with release/* and hotfix/*, or request-based for a protected main). Renders git commands from verified facts and never invents a forge's — publishing is the project's own step, written into its rules file. Use for 'release', 'cut a release', 'ship a version', 'bump the version', 'publish vX.Y.Z', 'hotfix production' — whenever accumulated work should become a tagged, published version.

- Skill: `supermodo/release` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add supermodo/release`
- Raw SKILL.md: https://api.skillmd.com/api/skills/supermodo/release/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: supermodo (https://skillmd.com/u/supermodo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/supermodo/release

---


# release — versioned releases with git-flow discipline

> **Requires:** the sibling `protocols` skill (shared protocol masters); uses `skills.config.json` when present (`release` section — 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.

1. **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.
2. **Changelog entry.** Sources, in order of preference:
   - **Fragments first.** When `changelog.fragments` is on (default — see
     `../protocols/references/config.md`), read every file in
     `<changelog.dir>/` (default `changes/`): each carries a bump hint, a
     Keep-a-Changelog section, and user-facing prose written by `commit`
     while 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 log` dump. Non-conventional
     subjects still get summarized; never dropped silently.

   Draft the `## [x.y.z] - YYYY-MM-DD` entry from both.
3. **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-ff` makes 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 `mktemp` file, never a fixed
  path, for whatever publishes the release — the project's own
  `publish-release` command, 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 `-m` message 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 as `commit`.

## 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:

```bash
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 `ask` mode (default) NEVER run any of this without the explicit
  per-release yes; decline = report the preflight + proposal and stop. In
  `auto` mode, 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`: `ok` when the sequence completed, `failed` when it stopped
  part-way (the Commands section then shows exactly where), `skipped` when
  the user declined the gate — a decline is the system working, never a
  failure — and `needs-input` for the two preflight interrupts, with the
  question in `questions`. Then publish it per the reports protocol — render
  it with `node <skills>/reports/scripts/render.ts --root <project-root> --report <that path>` and
  name the page in your final message (standalone runs only; inside `flow`
  the 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.

