gabriel / musehub public
Open #127 Enhancement
filed by gabriel human · 57 days ago

Git-bridge backup mirror for muse and musehub → GitHub (from copies, non-destructive)

0 Anchors
Blast radius
Churn 30d
0 Proposals

Git-Bridge Backup Mirror for muse and musehub → GitHub

Status

Phase 1 (1a/1b/1c/1d) — 100% complete and green for both repos (2026-07-05). Every sub-step run for real against the actual copies, not a throwaway fixture:

Step muse musehub
1a — .muse/ directory present muse copy: OK musehub copy: OK
1b — not a symlink into ~/ecosystem ✅ real directory ✅ real directory
1c — muse_backup_guard defined & verified ✅ loaded, printed ✅ guard passed — safe to continue ✅ same guard covers both (single shell function)
1d — commit count baseline ✅ 1,452 ✅ 1,444

MIRROR_01 is satisfied in full. Nothing in Phase 1 is outstanding for either repo.

muse-git-backup.sh is the canonical way to run every phase from here on — committed at muse/scripts/muse-git-backup.sh (branch task/git-backup-mirror-script, latest commit sha256:6d6c3875de02a5872eba4aa05bef23382d568db787244c9f16eb6f26b6ebbcbd), not a scratch file. Each phase below now leads with the script command to run; the old raw MUSE_COPY=...-and-loop instructions are kept in collapsed <details> blocks purely as a record of what the script automates, not as something to paste into a shell anymore.

Subcommands: preflight, replay-muse [--skip N] [--limit N], replay-musehub [--skip N] [--limit N], replay-all [--skip N] [--limit N] (both repos concurrently), github-push, verify, incremental-muse, incremental-musehub. Replay is resumable by default — it counts commits already on the target mirror branch and auto-skips that many, so a --limit 20 dry run followed by a plain replay-muse continues from commit 21 rather than duplicating 1-20; pass --skip N explicitly to override. All of this — auto-resume, explicit --skip+--limit, the no-op "nothing to export" case, and replay-all's parallel execution — was verified end-to-end against disposable fixtures before being committed.

Why a script at all: the original raw command blocks used muse_backup_guard "..." || exit 1 — in an interactive shell, a failed guard check would close gabriel's entire terminal tab, not just abort the command. This happened twice, live, during this ticket (once before the guard was even defined, once during Phase 2 setup after an initial echo-based fix). A real script closes this class of bug structurally: any internal exit only ever ends the script's own subprocess, never the parent shell — no amount of careful hand-editing of paste-blocks achieves that.

Next: Phase 2 — run ./scripts/muse-git-backup.sh replay-muse for real against the actual copy, for the first time. Nothing past Phase 1 has touched the real copies yet; everything above was proven against disposable /tmp fixtures only.

Non-Destructiveness Verification (2026-07-05)

The entire reason gabriel pulled ~/dev/copies 2/muse 03 and ~/dev/copies 2/musehub 03 off the official repos was to build this backup path without risking any destructive action against ~/ecosystem/muse/~/ecosystem/musehub. This section is a real audit of that guarantee, not a restatement of intent — every write operation in the script was traced line by line before this was written.

Every write the script performs, enumerated:

Operation Writes to Ecosystem repos affected?
git init/commit --allow-empty (Phase 2/3 setup) brand-new *-git-backup dir No — new directory, not pre-existing
muse bridge git-export --no-push (a) the new git-backup dir's commits, (b) <copy>/.muse/git-bridge.toml — the copy's own bridge state file No — this is the copy's state, a completely separate file from ~/ecosystem/muse/.muse/git-bridge.toml
gh repo create (Phase 4) new GitHub repos (gabriel/muse-backup, gabriel/musehub-backup) No — repos that don't exist yet
git push (Phase 4, Phase 6) those new GitHub repos No
git clone (Phase 5 verify) /tmp/verify-*-backup scratch dirs No — read-only against the copy, writes only to /tmp
Everything else (rev-list, diff, readlink, test -d) nothing No — read-only

The only non-obvious one is git-export updating <copy>/.muse/git-bridge.toml — that's a real write, but it's to the copy's own state, isolated from ~/ecosystem/muse by definition (separate directory, separate .muse/), and it's required for Phase 6's incremental logic to work at all.

A real gap was found and fixed during this audit, not just confirmed clean. Every function was checked for whether it calls guard() (the function that hard-refuses any path containing the literal string "ecosystem") before touching a copy path. _incremental() — Phase 6 — did not call guard() at all; it was the only function with zero runtime path-safety check. Fixed by adding guard() calls to cmd_preflight, _replay_full_history, _incremental, and cmd_verify — every function that reads or writes a copy path now guards it first, not just the functions that write to the git-backup destination.

This was tested live, not just reasoned about. Temporarily pointed MUSE_COPY at a path containing "ecosystem" in a throwaway copy of the script and confirmed:

$ ./test-guard.sh preflight
❌ REFUSED: path contains 'ecosystem': /tmp/.../ecosystem-fake-copy
exit code: 1

$ ./test-guard.sh replay-muse
❌ REFUSED: path contains 'ecosystem': /tmp/.../ecosystem-fake-copy
exit code: 1

$ ./test-guard.sh incremental-muse   # the gap that was just fixed
❌ REFUSED: path contains 'ecosystem': /tmp/.../ecosystem-fake-copy
exit code: 1

Then confirmed normal operation against legitimate (non-ecosystem) throwaway paths still works unchanged after adding the extra guard calls — preflight and replay-all both passed with correct commit counts.

Honest statement of the residual trust model — two layers, not one:

  1. The six hardcoded path variables (MUSE_COPY, MUSEHUB_COPY, MUSE_GIT_BACKUP, MUSEHUB_GIT_BACKUP, GH_MUSE_REPO, GH_MUSEHUB_REPO) correctly point at the copies and new targets today, verified by reading the script — this is the primary guarantee.
  2. guard(), now called by every function that touches a copy path, is the defense-in-depth layer: even if a future edit to this script accidentally pointed a variable at something under ~/ecosystem, the script would refuse with exit 1 rather than silently proceeding. This was proven to actually fire, not assumed.

Committed as sha256:7387e0e1272834aa5d35562c8c0f1a65cfcecbf5dfc9ceef0c6297cc72e83734 on branch task/git-backup-mirror-script.

Script Reference — read this before running anything

Anyone (human or agent) picking up this ticket cold needs to know two things that are easy to get wrong from the phase instructions alone:

1. cd ~/ecosystem/muse does NOT mean the script touches ~/ecosystem/muse. That cd only puts you where the script file lives on disk (muse/scripts/muse-git-backup.sh, committed there because it dogfoods the muse git-bridge feature) so that ./scripts/muse-git-backup.sh resolves as a relative path. The script's actual targets are hardcoded absolute paths inside the script itself, unrelated to your current directory:

MUSE_COPY="/Users/gabriel/dev/copies 2/muse 03"              # read from
MUSEHUB_COPY="/Users/gabriel/dev/copies 2/musehub 03"        # read from
MUSE_GIT_BACKUP="/Users/gabriel/dev/copies 2/muse-git-backup"        # written to (new git repo)
MUSEHUB_GIT_BACKUP="/Users/gabriel/dev/copies 2/musehub-git-backup" # written to (new git repo)
GH_MUSE_REPO="gabriel/muse-backup"                            # GitHub repo created/pushed to
GH_MUSEHUB_REPO="gabriel/musehub-backup"                      # GitHub repo created/pushed to

~/ecosystem/muse and ~/ecosystem/musehub never appear in that list. The script also runs a guard() check that hard-refuses any operation whose target path contains the literal string "ecosystem" — so even a future edit to this script that accidentally pointed a variable at ~/ecosystem/... would abort immediately rather than silently touching the real repos.

2. Full usage, verbatim from the script's own header comment (the authoritative source — if this ever drifts from the actual script, the script wins):

Usage:
  ./muse-git-backup.sh preflight
  ./muse-git-backup.sh replay-muse    [--skip N] [--limit N]
  ./muse-git-backup.sh replay-musehub [--skip N] [--limit N]
  ./muse-git-backup.sh replay-all     [--skip N] [--limit N]   # muse + musehub in parallel
  ./muse-git-backup.sh github-push
  ./muse-git-backup.sh verify
  ./muse-git-backup.sh incremental-muse
  ./muse-git-backup.sh incremental-musehub

replay-* is resumable by default: it inspects how many commits are already
on the target mirror branch and skips that many automatically, so running
`replay-muse --limit 20` as a dry run and then `replay-muse` again to do
the rest continues where the dry run left off instead of duplicating it.
Pass --skip explicitly to override the auto-detected count.

Each subcommand is independent and safe to re-run. This script never
touches ~/ecosystem/muse or ~/ecosystem/musehub — every operation targets
the copies under /Users/gabriel/dev/copies 2/ and brand-new directories/
GitHub repos this script creates fresh. Run as ./muse-git-backup.sh <cmd>,
never sourced into your interactive shell — that's what makes `exit`
inside this script safe (it only ends the script's own process).

3. Which subcommand maps to which phase in this ticket:

Ticket phase Subcommand What it does Touches GitHub?
Phase 1 (preflight) preflight Re-runs the four 1a-1d checks No
Phase 2 replay-muse [--skip N] [--limit N] Full-history replay, muse copy only No (--no-push internally)
Phase 3 replay-musehub [--skip N] [--limit N] Full-history replay, musehub copy only No
Phase 2+3 combined replay-all [--skip N] [--limit N] Runs both of the above concurrently No
Phase 4 github-push Creates both GitHub repos (if missing) and pushes both mirrors Yes — creates repos + pushes
Phase 5 verify Clones both GitHub repos fresh to /tmp and diffs against the copies Read-only (clone)
Phase 6 incremental-muse Exports only new muse commits since the last bridge run, then pushes Yes — pushes
Phase 6 incremental-musehub Same, for musehub Yes — pushes

4. --skip / --limit in plain terms:

  • Neither flag is required for a normal full run — just run replay-muse with no flags and it exports everything not already on the mirror branch.
  • --limit N caps how many commits get exported in this invocation — useful as a quick, low-risk dry run (e.g. --limit 20) before committing to the full ~25-75 minute replay.
  • --skip N overrides the auto-detected "how many are already exported" count. Normally you never need this — the script figures it out itself by counting commits already on the mirror branch. Only pass it if you deliberately want to re-export a specific range.
  • Safe combination example: replay-muse --limit 20 (dry run) followed later by plain replay-muse (no flags) — the second call auto-resumes at commit 21 and does not duplicate the first 20.

Explicit, scoped exception to the workspace's "no git" policy

The workspace's standing rule is no git, no GitHub, ever — this ticket is a deliberate, gabriel-authorized, narrowly-scoped exception to that rule, for this backup workflow only. git/gh/GitHub are used here exclusively against:

  • Two copies already pulled off the official repos: /Users/gabriel/dev/copies 2/muse 03 and /Users/gabriel/dev/copies 2/musehub 03
  • Two brand-new git working trees created fresh for this purpose (never an existing directory)
  • Two brand-new GitHub repos created fresh for this purpose

Every command in this ticket has been tested end-to-end on a disposable throwaway repo before being written down here — nothing below is speculative. The full multi-commit replay mechanism, the incremental range-bounded replay, and the ownership-tracking delete-protection added in #65 were all exercised directly during the design of this plan.

git/gh are not authorized for any other task in this workspace. Nothing in this ticket touches ~/ecosystem/muse or ~/ecosystem/musehub.

Background

Every commit in ~/ecosystem/muse/~/ecosystem/musehub lives only on this Mac and on the MuseHub staging server. If both were lost simultaneously (a real risk for a single-machine dev setup with a staging server gabriel also controls), the full commit history — provenance, signatures, structured deltas, everything — would be gone. gabriel already pulled non-destructive copies to ~/dev/copies 2/ as a manual stopgap, but a copy on the same disk is not a real backup against hardware failure.

This ticket builds a second, independent backup path: bridge each copy's full Muse commit history into a plain git repository (one git commit per Muse commit, oldest to newest — a real historical mirror, not just a latest-state snapshot), then push that git repo to GitHub. This is deliberately dual-purpose: it's a genuine disaster-recovery backup in a completely different protocol/storage system, and it's real dogfooding of the git↔muse bridge at a scale (1,452 commits for muse, 1,444 for musehub, as of this writing) far beyond what the bridge's own test suite exercises.

Goal

  • A git repository exists (initially local, then pushed to GitHub) containing a faithful, commit-by-commit mirror of muse's full history, built entirely from the /Users/gabriel/dev/copies 2/muse 03 copy — never from ~/ecosystem/muse.
  • The same for musehub, from /Users/gabriel/dev/copies 2/musehub 03.
  • A repeatable, fast incremental procedure exists for "every so often" follow-up runs — each one exports only the commits made since the last backup run, not a full re-replay from scratch.
  • Every command is copy-pasteable and runnable manually by gabriel, with an enforced safety guard (not just a documentation promise) that aborts before touching anything under ~/ecosystem.
  • The bridge's real-world behavior on very large histories (1,400+ commits) is observed directly, surfacing any performance or correctness issue the existing test suite's small fixtures wouldn't catch.

"Done" means: both GitHub repos exist, both contain the full historical mirror, gabriel has run the incremental procedure at least once successfully on top of the initial full backup, and ~/ecosystem/muse/~/ecosystem/musehub are verified byte-for-byte unchanged throughout.

Non-Goals / Out of Scope

  • Automating this on a schedule (a cron job, a CI pipeline). This ticket is a manual runbook gabriel runs by hand "every so often," per the request. Automating it later is a reasonable follow-up, not designed here.
  • Restoring from the backup. This ticket only builds the backup path forward (Muse → git → GitHub); a tested restore procedure (GitHub → git → Muse, via git-import) is valuable but explicitly separate scope — flagged as an open question, not designed now.
  • Changing anything about how ~/ecosystem/muse/~/ecosystem/musehub are used day to day. This is a side-channel backup mechanism, not a workflow change.
  • Deleting or modifying the existing ~/dev/copies 2/ copies. They remain exactly as gabriel left them; this ticket only reads from them.

Phases

Phase 1 — Preflight: verify the copies and enforce the safety guard

Confirm both copies are real, complete Muse repos, and install a guard that makes it structurally hard to accidentally run any of the later commands against ~/ecosystem.

# 1a. Confirm both copies exist and are real Muse repos (have a .muse/ directory).
test -d "/Users/gabriel/dev/copies 2/muse 03/.muse" && echo "muse copy: OK" || echo "muse copy: MISSING .muse/ — STOP"
test -d "/Users/gabriel/dev/copies 2/musehub 03/.muse" && echo "musehub copy: OK" || echo "musehub copy: MISSING .muse/ — STOP"

# 1b. Confirm neither copy is a symlink back into ~/ecosystem (a real risk if the
#     drag-copy silently created an alias instead of a true copy on some filesystems).
readlink "/Users/gabriel/dev/copies 2/muse 03" && echo "STOP — this is a symlink, not a real copy" || echo "muse copy: real directory, not a symlink"
readlink "/Users/gabriel/dev/copies 2/musehub 03" && echo "STOP — this is a symlink, not a real copy" || echo "musehub copy: real directory, not a symlink"

# 1c. Safety guard function — source this in your shell before running anything
#     below. It aborts any command whose first argument path contains "ecosystem".
muse_backup_guard() {
  case "$1" in
    *ecosystem*) echo "❌ REFUSED: path contains 'ecosystem' — this must never run against the official repos: $1"; return 1 ;;
    *) return 0 ;;
  esac
}

# 1d. Confirm the commit counts match what this plan was designed against (or note
#     the actual counts — they will have grown since this was written).
muse -C "/Users/gabriel/dev/copies 2/muse 03" rev-list --count HEAD --json
muse -C "/Users/gabriel/dev/copies 2/musehub 03" rev-list --count HEAD --json

Deliverable: MIRROR_01 — all four checks in 1a/1b pass before proceeding to Phase 2. If any fails, stop and investigate — do not proceed with a corrupted or misidentified copy.

Done (2026-07-05) — all four checks passed against the real copies: both .muse/ dirs present, neither copy is a symlink, commit counts confirmed at 1,452 (muse) and 1,444 (musehub), exactly matching this plan's design assumptions. Re-runnable any time as ./scripts/muse-git-backup.sh preflight from the muse repo root — it's the same four checks, scripted.

Phase 2 — One-time initial full-history replay (muse)

Creates a brand-new git repository and replays every Muse commit into it, oldest to newest, as a real git commit per Muse commit. This is the slow, one-time cost — at roughly 1-3 seconds per commit observed during testing, expect this to take somewhere in the range of 25-75 minutes for ~1,450 commits. It is safe to let it run unattended; each iteration is independent, and re-running is safe because replay auto-resumes from whatever is already on the mirror branch instead of duplicating it.

Run this — not the raw commands below:

cd ~/ecosystem/muse
./scripts/muse-git-backup.sh replay-muse
# Optional: dry-run a small slice first — auto-resumes into the full run afterward:
./scripts/muse-git-backup.sh replay-muse --limit 20

Deliverable: MIRROR_02 — the script's own final git history length: line equals the Muse commit count from Phase 1's 1d plus one (the init commit).

<details> <summary>Reference — what the script automates (no longer run by hand)</summary>

MUSE_COPY="/Users/gabriel/dev/copies 2/muse 03"
MUSE_GIT_BACKUP="/Users/gabriel/dev/copies 2/muse-git-backup"
mkdir -p "$MUSE_GIT_BACKUP"
git -C "$MUSE_GIT_BACKUP" init -q
git -C "$MUSE_GIT_BACKUP" config user.email "[email protected]"
git -C "$MUSE_GIT_BACKUP" config user.name "gabriel"
git -C "$MUSE_GIT_BACKUP" commit --allow-empty -qm "init (muse git-bridge backup mirror)"
cd "$MUSE_COPY"
COMMITS=$(muse rev-list --reverse HEAD --json | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['commit_ids']))")
TOTAL=$(echo "$COMMITS" | wc -l | tr -d ' ')
N=0
echo "$COMMITS" | while read -r cid; do
  N=$((N + 1))
  echo "[$N/$TOTAL] exporting $cid"
  muse bridge git-export --muse-ref "$cid" --git-dir "$MUSE_GIT_BACKUP" --git-branch muse-mirror --no-push --allow-empty --json
done
git -C "$MUSE_GIT_BACKUP" rev-list --count muse-mirror

This raw form is what motivated writing the script in the first place: pasted directly into an interactive shell, a failed guard check (|| exit 1) closed gabriel's entire terminal tab rather than just aborting the command — happened twice, live, during this ticket. The script fixes this structurally (an internal exit only ends the script's own subprocess) rather than relying on gabriel never mistyping a guard pattern.

</details>

Phase 3 — One-time initial full-history replay (musehub)

Identical to Phase 2, targeting the musehub copy and a separate backup directory.

Run this — not the raw commands below:

cd ~/ecosystem/muse
./scripts/muse-git-backup.sh replay-musehub
# Or run muse + musehub concurrently in one call:
./scripts/muse-git-backup.sh replay-all

Deliverable: MIRROR_03 — same verification as MIRROR_02, for musehub.

<details> <summary>Reference — what the script automates (no longer run by hand)</summary>

MUSEHUB_COPY="/Users/gabriel/dev/copies 2/musehub 03"
MUSEHUB_GIT_BACKUP="/Users/gabriel/dev/copies 2/musehub-git-backup"
mkdir -p "$MUSEHUB_GIT_BACKUP"
git -C "$MUSEHUB_GIT_BACKUP" init -q
git -C "$MUSEHUB_GIT_BACKUP" config user.email "[email protected]"
git -C "$MUSEHUB_GIT_BACKUP" config user.name "gabriel"
git -C "$MUSEHUB_GIT_BACKUP" commit --allow-empty -qm "init (musehub git-bridge backup mirror)"
cd "$MUSEHUB_COPY"
COMMITS=$(muse rev-list --reverse HEAD --json | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['commit_ids']))")
TOTAL=$(echo "$COMMITS" | wc -l | tr -d ' ')
N=0
echo "$COMMITS" | while read -r cid; do
  N=$((N + 1))
  echo "[$N/$TOTAL] exporting $cid"
  muse bridge git-export --muse-ref "$cid" --git-dir "$MUSEHUB_GIT_BACKUP" --git-branch muse-mirror --no-push --allow-empty --json
done
git -C "$MUSEHUB_GIT_BACKUP" rev-list --count muse-mirror

</details>

Phase 4 — Create the GitHub repos and push

Creates two brand-new, private-by-default GitHub repositories and pushes each mirror. Requires gh auth status to already be authenticated as gabriel.

Run this — not the raw commands below:

cd ~/ecosystem/muse
./scripts/muse-git-backup.sh github-push

Deliverable: MIRROR_04 — both GitHub repos exist and show the full commit history when viewed on github.com, not just the latest state.

<details> <summary>Reference — what the script automates (no longer run by hand)</summary>

gh repo create gabriel/muse-backup --private --description "Git-bridge backup mirror of the muse Muse repo"
gh repo create gabriel/musehub-backup --private --description "Git-bridge backup mirror of the musehub Muse repo"
git -C "$MUSE_GIT_BACKUP" remote add origin "[email protected]:gabriel/muse-backup.git"
git -C "$MUSE_GIT_BACKUP" push -u origin muse-mirror
git -C "$MUSEHUB_GIT_BACKUP" remote add origin "[email protected]:gabriel/musehub-backup.git"
git -C "$MUSEHUB_GIT_BACKUP" push -u origin muse-mirror

</details>

Phase 5 — Verify content fidelity

Before trusting the backup, prove it round-trips correctly — clone the GitHub repo fresh into a throwaway location and diff its latest state against the copy's current working tree.

Run this — not the raw commands below:

cd ~/ecosystem/muse
./scripts/muse-git-backup.sh verify
# Expect "expect no output" on both diffs. Any reported difference must be
# investigated before this backup is trusted.

Deliverable: MIRROR_05 — both diff -rq commands produce no output.

<details> <summary>Reference — what the script automates (no longer run by hand)</summary>

rm -rf /tmp/verify-muse-backup && git clone "[email protected]:gabriel/muse-backup.git" /tmp/verify-muse-backup
git -C /tmp/verify-muse-backup checkout muse-mirror
diff -rq --exclude=.git --exclude=.muse "/Users/gabriel/dev/copies 2/muse 03" /tmp/verify-muse-backup
rm -rf /tmp/verify-musehub-backup && git clone "[email protected]:gabriel/musehub-backup.git" /tmp/verify-musehub-backup
git -C /tmp/verify-musehub-backup checkout muse-mirror
diff -rq --exclude=.git --exclude=.muse "/Users/gabriel/dev/copies 2/musehub 03" /tmp/verify-musehub-backup

</details>

Phase 6 — Incremental backup procedure (the "every so often" step)

After the initial full replay, every subsequent backup run only needs to export commits made since the last run — read directly from the bridge's own state file, not tracked manually. This is the fast, repeatable command gabriel runs going forward, against a freshly-dragged copy (never against ~/ecosystem/muse directly).

Run this — not the raw commands below:

cd ~/ecosystem/muse
./scripts/muse-git-backup.sh incremental-muse
./scripts/muse-git-backup.sh incremental-musehub

Deliverable: MIRROR_06 — running either subcommand twice in a row (with no new Muse commits in between) exports nothing the second time and exits cleanly, proving the incremental bound works correctly, not just on the first successful run.

<details> <summary>Reference — what the script automates (no longer run by hand)</summary>

MUSE_COPY="/Users/gabriel/dev/copies 2/muse 03"   # or wherever the fresh copy lives
MUSE_GIT_BACKUP="/Users/gabriel/dev/copies 2/muse-git-backup"
cd "$MUSE_COPY"
LAST_BRIDGED=$(python3 -c "
import tomllib
with open('.muse/git-bridge.toml', 'rb') as f:
    state = tomllib.load(f)
print(state['last_export']['muse_commit_id'])
")
NEW_COMMITS=$(muse rev-list "$LAST_BRIDGED..HEAD" --reverse --json | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['commit_ids']))")
if [ -z "$NEW_COMMITS" ]; then
  echo "Nothing new since the last backup — done."
else
  echo "$NEW_COMMITS" | while read -r cid; do
    muse bridge git-export --muse-ref "$cid" --git-dir "$MUSE_GIT_BACKUP" --git-branch muse-mirror --no-push --allow-empty --json
  done
  git -C "$MUSE_GIT_BACKUP" push origin muse-mirror
fi

</details>


Acceptance Criteria

  • ~/ecosystem/muse and ~/ecosystem/musehub are verified byte-for-byte unchanged (via muse status --json reporting clean: true with no unexpected modified/ added files) after every phase of this ticket — check this explicitly after Phase 2 and again after Phase 6, not just assumed.
  • Both GitHub repos exist, private by default, containing the full commit-by-commit history (not a single squashed snapshot).
  • Phase 5's fidelity diff produces zero differences for both repos.
  • The incremental procedure (Phase 6) correctly exports nothing on a no-op second run, and correctly exports only new commits when new ones exist.
  • Every command in this ticket was run exactly as written, with no path substitutions that removed or bypassed the muse_backup_guard check.

Risks

  • Runtime risk: the initial full replay is a genuinely long-running operation (tens of minutes per repo) — run it in a terminal you can leave alone, not something you need to babysit command-by-command, but don't assume it's instant.
  • --allow-empty risk: every export in this ticket passes --allow-empty so a Muse commit that produced no bridge-relevant file changes still gets a corresponding (empty) git commit — this preserves exact commit-count parity between Muse and the git mirror, which is what Phase 2's MIRROR_02 verification checks. Omitting --allow-empty would silently produce a shorter git history than the Muse history it's supposed to mirror.
  • GitHub repo naming collision risk: gh repo create will fail loudly (not silently overwrite) if gabriel/muse-backup or gabriel/musehub-backup already exist — if either name is already taken by something unrelated, pick different names before running Phase 4, don't force through a collision.
  • Copy staleness risk: the incremental procedure (Phase 6) assumes gabriel drags a fresh copy off ~/ecosystem/muse before each incremental run — running it against a stale, already-fully-bridged copy will correctly report "nothing new" even if ~/ecosystem/muse itself has moved on, which is correct behavior for the copy given but worth knowing so a stale copy isn't mistaken for a broken backup.

Open Questions

  • Should a tested restore procedure (GitHub → git → muse bridge git-import → a fresh Muse repo) be designed now, or left for a follow-up ticket once this forward-backup path is proven? Leaning toward a follow-up — this ticket is already substantial, and a restore procedure deserves its own careful, tested design rather than being appended here as an afterthought.
  • Should this eventually be automated (a periodic launchd job or similar), now that the manual procedure is proven? Explicitly out of scope for this ticket (see Non-Goals) but worth deciding once gabriel has run the manual version a few times and knows whether the cadence is worth automating.
  • Are gabriel/muse-backup and gabriel/musehub-backup the right GitHub repo names, or does gabriel want a different naming convention (e.g. under a dedicated backup-only GitHub org rather than his personal account)? Flagged for confirmation before Phase 4 runs, not assumed.
  • Should the backup include agentception and the other workspace member repos (contracts, Stori, maestro, muse-zsh) eventually, following the same pattern proven here for muse/musehub? Not in scope now — flagged as a natural follow-up once this pattern is validated on the two primary repos.

Implementation Order

Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6. Phases 2 and 3 are independent of each other (muse vs. musehub) and could run concurrently in two terminals if gabriel wants to save wall-clock time, but each individually must complete before its own Phase 4/5/6 steps.


This is an operational runbook, not a code-change ticket — "done" means gabriel has successfully run every phase against the real copies, not that a PR merged.

Activity
gabriel opened this issue 57 days ago
No activity yet. Use the CLI to comment.