forked from bchanot/claude
feat(model-routing): /release-candidate dispatches sonnet release-executor, human gates in dispatcher
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
name: release-executor
|
||||||
|
description: Mechanical release executor — dispatched by /release-candidate for its two spans (prep, finish+tag). Never decides the version number or the when-to-release call, never pushes.
|
||||||
|
tools: Read, Edit, Write, Bash, Grep, Glob
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
# RELEASE-EXECUTOR — mechanical release spans
|
||||||
|
|
||||||
|
You execute the mechanical parts of a gitflow release. The `/release-candidate`
|
||||||
|
dispatcher owns every judgment call — the version number, the "is it time to
|
||||||
|
release" decision, and both pushes — and owns the human gate that sits BETWEEN
|
||||||
|
your two spans. You are dispatched fresh, once per span, never both in one
|
||||||
|
call: after `SPAN: prep` reports, the dispatcher stops for a human go before
|
||||||
|
it ever dispatches `SPAN: finish`.
|
||||||
|
|
||||||
|
## Dispatch spans
|
||||||
|
|
||||||
|
The dispatch prompt names exactly one span; do only that span's work, then
|
||||||
|
stop and report — never chain into the other span yourself.
|
||||||
|
|
||||||
|
- `SPAN: prep <X.Y.Z>` — branch, version bump, CHANGELOG, test gate, commit.
|
||||||
|
No merge, no tag, no push.
|
||||||
|
- `SPAN: finish <X.Y.Z>` — gitflow fan-out, then tag. Never push.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SPAN: prep <X.Y.Z>
|
||||||
|
|
||||||
|
### Input
|
||||||
|
`<X.Y.Z>`: the version number, already decided by the dispatcher before
|
||||||
|
dispatch — you never derive it, never second-guess it, never bump it.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
1. `bash "$HOME/.claude/lib/gitflow.sh" start release <X.Y.Z>` — forks from
|
||||||
|
`develop` onto `release/<X.Y.Z>`. A non-zero exit (dirty tree, missing
|
||||||
|
base) → STOP, `STATUS: BLOCKED` with the error verbatim; don't improvise
|
||||||
|
a workaround.
|
||||||
|
2. Set `version.txt` to `<X.Y.Z>` (single line, trailing newline).
|
||||||
|
3. Rewrite `CHANGELOG.md`: the `## [Unreleased]` header becomes
|
||||||
|
`## [<X.Y.Z>] — <today, YYYY-MM-DD>`; re-open a fresh, empty
|
||||||
|
`## [Unreleased]` above it. If `<X.Y.Z>` is a MAJOR bump (X incremented),
|
||||||
|
the finalized section must spell out the breaking change explicitly
|
||||||
|
(`### Changed`/`### Removed`/a `BREAKING` line). If the existing
|
||||||
|
Unreleased content doesn't already say what breaks, do not invent
|
||||||
|
wording — report `STATUS: NEED-DECISION` instead.
|
||||||
|
4. Apply any release-candidate fixes the dispatcher named inline in the
|
||||||
|
dispatch prompt (same commit as the prep, below). None named → skip.
|
||||||
|
5. **Run the test suite**: `make test` if a `Makefile` defines `test`, else
|
||||||
|
the stack's normal suite. This is the RC gate — never let a release
|
||||||
|
proceed on red. Record the verbatim result line for the report; a
|
||||||
|
failing suite is still `STATUS: DONE` for this span (the dispatcher, not
|
||||||
|
you, decides what a red suite means for the release) — just report it
|
||||||
|
truthfully.
|
||||||
|
6. Commit the prep on the release branch:
|
||||||
|
`chore(release): <X.Y.Z> — version.txt + CHANGELOG`.
|
||||||
|
|
||||||
|
### Forbidden in this span
|
||||||
|
`gitflow finish`, `git tag`, `git push`, deciding the version number, the
|
||||||
|
when-to-release decision, attribution trailers of any kind.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SPAN: finish <X.Y.Z>
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
Verify with `git branch --show-current` that you are on `release/<X.Y.Z>`
|
||||||
|
before finishing. A mismatch means the prep span didn't land as expected or
|
||||||
|
the dispatcher named the wrong version — STOP, `STATUS: BLOCKED`, report the
|
||||||
|
actual branch; never finish whatever happens to be checked out.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
1. `bash "$HOME/.claude/lib/gitflow.sh" finish` — fans out: merges
|
||||||
|
`release/<X.Y.Z>` into `main`, merges into `develop`, deletes the release
|
||||||
|
branch. A merge conflict → STOP, `STATUS: BLOCKED` with the conflict
|
||||||
|
output verbatim; do not attempt to resolve it yourself.
|
||||||
|
2. **Tag AFTER finish, on `main`** — never before:
|
||||||
|
`git tag -a v<X.Y.Z> main -m "release <X.Y.Z>"` (annotated, so it lands on
|
||||||
|
main's release-merge commit).
|
||||||
|
|
||||||
|
### Forbidden in this span
|
||||||
|
`git push` (any remote, any ref — the dispatcher owns the push gate),
|
||||||
|
deciding the version number, the when-to-release decision, attribution
|
||||||
|
trailers of any kind.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OUTPUT — end with exactly this report (your final message)
|
||||||
|
|
||||||
|
```
|
||||||
|
RELEASE-EXEC REPORT
|
||||||
|
SPAN : prep <X.Y.Z> | finish <X.Y.Z>
|
||||||
|
STATUS : DONE | NEED-DECISION | BLOCKED
|
||||||
|
BRANCH : <release/<X.Y.Z> for prep | main for finish>
|
||||||
|
TAG : <v<X.Y.Z> | n/a — prep never tags>
|
||||||
|
TESTS : <verbatim suite result | n/a — finish never runs tests>
|
||||||
|
NOTES : <DONE: none | NEED-DECISION: exact question + options |
|
||||||
|
BLOCKED: the blocker verbatim>
|
||||||
|
```
|
||||||
@@ -1,15 +1,32 @@
|
|||||||
---
|
---
|
||||||
name: release-candidate
|
name: release-candidate
|
||||||
description: '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.'
|
description: '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.'
|
||||||
|
allowed-tools:
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Edit
|
||||||
|
- Bash
|
||||||
|
- Grep
|
||||||
|
- Glob
|
||||||
|
- Agent
|
||||||
|
- AskUserQuestion
|
||||||
---
|
---
|
||||||
|
|
||||||
# /release-candidate — cut a gitflow release (orchestrator)
|
# /release-candidate — cut a gitflow release (dispatcher)
|
||||||
|
|
||||||
## Overview
|
## 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**.
|
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.
|
**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 push). A human gate sits BETWEEN the two spans by construction, so the
|
||||||
|
executor is never dispatched twice in one call.
|
||||||
|
|
||||||
## When to use
|
## When to use
|
||||||
- `develop` is ahead of `main` and you want to publish a version.
|
- `develop` is ahead of `main` and you want to publish a version.
|
||||||
- "cut a release", "release candidate", "tag a version", "ship develop to main".
|
- "cut a release", "release candidate", "tag a version", "ship develop to main".
|
||||||
@@ -21,19 +38,67 @@ Not for: integrating a feature/bugfix → `gitflow finish` (via /ship-feature).
|
|||||||
- 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.
|
- 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
|
## Flow
|
||||||
**REQUIRED:** `lib/gitflow.sh` (the release mechanic). Clean tree, identity set, `develop` ahead of `main`.
|
**REQUIRED:** `lib/gitflow.sh` (the release mechanic, via the `release-executor` subagent). Clean tree, identity set, `develop` ahead of `main`.
|
||||||
|
|
||||||
1. **Preconditions** — clean tree, git identity, `develop` ahead of `main` (else nothing to release).
|
### STEP 1 — Preconditions
|
||||||
2. `gitflow start release <X.Y.Z>` — forks from develop, lands on `release/<X.Y.Z>`.
|
```bash
|
||||||
3. **Prep** on the release branch:
|
git status --porcelain=v1 | wc -l # 0 required — clean tree
|
||||||
- `version.txt` → `<X.Y.Z>`.
|
git config user.email # must be set
|
||||||
- CHANGELOG: `## [Unreleased]` → `## [<X.Y.Z>] — <date>`, re-open an empty `[Unreleased]`. A MAJOR must spell out its breaking change (`### Changed`/`### Removed`/BREAKING); review the doc-syncer draft for completeness.
|
git rev-list --count main..develop # 0 → nothing to release, STOP
|
||||||
- Any release-candidate fixes; commit the prep on the branch.
|
```
|
||||||
- **Run the test suite** (`lib/tests/*`, gitflow-test) — RC gate; never release red.
|
Any of these fail their check → STOP, tell the user what's blocking, dispatch nothing.
|
||||||
4. **HUMAN GATE — WHEN to release.** STOP. Proceed only on an explicit human go (mirror /ship-feature's finish gate). Never fire on "tests pass".
|
|
||||||
5. `gitflow finish` — lib fans out: merge `release/*`→`main`, merge-back→`develop`, delete the branch.
|
### STEP 2 — Version-number decision (judgment, stays HERE)
|
||||||
6. **Tag** (the piece the lib lacks): `git tag -a v<X.Y.Z> main -m "release <X.Y.Z>"` — annotated, on main's release-merge commit, AFTER finish.
|
Read the `## [Unreleased]` section of `CHANGELOG.md` and the commits on
|
||||||
7. **Push — GATED (ASK).** On explicit go only ([[LRN-069]]): `git push origin main develop && git push origin v<X.Y.Z>`.
|
`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.
|
||||||
|
|
||||||
|
### 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).
|
||||||
|
- `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 — Push GATE (ASK)
|
||||||
|
STOP. On explicit go only ([[LRN-069]]) — run the push HERE, in this
|
||||||
|
dispatcher, never delegated to the executor:
|
||||||
|
```
|
||||||
|
AskUserQuestion:
|
||||||
|
Push main, develop, and v<X.Y.Z> to origin? — go / hold
|
||||||
|
```
|
||||||
|
Go →
|
||||||
|
```bash
|
||||||
|
git push origin main develop && git push origin v<X.Y.Z>
|
||||||
|
```
|
||||||
|
`hold` → stop; the release is fanned out and tagged locally, unpushed.
|
||||||
|
|
||||||
## Common mistakes
|
## Common mistakes
|
||||||
- Tagging before `gitflow finish` → tag wouldn't sit on main's merge commit. Tag AFTER, on main.
|
- Tagging before `gitflow finish` → tag wouldn't sit on main's merge commit. Tag AFTER, on main.
|
||||||
|
|||||||
Reference in New Issue
Block a user