feat(release-candidate): orchestrator skill over gitflow release + the version tag

/release-candidate cuts a release by orchestrating the existing gitflow
release mechanic (start from develop; finish fan-out main+develop+delete)
and adding the one piece the lib lacks: the version tag.

- skills/release-candidate/SKILL.md: thin orchestrator — preconditions →
  gitflow start release → prep (version.txt + CHANGELOG, breaking doc'd) →
  run-tests gate → human WHEN-to-release gate → gitflow finish → git tag -a
  vX.Y.Z (in the skill, lib untouched) → push (gated).
- lib/tests/run-release-candidate.sh: throwaway-repo flow replay. RC_TAG=0
  reds the tag (gitflow fans out but never tags); RC_TAG=1 → 5/5.
- CLAUDE.md: Skill routing line. CHANGELOG [Unreleased]: /reconcile +
  /release-candidate under Added (so the eventual v4.0.0 captures them).

Tag scheme vX.Y.Z continues the version.txt/CHANGELOG lineage. writing-skills TDD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj
This commit is contained in:
Bastien Chanot 2026-06-30 14:41:12 +02:00
parent 01cf0a1a6e
commit d3d6cede65
4 changed files with 109 additions and 0 deletions

View File

@ -9,6 +9,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
<!-- DRAFT (doc-syncer): grounded in commits since 3.4.0; review wording + completeness before release. --> <!-- DRAFT (doc-syncer): grounded in commits since 3.4.0; review wording + completeness before release. -->
### Added ### Added
- `/reconcile` — declared-vs-real reconciler: confronts TODO checkboxes + registry statuses (never the `## Index`) against real git/fs and surfaces the gaps in four categories + contradiction candidates, with a gated TODO write-back. Engine `lib/reconcile.sh` (body enumeration, git/fs oracles, last-block-wins status); thin skill
- `/release-candidate` — orchestrator over the gitflow release mechanic that adds the version tag the lib doesn't: finalize `version.txt` + CHANGELOG, fan-out `develop`→`main` + back, tag `vX.Y.Z`, push (gated). Lib stays the generic mechanic; the skill owns the tag
- Coupled-capitalize: dev flows (feat / hotfix / bugfix / commit-change, ship-feature, init-project) auto-commit their memory in the same breath, via shared `lib/capitalize-commit.md` + `lib/memory-commit.sh` (surgical — `.claude/memory` + `.claude/tasks` only, never `git add -A`) - Coupled-capitalize: dev flows (feat / hotfix / bugfix / commit-change, ship-feature, init-project) auto-commit their memory in the same breath, via shared `lib/capitalize-commit.md` + `lib/memory-commit.sh` (surgical — `.claude/memory` + `.claude/tasks` only, never `git add -A`)
- Coupled doc-sync: dev flows (feat / bugfix / hotfix, ship-feature, init-project) auto-commit the public docs `doc-syncer` patches, via shared `lib/doc-commit.md` + `lib/doc-commit.sh` (surgical — only the patched files, never `git add -A`, never `.claude/` / `CLAUDE.md`; refuses an out-of-scope path loudly with exit 4). `doc-syncer` surfaces `PATCHED_FILES` (one path per line) as the handoff - Coupled doc-sync: dev flows (feat / bugfix / hotfix, ship-feature, init-project) auto-commit the public docs `doc-syncer` patches, via shared `lib/doc-commit.md` + `lib/doc-commit.sh` (surgical — only the patched files, never `git add -A`, never `.claude/` / `CLAUDE.md`; refuses an out-of-scope path loudly with exit 4). `doc-syncer` surfaces `PATCHED_FILES` (one path per line) as the handoff
- `lib/doc-shape.sh` — deterministic MINOR-shape oracle for `doc-syncer` AUTO MODE: re-checks each LLM-classified MINOR patch (added-heading / size / new-file / non-doc envelope, thresholds env-overridable) and escalates a shape-suspect patch to the existing SIGNIFICANT gate instead of silently auto-committing it. A structural floor under the LLM's classification, not a blocking gate (genuine MINOR still auto-commits, zero friction); catches structural/size significance, not semantic - `lib/doc-shape.sh` — deterministic MINOR-shape oracle for `doc-syncer` AUTO MODE: re-checks each LLM-classified MINOR patch (added-heading / size / new-file / non-doc envelope, thresholds env-overridable) and escalates a shape-suspect patch to the existing SIGNIFICANT gate instead of silently auto-committing it. A structural floor under the LLM's classification, not a blocking gate (genuine MINOR still auto-commits, zero friction); catches structural/size significance, not semantic

View File

@ -262,6 +262,7 @@ only the non-obvious cases: gstack fallbacks, disambiguation, cryptic names.
- Bug / error / 500 → investigate (bugfix if gstack off) - Bug / error / 500 → investigate (bugfix if gstack off)
- feat / hotfix / bugfix distinguished by file count → see descriptions - feat / hotfix / bugfix distinguished by file count → see descriptions
- Ship / deploy / PR → ship (ship-feature if gstack off) - Ship / deploy / PR → ship (ship-feature if gstack off)
- Cut a release / tag a version (develop ahead of main) → release-candidate
- Docs post-ship → document-release (doc if gstack off); stale-doc audit → doc - Docs post-ship → document-release (doc if gstack off); stale-doc audit → doc
- Audit of changes since last run → audit-delta - Audit of changes since last run → audit-delta
- Open-work inventory / "queue empty?" / stale TODO vs real git → reconcile - Open-work inventory / "queue empty?" / stale TODO vs real git → reconcile

View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# run-release-candidate.sh — TDD harness for /release-candidate.
#
# The skill is an ORCHESTRATOR over the existing gitflow release mechanic + the ONE
# piece the lib lacks: the version tag. This harness replays the skill's prescribed
# sequence on a throwaway repo (gitflow-test style) and asserts the release outcome.
#
# RED (RC_TAG=0): run start→prep→finish only (the existing mechanic) → the tag
# assertion REDS, proving gitflow fans out main+develop but never tags.
# GREEN(RC_TAG=1): the skill's flow adds `git tag` → tag present on main's merge commit.
set -uo pipefail
GREP=/usr/bin/grep # LRN-074: pin grep
LIBDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # repo lib/
GITFLOW="$LIBDIR/gitflow.sh"
RC_TAG="${RC_TAG:-0}" # 0=RED (no tag), 1=GREEN
WORK="${RC_WORK:?set RC_WORK to a throwaway dir}"
pass=0; fail=0
ok(){ echo "GREEN ✓ $*"; pass=$((pass+1)); }
no(){ echo "RED ✗ $*"; fail=$((fail+1)); }
# ── seed a throwaway repo: main (v3.5.0) + develop ahead ──────────────────────
rm -rf "$WORK"; mkdir -p "$WORK"
git -C "$WORK" init -q
git -C "$WORK" config user.email t@t; git -C "$WORK" config user.name T
printf '3.5.0\n' > "$WORK/version.txt"
printf '# Changelog\n\n## [Unreleased]\n\n### Added\n- new skill foo\n\n## [3.4.0] — 2026-04-15\n- prev\n' > "$WORK/CHANGELOG.md"
git -C "$WORK" add -A; git -C "$WORK" commit -qm "initial"
git -C "$WORK" branch -M main
git -C "$WORK" branch develop
git -C "$WORK" checkout -q develop
printf 'feature work\n' > "$WORK/feat.txt"; git -C "$WORK" add -A; git -C "$WORK" commit -qm "feat on develop"
echo "setup: develop +$(git -C "$WORK" rev-list --count main..develop) vs main, version.txt=$(cat "$WORK/version.txt"), tags=$(git -C "$WORK" tag | wc -l)"
echo
# ── the /release-candidate flow (what the skill prescribes) ──────────────────
( cd "$WORK" || exit 1
bash "$GITFLOW" start release 4.0.0 >/dev/null # base develop → release/4.0.0 (lib L49/L71)
printf '4.0.0\n' > version.txt # prep: version bump
sed -i 's/## \[Unreleased\]/## [Unreleased]\n\n## [4.0.0] — 2026-06-30/' CHANGELOG.md
git commit -qam "chore(release): 4.0.0 — version.txt + CHANGELOG"
bash "$GITFLOW" finish >/dev/null # fan-out main+develop+delete (lib L108-111)
# TAG = the gap. Lives in the SKILL (lib untouched). RED skips it, GREEN does it.
if [ "$RC_TAG" = "1" ]; then git tag -a v4.0.0 main -m "release 4.0.0"; fi
)
# ── assertions: fan-out (existing mechanic) + the tag (the new piece) ─────────
echo "=== assertions (RC_TAG=$RC_TAG) ==="
if [ "$(git -C "$WORK" show main:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "fan-out: main carries the release (version.txt 4.0.0)"; else no "fan-out: main version.txt != 4.0.0"; fi
if [ "$(git -C "$WORK" show develop:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "merge-back: develop carries 4.0.0"; else no "merge-back failed"; fi
if git -C "$WORK" show-ref --verify -q refs/heads/release/4.0.0; then no "release/4.0.0 NOT deleted"; else ok "release/4.0.0 branch deleted"; fi
if git -C "$WORK" show main:CHANGELOG.md | $GREP -q '## \[4.0.0\]'; then ok "CHANGELOG [4.0.0] on main"; else no "CHANGELOG not finalized"; fi
if git -C "$WORK" rev-parse -q --verify refs/tags/v4.0.0 >/dev/null; then
if [ "$(git -C "$WORK" rev-list -n1 v4.0.0)" = "$(git -C "$WORK" rev-parse main)" ]; then ok "tag v4.0.0 on main's release-merge commit"; else no "tag v4.0.0 exists but not on main HEAD"; fi
else
no "tag v4.0.0 ABSENT — gitflow finish fans out but does NOT tag (the gap /release-candidate fills)"
fi
echo; echo "================ $pass GREEN / $fail RED (RC_TAG=$RC_TAG) ================"
[ "$fail" -eq 0 ] && exit 0 || exit 1

View File

@ -0,0 +1,45 @@
---
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.
---
# /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.
## 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). Clean tree, identity set, `develop` ahead of `main`.
1. **Preconditions** — clean tree, git identity, `develop` ahead of `main` (else nothing to release).
2. `gitflow start release <X.Y.Z>` — forks from develop, lands on `release/<X.Y.Z>`.
3. **Prep** on the release branch:
- `version.txt``<X.Y.Z>`.
- 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.
- Any release-candidate fixes; commit the prep on the branch.
- **Run the test suite** (`lib/tests/*`, gitflow-test) — RC gate; never release red.
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.
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.
7. **Push — GATED (ASK).** On explicit go only ([[LRN-069]]): `git push origin main develop && git push origin v<X.Y.Z>`.
## 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 without the ASK gate → [[LRN-069]].
## 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).