Git-bridge backup mirror for muse and musehub → GitHub (from copies, non-destructive)
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:
- 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. 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-musewith no flags and it exports everything not already on the mirror branch. --limit Ncaps 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 Noverrides 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 plainreplay-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 03and/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 03copy — 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/musehubare 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/museand~/ecosystem/musehubare verified byte-for-byte unchanged (viamuse status --jsonreportingclean: truewith 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_guardcheck.
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-emptyrisk: every export in this ticket passes--allow-emptyso 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'sMIRROR_02verification checks. Omitting--allow-emptywould silently produce a shorter git history than the Muse history it's supposed to mirror.- GitHub repo naming collision risk:
gh repo createwill fail loudly (not silently overwrite) ifgabriel/muse-backuporgabriel/musehub-backupalready 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/musebefore each incremental run — running it against a stale, already-fully-bridged copy will correctly report "nothing new" even if~/ecosystem/museitself 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-backupandgabriel/musehub-backupthe 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
agentceptionand the other workspace member repos (contracts,Stori,maestro,muse-zsh) eventually, following the same pattern proven here formuse/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.