feat(gitflow): chore branch type + aiguillage for standalone memory/doc skills

Standalone /capitalize /close /prune-memory /reconcile no longer lean on the .claude/** hook exemption when run on main/develop: the aiguillage branches them to chore/* off develop before writing. New chore type (base develop, finish->develop) added to the lib; hook unchanged (chore/* non-protected). Closes the leak where standalone memory work (memory IS the work, no code branch to follow) landed direct on a protected base. 64/64 gitflow-test green, shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNaYKPEkjH1jbgoX1TwKMX
This commit is contained in:
Bastien Chanot
2026-07-01 13:25:36 +02:00
co-authored by Claude Opus 4.8
parent 53bd7beee8
commit e8807a7333
9 changed files with 82 additions and 28 deletions
+28 -13
View File
@@ -1,26 +1,41 @@
# Gitflow aiguillage — assistance flows branch on a protected base
# Gitflow aiguillage — branch on a protected base before writing
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`.
Flows that WRITE — code, OR standalone memory/doc work — must NEVER commit on a
protected base (`main`/`develop`). Run this check **before editing any file**.
```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.
- **WORKING** (`feature/*`, `bugfix/*`, `hotfix/*`, `chore/*`, 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).
The caller passes its TYPE:
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).
| Caller | TYPE | Base |
|--------|------|------|
| `/feat` | `feature` | develop |
| `/bugfix` | `bugfix` | develop |
| `/hotfix` | `hotfix` | main |
| `/capitalize` · `/close` · `/prune-memory` · `/reconcile` | `chore` | develop |
The `chore` row = **standalone memory/doc work**: the registry / TODO / doc
reconciliation & curation skills, run OUTSIDE an assistance flow. Inside `/feat`
`/bugfix` `/hotfix` `/ship-feature` a working branch already exists (this check
returns WORKING) and the memory commit rides it. The aiguillage only fires when
such a skill is invoked directly on `main`/`develop` — i.e. memory IS the work,
with no code branch to follow. That is the leak it closes: the `.claude/**` hook
exemption still lets a *manual* memory commit through on a protected base, but a
skill-driven one now branches to `chore/*` first.
**Never run `gitflow finish`** — these 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).
+14 -1
View File
@@ -32,6 +32,9 @@ 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 ]'
chk "type chore" '[ "$(gitflow_branch_type chore/x)" = chore ]'
chk "base chore=develop" '[ "$(gitflow_base_for chore)" = develop ]'
chk "not protected chore" '! gitflow_protected_base chore/x'
echo "T2 — init fresh (BLK-010 root commit)"
newrepo fresh; echo scaffold > README.md; hookon
@@ -92,6 +95,16 @@ chk "merged into develop" 'git log develop --oneline | grep -q "Merge feature/f1
chk "main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
chk "branch deleted" '! git rev-parse --verify -q refs/heads/feature/f1 >/dev/null'
echo "T6b — finish chore → develop only (standalone memory/doc maintenance)"
newrepo finchore; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start chore c1 >/dev/null 2>&1
mkdir -p .claude/memory; echo m>.claude/memory/x.md; git add -A; git commit -q -m "chore(memory)"
main_before="$(git rev-parse main)"
gitflow_finish >/dev/null 2>&1
chk "chore merged into develop" 'git log develop --oneline | grep -q "Merge chore/c1 into develop"'
chk "chore main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
chk "chore branch deleted" '! git rev-parse --verify -q refs/heads/chore/c1 >/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
@@ -119,7 +132,7 @@ 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
for br in main develop feature/x bugfix/y release/z hotfix/w chore/m 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
+4 -3
View File
@@ -21,7 +21,7 @@ GITFLOW_GITIGNORE_TEMPLATE="${GITFLOW_GITIGNORE_TEMPLATE:-$_GITFLOW_LIB_DIR/../t
# ── predicates / pure helpers ────────────────────────────────────────────────
# echo the gitflow type of a branch: feature|bugfix|release|hotfix|main|develop|other
# echo the gitflow type of a branch: feature|bugfix|release|hotfix|chore|main|develop|other
gitflow_branch_type() {
local br="${1:-$(git symbolic-ref --short -q HEAD 2>/dev/null)}"
case "$br" in
@@ -31,6 +31,7 @@ gitflow_branch_type() {
bugfix/*) echo bugfix ;;
release/*) echo release ;;
hotfix/*) echo hotfix ;;
chore/*) echo chore ;;
*) echo other ;;
esac
}
@@ -46,7 +47,7 @@ gitflow_protected_base() {
# echo the base a given type must fork from.
gitflow_base_for() {
case "$1" in
feature|bugfix|release) echo "$GITFLOW_DEVELOP" ;;
feature|bugfix|release|chore) echo "$GITFLOW_DEVELOP" ;;
hotfix) echo "$GITFLOW_MAIN" ;;
*) echo "gitflow: unknown type '$1'" >&2; return 2 ;;
esac
@@ -103,7 +104,7 @@ gitflow_finish() {
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)
feature|bugfix|chore)
_gitflow_merge_into "$GITFLOW_DEVELOP" "$br" && _gitflow_delete "$br" ;;
release)
_gitflow_merge_into "$GITFLOW_MAIN" "$br" \