igneum/tools/repo/fresh-repo.sh
igneum-labs ff0d0dde61 Scrub: the founder list leaves the repository in every encoding; the never-push founder check decodes base64, hex and .b64 blobs too (7 October 2026, 21:4x UK)
tools/ci/founder-strings.b64 was the founder check's pattern list base64-encoded: a grep could not read it, any reader of the public host could (git.igneum.network was public from 21:26 UK; read off at 21:36). The list now lives only in a private file (~/.config/igneum/founder-strings on the Mac, /srv/discord-hooks/founder-strings on build-1 for the Discord guard; $IGNEUM_FOUNDER_STRINGS overrides) and site/forbidden-strings.txt carries no encoded copy. Readers: founder-strings-check.sh (skips with a line where the file is absent; the Mac's hook is the guard), site/scrub.mjs and launch-gates-check.mjs (the private file's patterns added where it exists), discord-hooks.mjs (three locations; the test writes a fixture list and loads the module after it), fresh-repo.sh (the private file; drops tools/ci/founder-strings.b64 from every commit; rewrites the base64 of every list regex out of every blob and scans for it).

The check's second pass (main's addition): every base64 literal of 24 characters or more, every hex literal of 24 or more and every *.b64 file is decoded and scanned, so no encoding hides a term again; the self-test plants each fixture sample in plain text, in a .b64 file, as a base64 literal and as a hex literal, each caught and named, and a tree without the list skips with its line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 20:40:36 +00:00

276 lines
20 KiB
Bash
Executable file

#!/usr/bin/env bash
# The history rewrite of docs/plans/history-rewrite.md, section 2, as one script: a fresh mirror clone, one
# git-filter-repo pass with the plan's rules, the greps that must read zero, and the commands (printed, never run)
# that create the fresh repository under the organisation and push the rewritten refs there (the owner's decision of
# 5 October 2026: option B, a fresh repository, never a force-push over the old one).
#
# tools/repo/fresh-repo.sh [--source <url|path>] [--work <dir>] [--new-repo <org/name>] [--new-login <login>]
# [--public-claude-md <file>] [--clean]
#
# --source what to clone (default: this checkout's origin URL; a local path makes a throwaway dry run)
# --work where the clone and the report go (default: a fresh directory under $TMPDIR); never inside a checkout
# --new-repo the repository the printed commands create (default: igneum-network/igneum-core)
# --new-login the renamed GitHub login (the owner renames it first; the numeric noreply id stays): every author
# line and every file mention of the standing login is rewritten to it, and the identity grep then
# demands zero hits for the old login too. Without it the standing login stays and is reported as such
# --public-claude-md a scrubbed CLAUDE.md that replaces the file in EVERY commit (docs/fud-fixes.md section 5 step 2)
# --clean remove the work directory at the end (the default keeps it: the push runs from that clone)
#
# Reads (never prints): ~/.config/igneum/log-intake-key, log-intake-key.next, dl-token, dl-token.next (those that
# exist) for the secret rules and the secret grep; the personal identities and the second owner login are read from
# the history itself (every author or committer that is not the standing login). The rule files are written 0600
# in a 0700 directory and removed (rm -P) as soon as the pass has run. Nothing is pushed; nothing in --source changes.
#
# Needs git-filter-repo 2.38 or later: `git filter-repo` on PATH, or IGNEUM_FILTER_REPO=<path to git_filter_repo.py>
# (pip: python3 -m pip install --target <dir> git-filter-repo). TZ is forced to UTC for everything this script runs.
set -euo pipefail
export TZ=UTC
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
FOUNDER_LIST="${IGNEUM_FOUNDER_STRINGS:-$HOME/.config/igneum/founder-strings}" # the PRIVATE list (perl regex, tab, sample per row); never a tracked file in any encoding
[ -s "$FOUNDER_LIST" ] || { echo "fresh-repo: no private founder list at $FOUNDER_LIST (the Mac holds it; the rewrite needs its rows)" >&2; exit 1; }
STANDING_LOGIN="${IGNEUM_STANDING_LOGIN:-$(grep -vE '^#' "$FOUNDER_LIST" | sed -n '10p' | cut -f2)}" # row 10: the login's pre-rename spelling
ORG="igneum-network"
SOURCE="" WORK="" NEW_REPO="$ORG/igneum-core" NEW_LOGIN="" PUBLIC_CLAUDE="" CLEAN=0
while [ $# -gt 0 ]; do
case "$1" in
--source) SOURCE="$2"; shift 2 ;;
--work) WORK="$2"; shift 2 ;;
--new-repo) NEW_REPO="$2"; shift 2 ;;
--new-login) NEW_LOGIN="$2"; shift 2 ;;
--public-claude-md) PUBLIC_CLAUDE="$2"; shift 2 ;;
--clean) CLEAN=1; shift ;;
-h|--help) sed -n '2,24p' "$0"; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
[ -n "$SOURCE" ] || SOURCE="$(git -C "$ROOT" remote get-url origin)"
[ -n "$WORK" ] || WORK="$(mktemp -d "${TMPDIR:-/tmp}/igneum-fresh-repo.XXXXXX")"
case "$WORK" in /*) ;; *) WORK="$PWD/$WORK" ;; esac
mkdir -p "$WORK"
CLONE="$WORK/clone"
[ ! -e "$CLONE" ] || { echo "$CLONE exists; the pass runs on a fresh clone only (remove it or give another --work)" >&2; exit 1; }
if [ -n "$PUBLIC_CLAUDE" ]; then [ -f "$PUBLIC_CLAUDE" ] || { echo "no $PUBLIC_CLAUDE" >&2; exit 1; }; PUBLIC_CLAUDE="$(cd "$(dirname "$PUBLIC_CLAUDE")" && pwd)/$(basename "$PUBLIC_CLAUDE")"; fi
case "$NEW_LOGIN" in *[!A-Za-z0-9-]*) echo "--new-login must be a GitHub login (letters, digits, hyphens)" >&2; exit 2 ;; esac
[ "$NEW_LOGIN" != "$STANDING_LOGIN" ] || NEW_LOGIN=""
# the filter
if [ -n "${IGNEUM_FILTER_REPO:-}" ]; then FILTER=(python3 "$IGNEUM_FILTER_REPO")
elif git filter-repo --version >/dev/null 2>&1; then FILTER=(git filter-repo)
elif python3 -c 'import git_filter_repo' 2>/dev/null; then FILTER=(python3 -m git_filter_repo)
else echo "git-filter-repo is not installed: python3 -m pip install --target <dir> git-filter-repo, then IGNEUM_FILTER_REPO=<dir>/git_filter_repo.py" >&2; exit 1; fi
command -v perl >/dev/null || { echo "perl is needed for the greps" >&2; exit 1; }
say() { printf '%s\n' "$*" | tee -a "$WORK/report.txt"; }
: > "$WORK/report.txt"
say "fresh-repo: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
say "source: $SOURCE"
say "work: $WORK"
say "filter: ${FILTER[*]} ($("${FILTER[@]}" --version 2>/dev/null | head -1 || echo '?'))"
# ---- 1. the fresh mirror clone ------------------------------------------------------------------------------------
git clone --quiet --mirror --no-hardlinks "$SOURCE" "$CLONE"
cd "$CLONE"
# ---- 2. the values, read at run time, never printed ------------------------------------------------------------------
umask 077
RULES="$WORK/rules"; mkdir -p "$RULES"; chmod 700 "$RULES"
cleanup_rules() { if [ -d "$RULES" ]; then for f in "$RULES"/*; do [ -f "$f" ] && { rm -P "$f" 2>/dev/null || rm -f "$f"; }; done; rmdir "$RULES" 2>/dev/null || true; fi; }
trap cleanup_rules EXIT
# the standing login's noreply address, from the history
STANDING_EMAIL="$(git log --all --format='%ae%n%ce' | grep -E "^[0-9]+\+$STANDING_LOGIN@users\.noreply\.github\.com$" | sort -u | head -1 || true)"
[ -n "$STANDING_EMAIL" ] || { echo "the history carries no commit by $STANDING_LOGIN (set IGNEUM_STANDING_LOGIN)" >&2; exit 1; }
STANDING_ID="${STANDING_EMAIL%%+*}"
if [ -n "$NEW_LOGIN" ]; then TARGET_LOGIN="$NEW_LOGIN"; else TARGET_LOGIN="$STANDING_LOGIN"; fi
TARGET_EMAIL="$STANDING_ID+$TARGET_LOGIN@users.noreply.github.com"
TARGET_IDENT="$TARGET_LOGIN <$TARGET_EMAIL>"
# every other identity: "name|email" pairs (author and committer)
PERSONAL_PAIRS="$(git log --all --format='%an|%ae%n%cn|%ce' | grep -v -F -e "|$STANDING_EMAIL" -e "|$TARGET_EMAIL" | sort -u || true)" # fixed strings: the addresses carry a +, a quantifier under -E (dry run 3, 7 Oct 2026: the target's address was read as personal and matched every rewritten line) # never the standing or the target address (7 Oct 2026: the target's own commits read as personal and every rewritten author line then counted as a hit)
PERSONAL_EMAILS="$(printf '%s\n' "$PERSONAL_PAIRS" | awk -F'|' 'NF==2{print $2}' | sort -u)"
PERSONAL_NAMES="$(printf '%s\n' "$PERSONAL_PAIRS" | awk -F'|' 'NF==2 && $1 ~ / /{print $1}' | grep -v -E "^($STANDING_LOGIN|$TARGET_LOGIN)$" | sort -u || true)" # a name has a space; a login is not a name
# the founder's first name, surname and second login from the encoded list (rows 1 to 3, sample column), so the rules never depend on
# which commits a given mirror carries (7 Oct 2026: build-1's mirror holds none of the 40 personal-identity commits)
LIST_ROWS="$(grep -vE '^#' "$FOUNDER_LIST")"
LIST_FIRST="$(printf '%s\n' "$LIST_ROWS" | sed -n '1p' | cut -f2 | awk '{print $1}')"; LIST_LAST="$(printf '%s\n' "$LIST_ROWS" | sed -n '2p' | cut -f2)"; LIST_SECOND="$(printf '%s\n' "$LIST_ROWS" | sed -n '3p' | cut -f2)"
FIRST_NAMES="$( { printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $1}'; printf '%s\n' "$LIST_FIRST"; } | grep . | sort -u)"
LAST_NAMES="$( { printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $NF}'; printf '%s\n' "$LIST_LAST"; } | grep . | sort -u)"
SECOND_LOGINS="$( { printf '%s\n' "$PERSONAL_EMAILS" | sed -nE 's/^[0-9]+\+([A-Za-z0-9-]+)@users\.noreply\.github\.com$/\1/p'; printf '%s\n' "$LIST_SECOND"; } | grep . | grep -v -E "^($STANDING_LOGIN|$TARGET_LOGIN)$" | sort -u || true)"
# the other businesses named in the plan (brand names, not people), from the encoded list tools/ci/founder-strings.b64 (rows 4 to 8;
# no tracked file spells them: the founder-strings check reads every tracked file)
OTHER_BUSINESSES="$(grep -vE '^#' "$FOUNDER_LIST" | sed -n '4,8p' | cut -f1 | sed -E 's/\\b//g' | paste -sd'|' -)"
[ -n "$OTHER_BUSINESSES" ] || { echo "fresh-repo: the encoded founder list decoded to no business names" >&2; exit 1; }
# the secrets: whichever of the four files exist, plus every dated copy the rotation left behind
# (log-intake-key.old-<date>, dl-token.old-<date>: rotation phase 2 section 5 step 2 renames the .next files to the
# plain ones and keeps the old values dated, and those old values are the ones the history carries)
SECRET_FILES=(); for n in log-intake-key log-intake-key.next dl-token dl-token.next; do [ -f "$HOME/.config/igneum/$n" ] && SECRET_FILES+=("$HOME/.config/igneum/$n"); done
for f in "$HOME/.config/igneum"/log-intake-key.old-* "$HOME/.config/igneum"/dl-token.old-*; do [ -f "$f" ] && SECRET_FILES+=("$f"); done
say "standing login: $STANDING_LOGIN (noreply id $STANDING_ID)${NEW_LOGIN:+ -> $NEW_LOGIN}"
say "personal identities in the history: $(printf '%s\n' "$PERSONAL_PAIRS" | grep -c . || true) (names $(printf '%s\n' "$PERSONAL_NAMES" | grep -c . || true), addresses $(printf '%s\n' "$PERSONAL_EMAILS" | grep -c . || true), second owner logins $(printf '%s\n' "$SECOND_LOGINS" | grep -c . || true))"
say "secret files for the rules: ${#SECRET_FILES[@]} (the four names plus dated .old-* copies; 4 are needed once the rotation has renamed: 2 current, 2 old)"
# the rule files
REPLACE="$RULES/replace.txt"; MAILMAP="$RULES/mailmap"; IDENT="$RULES/identity.pl"; SECRETS="$RULES/secrets.pl"
: > "$REPLACE"; : > "$MAILMAP"; : > "$IDENT"; : > "$SECRETS"
for f in ${SECRET_FILES[@]+"${SECRET_FILES[@]}"}; do
v="$(tr -d '[:space:]' < "$f")"; [ ${#v} -ge 8 ] || continue
case "$(basename "$f")" in log-intake-key*) tag='***INTAKE-KEY-REMOVED***' ;; *) tag='***DL-TOKEN-REMOVED***' ;; esac
printf 'literal:%s==>%s\n' "$v" "$tag" >> "$REPLACE"
printf '%s\n' "$v" | sed 's/[][\\.*^$?+(){}|/]/\\&/g' >> "$SECRETS" # a regex for the scanner: metacharacters escaped (never \Q, which qr// does not expand from a variable)
done
while IFS='|' read -r name email; do
[ -n "$email" ] || continue
printf 'literal:%s <%s>==>%s\n' "$name" "$email" "$TARGET_IDENT" >> "$REPLACE"
printf '%s <%s> <%s>\n' "$TARGET_LOGIN" "$TARGET_EMAIL" "$email" >> "$MAILMAP"
done <<< "$PERSONAL_PAIRS"
while IFS= read -r email; do
[ -n "$email" ] || continue
printf 'literal:%s==>[removed]\n' "$email" >> "$REPLACE"
printf '%s\n' "$email" | sed 's/[][\\.*^$?+(){}|/]/\\&/g' >> "$IDENT"
done <<< "$PERSONAL_EMAILS"
if [ -n "$NEW_LOGIN" ]; then
printf 'literal:%s==>%s\n' "$STANDING_EMAIL" "$TARGET_EMAIL" >> "$REPLACE"
printf '%s <%s> <%s>\n' "$TARGET_LOGIN" "$TARGET_EMAIL" "$STANDING_EMAIL" >> "$MAILMAP"
printf 'regex:\\b%s\\b==>%s\n' "$STANDING_LOGIN" "$NEW_LOGIN" >> "$REPLACE"
printf '\\b%s\\b\n' "$STANDING_LOGIN" >> "$IDENT"
fi
while IFS= read -r first; do
[ -n "$first" ] || continue
printf 'regex:\\b%s%ss\\b==>the project lead%ss\n' "$first" "'" "'" >> "$REPLACE"
while IFS= read -r last; do [ -n "$last" ] && printf 'regex:(?i)\\b%s\\s+%s\\b==>the project lead\n' "$first" "$last" >> "$REPLACE"; done <<< "$LAST_NAMES"
printf 'regex:\\b%s\\b==>the project lead\n' "$first" >> "$REPLACE"
done <<< "$FIRST_NAMES"
while IFS= read -r last; do
[ -n "$last" ] || continue
printf 'regex:(?i)\\b%s\\b==>[removed]\n' "$last" >> "$REPLACE"
printf '\\b%s\\b\n' "$last" >> "$IDENT"
done <<< "$LAST_NAMES"
while IFS= read -r first; do
[ -n "$first" ] || continue
# the lower-case user-name form (Windows and WSL paths, the browser profile), never the standing login's suffix
printf 'regex:(?i)(?<!%s-)\\b%s\\b==>[user]\n' "${STANDING_LOGIN%%-*}" "$first" >> "$REPLACE"
printf '(?<!%s-)\\b%s\\b\n' "${STANDING_LOGIN%%-*}" "$first" >> "$IDENT"
done <<< "$FIRST_NAMES"
while IFS= read -r login; do
[ -n "$login" ] || continue
printf 'regex:(?i)\\b%s\\b==>[second-owner-login]\n' "$login" >> "$REPLACE"
printf '\\b%s\\b\n' "$login" >> "$IDENT"
done <<< "$SECOND_LOGINS"
printf 'regex:(?i)\\b(%s)\\b==>[other-business]\n' "$OTHER_BUSINESSES" >> "$REPLACE"
# the encoded forms: the base64 of every list regex (site/forbidden-strings.txt carried them as b64: lines until 21:3x UK on 7 October 2026)
while IFS= read -r re; do
[ -n "$re" ] || continue; b="$(printf '%s' "$re" | base64 | tr -d '\n')"
printf 'literal:%s==>[encoded-pattern-removed]\n' "$b" >> "$REPLACE"
printf '%s\n' "$b" | sed 's/[][\\.*^$?+(){}|/]/\\&/g' >> "$IDENT"
done < <(printf '%s\n' "$LIST_ROWS" | cut -f1)
printf '\\b(%s)\\b\n' "$OTHER_BUSINESSES" >> "$IDENT"
say "rules: $(grep -c . "$REPLACE") replace lines, $(grep -c . "$MAILMAP") mailmap lines, $(grep -c . "$IDENT") identity patterns, $(grep -c . "$SECRETS") secret patterns (files under $RULES, removed at exit)"
# ---- 3. the counts, before and after ---------------------------------------------------------------------------------
# every blob in the object store (reachable or not: before the pass the mirror holds everything, after it filter-repo's gc
# has pruned), one count of matching lines over a pattern file (perl regexes, case-insensitive)
scan_blobs() {
git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype)' --unordered 2>/dev/null | awk '$2 == "blob" { print $1 }' \
| git cat-file --batch 2>/dev/null \
| perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1"
}
scan_meta() { git log --all --format='%an%n%ae%n%cn%n%ce%n%s%n%b' | perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1"; }
DROPPED=(docs/review tools/ci/founder-strings.b64) # the encoded founder list (19:5x to 21:3x UK, 7 October 2026) leaves every commit # the ledger, its fixes file and the ledger page are published with the repository (decision of 5 October 2026, docs/fud-fixes.md section 5); the review folder stays internal
counts() { # <label>
local label="$1"
say ""
say "[$label]"
say " commits (all refs): $(git rev-list --all --count)"
say " refs: $(git for-each-ref | wc -l | tr -d ' ')"
say " author+committer identities: $(git log --all --format='%an <%ae>%n%cn <%ce>' | sort -u | wc -l | tr -d ' ')"
say " stamps not +0000 (of $(git log --all --format='%ad%n%cd' --date=raw | wc -l | tr -d ' ')): $(git log --all --format='%ad%n%cd' --date=raw | grep -vc ' +0000$' || true)"
say " commits touching the dropped files: $(git log --all --format=%H -- "${DROPPED[@]}" | sort -u | wc -l | tr -d ' ')"
say " secret lines in any blob: $(scan_blobs "$SECRETS")"
say " identity lines in any blob: $(scan_blobs "$IDENT")"
say " identity lines in commit metadata: $(scan_meta "$IDENT")"
say " standing login lines in any blob: $(printf '\\b%s\\b\n' "$STANDING_LOGIN" > "$RULES/login.pl"; scan_blobs "$RULES/login.pl")${NEW_LOGIN:+ (must be 0 with --new-login)}"
}
counts "before"
# ---- 4. the pass --------------------------------------------------------------------------------------------------
say ""
say "running: ${FILTER[*]} --force --invert-paths ${DROPPED[*]/#/--path } --replace-text <rules> --replace-message <rules> --mailmap <rules> --commit-callback <offsets to +0000>${PUBLIC_CLAUDE:+ --file-info-callback <CLAUDE.md from $PUBLIC_CLAUDE>}"
PATH_ARGS=(); for p in "${DROPPED[@]}"; do PATH_ARGS+=(--path "$p"); done
CALLBACK_ARGS=()
if [ -n "$PUBLIC_CLAUDE" ]; then
cat > "$RULES/file-info.py" <<PY
if filename == b'CLAUDE.md':
if 'claude' not in value.data:
value.data['claude'] = value.insert_file_with_contents(open('$PUBLIC_CLAUDE', 'rb').read())
return (filename, mode, value.data['claude'])
return (filename, mode, blob_id)
PY
CALLBACK_ARGS+=(--file-info-callback "$RULES/file-info.py")
fi
T0=$(date +%s)
# pass 1: the text, the messages, the identities and the dates. Pass 2 (below): the public CLAUDE.md in every commit. Two passes
# because filter-repo does not run --replace-text over blobs when a --file-info-callback is present (7 October 2026, 20:3x UK:
# two dry runs rewrote identities and dates and not one line of text).
"${FILTER[@]}" --force --quiet \
--invert-paths "${PATH_ARGS[@]}" \
--replace-text "$REPLACE" \
--replace-message "$REPLACE" \
--mailmap "$MAILMAP" \
--commit-callback '
for attr in ("author_date", "committer_date"):
d = getattr(commit, attr); parts = d.split(b" ")
if len(parts) == 2 and parts[1] != b"+0000":
setattr(commit, attr, parts[0] + b" +0000")
'
say "pass 1 (text, messages, identities, dates) done in $(( $(date +%s) - T0 )) s"
if [ ${#CALLBACK_ARGS[@]} -gt 0 ]; then
T1=$(date +%s); "${FILTER[@]}" --force --quiet "${CALLBACK_ARGS[@]}"; say "pass 2 (the public CLAUDE.md in every commit) done in $(( $(date +%s) - T1 )) s"
fi
[ -f .git/filter-repo/commit-map ] && cp .git/filter-repo/commit-map "$WORK/commit-map" && say "commit-map: $WORK/commit-map ($(grep -c . "$WORK/commit-map") lines; keep it with the private notes)"
[ -f filter-repo/commit-map ] && cp filter-repo/commit-map "$WORK/commit-map" && say "commit-map: $WORK/commit-map ($(grep -c . "$WORK/commit-map") lines; keep it with the private notes)"
# ---- 5. the verification: every count that must read zero ------------------------------------------------------------
counts "after"
FAIL=0
must_zero() { local what="$1" n="$2"; if [ "$n" != "0" ]; then say " FAIL $what: $n (must be 0)"; FAIL=1; else say " ok $what: 0"; fi; }
say ""
say "[verdict]"
must_zero "secret lines in any blob" "$(scan_blobs "$SECRETS")"
must_zero "identity lines in any blob" "$(scan_blobs "$IDENT")"
must_zero "identity lines in commit metadata" "$(scan_meta "$IDENT")"
must_zero "stamps not +0000" "$(git log --all --format='%ad%n%cd' --date=raw | grep -vc ' +0000$' || true)"
must_zero "commits touching the dropped files" "$(git log --all --format=%H -- "${DROPPED[@]}" | sort -u | wc -l | tr -d ' ')"
must_zero "identities other than $TARGET_IDENT" "$(git log --all --format='%an <%ae>%n%cn <%ce>' | sort -u | grep -vcF "$TARGET_IDENT" || true)"
[ -n "$NEW_LOGIN" ] && must_zero "old login $STANDING_LOGIN in any blob" "$(scan_blobs "$RULES/login.pl")"
if [ -n "$PUBLIC_CLAUDE" ]; then
for ref in $(git for-each-ref --format='%(refname)' refs/heads | head -3); do
if git cat-file -p "$ref:CLAUDE.md" 2>/dev/null | cmp -s - "$PUBLIC_CLAUDE"; then say " ok CLAUDE.md on $ref is the public text"; else say " FAIL CLAUDE.md on $ref is not the public text"; FAIL=1; fi
done
fi
cleanup_rules; trap - EXIT
if [ "$FAIL" = 1 ]; then say ""; say "NOT CLEAN: fix the rules and run again on a fresh clone (nothing was pushed)"; [ "$CLEAN" = 1 ] && rm -rf "$WORK"; exit 1; fi
# ---- 6. the commands that create the fresh repository and push (printed, never run) ---------------------------------
say ""
say "clean. The push, when the owner says so (option B of docs/plans/history-rewrite.md section 5; every line by hand):"
say ""
say " # 1. freeze: every agent has committed and pushed; gh pr list --repo $ORG/igneum is empty; git worktree list recorded"
say " gh auth switch --user $TARGET_LOGIN && gh auth status"
say " # 2. the fresh repository (private; the name is the owner's; igneum-core is the suggestion)"
say " gh repo create $NEW_REPO --private --description 'Igneum: the GPU-mined zkEVM L1' --disable-wiki"
say " # 3. push every rewritten ref from the clone (the pass removed its origin remote on purpose)"
say " cd $CLONE"
say " git remote add origin https://github.com/$NEW_REPO.git"
say " git push --mirror origin"
say " # 4. after the push, on GitHub: default branch master; Settings > Secrets: DL_TOKEN, DL_TOKEN_NEXT, LOG_INTAKE_KEY,"
say " # LOG_INTAKE_KEY_NEXT (tr -d '[:space:]' < ~/.config/igneum/<file> | gh secret set <NAME> --repo $NEW_REPO);"
say " # Vercel project igneum (team igneum): Git > disconnect $ORG/igneum, connect $NEW_REPO, production branch master;"
say " # archive $ORG/igneum (Settings > Archive), keep it private; never delete it the same day"
say " # 5. re-clone the main checkout from the new history and re-create every worktree from its rewritten branch:"
say " cd ~/Projects && mv igneum igneum-old-history && git clone https://github.com/$NEW_REPO.git igneum"
say " # for each worktree: git -C ~/Projects/igneum worktree add ../igneum-wt-<name> <branch>; vendor/ is copied back by hand (gitignored)"
say " # 6. TZ=UTC in every shell that commits; tools/ci/identity-check.sh and the +0100 count stay the daily check"
[ "$CLEAN" = 1 ] && { cd /; rm -rf "$WORK"; say "work directory removed (--clean)"; } || say "report: $WORK/report.txt; clone kept at $CLONE"
exit 0