From 9b89da29bef86733f9adb9650d2776b12c32edff Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 22 Jul 2026 15:12:22 +0200 Subject: [PATCH 1/2] feat(gitflow): auto-purge transient superpowers artifacts at finish (BDR-065) _gitflow_purge_transient removes docs/superpowers/{specs,plans} on the feature/bugfix branch just before the directed merge, so develop's tip lands clean while the feature commits stay reachable as the archive (git show :...). Best-effort: never aborts a finish (no-op when absent, skip on dirty paths, restore index+tree on commit failure). Opt-out GITFLOW_PURGE_TRANSIENT=0; purge-transient CLI verb. Automates the manual post-merge cleanup BDR-065 left as doctrine (slipped once, 655e364). Universal via the ~/.claude/lib symlink. gitflow-test T17 a-d; shellcheck clean; make test exit 0. --- CLAUDE.md | 15 +++++++++----- lib/gitflow-test.sh | 48 +++++++++++++++++++++++++++++++++++++++++++ lib/gitflow.sh | 50 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 61a3c6d..a7b5941 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,8 +32,13 @@ or re-run `make plugin`. `docs/superpowers/specs/**` and `docs/superpowers/plans/**` are run-time artifacts of a feature pipeline (subagent briefs, reviewer references). -They are committed DURING the run and DELETED in the post-merge cleanup -(BDR-065) — git history at the feature commits is their archive. Durable -knowledge goes to `.claude/memory/` registries, never to these files. -Derived scan/audit outputs (`.audit/**`) are gitignored and never -committed, even redacted (LRN-124). +They are committed DURING the run (the SDD worktree + reviewers read them +from disk — NOT gitignored), then AUTO-PURGED by `gitflow finish` on a +`feature`/`bugfix` branch, before the merge, so develop's tip stays clean +(BDR-065, `lib/gitflow.sh` `_gitflow_purge_transient`). The feature commits +stay reachable from develop, so `git show :docs/…` is still the archive. +Opt out with `GITFLOW_PURGE_TRANSIENT=0`. NOT in scope: `.claude/tasks/{contracts,plans}` +(durable, versioned, referenced by decisions.md). Durable knowledge goes to +`.claude/memory/` registries, never to these files. Derived scan/audit +outputs (`.audit/**`) are gitignored and never committed, even redacted +(LRN-124). diff --git a/lib/gitflow-test.sh b/lib/gitflow-test.sh index 0080a58..1aa1c7e 100644 --- a/lib/gitflow-test.sh +++ b/lib/gitflow-test.sh @@ -239,6 +239,7 @@ gitflow_start feature glwork >/dev/null 2>&1 # proving this backstop is NOT gated by the branch-protection check above it) printf 'aws_access_key_id = AKIA%s\n' "GDR5XRBXYARW2I5N" > secret.txt git add secret.txt +# shellcheck disable=SC2034 # gl_out is used in the deferred chk eval strings gl_out="$(git commit -q -m "add secret" 2>&1)"; gl_rc=$? chk "T16a fake secret on feature branch → blocked" "[ $gl_rc -ne 0 ]" chk "T16a message mentions gitleaks" 'printf "%s" "$gl_out" | grep -qi gitleaks' @@ -252,10 +253,57 @@ chk "T16b clean commit still succeeds" 'git commit -q -m "clean work" 2>/dev/nul # T16c — gitleaks missing from PATH → warn, never block (defense in depth # must not become a new single point of failure) echo clean2 > clean2.txt; git add clean2.txt +# shellcheck disable=SC2034 # noleaks_out is used in the deferred chk eval strings noleaks_out="$(PATH=/usr/bin:/bin git commit -q -m "clean work 2" 2>&1)"; noleaks_rc=$? chk "T16c missing-gitleaks → still commits (rc0)" "[ $noleaks_rc -eq 0 ]" chk "T16c missing-gitleaks → warns" 'printf "%s" "$noleaks_out" | grep -qi "not installed"' +echo "T17 — finish auto-purges transient superpowers artifacts (BDR-065)" +# T17a — feature carrying docs/superpowers spec+plan: purged before merge, +# develop TIP clean, artifacts still recoverable from history (archive property) +newrepo purgefeat; echo a>a; hookon; gitflow_init >/dev/null 2>&1 +gitflow_start feature pf >/dev/null 2>&1 +mkdir -p docs/superpowers/specs docs/superpowers/plans +echo spec > docs/superpowers/specs/s.md +echo plan > docs/superpowers/plans/p.md +echo code > feat.txt +git add -A; git commit -q -m "feat + transient spec/plan" +gitflow_finish >/dev/null 2>&1 +# the add-commit stays reachable from develop via the --no-ff merge's 2nd parent; +# --full-history defeats the path simplification that hides it, and `git show +# :path` proves BDR-065's "git history = the archive" recovery. +# shellcheck disable=SC2034 # pf_add_sha is used in the deferred chk eval string +pf_add_sha="$(git log develop --full-history --format=%H -- docs/superpowers/specs/s.md | tail -1)" +chk "T17a merged into develop" 'git log develop --oneline | grep -q "Merge feature/pf into develop"' +chk "T17a develop TIP has no transient" '[ -z "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]' +chk "T17a purge commit on record" 'git log develop --oneline | grep -q "purge transient planning artifacts"' +chk "T17a artifact recoverable from history" '[ "$(git show "$pf_add_sha":docs/superpowers/specs/s.md 2>/dev/null)" = spec ]' +chk "T17a non-transient code survives" 'git ls-tree -r develop --name-only | grep -qx feat.txt' +chk "T17a feature branch deleted" '! git rev-parse --verify -q refs/heads/feature/pf >/dev/null' + +# T17b — no artifacts → purge is a silent no-op, no spurious commit +newrepo purgenone; echo a>a; hookon; gitflow_init >/dev/null 2>&1 +gitflow_start feature pn >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w +gitflow_finish >/dev/null 2>&1 +chk "T17b merged into develop" 'git log develop --oneline | grep -q "Merge feature/pn into develop"' +chk "T17b no purge commit created" '! git log develop --oneline | grep -q "purge transient"' + +# T17c — opt-out (GITFLOW_PURGE_TRANSIENT=0) keeps the artifacts on develop +newrepo purgeoff; echo a>a; hookon; gitflow_init >/dev/null 2>&1 +gitflow_start feature po >/dev/null 2>&1 +mkdir -p docs/superpowers/specs; echo spec > docs/superpowers/specs/s.md +git add -A; git commit -q -m "feat + spec" +GITFLOW_PURGE_TRANSIENT=0 gitflow_finish >/dev/null 2>&1 +chk "T17c opt-out keeps transient on develop TIP" '[ -n "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]' + +# T17d — chore is OUT of purge scope (only feature/bugfix originate artifacts) +newrepo purgechore; echo a>a; hookon; gitflow_init >/dev/null 2>&1 +gitflow_start chore pc >/dev/null 2>&1 +mkdir -p docs/superpowers/specs; echo spec > docs/superpowers/specs/s.md +git add -A; git commit -q -m "chore + spec" +gitflow_finish >/dev/null 2>&1 +chk "T17d chore leaves transient (not in scope)" '[ -n "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]' + echo echo "==== RESULT: $PASS passed, $FAIL failed ====" [ "$FAIL" -eq 0 ] diff --git a/lib/gitflow.sh b/lib/gitflow.sh index 06d9b14..10c5ba2 100644 --- a/lib/gitflow.sh +++ b/lib/gitflow.sh @@ -18,6 +18,12 @@ 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}" +# Transient planning artifacts (superpowers spec/plan). A feature/bugfix run +# COMMITS them (SDD worktree + reviewers read them from disk); finish PURGES +# them before the merge reaches develop's tip (BDR-065). Fixed path list; +# read GITFLOW_PURGE_TRANSIENT=0 at finish time to opt out (read in the helper, +# never cached here, so an inline `VAR=0 gitflow_finish` override works). +GITFLOW_TRANSIENT_PATHS=("docs/superpowers/specs" "docs/superpowers/plans") # ── predicates / pure helpers ──────────────────────────────────────────────── @@ -97,6 +103,42 @@ _gitflow_delete() { # git branch -q -d "$br" || { echo "gitflow: '$br' not fully merged — branch kept" >&2; return 5; } } +# _gitflow_purge_transient → remove the committed transient planning artifacts +# (BDR-065) from the CURRENT branch just before the directed merge. Result: the +# removal rides the feature/bugfix branch, whose earlier commits stay reachable +# from develop through the --no-ff merge (`git show :…` = the archive), +# while develop's TIP lands clean. Automates the manual post-merge chore that +# BDR-065 left as doctrine (and that slipped once — commit 655e364). +# +# BEST-EFFORT BY CONTRACT: this NEVER aborts a finish. Nothing tracked → no-op; +# uncommitted changes under those paths, or a failed commit → warn + degrade to +# the old manual-cleanup behaviour, index/tree restored, merge still proceeds. +# The scoped commit (`-- `) records only the deletions, so a dirty index +# is never swept in. Opt out with GITFLOW_PURGE_TRANSIENT=0. +_gitflow_purge_transient() { + [ "${GITFLOW_PURGE_TRANSIENT:-1}" = 1 ] || return 0 + local p; local -a tracked=() + for p in "${GITFLOW_TRANSIENT_PATHS[@]}"; do + [ -n "$(git ls-files -- "$p")" ] && tracked+=("$p") + done + [ "${#tracked[@]}" -gt 0 ] || return 0 # nothing tracked → no-op + # only purge paths with no pending changes → git rm is all-or-nothing safe and + # never discards uncommitted work under docs/superpowers. + if ! git diff --quiet HEAD -- "${tracked[@]}" 2>/dev/null; then + echo "gitflow: transient artifacts have uncommitted changes — purge skipped, finishing without it (clean up by hand)" >&2 + return 0 + fi + if git rm -r -q -- "${tracked[@]}" >/dev/null 2>&1 \ + && git commit -q -m "chore: purge transient planning artifacts (BDR-065)" -- "${tracked[@]}"; then + echo "gitflow: purged transient planning artifacts before merge (${tracked[*]})" >&2 + else + echo "gitflow: transient-artifact purge failed — finishing without it (clean up by hand)" >&2 + git reset -q HEAD -- "${tracked[@]}" 2>/dev/null || true # unstage any partial rm + git checkout -q -- "${tracked[@]}" 2>/dev/null || true # restore working tree + fi + return 0 +} + # gitflow_finish [ ] → directed merge of the CURRENT branch per its # type, then delete. WHEN to call this is the human gate (SKILL.md). # @@ -117,7 +159,10 @@ gitflow_finish() { fi type="$(gitflow_branch_type "$br")" case "$type" in - feature|bugfix|chore) + feature|bugfix) + _gitflow_purge_transient # BDR-065 auto-cleanup, on HEAD, pre-merge; never blocks + _gitflow_merge_into "$GITFLOW_DEVELOP" "$br" && _gitflow_delete "$br" ;; + chore) _gitflow_merge_into "$GITFLOW_DEVELOP" "$br" && _gitflow_delete "$br" ;; release) _gitflow_merge_into "$GITFLOW_MAIN" "$br" \ @@ -283,8 +328,9 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then finish) gitflow_finish "$@" ;; init) gitflow_init "$@" ;; reconcile) gitflow_reconcile_gitignore "$@" ;; + purge-transient) _gitflow_purge_transient ;; 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 ;; + *) echo "usage: gitflow.sh {type|protected-base|base-for|release-open|start|finish|init|reconcile|purge-transient|install-hook|emit-hook}" >&2; exit 2 ;; esac fi From 78a25aeb5e9f6975a252b8ed7e370fadec25d373 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 22 Jul 2026 15:12:28 +0200 Subject: [PATCH 2/2] chore(memory): BDR-065 amendment (auto-purge coded) + LRN-138 + journal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BDR-065 delete-side now automated (lib/gitflow.sh _gitflow_purge_transient). LRN-138: gitignore != delete for run-time artifacts read from disk — use commit-during-run + auto-delete at the integration boundary. TODO checked, journal line. --- .claude/memory/decisions.md | 1 + .claude/memory/journal.md | 4 ++++ .claude/memory/learnings.md | 6 ++++++ .claude/tasks/TODO.md | 21 +++++++++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 2b68e73..15c51ba 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -981,6 +981,7 @@ rules: - **Why**: user call 2026-07-14 — registries already capture decisions; a stale plan describes a superseded intermediate state and misleads future readers; accumulation pollutes the repo. Precedent: gsc-crux cleanup (8a1fac0, 2026-07-10) did the same — this makes it law, not habit. - **Alternatives rejected**: never-commit (gitignore docs/superpowers) — breaks mid-run: briefs, reviewers, other-machine checkouts need the files; superpowers brainstorming commits the spec by convention. Keep-forever — the drift + pollution complained about. - **Reference**: project CLAUDE.md; cleanup commit this chore; precedent 8a1fac0. Linked [[BDR-064]], [[LRN-124]]. +- **Amendment (2026-07-22)**: DELETE side now AUTOMATED — `lib/gitflow.sh` `_gitflow_purge_transient` at `gitflow finish` (feature/bugfix, pre-merge, on HEAD) git-rm's `docs/superpowers/{specs,plans}` + scoped commit → develop TIP clean, feature commits stay reachable (`git show :…` archive intact). Best-effort: NEVER aborts finish (nothing-tracked no-op / dirty-path skip / commit-fail index+tree restore). Opt-out `GITFLOW_PURGE_TRANSIENT=0`. Retires the manual chore that slipped (655e364). Universal via `~/.claude/lib`→repo symlink (ship-feature STEP 9 + init-project STEP 11 both finish through it). gitignore STILL rejected — unchanged: breaks superpowers' `git add` of the spec (silently skipped, no travel to SDD worktree). `.claude/tasks/{contracts,plans}` kept versioned (user call — durable, referenced by decisions.md). Tests: gitflow-test.sh T17 a-d. [[LRN-138]]. --- diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 889fd6e..78a43c9 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -423,3 +423,7 @@ rules: ## 2026-07-21 - Skill audit (user ask "pourquoi pas investigate dans bugfix ?") → same core doctrine, incompatible wrappers: investigate = monolithic gstack (own memory ~/.gstack, no gitflow/gates, ~1075-line preamble), bugfix = orchestrator (contract, fresh verifier+security gates, registries). Routing inverted in CLAUDE.global.md: bugfix primary, investigate explicit-only → BDR-080. chore/skill-routing-bugfix, UNMERGED. + +## 2026-07-22 +- User: auto-gitignore+delete transient pipeline artifacts in all projects. Investigation reframed the ask — gitignore = WRONG tool (files read from disk during run; would break superpowers SDD `git add` of spec). BDR-065 already rejected gitignore + its DELETE side was doctrine-only (no code, manual chore slipped once — 655e364). User picks (2 recommended): keep committed-during-run + AUTOMATE delete; keep `.claude/tasks/{contracts,plans}` versioned. +- Built `lib/gitflow.sh` `_gitflow_purge_transient` at finish (feature/bugfix, pre-merge, best-effort never-abort, opt-out `GITFLOW_PURGE_TRANSIENT=0`) + `purge-transient` CLI verb. Universal via `~/.claude/lib`→repo symlink. gitflow-test T17 a-d (10 checks, `--full-history` recovery), shellcheck clean, make test exit 0. BDR-065 amendment + [[LRN-138]]. feature/gitflow-auto-purge-transient. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 61472ca..2a4d04a 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -1349,3 +1349,9 @@ rules: - **fail-safe pin rule**: keep the HIGHEST tier as the frontmatter pin and override DOWN at call sites — a forgotten override then over-tiers (costs money) instead of silently downgrading judgment (costs correctness). - **future application**: before splitting any agent across model tiers, try MODE + `model=` first; create a new agent file only for a genuinely new role. Run-scoped `.audit/-` files + completeness sentinel + fail-closed consumer for any cross-dispatch artifact. - **cousin**: [[LRN-125]] [[LRN-126]] [[BDR-077]]. + +## LRN-138 — gitignore ≠ delete for run-time artifacts read from disk (2026-07-22) +- **pattern**: gitignore is the WRONG tool for an artifact a pipeline READS FROM DISK during a run — it blocks the commit but leaves the file (cleans nothing) AND breaks git-travel flows (superpowers commits the spec via `git add` so it reaches the SDD worktree; a gitignored path is silently skipped w/o `-f`). Right tool = commit-during-run + AUTO-DELETE at the integration boundary (`gitflow finish`, pre-merge, on the working branch → history keeps the archive, develop tip clean). +- **context**: user asked to gitignore transient planning artifacts (`docs/superpowers/{specs,plans}`, `.claude/tasks/{contracts,plans}`) to stop them merging. BDR-065 had already REJECTED gitignore for docs/superpowers on the git-travel ground; the real gap was the DELETE side never being coded (doctrine-only manual chore, slipped once — 655e364). Built `_gitflow_purge_transient`. +- **future application**: "don't merge transient X" → ask: does the run read X from disk? does X travel via git (worktree, foreign checkout)? Yes → auto-purge at finish, not gitignore. Scoped commit `-- ` avoids sweeping a dirty index; `git diff --quiet HEAD -- paths` precheck makes `git rm` all-or-nothing safe; keep the purge best-effort so cleanup NEVER blocks a merge. Prove archive-reachability with `git log --full-history` / `git show :path` — plain `git log -- path` prunes the purged add-commit via history simplification (bit me writing T17). +- **link**: [[BDR-065]]. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 4ed061e..cc2c8cb 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1,5 +1,26 @@ # TODO +## 2026-07-22 — auto-purge transient superpowers artifacts at finish (feature/gitflow-auto-purge-transient) +User: transient planning artifacts (`docs/superpowers/{specs,plans}`) leak into +develop; BDR-065 "post-merge cleanup" is DOCTRINE ONLY (no code) — manual chore, +already missed once (655e364). Decision (user 2026-07-22, 2 recommended picks): +keep committed-during-run (SDD worktree + reviewers read them), AUTOMATE the +delete at `gitflow finish`. NO gitignore (would break superpowers' `git add` of +the spec → no travel to SDD worktree). `.claude/tasks/{contracts,plans}` stay +versioned (durable, referenced by decisions.md e.g. BDR-076). Universal via the +`~/.claude/lib` → repo `lib` symlink: every project's finish gets it. +- [x] lib/gitflow.sh: `_gitflow_purge_transient` (clean-precheck → git rm → + scoped commit `-- paths`; best-effort, NEVER aborts finish; opt-out + `GITFLOW_PURGE_TRANSIENT=0`) wired into finish `feature|bugfix` pre-merge; + `purge-transient` CLI verb. +- [x] lib/gitflow-test.sh T17 a/b/c/d (purge+recover-from-history via + --full-history+`git show`, no-op when absent, opt-out keeps, chore scope). + Also fixed 2 pre-existing SC2034 warnings (T16 gl_out/noleaks_out). +- [x] Gate: shellcheck lib/*.sh CLEAN + `make test` exit 0 (gitflow 106/0, full + suite green). Universal via ~/.claude/lib → repo lib symlink (verified). +- [x] CLAUDE.md §Transient planning artifacts: → "AUTO-PURGED by gitflow finish". +- [ ] Capitalize: BDR-065 amendment (delete side now automated) + LRN — pending user OK. + ## 2026-07-20 — pending merge gates (reconcile) - [x] merge feature/profile-managed-externals → develop (BDR-079 profile symmetry + /doc clean pass: README/USAGE/ARCHITECTURE.md) — 37c79f0