feat(gitflow): universal gitflow model — lib + skill + orchestrator wiring
lib core (start/finish/init, transactional bootstrap) + migrate + 57-test suite + aiguillage; skills/gitflow + gitignore template; CLAUDE.md gitflow rule; wiring init-project (5f/8/11), onboard (2.6), ship-feature (0/4/9), feat/bugfix/hotfix aiguillage.
This commit is contained in:
parent
f1f6feb21a
commit
167ea9678e
33
CLAUDE.md
33
CLAUDE.md
@ -162,6 +162,39 @@ All web API endpoints must be versioned from day one: `/api/v1/...`.
|
||||
behavior to match newer one.
|
||||
- Router structure reflects versioning explicitly (e.g. `api/v1/routes/`).
|
||||
|
||||
## Version control — gitflow (universal)
|
||||
|
||||
Every git action follows gitflow — inside a skill AND for ad-hoc commits made
|
||||
outside one on direct request. The model is universal across all projects.
|
||||
|
||||
### Branch model
|
||||
`main` (prod) · `develop` (integration, off main) · `feature/*` + `bugfix/*`
|
||||
(off develop → develop) · `release/*` (off develop → main + back-merge develop)
|
||||
· `hotfix/*` (off main → main + develop [+ any open release/*]). `master`→`main`
|
||||
everywhere.
|
||||
|
||||
### Rules for every git action
|
||||
- **Never commit code directly on `main` or `develop`.** Branch first from the
|
||||
correct base, named `<type>/<name>`. (`.claude/**` memory/config commits are
|
||||
exempt — they follow the work, not the code's gitflow.)
|
||||
- **Branch + merge via the lib, never by hand** — the directed-merge + hotfix
|
||||
fan-out logic lives there once:
|
||||
`bash ~/.claude/lib/gitflow.sh start <type> <name>` · `… finish`.
|
||||
- **`gitflow finish` (merge) only on an explicit human signal** ("merge it",
|
||||
"feature OK") — never because tests pass, a plan step says "merge", or a verb
|
||||
("ship") implied it.
|
||||
- **Assistance flows** (`/feat` `/bugfix` `/hotfix`) auto-branch on a protected
|
||||
base (the aiguillage); on a working branch they commit in place, never finish.
|
||||
- **New/onboarded projects** get the model + the versioned pre-commit hook via
|
||||
`gitflow init` (init-project STEP 5f, onboard STEP 2.6).
|
||||
|
||||
### Enforcement layers
|
||||
Advisory — it can be forgotten on a long conversation (no reliable oracle). The
|
||||
deterministic backstops are the per-repo **pre-commit hook** (`gitflow init`
|
||||
installs it: blocks code commits on main/develop, exempts `.claude/**` + merges +
|
||||
the root commit) and **Gitea branch protection** on `main`/`develop` (set up by
|
||||
the migration). Don't lean on `--no-verify` to bypass them.
|
||||
|
||||
## Security — non-negotiable defaults
|
||||
|
||||
Apply at every dev step: design, scaffolding, implementation, review.
|
||||
|
||||
@ -102,6 +102,10 @@ RISK: <low/medium — what could go wrong>
|
||||
|
||||
## STEP 4 — FIX
|
||||
|
||||
**Gitflow aiguillage (before editing):** follow `$HOME/.claude/lib/gitflow-aiguillage.md`
|
||||
— your type = `bugfix`. On `main`/`develop` it branches first; on a working
|
||||
branch it's a no-op (commit in place). Never `finish`.
|
||||
|
||||
Apply the fix following the plan:
|
||||
|
||||
- Fix the root cause, not the symptom.
|
||||
|
||||
@ -90,6 +90,10 @@ If ambiguous: ask the user one focused question, then proceed.
|
||||
|
||||
## STEP 2 — IMPLEMENT
|
||||
|
||||
**Gitflow aiguillage (before editing):** follow `$HOME/.claude/lib/gitflow-aiguillage.md`
|
||||
— your type = `feature`. On `main`/`develop` it branches first; on a working
|
||||
branch it's a no-op (commit in place). Never `finish`.
|
||||
|
||||
Work through the plan:
|
||||
|
||||
- Implement directly (no subagents).
|
||||
|
||||
@ -49,6 +49,10 @@ Follow `$HOME/.claude/lib/design-gate.md`:
|
||||
|
||||
## STEP 2 — PRE-FLIGHT + FIX
|
||||
|
||||
**Gitflow aiguillage (before editing):** follow `$HOME/.claude/lib/gitflow-aiguillage.md`
|
||||
— your type = `hotfix`. On `main`/`develop` it branches first; on a working
|
||||
branch it's a no-op (commit in place). Never `finish`.
|
||||
|
||||
### Pre-flight (mandatory)
|
||||
|
||||
Before editing, snapshot current state so revert is possible:
|
||||
|
||||
26
lib/gitflow-aiguillage.md
Normal file
26
lib/gitflow-aiguillage.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Gitflow aiguillage — assistance flows branch on a protected base
|
||||
|
||||
Assistance flows (`/feat`, `/bugfix`, `/hotfix`) commit IN PLACE on a working
|
||||
branch — the frequent case, behavior unchanged. But they must NEVER commit code
|
||||
on a protected base (`main`/`develop`). Run this check **before editing any
|
||||
file**. The caller passes its TYPE: feat→`feature`, bugfix→`bugfix`,
|
||||
hotfix→`hotfix`.
|
||||
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" protected-base && echo PROTECTED || echo WORKING
|
||||
```
|
||||
|
||||
- **WORKING** (`feature/*`, `bugfix/*`, `hotfix/*`, or any non-protected branch)
|
||||
→ proceed; you commit in place on this branch. Nothing changes.
|
||||
- **PROTECTED** (`main`/`develop`) → branch first, do NOT commit here:
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" start <YOUR-TYPE> <short-kebab-name>
|
||||
```
|
||||
`<short-kebab-name>` derived from the request. Then do the work on the new branch.
|
||||
|
||||
**Never run `gitflow finish`** — assistance flows commit, they do not merge.
|
||||
Integration is a separate, human-gated step (the `gitflow` skill).
|
||||
|
||||
Note: `hotfix` branches off **main** (prod) even when invoked from `develop` —
|
||||
that is the gitflow definition of a hotfix. For a dev-scoped small fix, use
|
||||
`/bugfix` (branches off develop).
|
||||
95
lib/gitflow-migrate.sh
Executable file
95
lib/gitflow-migrate.sh
Executable file
@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# gitflow-migrate.sh — migrate an existing repo to the gitflow model.
|
||||
# LOCAL (no token): gitflow init existing → master→main, develop, socle, hook.
|
||||
# PROBE (token, READ-ONLY): identity + scope/rights, before any write.
|
||||
# REMOTE (token, DESTRUCTIVE): push, default→main, protection, delete master.
|
||||
# Writes ordered reversible→irreversible; DELETE master is LAST and only
|
||||
# runs if every prior step succeeded. Halts on first failure.
|
||||
# No `... | grep -q` under pipefail (SIGPIPE false-negative gotcha). Never echo the token.
|
||||
set -uo pipefail
|
||||
GITEA="${GITEA_URL:-https://git.bchanot.fr}"
|
||||
OWNER="${GITEA_OWNER:-bchanot}"
|
||||
|
||||
# ── LOCAL half (token-free) ──────────────────────────────────────────────────
|
||||
migrate_local() { # <repo-path>
|
||||
local repo="$1" renamed="no"
|
||||
cd "$repo" || { echo " ✗ cannot cd $repo" >&2; return 1; }
|
||||
[ -z "$(git status --porcelain)" ] || { echo " ✗ working tree not clean — stash/commit first" >&2; return 2; }
|
||||
{ [ -n "$(git config user.name)" ] && [ -n "$(git config user.email)" ]; } \
|
||||
|| { echo " ✗ git identity unset (user.name/user.email) — set it before migrating $repo" >&2; return 3; }
|
||||
git show-ref --verify -q refs/heads/master && renamed="yes"
|
||||
bash "$HOME/.claude/lib/gitflow.sh" init || return 1
|
||||
git show-ref --verify -q refs/heads/main || { echo " ✗ no main" >&2; return 1; }
|
||||
git show-ref --verify -q refs/heads/develop || { echo " ✗ no develop" >&2; return 1; }
|
||||
[ "$(git config core.hooksPath)" = ".githooks" ] || { echo " ✗ hook not active" >&2; return 1; }
|
||||
[ -z "$(git status --porcelain)" ] || { echo " ✗ tree dirty after init" >&2; return 1; }
|
||||
echo " ✓ local: main+develop, hook active, tree clean (master→main: $renamed)"
|
||||
}
|
||||
|
||||
# ── Gitea API helper (token in header only; never printed) ────────────────────
|
||||
_gitea() { # <METHOD> <api-path> [json-body]
|
||||
local m="$1" p="$2" body="${3:-}"
|
||||
curl -fsS -X "$m" -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" ${body:+-d "$body"} "$GITEA/api/v1$p"
|
||||
}
|
||||
_json() { python3 -c "import sys,json;$1" 2>/dev/null; } # tiny JSON field reader
|
||||
|
||||
# ── PROBE (READ-ONLY: identity informational, rights = the real gate) ─────────
|
||||
# /user needs read:user (cosmetic — the migration never calls it) → informational.
|
||||
# The gates are the repo-scoped rights the writes actually require: admin+push on
|
||||
# the repo, and admin scope confirmed by a readable branch_protections list.
|
||||
gitea_probe() { # <repo-name to test rights against>
|
||||
local name="$1" me pj perm
|
||||
[ -n "${GITEA_TOKEN:-}" ] || { echo " ✗ GITEA_TOKEN unset" >&2; return 1; }
|
||||
|
||||
# [a] identity — INFORMATIONAL (needs read:user scope the migration never uses)
|
||||
if me=$(_gitea GET "/user" 2>/dev/null | _json "print(json.load(sys.stdin).get('login','?'))") && [ -n "$me" ]; then
|
||||
echo " ✓ token identity: $me"
|
||||
else
|
||||
echo " ⚠ token identity unavailable (no read:user scope) — cosmetic, migration is repo-scoped"
|
||||
fi
|
||||
|
||||
# [b] repo rights — GATE: admin AND push must be true (default_branch, protections, push)
|
||||
pj=$(_gitea GET "/repos/$OWNER/$name") \
|
||||
|| { echo " ✗ GET /repos/$OWNER/$name failed — token lacks repo read scope" >&2; return 1; }
|
||||
perm=$(printf '%s' "$pj" | _json "p=json.load(sys.stdin).get('permissions',{});print('admin=%s push=%s pull=%s'%(p.get('admin'),p.get('push'),p.get('pull')))")
|
||||
printf '%s' "$pj" | _json "p=json.load(sys.stdin).get('permissions',{});sys.exit(0 if (p.get('admin') and p.get('push')) else 1)" \
|
||||
|| { echo " ✗ insufficient rights on $name ($perm) — need admin+push" >&2; return 1; }
|
||||
echo " ✓ rights on $name: $perm (admin+push confirmed)"
|
||||
|
||||
# [c] admin-scope canary — GATE: branch_protections readable (POST/PATCH/DELETE need repo-admin)
|
||||
_gitea GET "/repos/$OWNER/$name/branch_protections" >/dev/null \
|
||||
|| { echo " ✗ cannot read branch_protections — token lacks repo-admin scope; protection step would fail" >&2; return 1; }
|
||||
echo " ✓ repo-admin scope confirmed (branch_protections readable → POST/PATCH/DELETE OK)"
|
||||
}
|
||||
|
||||
# ── REMOTE half (DESTRUCTIVE; reversible→irreversible; delete master LAST) ────
|
||||
_protect() { # <repo-name> <branch> (Option 1: owner-pushable)
|
||||
_gitea POST "/repos/$OWNER/$1/branch_protections" \
|
||||
"{\"branch_name\":\"$2\",\"enable_push\":true,\"enable_push_whitelist\":true,\"push_whitelist_usernames\":[\"$OWNER\"]}"
|
||||
}
|
||||
migrate_remote() { # <repo-name> (cwd = the local repo)
|
||||
local name="$1"
|
||||
[ -n "${GITEA_TOKEN:-}" ] || { echo " ✗ GITEA_TOKEN unset" >&2; return 1; }
|
||||
echo " [1/4] push main + develop (ADDITIVE/reversible)…"
|
||||
git push -u origin main || { echo " ✗ push main failed (push scope?) — STOP, nothing irreversible done" >&2; return 1; }
|
||||
git push -u origin develop || { echo " ✗ push develop failed — STOP" >&2; return 1; }
|
||||
echo " [2/4] default_branch → main (REVERSIBLE — scope canary)…"
|
||||
_gitea PATCH "/repos/$OWNER/$name" '{"default_branch":"main"}' >/dev/null \
|
||||
|| { echo " ✗ PATCH default_branch failed (admin/write scope?) — STOP before protection & delete" >&2; return 1; }
|
||||
echo " [3/4] branch protection main + develop (REVERSIBLE)…"
|
||||
_protect "$name" main >/dev/null || { echo " ✗ protect main failed — STOP before delete" >&2; return 1; }
|
||||
_protect "$name" develop >/dev/null || { echo " ✗ protect develop failed — STOP before delete" >&2; return 1; }
|
||||
echo " [4/4] DELETE remote master (IRREVERSIBLE — last; default already repointed)…"
|
||||
git push origin --delete master || { echo " ✗ delete master failed (left in place — safe)" >&2; return 1; }
|
||||
echo " ✓ remote: default=main, main/develop protected (owner-pushable), remote master deleted"
|
||||
}
|
||||
|
||||
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
||||
case "${1:-}" in
|
||||
local) migrate_local "$2" ;;
|
||||
probe) gitea_probe "$2" ;;
|
||||
remote) migrate_remote "$2" ;;
|
||||
*) echo "usage: gitflow-migrate.sh {local <repo>|probe <name>|remote <name>}" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
148
lib/gitflow-test.sh
Normal file
148
lib/gitflow-test.sh
Normal file
@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# Throwaway-repo test suite for lib/gitflow.sh. Each test builds an isolated
|
||||
# repo under $WORK, asserts, and cleans up. Run: bash lib/gitflow-test.sh
|
||||
#
|
||||
# shellcheck disable=SC2016
|
||||
# (the chk helper EVALs its second arg; single-quoted assertion strings are
|
||||
# intentional — they must not expand at definition time.)
|
||||
set -uo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
# Do NOT override GITFLOW_GITIGNORE_TEMPLATE: the lib self-resolves it from its
|
||||
# own location (../templates), which is correct in both the repo and installed.
|
||||
# shellcheck source=/dev/null
|
||||
source "$HERE/gitflow.sh"
|
||||
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
PASS=0; FAIL=0
|
||||
ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; }
|
||||
no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; }
|
||||
chk() { if eval "$2"; then ok "$1"; else no "$1 [ $2 ]"; fi; }
|
||||
newrepo() { local d="$WORK/$1"; rm -rf "$d"; mkdir -p "$d"; cd "$d" || return 1; git init -q; \
|
||||
git config user.email t@t; git config user.name t; \
|
||||
git config core.hooksPath /dev/null; } # hooks off during setup
|
||||
hookon() { git config --unset core.hooksPath 2>/dev/null || true; } # use repo default .githooks
|
||||
|
||||
echo "T1 — pure predicates"
|
||||
chk "type feature" '[ "$(gitflow_branch_type feature/x)" = feature ]'
|
||||
chk "type hotfix" '[ "$(gitflow_branch_type hotfix/x)" = hotfix ]'
|
||||
chk "type main" '[ "$(gitflow_branch_type main)" = main ]'
|
||||
chk "type other" '[ "$(gitflow_branch_type wip/x)" = other ]'
|
||||
chk "protected main" 'gitflow_protected_base main'
|
||||
chk "protected develop" 'gitflow_protected_base develop'
|
||||
chk "not protected feat" '! gitflow_protected_base feature/x'
|
||||
chk "base feature=develop" '[ "$(gitflow_base_for feature)" = develop ]'
|
||||
chk "base hotfix=main" '[ "$(gitflow_base_for hotfix)" = main ]'
|
||||
|
||||
echo "T2 — init fresh (BLK-010 root commit)"
|
||||
newrepo fresh; echo scaffold > README.md; hookon
|
||||
gitflow_init "chore: scaffold" >/dev/null 2>&1
|
||||
chk "main exists" 'git rev-parse --verify -q refs/heads/main >/dev/null'
|
||||
chk "develop exists" 'git rev-parse --verify -q refs/heads/develop >/dev/null'
|
||||
chk "root commit on main" '[ -n "$(git rev-parse -q --verify main)" ]'
|
||||
chk "gitignore created" '[ -f .gitignore ]'
|
||||
chk "socle: !.claude/deploy/" 'grep -qxF "!.claude/deploy/" .gitignore'
|
||||
chk "socle: re-ignore PENDING" 'grep -qxF ".claude/deploy/PENDING.json" .gitignore'
|
||||
chk "hook installed" '[ -x .githooks/pre-commit ] && [ "$(git config core.hooksPath)" = .githooks ]'
|
||||
chk "tree CLEAN after init" '[ -z "$(git status --porcelain)" ]'
|
||||
chk "hook TRACKED in commit" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
|
||||
chk "socle IN root commit" 'git show HEAD:.gitignore | grep -qxF ".claude/deploy/PENDING.json"'
|
||||
|
||||
echo "T2b — init existing (master→main rename + adoption commit, hook inactive during it)"
|
||||
newrepo existing
|
||||
git symbolic-ref HEAD refs/heads/master # force the repo onto 'master'
|
||||
echo a > a.txt; printf 'node_modules/\n' > .gitignore; git add -A
|
||||
git -c core.hooksPath=/dev/null commit -q -m "pre-existing on master"
|
||||
hookon
|
||||
gitflow_init >/dev/null 2>&1
|
||||
chk "master→main renamed" 'git rev-parse --verify -q refs/heads/main >/dev/null && ! git rev-parse --verify -q refs/heads/master >/dev/null'
|
||||
chk "develop created" 'git rev-parse --verify -q refs/heads/develop >/dev/null'
|
||||
chk "adoption commit" 'git log main --oneline | grep -q "adopt gitflow"'
|
||||
chk "existing tree CLEAN" '[ -z "$(git status --porcelain)" ]'
|
||||
chk "existing hook tracked" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
|
||||
chk "kept project rule" 'git show HEAD:.gitignore | grep -qxF "node_modules/"'
|
||||
|
||||
echo "T3 — hook blocks/permits after init"
|
||||
cd "$WORK/fresh" || exit 1
|
||||
git checkout -q main
|
||||
echo x >> README.md; git add README.md
|
||||
chk "block direct code on main" '! git commit -q -m onmain 2>/dev/null'
|
||||
git restore --staged README.md 2>/dev/null; git checkout -q -- README.md
|
||||
mkdir -p .claude/memory; echo m > .claude/memory/decisions.md; git add .claude/memory/decisions.md
|
||||
chk "allow .claude/** on main" 'git commit -q -m "chore(memory)" 2>/dev/null'
|
||||
gitflow_start feature demo >/dev/null 2>&1
|
||||
echo f > feat.txt; git add feat.txt
|
||||
chk "allow code on feature" 'git commit -q -m "feat work" 2>/dev/null'
|
||||
|
||||
echo "T4/T5 — start picks correct base"
|
||||
newrepo starts; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||
gitflow_start feature foo >/dev/null 2>&1
|
||||
chk "feature off develop" '[ "$(git symbolic-ref --short HEAD)" = feature/foo ]'
|
||||
chk "feature has develop ancestry" 'git merge-base --is-ancestor develop HEAD'
|
||||
git checkout -q develop
|
||||
gitflow_start hotfix bar >/dev/null 2>&1
|
||||
chk "hotfix branch named" '[ "$(git symbolic-ref --short HEAD)" = hotfix/bar ]'
|
||||
chk "hotfix off main" 'git merge-base --is-ancestor main HEAD'
|
||||
|
||||
echo "T6 — finish feature → develop only"
|
||||
newrepo finfeat; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||
gitflow_start feature f1 >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
|
||||
main_before="$(git rev-parse main)"
|
||||
gitflow_finish >/dev/null 2>&1
|
||||
chk "merged into develop" 'git log develop --oneline | grep -q "Merge feature/f1 into develop"'
|
||||
chk "main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
|
||||
chk "branch deleted" '! git rev-parse --verify -q refs/heads/feature/f1 >/dev/null'
|
||||
|
||||
echo "T7 — finish hotfix → main + develop fan-out"
|
||||
newrepo finhot; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||
gitflow_start hotfix h1 >/dev/null 2>&1; echo p>patch.txt; git add patch.txt; git commit -q -m patch
|
||||
gitflow_finish >/dev/null 2>&1
|
||||
chk "hotfix in main" 'git log main --oneline | grep -q "Merge hotfix/h1 into main"'
|
||||
chk "hotfix in develop" 'git log develop --oneline | grep -q "Merge hotfix/h1 into develop"'
|
||||
chk "hotfix branch gone" '! git rev-parse --verify -q refs/heads/hotfix/h1 >/dev/null'
|
||||
|
||||
echo "T8 — finish hotfix also lands in OPEN release"
|
||||
newrepo finhotrel; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||
gitflow_start release 1.0 >/dev/null 2>&1; echo r>rel.txt; git add rel.txt; git commit -q -m relwork
|
||||
gitflow_start hotfix h2 >/dev/null 2>&1; echo p>p2.txt; git add p2.txt; git commit -q -m patch2
|
||||
gitflow_finish >/dev/null 2>&1
|
||||
chk "hotfix in open release" 'git log release/1.0 --oneline | grep -q "Merge hotfix/h2 into release/1.0"'
|
||||
|
||||
echo "T9 — reconcile is additive + idempotent + preserves project rules"
|
||||
newrepo recon; echo a>a; git add a; git commit -q -m a
|
||||
printf '%s\n' "node_modules/" "# my project rule" > .gitignore
|
||||
gitflow_reconcile_gitignore 2>/dev/null
|
||||
chk "kept project rule" 'grep -qxF "node_modules/" .gitignore'
|
||||
chk "added socle" 'grep -qxF ".claude/*" .gitignore'
|
||||
before="$(md5sum .gitignore)"
|
||||
gitflow_reconcile_gitignore 2>/dev/null
|
||||
chk "idempotent 2nd run" "[ \"$before\" = \"\$(md5sum .gitignore)\" ]"
|
||||
|
||||
echo "T10 — COHERENCE: hook verdict == lib predicate (drift detector, #4)"
|
||||
newrepo coh; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||
for br in main develop feature/x bugfix/y release/z hotfix/w master mainline qa; do
|
||||
if gitflow_protected_base "$br"; then lib=protected; else lib=open; fi
|
||||
git checkout -q -B "$br" 2>/dev/null
|
||||
printf 'x\n' >> a; git add a
|
||||
if .githooks/pre-commit 2>/dev/null; then hook=allow; else hook=block; fi
|
||||
git restore --staged a 2>/dev/null || true
|
||||
if { [ "$lib" = protected ] && [ "$hook" = block ]; } || { [ "$lib" = open ] && [ "$hook" = allow ]; }; then
|
||||
ok "coherent($br): lib=$lib hook=$hook"
|
||||
else
|
||||
no "DRIFT($br): lib=$lib hook=$hook"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "T11 — CLI executable mode (the contract orchestrators call)"
|
||||
newrepo cli; echo a>a
|
||||
bash "$HERE/gitflow.sh" init >/dev/null 2>&1
|
||||
chk "cli init → develop" 'git rev-parse --verify -q refs/heads/develop >/dev/null'
|
||||
cli_out="$(bash "$HERE/gitflow.sh" start feature cli-foo 2>/dev/null)"
|
||||
chk "cli start echoes branch" "[ \"$cli_out\" = feature/cli-foo ]"
|
||||
chk "cli start switched HEAD" '[ "$(git symbolic-ref --short HEAD)" = feature/cli-foo ]'
|
||||
if bash "$HERE/gitflow.sh" protected-base main; then ok "cli protected-base main → rc0"; else no "cli protected-base main"; fi
|
||||
if bash "$HERE/gitflow.sh" protected-base feature/x; then no "cli protected-base feature (rc0?)"; else ok "cli protected-base feature → rc1"; fi
|
||||
chk "cli base-for hotfix=main" '[ "$(bash "$HERE/gitflow.sh" base-for hotfix)" = main ]'
|
||||
|
||||
echo
|
||||
echo "==== RESULT: $PASS passed, $FAIL failed ===="
|
||||
[ "$FAIL" -eq 0 ]
|
||||
263
lib/gitflow.sh
Normal file
263
lib/gitflow.sh
Normal file
@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env bash
|
||||
# gitflow.sh — mechanical core of the gitflow model.
|
||||
#
|
||||
# Two ways in:
|
||||
# - SOURCED by tests / skills that want the functions.
|
||||
# - EXECUTED as a CLI dispatcher: `gitflow.sh <op> [args]` (how skills call it,
|
||||
# one Bash invocation per operation).
|
||||
#
|
||||
# The judgment layer (WHEN to finish — the human gate) lives in skills/gitflow/
|
||||
# SKILL.md, never here. This file only does the deterministic mechanics, so it
|
||||
# can be tested on throwaway repos. Mirrors the surgical-commit helper style:
|
||||
# `set -uo pipefail` on execute, argv arrays, fail loud, no global state.
|
||||
|
||||
_GITFLOW_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── branch model ─────────────────────────────────────────────────────────────
|
||||
GITFLOW_MAIN="main"
|
||||
GITFLOW_DEVELOP="develop"
|
||||
# template resolved relative to the lib; overridable for tests.
|
||||
GITFLOW_GITIGNORE_TEMPLATE="${GITFLOW_GITIGNORE_TEMPLATE:-$_GITFLOW_LIB_DIR/../templates/gitignore/standard.gitignore}"
|
||||
|
||||
# ── predicates / pure helpers ────────────────────────────────────────────────
|
||||
|
||||
# echo the gitflow type of a branch: feature|bugfix|release|hotfix|main|develop|other
|
||||
gitflow_branch_type() {
|
||||
local br="${1:-$(git symbolic-ref --short -q HEAD 2>/dev/null)}"
|
||||
case "$br" in
|
||||
"$GITFLOW_MAIN") echo main ;;
|
||||
"$GITFLOW_DEVELOP") echo develop ;;
|
||||
feature/*) echo feature ;;
|
||||
bugfix/*) echo bugfix ;;
|
||||
release/*) echo release ;;
|
||||
hotfix/*) echo hotfix ;;
|
||||
*) echo other ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# THE shared predicate — rc 0 iff (given or current) branch is a protected base.
|
||||
# Consumed by: start/finish (here), the assistance skills (aiguillage), and the
|
||||
# pre-commit hook (mirrored, coherence-tested — see gitflow-test.sh T10).
|
||||
gitflow_protected_base() {
|
||||
local br="${1:-$(git symbolic-ref --short -q HEAD 2>/dev/null)}"
|
||||
[ "$br" = "$GITFLOW_MAIN" ] || [ "$br" = "$GITFLOW_DEVELOP" ]
|
||||
}
|
||||
|
||||
# echo the base a given type must fork from.
|
||||
gitflow_base_for() {
|
||||
case "$1" in
|
||||
feature|bugfix|release) echo "$GITFLOW_DEVELOP" ;;
|
||||
hotfix) echo "$GITFLOW_MAIN" ;;
|
||||
*) echo "gitflow: unknown type '$1'" >&2; return 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# rc 0 iff at least one release/* branch exists (hotfix fan-out condition).
|
||||
gitflow_release_open() {
|
||||
[ -n "$(git for-each-ref --format='%(refname:short)' 'refs/heads/release/*')" ]
|
||||
}
|
||||
|
||||
# ── start ────────────────────────────────────────────────────────────────────
|
||||
|
||||
# gitflow_start <type> <name> → checkout -b <type>/<name> from the correct base.
|
||||
gitflow_start() {
|
||||
local type="${1:-}" name="${2:-}" base
|
||||
base="$(gitflow_base_for "$type")" || return 2
|
||||
[ -n "$name" ] || { echo "gitflow_start: missing <name>" >&2; return 2; }
|
||||
git rev-parse --verify -q "$base" >/dev/null \
|
||||
|| { echo "gitflow_start: base '$base' missing — run 'gitflow init' first" >&2; return 3; }
|
||||
git checkout -q "$base" || return 1
|
||||
git pull --ff-only -q 2>/dev/null || true # best-effort sync; offline / no-upstream ok
|
||||
git checkout -q -b "$type/$name" || return 1
|
||||
echo "$type/$name"
|
||||
}
|
||||
|
||||
# ── finish (directed merge + hotfix fan-out) ─────────────────────────────────
|
||||
|
||||
_gitflow_merge_into() { # _gitflow_merge_into <target> <source>
|
||||
local target="$1" source="$2"
|
||||
git checkout -q "$target" || return 1
|
||||
git pull --ff-only -q 2>/dev/null || true
|
||||
git merge --no-ff -q -m "Merge $source into $target" "$source" \
|
||||
|| { echo "gitflow: conflict merging $source → $target — resolve, commit, re-run finish" >&2; return 4; }
|
||||
}
|
||||
|
||||
_gitflow_merge_into_open_releases() { # <source>
|
||||
local source="$1" rel
|
||||
while IFS= read -r rel; do
|
||||
[ -n "$rel" ] || continue
|
||||
_gitflow_merge_into "$rel" "$source" || return 4
|
||||
done < <(git for-each-ref --format='%(refname:short)' 'refs/heads/release/*')
|
||||
}
|
||||
|
||||
_gitflow_delete() { # <branch>
|
||||
local br="$1"
|
||||
git checkout -q "$GITFLOW_DEVELOP" 2>/dev/null || git checkout -q "$GITFLOW_MAIN"
|
||||
git branch -q -d "$br" || { echo "gitflow: '$br' not fully merged — branch kept" >&2; return 5; }
|
||||
}
|
||||
|
||||
# gitflow_finish → directed merge of the CURRENT branch per its type, then delete.
|
||||
# WHEN to call this is the human gate (SKILL.md). This only performs the merge.
|
||||
gitflow_finish() {
|
||||
local br type
|
||||
br="$(git symbolic-ref --short -q HEAD)" || { echo "gitflow_finish: detached HEAD" >&2; return 3; }
|
||||
type="$(gitflow_branch_type "$br")"
|
||||
case "$type" in
|
||||
feature|bugfix)
|
||||
_gitflow_merge_into "$GITFLOW_DEVELOP" "$br" && _gitflow_delete "$br" ;;
|
||||
release)
|
||||
_gitflow_merge_into "$GITFLOW_MAIN" "$br" \
|
||||
&& _gitflow_merge_into "$GITFLOW_DEVELOP" "$br" \
|
||||
&& _gitflow_delete "$br" ;;
|
||||
hotfix)
|
||||
_gitflow_merge_into "$GITFLOW_MAIN" "$br" \
|
||||
&& _gitflow_merge_into "$GITFLOW_DEVELOP" "$br" \
|
||||
&& { gitflow_release_open && _gitflow_merge_into_open_releases "$br" || true; } \
|
||||
&& _gitflow_delete "$br" ;;
|
||||
*) echo "gitflow_finish: '$br' is not a finishable gitflow branch" >&2; return 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── init (resolves BLK-010) + reconcile + hook install ───────────────────────
|
||||
|
||||
_gitflow_init_fresh() { # unborn HEAD → deterministic root commit on main
|
||||
local msg="${1:-chore: initial commit}"
|
||||
git symbolic-ref HEAD "refs/heads/$GITFLOW_MAIN" # name the unborn branch 'main'
|
||||
git add -A
|
||||
git commit -q -m "$msg" \
|
||||
|| { echo "gitflow_init: nothing staged for the root commit (scaffold first)" >&2; return 1; }
|
||||
git branch "$GITFLOW_DEVELOP"
|
||||
}
|
||||
|
||||
_gitflow_init_existing() { # has commits → ensure main (rename master) + develop
|
||||
if ! git rev-parse --verify -q "refs/heads/$GITFLOW_MAIN" >/dev/null; then
|
||||
if git rev-parse --verify -q refs/heads/master >/dev/null; then
|
||||
git branch -m master "$GITFLOW_MAIN"
|
||||
else
|
||||
echo "gitflow_init: no '$GITFLOW_MAIN' and no 'master' — refusing to guess the prod branch" >&2
|
||||
return 2
|
||||
fi
|
||||
fi
|
||||
git checkout -q "$GITFLOW_MAIN" || return 1
|
||||
# commit the socle + versioned hook now, while hooksPath is NOT yet active
|
||||
# (activation is the last step of gitflow_init) → never self-blocked.
|
||||
git add -- .gitignore .githooks 2>/dev/null || true
|
||||
# socle commit failure is FATAL — abort BEFORE develop/hook-activation so a
|
||||
# partial run can't activate the hook and self-block every re-run (was a bug:
|
||||
# the `|| commit` form swallowed the failure, then init activated the hook).
|
||||
if ! git diff --cached --quiet -- .gitignore .githooks 2>/dev/null; then
|
||||
git commit -q -m "chore: adopt gitflow socle + pre-commit hook" \
|
||||
|| { echo "gitflow_init: socle commit failed — aborting before hook activation (recoverable)" >&2; return 1; }
|
||||
fi
|
||||
git rev-parse --verify -q "refs/heads/$GITFLOW_DEVELOP" >/dev/null \
|
||||
|| git branch "$GITFLOW_DEVELOP" "$GITFLOW_MAIN"
|
||||
}
|
||||
|
||||
# gitflow_init [msg] → idempotent. Order matters (full BLK-010 closure):
|
||||
# reconcile .gitignore + write the versioned hook FIRST, so the fresh root
|
||||
# commit / existing adoption commit EMBED them; activate the hook LAST so the
|
||||
# bootstrap commits are never self-blocked by the hook they install.
|
||||
gitflow_init() {
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || { echo "gitflow_init: not a git repo" >&2; return 1; }
|
||||
# identity precheck — without it the root/socle commit fails mid-run (see fatal
|
||||
# guard in _gitflow_init_existing). Fail loud up front instead of half-applying.
|
||||
{ [ -n "$(git config user.name)" ] && [ -n "$(git config user.email)" ]; } \
|
||||
|| { echo "gitflow_init: git identity unset (user.name/user.email) — set it first" >&2; return 1; }
|
||||
gitflow_reconcile_gitignore || return $? # socle into .gitignore BEFORE any commit
|
||||
_gitflow_write_hook || return $? # write .githooks/pre-commit (inactive)
|
||||
if ! git rev-parse --verify -q HEAD >/dev/null 2>&1; then
|
||||
_gitflow_init_fresh "$@" || return $? # root commit embeds scaffold + socle + hook
|
||||
else
|
||||
_gitflow_init_existing || return $? # adoption commit (hook still inactive)
|
||||
fi
|
||||
gitflow_activate_hook || return $? # activate LAST
|
||||
}
|
||||
|
||||
# Additive reconcile: ensure every non-comment template line is present; append
|
||||
# only what's missing under a managed marker. NEVER rewrites project-own rules.
|
||||
gitflow_reconcile_gitignore() {
|
||||
local tmpl="$GITFLOW_GITIGNORE_TEMPLATE" gi=".gitignore" line
|
||||
local -a missing=()
|
||||
[ -f "$tmpl" ] || { echo "gitflow: gitignore template missing: $tmpl" >&2; return 1; }
|
||||
[ -e "$gi" ] || : > "$gi"
|
||||
while IFS= read -r line; do
|
||||
case "$line" in ''|\#*) continue ;; esac
|
||||
grep -qxF -- "$line" "$gi" || missing+=("$line")
|
||||
done < "$tmpl"
|
||||
[ "${#missing[@]}" -gt 0 ] || return 0 # idempotent no-op
|
||||
{
|
||||
echo ""
|
||||
echo "# ── gitflow standard socle (added by gitflow_init; additive, safe to edit) ──"
|
||||
printf '%s\n' "${missing[@]}"
|
||||
} >> "$gi"
|
||||
echo "gitflow: appended ${#missing[@]} socle line(s) to $gi" >&2
|
||||
}
|
||||
|
||||
# Emit the self-contained pre-commit hook. The protected-base test is INLINED
|
||||
# (mirror of gitflow_protected_base) because the hook runs in arbitrary project
|
||||
# repos with no access to this lib. Coherence guaranteed by gitflow-test.sh T10.
|
||||
_gitflow_emit_pre_commit() {
|
||||
cat <<HOOK
|
||||
#!/bin/sh
|
||||
# gitflow pre-commit — generated by gitflow_init. Do not hand-edit.
|
||||
# Mirrors gitflow_protected_base (lib/gitflow.sh). Drift caught by T10.
|
||||
gd=\$(git rev-parse --git-dir)
|
||||
br=\$(git symbolic-ref --short -q HEAD 2>/dev/null)
|
||||
|
||||
git rev-parse --verify -q HEAD >/dev/null 2>&1 || exit 0 # root commit — allow
|
||||
[ -f "\$gd/MERGE_HEAD" ] && exit 0 # merge in progress — allow
|
||||
|
||||
case "\$br" in
|
||||
$GITFLOW_MAIN|$GITFLOW_DEVELOP) ;; # protected — keep checking
|
||||
*) exit 0 ;; # working branch — allow
|
||||
esac
|
||||
|
||||
# whitelist: all-staged-under-.claude/ (memory/doc/deploy helpers) — allow
|
||||
if [ -z "\$(git diff --cached --name-only | grep -v '^\.claude/' | head -1)" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "gitflow pre-commit: BLOCKED — direct commit on '\$br'." >&2
|
||||
echo " Branch from the right base (feature/bugfix->develop, hotfix->main), or merge." >&2
|
||||
echo " (.claude/** memory commits are exempt; --no-verify bypasses locally.)" >&2
|
||||
exit 1
|
||||
HOOK
|
||||
}
|
||||
|
||||
# write the versioned hook file — does NOT activate (see gitflow_activate_hook).
|
||||
_gitflow_write_hook() {
|
||||
local hd=".githooks"
|
||||
mkdir -p "$hd"
|
||||
_gitflow_emit_pre_commit > "$hd/pre-commit"
|
||||
chmod +x "$hd/pre-commit"
|
||||
}
|
||||
|
||||
# point git at the versioned hook dir. Run LAST in init so the bootstrap commits
|
||||
# (socle / adoption / root) are never blocked by the hook they install.
|
||||
gitflow_activate_hook() {
|
||||
git config core.hooksPath .githooks
|
||||
}
|
||||
|
||||
# convenience: write + activate in one call (re-install / CLI 'install-hook').
|
||||
gitflow_install_hook() {
|
||||
_gitflow_write_hook && gitflow_activate_hook
|
||||
}
|
||||
|
||||
# ── CLI dispatch (only when executed, not sourced) ───────────────────────────
|
||||
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
||||
set -uo pipefail
|
||||
cmd="${1:-}"; shift 2>/dev/null || true
|
||||
case "$cmd" in
|
||||
type) gitflow_branch_type "$@" ;;
|
||||
protected-base) gitflow_protected_base "$@" ;;
|
||||
base-for) gitflow_base_for "$@" ;;
|
||||
release-open) gitflow_release_open ;;
|
||||
start) gitflow_start "$@" ;;
|
||||
finish) gitflow_finish "$@" ;;
|
||||
init) gitflow_init "$@" ;;
|
||||
reconcile) gitflow_reconcile_gitignore "$@" ;;
|
||||
install-hook) gitflow_install_hook "$@" ;;
|
||||
emit-hook) _gitflow_emit_pre_commit ;;
|
||||
*) echo "usage: gitflow.sh {type|protected-base|base-for|release-open|start|finish|init|reconcile|install-hook|emit-hook}" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
81
skills/gitflow/SKILL.md
Normal file
81
skills/gitflow/SKILL.md
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
name: gitflow
|
||||
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` 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/*` and `bugfix/*`
|
||||
(off develop → develop) · `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 protected-base [br] # rc 0 on main/develop — the shared predicate
|
||||
```
|
||||
|
||||
`finish` merges by the current branch's type:
|
||||
|
||||
| Current branch | Merges into | then |
|
||||
|---|---|---|
|
||||
| `feature/*` · `bugfix/*` | develop | delete |
|
||||
| `release/*` | main + develop | delete |
|
||||
| `hotfix/*` | main + develop + any open `release/*` | delete |
|
||||
|
||||
## 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 skills)
|
||||
|
||||
On a protected base, assistance skills (`feat`/`bugfix`/`hotfix`) call
|
||||
`start <type>` to branch first; on a working branch they commit in place. Same
|
||||
`protected-base` predicate the out-of-skill hook uses.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- Using `finishing-a-development-branch` for a gitflow merge → it can't do directed/fan-out merges. Use `gitflow finish`.
|
||||
- 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.
|
||||
@ -129,6 +129,18 @@ Rules:
|
||||
- React Native, Flutter, backend, embedded, static HTML → skipped.
|
||||
- If another animation lib (gsap, lottie-react, react-spring, …) is already present → skipped.
|
||||
|
||||
## STEP 5f — GITFLOW INIT
|
||||
After every scaffold file exists (STEP 5–5e have run), establish the gitflow
|
||||
layout and the deterministic root commit:
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" init "chore: scaffold <project-name>"
|
||||
```
|
||||
Creates `main`+`develop`, root-commits the FULL scaffold (CLAUDE.md, README,
|
||||
config, `.gitignore`, deps), reconciles the `.gitignore` socle, and installs the
|
||||
versioned pre-commit hook — all embedded in the root commit, working tree clean.
|
||||
This is the deterministic scaffold commit owner (closes BLK-010). The MVP is
|
||||
implemented on a `feature/*` branch off `develop` (STEP 8).
|
||||
|
||||
## STEP 6 — PLAN
|
||||
Invoke `superpowers:writing-plans` with BRIEF + skeleton.
|
||||
Granular tasks (2-5 min each), exact file paths, TDD: tests before code.
|
||||
@ -144,7 +156,15 @@ Approve and start? (yes / request changes)
|
||||
Changes → back to STEP 6. Approved → continue.
|
||||
|
||||
## STEP 8 — IMPLEMENT
|
||||
Invoke `superpowers:subagent-driven-development`. Isolated subagents, TDD, 2-stage review per task.
|
||||
Start the MVP feature branch off develop, then implement on it:
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" start feature mvp
|
||||
```
|
||||
Invoke `superpowers:subagent-driven-development` for the per-task implement loop
|
||||
**and** the final whole-branch review **only**. Do NOT run its terminal
|
||||
`finishing-a-development-branch` step — this orchestrator owns integration via
|
||||
`gitflow finish` (STEP 11). When SDD's flow reaches "Use
|
||||
finishing-a-development-branch", stop and return.
|
||||
|
||||
## STEP 8b — GRAPHIFY FULL (after implementation)
|
||||
If `graphify` CLI is installed AND complexity >= 30%:
|
||||
@ -214,7 +234,7 @@ nothing was capitalized, the helper no-ops — no commit.
|
||||
|
||||
## STEP 10c — DOC SYNC
|
||||
Run BEFORE STEP 11 FINISH (moved here from post-FINISH). doc-syncer PATCHES public docs but
|
||||
does NOT commit them, and `finishing-a-development-branch` integrates only COMMITTED history
|
||||
does NOT commit them, and `gitflow finish` integrates only COMMITTED history
|
||||
— so a patch left uncommitted never reaches the merge/PR. Same PR-stranding class as the
|
||||
STEP 10b capitalize fix (BDR-034).
|
||||
|
||||
@ -226,14 +246,17 @@ ONLY the files doc-syncer patched (its `PATCHED_FILES` output, one path per line
|
||||
arg each), never `git add -A`, never `.claude/`/`CLAUDE.md`, and no-ops if nothing was
|
||||
patched. Report per its rc table — rc 4 = a LOUD upstream BDR-022 anomaly, not a silent skip.
|
||||
|
||||
> **Partial fix (conscious).** This commits the docs doc-sync patched, so they reach the
|
||||
> merge/PR. It does NOT fix the scaffold (STEP 5) + bootstrap-README (STEP 5b) commit gap
|
||||
> (BLK-010: no deterministic commit owner; `git worktree add -b` on an unborn HEAD) — a
|
||||
> separate chantier. After this, init-project's doc-sync is fixed but the scaffold/bootstrap
|
||||
> commit gap stays open; GSD STEP 12 still creates ROADMAP.md post-FINISH (BLK-011) — also separate.
|
||||
> **Scaffold commit owner = STEP 5f `gitflow init`** (root commit embeds scaffold + README +
|
||||
> `.gitignore` socle + hook, tree clean — BLK-010 closed). This doc-sync commit lands the
|
||||
> patched docs on the MVP feature branch so they reach the merge. GSD STEP 12 still creates
|
||||
> ROADMAP.md post-FINISH (BLK-011) — separate.
|
||||
|
||||
## STEP 11 — FINISH
|
||||
Invoke `superpowers:finishing-a-development-branch`. Tests pass, build clean, no placeholders, initial commit ready.
|
||||
Tests pass, build clean, no placeholders. Integrate the MVP feature into develop
|
||||
— **only on the user's explicit go** (the `gitflow` finish gate):
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" finish # feature/mvp → develop
|
||||
```
|
||||
|
||||
## STEP 12 — GSD v2 INIT (optional)
|
||||
If `multi-session` signal was detected in STEP 0 OR the project has >3 planned milestones:
|
||||
|
||||
@ -134,6 +134,26 @@ Cas :
|
||||
|
||||
---
|
||||
|
||||
## STEP 2.6 — GITFLOW INIT
|
||||
|
||||
Adopter le modèle gitflow sur ce repo existant :
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" init
|
||||
```
|
||||
Sur un repo existant, cela : renomme `master`→`main` si besoin (LOCAL), crée
|
||||
`develop` depuis main, réconcilie le socle `.gitignore` (additif — n'écrase
|
||||
jamais les règles du projet), installe le hook pre-commit versionné, et fait UN
|
||||
commit `chore: adopt gitflow socle + hook` sur main (pendant que le hook est
|
||||
inactif → jamais auto-bloqué). Idempotent — un re-run est un no-op.
|
||||
|
||||
**Annoncer le renommage master→main** s'il a lieu. Le renommage est LOCAL ;
|
||||
repointer la branche par défaut du remote vers `main` + la protection de branche
|
||||
sur `main`/`develop` est une étape de migration séparée (sous-chantier B) — pas
|
||||
faite ici. Pré-condition : working tree raisonnablement propre (le commit
|
||||
d'adoption ne stage que `.gitignore` + `.githooks`).
|
||||
|
||||
---
|
||||
|
||||
## STEP 3 — DEEP INTERVIEW
|
||||
|
||||
L'orchestrateur pilote directement l'interview (l'agent `interviewer.md` est laissé pour `/init-project` où le BRIEF format est attendu ; ici on reste en markdown libre dans la CLAUDE.md).
|
||||
|
||||
@ -121,7 +121,15 @@ assert a check not performed). No RELATED MEMORY from 0d → omit the block.
|
||||
Changes → back to STEP 2. Approved → continue.
|
||||
|
||||
## STEP 4 — IMPLEMENT
|
||||
Invoke `superpowers:subagent-driven-development`. Isolated subagents. 2-stage review per task: spec compliance → code quality.
|
||||
Start the feature branch off develop, then implement on it:
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" start feature <name>
|
||||
```
|
||||
Invoke `superpowers:subagent-driven-development` for the per-task implement loop
|
||||
**and** the final whole-branch review **only**. Do NOT run its terminal
|
||||
`finishing-a-development-branch` step — this orchestrator owns integration via
|
||||
`gitflow finish` (STEP 9). When SDD's flow reaches "Use
|
||||
finishing-a-development-branch", stop and return.
|
||||
|
||||
## STEP 4b — ERROR RECOVERY (if STEP 4 fails)
|
||||
If a subagent returns a build error, failing test, or type error:
|
||||
@ -191,7 +199,7 @@ memory is integrated with the branch, not stranded outside the PR.
|
||||
|
||||
## STEP 8 — DOC SYNC
|
||||
Run BEFORE STEP 9 FINISH. doc-syncer PATCHES public docs but does NOT commit them, and
|
||||
`finishing-a-development-branch` integrates only COMMITTED history — so a patch left
|
||||
`gitflow finish` integrates only COMMITTED history — so a patch left
|
||||
uncommitted (or committed after) never reaches the merge/PR. Same PR-stranding class as the
|
||||
STEP 7 capitalize fix (BDR-034).
|
||||
|
||||
@ -206,7 +214,11 @@ surfaced a forbidden path), not a silent skip. It runs BEFORE FINISH so the doc
|
||||
on the branch FINISH integrates.
|
||||
|
||||
## STEP 9 — FINISH
|
||||
Invoke `superpowers:finishing-a-development-branch`. Tests pass, build clean, ready to merge.
|
||||
Tests pass, build clean. Integrate the feature into develop — **only on the
|
||||
user's explicit go** (the `gitflow` finish gate):
|
||||
```bash
|
||||
bash "$HOME/.claude/lib/gitflow.sh" finish # feature/<name> → develop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
26
templates/gitignore/standard.gitignore
Normal file
26
templates/gitignore/standard.gitignore
Normal file
@ -0,0 +1,26 @@
|
||||
# ── env / secrets ──
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
# ── editors / IDE ──
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
# ── OS ──
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# ── logs / debug ──
|
||||
*.log
|
||||
# ── Claude config: local by default, project-memory versioned ──
|
||||
.claude/*
|
||||
!.claude/memory/
|
||||
!.claude/tasks/
|
||||
!.claude/audits/
|
||||
!.claude/settings.json
|
||||
!.claude/deploy/
|
||||
# re-ignore transients inside the versioned subtrees (the PENDING.json trap)
|
||||
.claude/settings.local.json
|
||||
.claude/agent-memory/
|
||||
.claude/gstack/
|
||||
.claude/deploy/PENDING.json
|
||||
.claude/deploy/NEXT.sh
|
||||
Loading…
Reference in New Issue
Block a user