docs(spec): de-Claude repo + git-history rewrite runbook (beta-prep #2)
Deferred runbook for the final beta-prep step: purge CLAUDE.md, rules.md, handoff.md, docs/session-handoff.md and docs/superpowers from all history, strip Co-Authored-By trailers, keep the files locally + gitignored, then a gated force-push. Execute at beta-cut time only, behind explicit confirmation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
3d86c8db8d
commit
db179ad98a
|
|
@ -0,0 +1,189 @@
|
|||
# de-Claude the repo + git history (beta-prep #2) — Design / Runbook
|
||||
|
||||
**Date:** 2026-06-15 · **Branch:** `feat/v1-foundation` · **Status:** approved, execution DEFERRED to
|
||||
beta/staging cut · Beta-prep #2 (the final, irreversible roadmap item)
|
||||
|
||||
Strip every trace of AI-assisted development from the repository **and from its entire git history**,
|
||||
so the codebase that ships to beta/staging looks like an ordinary, human-authored product repo. This
|
||||
is a **history rewrite + force-push** — irreversible, and it rewrites the shared branch. It runs
|
||||
**once, last**, after all content is complete and tagged, and **only on the operator's explicit go**
|
||||
for the force-push.
|
||||
|
||||
> **Timing:** the operator scoped this to beta/staging time ("in der dev Variante ist das nicht so
|
||||
> schlimm … später wenn es in beta oder staging muss das weg sein"). During dev the AI traces stay.
|
||||
> This document is the prepared runbook; it is **not** executed now.
|
||||
|
||||
## Operator-approved decisions
|
||||
- **File scope — purge ALL internal process docs from every commit:** `CLAUDE.md`, `rules.md`,
|
||||
`handoff.md`, `docs/session-handoff.md`, and the whole `docs/superpowers/` tree. Product code,
|
||||
`README.md`, `CHANGELOG.md`, `install.sh`, `docker/`, etc. stay.
|
||||
- **Keep on disk, gitignore:** after purging from git, the files remain on disk (dev keeps using
|
||||
`rules.md`/`CLAUDE.md` as references); they are added to `.gitignore` so they never re-enter git.
|
||||
- **Message scrub — trailers only:** strip the 167 `Co-Authored-By: Claude …` trailer lines. Keep all
|
||||
descriptive subjects/bodies (they describe real changes). The 23 "Codex" / 19 "spec" message
|
||||
mentions are **left as-is** (operator's call). Commits that only added a now-purged doc become empty
|
||||
and are pruned automatically.
|
||||
|
||||
## Ground truth (scanned 2026-06-15)
|
||||
- **170 commits, single author** `boban <boban@git.bave.dev>` — no "Claude" author/committer identity
|
||||
to fix.
|
||||
- **167/170** commits carry a `Co-Authored-By: Claude …` trailer; **0** "Generated with Claude Code"
|
||||
lines.
|
||||
- Tracked artifacts to purge: `CLAUDE.md`, `rules.md`, `handoff.md`, `docs/session-handoff.md`,
|
||||
`docs/superpowers/**` (16 spec/plan files).
|
||||
- **No** `.claude/`, `.cursor/`, `AGENTS.md`, `.mcp.json` anywhere in history.
|
||||
- **One branch** (`feat/v1-foundation`) + tags `v0.1.0 … v0.9.2`. No `main`.
|
||||
- `git-filter-repo` is **not** installed; no `pip3`; `python3` 3.13 is present → use the single-file
|
||||
script via `python3` (no sudo needed).
|
||||
|
||||
## Tool: git-filter-repo (single-file, no install)
|
||||
`git filter-repo` is the modern, git-recommended history-rewrite tool (correct tag rewriting, empty-
|
||||
commit pruning, ref-safe — unlike the deprecated `filter-branch`). It is one self-contained Python
|
||||
script. Fetch it once into the repo-adjacent tooling dir and run via `python3` — no `pip`, no `sudo`:
|
||||
|
||||
```bash
|
||||
# from a machine with internet; the script is pure Python, ~250 KB, MIT-licensed
|
||||
curl -fsSL https://raw.githubusercontent.com/newren/git-filter-repo/main/git-filter-repo \
|
||||
-o /tmp/git-filter-repo && chmod +x /tmp/git-filter-repo
|
||||
python3 /tmp/git-filter-repo --version # sanity
|
||||
```
|
||||
(Alternative if preferred and sudo is available interactively: `sudo apt-get install git-filter-repo`.)
|
||||
|
||||
## Runbook (execute top-to-bottom AT BETA TIME)
|
||||
|
||||
### Phase 0 — preconditions (abort if any fails)
|
||||
- This is the **last** operation. All feature work merged, all releases tagged, `CHANGELOG` current.
|
||||
- **Working tree clean** — `git status --short` empty. (In particular the parallel timing-fix
|
||||
refinement to `app/Livewire/Auth/ForgotPassword.php` must already be committed.)
|
||||
- Full suite green; app boots (R12).
|
||||
- No open Gitea PRs against the branch (a force-push invalidates them). Solo repo → expected none.
|
||||
|
||||
### Phase 1 — backup (the safety net for an irreversible op)
|
||||
```bash
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
# 1a. complete mirror of all refs (commits, branches, tags) — the real recovery artifact
|
||||
git bundle create "$HOME/clusev-pre-declaude-$TS.bundle" --all
|
||||
# 1b. on-disk copies of the to-be-kept files (filter-repo will delete them from the work tree)
|
||||
mkdir -p "$HOME/clusev-keep-$TS"
|
||||
cp -a CLAUDE.md rules.md handoff.md docs/session-handoff.md "$HOME/clusev-keep-$TS/"
|
||||
mkdir -p "$HOME/clusev-keep-$TS/superpowers" && cp -a docs/superpowers/. "$HOME/clusev-keep-$TS/superpowers/"
|
||||
# verify the bundle is restorable BEFORE proceeding
|
||||
git bundle verify "$HOME/clusev-pre-declaude-$TS.bundle"
|
||||
```
|
||||
Do **not** delete these backups until Phase 6 confirms a fresh clone of the rewritten remote builds.
|
||||
|
||||
### Phase 2 — rewrite history (in-place, --force; backups exist)
|
||||
```bash
|
||||
python3 /tmp/git-filter-repo \
|
||||
--path CLAUDE.md \
|
||||
--path rules.md \
|
||||
--path handoff.md \
|
||||
--path docs/session-handoff.md \
|
||||
--path docs/superpowers \
|
||||
--invert-paths \
|
||||
--message-callback '
|
||||
import re
|
||||
return re.sub(rb"(?im)^\s*Co-authored-by:\s*Claude.*\n?", b"", message).rstrip() + b"\n"
|
||||
' \
|
||||
--force
|
||||
```
|
||||
- `--invert-paths` deletes the listed paths from **every** commit; commits that become empty are
|
||||
pruned. Tags pointing at rewritten commits are rewritten automatically.
|
||||
- The `--message-callback` strips any `Co-authored-by: Claude …` line (case-insensitive) and tidies the
|
||||
trailing blank line. Bodies/subjects are otherwise untouched.
|
||||
- filter-repo **removes the `origin` remote** afterward (a deliberate guard) and `reset --hard`s the
|
||||
work tree to the rewritten HEAD — so `CLAUDE.md` etc. now vanish from disk too (restored in Phase 4).
|
||||
|
||||
### Phase 3 — verify the rewrite
|
||||
```bash
|
||||
git log --all -i --grep='co-authored-by: claude' --oneline | wc -l # expect 0
|
||||
for f in CLAUDE.md rules.md handoff.md docs/session-handoff.md; do \
|
||||
echo -n "$f in history: "; git log --all --oneline -- "$f" | wc -l; done # expect 0 each
|
||||
git log --all --oneline -- docs/superpowers | wc -l # expect 0
|
||||
git rev-list --count HEAD # < 170 (empty doc-only commits pruned)
|
||||
git log -1 --format='%an <%ae>' # still boban — author identity unchanged
|
||||
```
|
||||
|
||||
### Phase 4 — restore kept files on disk + gitignore them
|
||||
```bash
|
||||
# Phase 1 copied these in by basename, so $KEEP holds: CLAUDE.md, rules.md, handoff.md,
|
||||
# session-handoff.md, and superpowers/. Restore to their repo paths:
|
||||
KEEP="$HOME/clusev-keep-$TS"
|
||||
cp -a "$KEEP/CLAUDE.md" "$KEEP/rules.md" "$KEEP/handoff.md" .
|
||||
mkdir -p docs && cp -a "$KEEP/session-handoff.md" docs/session-handoff.md
|
||||
mkdir -p docs/superpowers && cp -a "$KEEP/superpowers/." docs/superpowers/
|
||||
```
|
||||
Append to `.gitignore` (idempotently):
|
||||
```
|
||||
# Internal AI-assisted-dev docs — kept locally, never committed (de-Claude'd for beta)
|
||||
/CLAUDE.md
|
||||
/rules.md
|
||||
/handoff.md
|
||||
/docs/session-handoff.md
|
||||
/docs/superpowers/
|
||||
```
|
||||
Commit just the ignore change (and any tidy), **without** a Co-Authored-By trailer:
|
||||
```bash
|
||||
git add .gitignore && git commit -m "chore: ignore internal development docs"
|
||||
git status --short # clean — the kept files are now invisible to git
|
||||
```
|
||||
|
||||
### Phase 5 — re-add origin, rebuild, GATED force-push
|
||||
```bash
|
||||
git remote add origin https://git.bave.dev/boban/clusev.git # filter-repo dropped it
|
||||
```
|
||||
Rebuild + run the suite on the rewritten tree (R12 — app boots, tests green) before pushing.
|
||||
|
||||
**>>> EXPLICIT-CONFIRMATION GATE <<<** — the force-push is irreversible and rewrites the shared
|
||||
branch/tags. Do not run it without the operator's clear, in-the-moment "yes, force-push". Token is
|
||||
read inline from `~/.env.gitea` and **all** output is sanitized (never echo the token):
|
||||
```bash
|
||||
TOKEN=$(grep -E '^GIT_ACCESS_TOKEN=' "$HOME/.env.gitea" | head -1 | cut -d= -f2- | tr -d '\r\n')
|
||||
URL="https://boban:${TOKEN}@git.bave.dev/boban/clusev.git"
|
||||
# branch — plain --force to the explicit URL (filter-repo dropped origin and the local
|
||||
# origin/* tracking ref is stale from earlier explicit-URL pushes, so --force-with-lease
|
||||
# has no reliable lease to check here; the Phase-1 bundle is the safety net instead)
|
||||
git push "$URL" --force feat/v1-foundation 2>&1 | sed -E 's#https://[^@]*@#https://<redacted>@#g; s/[0-9a-f]{40,}/<redacted>/g'
|
||||
# tags moved to new commits — force-update them too
|
||||
git push "$URL" --force --tags 2>&1 | sed -E 's#https://[^@]*@#https://<redacted>@#g; s/[0-9a-f]{40,}/<redacted>/g'
|
||||
unset TOKEN URL
|
||||
```
|
||||
(Enumerate any additional remote branches first with `git ls-remote --heads "$URL"` — currently only
|
||||
`feat/v1-foundation` exists. Do **not** use `--mirror`: it would prune refs unexpectedly.)
|
||||
|
||||
### Phase 6 — post-checks, then clean up
|
||||
- Fresh-clone check: clone the remote into a temp dir, confirm it builds and contains **no**
|
||||
`Co-Authored-By: Claude`, no `CLAUDE.md`/`rules.md`/`handoff.md`/`docs/superpowers`.
|
||||
- Gitea: releases follow the force-updated tags; old commit SHAs in any links/PRs break (expected,
|
||||
solo repo).
|
||||
- Only **after** the fresh clone verifies: the Phase-1 backups (`bundle` + keep dir) may be archived
|
||||
or removed.
|
||||
|
||||
## Going forward (post-execution rule)
|
||||
Once de-Claude has run, **stop appending the `Co-Authored-By: Claude` trailer** to new commits, and do
|
||||
not re-add the gitignored docs to git — otherwise the traces re-enter the history this rewrite cleaned.
|
||||
(This overrides the default commit-footer convention, per the operator's beta requirement.)
|
||||
|
||||
## Recovery (if something goes wrong)
|
||||
The remote is force-pushed, but the Phase-1 `--all` bundle holds the complete pre-rewrite state:
|
||||
```bash
|
||||
git clone "$HOME/clusev-pre-declaude-<TS>.bundle" clusev-restored # full history back
|
||||
# or, in-place: git fetch "$HOME/…bundle" '+refs/*:refs/*' then reset/force-push the old refs
|
||||
```
|
||||
|
||||
## Security
|
||||
- Token read only at push time from `~/.env.gitea`; never echoed/committed; push output sanitized.
|
||||
- The rewrite never touches `.env*` (gitignored, not in history) or any secret.
|
||||
- filter-repo's `origin`-drop + `--force` requirement are guards; the mandatory pre-backup is the
|
||||
recovery path for the one irreversible step (the force-push).
|
||||
|
||||
## Notes / self-reference
|
||||
- This spec lives under `docs/superpowers/` — the rewrite purges it from git but Phase 4 restores it on
|
||||
disk (gitignored), so it survives as the executed-runbook reference.
|
||||
- 170 commits is small; filter-repo completes in seconds. `filter-branch` is a zero-dependency
|
||||
fallback but slower and deprecated — prefer filter-repo.
|
||||
|
||||
## Out of scope
|
||||
- Re-writing commit-message bodies beyond the Co-Authored-By trailer (operator chose trailers-only).
|
||||
- Squashing/reordering history (we preserve the real commit-by-commit story, minus traces).
|
||||
- Anything in beta-prep #3 (README, done), #4 (installer, done), #5 (MOTD, done).
|
||||
Loading…
Reference in New Issue