Files
claude/skills/gitflow/SKILL.md
T

126 lines
9.4 KiB
Markdown

---
name: gitflow
effort: medium
description: Use when a project needs gitflow branch operations — bootstrapping main+develop, starting a typed branch (feature/bugfix/release/hotfix), or integrating finished work by directed merge — or when an orchestrator must branch or merge under the gitflow model. Use when about to merge any branch into develop or main.
---
# gitflow
## Overview
The one place gitflow branch logic lives. The mechanics — branch, merge, hotfix
fan-out, init, `.gitignore` reconcile, the protected-base predicate — are in
`~/.claude/lib/gitflow.sh` (tested, deterministic). This skill governs **when**,
and bulletproofs the single judgment call: **`finish` merges only on an explicit
human signal.**
Replaces `finishing-a-development-branch` (upstream superpowers skill, not
vendored here; `gitflow finish` is the only integration path) for gitflow
flows — that skill is single-target and cannot do the directed / fan-out
merges below.
## When to Use
- An orchestrator (`ship-feature`) or an assistance skill (`feat`/`bugfix`/`hotfix`) must branch or merge.
- Bootstrapping a repo's branch model (`init-project`, `onboard`).
- You are about to integrate a finished branch into `develop` or `main`.
## Branch model
`main` (prod) · `develop` (integration, off main) · `feature/*`, `bugfix/*` and
`chore/*` (off develop → develop; chore = memory/doc maintenance) · `release/*`
(off develop → main + back-merge develop) · `hotfix/*` (off main → main +
develop [+ any open release/*]).
## Operations — all via the lib
```
bash ~/.claude/lib/gitflow.sh init [msg] # main+develop; root-commit (fresh) or ensure (existing); reconcile .gitignore; install hook
bash ~/.claude/lib/gitflow.sh start <type> <name> # branch from the correct base
bash ~/.claude/lib/gitflow.sh finish # directed merge of the CURRENT branch — HUMAN-GATED (below)
bash ~/.claude/lib/gitflow.sh delete <branch> # delete a merged branch, local + origin copy — refuses main/develop + anything unmerged
bash ~/.claude/lib/gitflow.sh protected-base [br] # rc 0 on main/develop — the shared predicate
bash ~/.claude/lib/gitflow.sh push-mode # auto | manual | invalid on stdout, rc 0: the only way a skill reads gitflow.autopush; pushes nothing
```
`finish` merges by the current branch's type:
| Current branch | Merges into | then |
|---|---|---|
| `feature/*` · `bugfix/*` · `chore/*` | develop | delete local + `origin/` copy |
| `release/*` | main + develop | delete local + `origin/` copy |
| `hotfix/*` | main + develop + any open `release/*` | delete local + `origin/` copy |
`delete` is `gitflow_delete`, the only path that removes a branch: it refuses
`main`/`develop` (rc 6) and any branch not merged into develop or main (rc 5),
and keeps the branch. The `origin/` copy is removed right after, once ITS
tip passes the same check; a remote tip holding commits the bases lack is
kept, loudly (T24). In manual-push mode (`git config gitflow.autopush
false`, human-set) nothing is pushed: `start` and `finish` stay local, and
the `origin/` copy is left in place (T18i-T18k). An invalid value (not a
boolean, or a failed read) is manual push mode too: nothing is pushed and the
stop is named on stderr (T18q). A `git push` Claude types is refused by
`hooks/push-guard.sh`; the user pushes with `! git push`. Hand
`git branch -d` is denied — with an auto-pushed upstream it checks the wrong
thing (T22a). A `reference-transaction` hook vetoes any deletion or rename of
`main`/`develop` at the ref layer, in every repo.
## The finish gate — merge ONLY on an explicit human signal
`finish` writes to shared branches (`develop`, `main`). Run it ONLY when the user
gives a **real-time, explicit go for THIS merge** — "merge it", "feature OK",
"finish it". Dev and testing happen out of git; finish never auto-fires.
**Violating the letter of this gate violates its spirit.**
| Rationalization | Reality |
|---|---|
| "Tests pass, so I'll merge." | Green = *ready to* merge, not *authorized*. Present "ready — merge?" and wait. |
| "The user said 'ship' / 'implement and ship'." | "Ship" ends at ready-to-merge **and ask**. The verb is not a merge signal. Pushing or opening a PR is still initiating integration — ask first. |
| "The plan's next step says 'merge into develop'." | A step written *before* the work cannot consent to integrating it. Stop at that step and ask. |
| "finish is the last pipeline step — I'll chain it." | The orchestrator STOPS at the finish gate and asks. Reaching it ≠ permission. |
### Red flags — STOP, do not finish
- About to run `gitflow finish` / `git merge` into develop or main, and the user has not, in THIS exchange, explicitly said to merge.
- The authorization you're leaning on is a plan step, a task description, or the word "ship" — not a live "merge it".
- "It's obviously done — surely they want it merged."
All of these mean: present the merge as a question, then wait for the explicit go.
## Aiguillage (assistance + standalone memory/doc skills)
On a protected base, assistance skills (`feat`/`bugfix`/`hotfix`) AND the standalone
memory/doc skills (`capitalize`/`close`/`prune-memory`/`reconcile`, TYPE `chore`)
call `start <type>` to branch first; on a working branch they commit in place. Same
`protected-base` predicate the out-of-skill hook uses. Caller→type map + rationale:
`lib/gitflow-aiguillage.md`. `/capitalize` and `/close` auto-finish their memory-only
`chore/*` branch into develop when THEY created it this run (BDR-068; `--no-push`
opts out) — the only finish that fires without a live human signal; everything else
stays human-gated.
## Failure modes (mechanical — lib return codes are the contract)
| Trigger | Move |
|---|---|
| `~/.claude/lib/gitflow.sh` absent (foreign machine, links broken) | STOP; remedy = `bash link.sh` from the config repo. Never emulate the model by hand-git |
| `finish` rc=4 — merge conflict (message: "resolve, commit, re-run finish") | The conflict sits in the tree ON the target branch. Show conflicted files, resolve WITH the user (it's shared-branch content), `git add` + commit, re-checkout the SOURCE branch, re-run `finish`. The human GO already given covers completing THIS merge — no new gate. A fan-out (hotfix/release) interrupted mid-way resumes on re-run; already-merged targets no-op ("Already up to date") |
| `start` rc=2 — bad/missing type or name | Fix the arguments (`<type>/<name>`), retry once |
| `start` rc=3 — base branch missing | `gitflow init` first, then retry `start` |
| `start`/`finish` rc=1 — checkout failed (dirty tree blocking, or branch already exists) | Report git's message verbatim; if the branch exists, ask resume-it vs new name. Never fall back to raw `git checkout -b` |
| finish warning "transient artifacts … purge skipped, finishing without it" | Non-fatal BY CONTRACT (purge is best-effort, never aborts a finish) — finish continues; clean `docs/superpowers/` by hand later |
| `init` rc=1 — socle commit failed | Recoverable: aborted BEFORE hook activation by design; fix the cause (hooks, perms), re-run `init` |
| `delete`/`finish` rc=5 — branch not merged into develop or main | The branch still holds unmerged work: KEEP it, report it, never fall back to `git branch -d`/`-D`. Merge first (human gate), then re-run |
| `delete` rc=6 — protected base | `main`/`develop` are never deleted. Stop; the request itself is the defect to report |
| `delete`/`finish` warning "remote copy KEPT" or "NOT removed" | Non-fatal BY CONTRACT (remote cleanup is best-effort). KEPT = origin/<br> has a tip the bases lack: fetch, look, merge or leave it — never `git push --delete` by hand. NOT removed = origin unreachable or refused: report the printed command to the user |
| `delete`/`finish` warning "origin/<br> left in place (manual push mode)" | Expected in manual-push mode or with an invalid `gitflow.autopush` (the verb's `gitflow.sh push-mode:` line precedes it), not a failure. Pass the printed `git push origin --delete <br>` to the user; never run it (manual mode: Claude never pushes, even when asked in the turn; the user runs it with `!`. `push --delete` is also denied by settings) |
| Hook stderr "gitflow post-commit: gitflow.autopush unreadable (git rc <n>) — NOT pushed, treated as manual push mode" (post-merge likewise), or `start`/`finish`/`delete` stderr "gitflow.sh push-mode: gitflow.autopush='<v>' is not a boolean" / "could not read gitflow.autopush" | Fail closed BY CONTRACT: the value is invalid, so nothing was pushed. Report the line to the user, who fixes the value by hand (`git config` on the key is denied to Claude); hand any pending push to the user as `! git push …`. Never retry the push |
| `start`/`finish` warning "<base> is behind origin/<base> by N and cannot fast-forward" | Non-fatal BY CONTRACT: the branch is still created and the merge still runs on the local base. The base has diverged from origin: report it to the user, who reconciles (`git pull`, then push). Never rebase or force-push a base |
## Common Mistakes
- Using `finishing-a-development-branch` (upstream superpowers skill, not vendored here) for a gitflow merge → it can't do directed/fan-out merges anyway. Use `gitflow finish`, the only integration path.
- Hand-writing `git merge` instead of `gitflow finish` → loses fan-out, branch delete, base sync.
- Calling `finish` because the work *looks* done → see the gate.
- `git branch -d`/`-D` by hand → denied; a branch the lib refuses to delete still holds work. Keep it, say so.