igneum/tools/repo/fresh-repo.sh
igneum-labs 09c2634d2c Pre-public scrub, second pass (7 October 2026, 20:0x UK, main's rulings 2 and 4): the public tree names igneum-labs only; the public ledger generated from the full ledger
Ruling 2: every tracked text mention of the login's pre-rename spelling becomes igneum-labs (29 files); the commit identity in the scripts becomes igneum-labs <337424239+igneum-labs@users.noreply.github.com> (the same noreply id); the three scripts that hand the stored gh keyring name to gh (register.sh, fetch-ci-artifacts.sh, ship-app.mjs) read it from ~/.config/igneum/gh-user (never in the repository), default igneum-labs; fresh-repo.sh reads the login to rewrite from the encoded list's row 10; the old spelling joins the founder-strings list, so no tracked file may spell it; CLAUDE.md's GitHub paragraph rewritten. The red watcher's self-test fixtures use a neutral author.

Ruling 4: docs/ledger-public.md, one row per ledger item (id, the claim in one line, status, what was done in one line, the evidence link or the evidence in words), generated by tools/ledger/export-public.mjs from docs/fud-ledger.md: no round or status-update sections, no commit ids, no time of day, no lane, agent or branch names. 190 items. The gate runs its self-test (a fixture with a commit id, a time, a lane and a branch name comes out clean; --check fails on drift) and its --check on every merge; the founder check reads the output like every tracked file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 18:56:08 +00:00

258 lines
18 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)"
STANDING_LOGIN="${IGNEUM_STANDING_LOGIN:-$(base64 -d < "$(cd "$(dirname "$0")/../.." && pwd)/tools/ci/founder-strings.b64" | grep -vE '^#' | sed -n '10p' | cut -f2)}" # the login's pre-rename spelling, from the encoded list (no tracked file spells it)
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 "|$STANDING_EMAIL$" | sort -u || true)"
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{print $1}' | grep -v "^$STANDING_LOGIN$" | sort -u || true)"
FIRST_NAMES="$(printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=1{print $1}' | sort -u)"
LAST_NAMES="$(printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $NF}' | sort -u)"
SECOND_LOGINS="$(printf '%s\n' "$PERSONAL_EMAILS" | sed -nE 's/^[0-9]+\+([A-Za-z0-9-]+)@users\.noreply\.github\.com$/\1/p' | grep -v "^$STANDING_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="$(base64 -d < "$ROOT/tools/ci/founder-strings.b64" | grep -vE '^#' | 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:\\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:\\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"
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/fud-ledger.md docs/fud-fixes.md docs/review site/ledger.html)
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)
"${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")
' ${CALLBACK_ARGS[@]+"${CALLBACK_ARGS[@]}"}
say "pass done in $(( $(date +%s) - T0 )) s"
[ -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