Files
claude/skills/release-candidate/SKILL.md
T
bchanot 6104545e76 feat(skills): push state read from facts, never pushed by the skills
Run C2 of manual-push mode (BDR-111/BDR-112). The four flows that pushed
on their own, or claimed the branch was on origin, now read the truth
after the fact and hand the user the exact command:

- client-handover-writer: the "Push to origin now?" question and its
  push block are gone (the hooks had already pushed in auto-push mode;
  push-guard denies it in manual mode). A reusable PUSH STATE READ
  (branch, origin probe, `git rev-list --count origin/<br>..<br>`, the
  verb only to word the reason) runs after commit-change, at the top of
  the deploy pause, after "Deployed" and before each end report. The
  branch name is validated against an allowlist before it is placed in
  any command or hint (a hostile branch name is otherwise a shell
  injection). Pending → the user pushes BEFORE the deploy pause; the
  deploy brief says "after your push". `Push:` line in both reports.
- release-candidate STEP 6: two ahead counts + the verb; anything other
  than auto with both counts 0 prints one user command
  `! git push --atomic origin main develop v<X.Y.Z>` and stops; the tag
  gate stays for auto mode; `hold` notes --follow-tags; version regex.
- release-executor: push claims qualified (auto-push mode, best effort).
- tour: mode-agnostic rule; STEP 3 reads one `git -C <project>` fact per
  project (suffix-aware branch, --remotes=origin, origin probe) and the
  summary row says on origin / local only with the user command.
2026-10-07 14:02:51 +02:00

7.0 KiB

name, effort, description, allowed-tools
name effort description allowed-tools
release-candidate low Use when develop is ahead of main and you want to cut a versioned release — finalize version.txt + CHANGELOG, merge develop→main via the gitflow fan-out, tag it, and push. Triggers: "cut a release", "release candidate", "tag a version", "ship develop to main". NOT feature/bugfix integration (that is gitflow finish via /ship-feature) nor a hotfix.
Read
Write
Edit
Bash
Grep
Glob
Agent
AskUserQuestion

/release-candidate — cut a gitflow release (orchestrator)

Overview

Turns the accumulated work on develop into a tagged release on main. THIN ORCHESTRATOR over lib/gitflow.sh: the lib does the generic fan-out (release branch → main + back to develop + delete the branch); the skill adds what the lib deliberately does not know — the version number, the CHANGELOG, the human "is it time?" gate, and the git tag.

Division of labour (lib = mechanic, skill = judgment): the tag lives HERE, not in gitflow.sh, because it is release-specific (version + message + human decision) while the lib's fan-out is generic. Consequence (accepted): a release cut by calling gitflow finish directly, bypassing this skill, fans out but is NOT tagged — /release-candidate is the canonical release path.

The two mechanical spans (prep, finish+tag) run on the sonnet-pinned release-executor subagent (dispatch makes the pin effective) — no model gate needed here, dispatch does the job. This dispatcher keeps everything the executor must never own: the version-NUMBER decision (judgment — derives from semver change nature), and the two human gates (when to release, and the tag push (auto-push mode; in manual push mode the user pushes main, develop and the tag in one command)). A human gate sits BETWEEN the two spans by construction, so the executor is never dispatched twice in one call.

When to use

  • develop is ahead of main and you want to publish a version.
  • "cut a release", "release candidate", "tag a version", "ship develop to main".

Not for: integrating a feature/bugfix → gitflow finish (via /ship-feature). A prod emergency fix off main → hotfix (different fan-out).

Versioning

  • Tag scheme vX.Y.Z (semver, v-prefix — Gitea/GitHub release convention). Continues the version.txt + CHANGELOG lineage (the repo's authority); never restart at v1.0.0 (desyncs from a CHANGELOG already at 3.x+).
  • The number DERIVES from the change nature (semver), not the reverse: a migration-requiring/breaking change → MAJOR; new features → MINOR; fixes → PATCH. Personal repo ⇒ "breaking" = requires a migration of your own usage. Decide the number BEFORE running.

Flow

REQUIRED: lib/gitflow.sh (the release mechanic, via the release-executor subagent). Clean tree, identity set, develop ahead of main.

STEP 1 — Preconditions

git status --porcelain=v1 | wc -l    # 0 required — clean tree
git config user.email                # must be set
git rev-list --count main..develop   # 0 → nothing to release, STOP

Any of these fail their check → STOP, tell the user what's blocking, dispatch nothing.

STEP 2 — Version-number decision (judgment, stays HERE)

Read the ## [Unreleased] section of CHANGELOG.md and the commits on develop since main. Apply the Versioning rule above (breaking → MAJOR, features → MINOR, fixes → PATCH) and settle <X.Y.Z> before dispatching anything — the executor never derives or second-guesses this number. The version must match ^[0-9]+\.[0-9]+\.[0-9]+$ before it is placed in any command or tag; anything else stops the run.

STEP 3 — Dispatch: prep

Agent(subagent_type="release-executor")
prompt: "SPAN: prep <X.Y.Z>
<any release-candidate fixes to fold into the prep commit, or 'none'>"

Parse the RELEASE-EXEC REPORT:

  • STATUS: DONE → continue to STEP 4, carrying the TESTS line forward.
  • STATUS: NEED-DECISION → surface the exact question to the user, STOP (don't guess the CHANGELOG wording on its behalf). Resume note: prep may have already run gitflow start release (the release branch exists) — do NOT re-dispatch SPAN: prep (it would BLOCK on the existing branch); resolve the CHANGELOG on the current release branch, commit the prep, then resume at STEP 4.
  • STATUS: BLOCKED → surface the blocker verbatim, STOP.

STEP 4 — HUMAN GATE: when to release

STOP. Show the prep report's TESTS result, then:

AskUserQuestion:
  Release <X.Y.Z> now? (tests: <TESTS line from STEP 3>) — go / hold

Proceed only on an explicit human go. Never fire on "tests pass" — a green suite means ready to release, not authorized to. hold → stop here; the prepped release/<X.Y.Z> branch stays as-is for a later run.

STEP 5 — Dispatch: finish + tag

Agent(subagent_type="release-executor")
prompt: "SPAN: finish <X.Y.Z>"

Parse the RELEASE-EXEC REPORT:

  • STATUS: DONE → continue to STEP 6, carrying the TAG value forward.
  • STATUS: BLOCKED → surface the blocker verbatim (e.g. a merge conflict the fan-out hit), STOP — resolving a conflicted fan-out is a human call, not an auto-retry.

STEP 6 — Tag push GATE (ASK)

Read the state, separate Bash calls: git rev-list --count origin/main..main 2>/dev/null || echo unknown, git rev-list --count origin/develop..develop 2>/dev/null || echo unknown, bash "$HOME/.claude/lib/gitflow.sh" push-mode.

  • Anything other than auto from the verb (manual, invalid, empty, usage error) OR either count ≠ 0 or unknown → Claude pushes nothing (push-guard would refuse it in manual mode; a failed lib push is the user's call, BDR-095). Print ONE command for the user and STOP, no question: ! git push --atomic origin main develop v<X.Y.Z> (invalid: quote the verb's stderr line verbatim; auto with a count ≠ 0 or unknown: say main/develop not on origin (no remote-tracking ref or the lib's push did not land); no origin remote (git remote get-url origin fails): say add an origin remote first).
  • Push mode auto and both counts 0 → main and develop are on origin; only the tag is left. STOP. On explicit go only (LRN-069) — run the tag push HERE, never delegated:
AskUserQuestion:
  Push tag v<X.Y.Z> to origin? — go / hold

Go →

git push origin v<X.Y.Z>

hold → stop; the release is on origin (main + develop), the tag stays local until the next push of main (--follow-tags on every lib and hook push).

Common mistakes

  • Tagging before gitflow finish → tag wouldn't sit on main's merge commit. Tag AFTER, on main.
  • Auto-firing finish because tests pass → finish is a HUMAN gate.
  • Restarting the tag at v1.0.0 → desyncs from the CHANGELOG lineage. Continue it.
  • Pushing the tag without the ASK gate → LRN-069.
  • Pushing anything in manual push mode → print the one user command, push nothing.

Validation

RC_WORK=$(mktemp -d) RC_TAG=1 bash lib/tests/run-release-candidate.sh → 5/5 (fan-out + tag on main). RC_TAG=0 reds the tag assertion — proves the lib alone never tags (the gap this skill fills).