Real-dir migration
What this layer does
Phase 11 converts pre-existing real directories under each runtime's skills path to symlinks pointing at canonical. Only Robert's machine needs this — Joes start clean and bootstrap symlinks straight away. The migration is non-destructive: real dirs move to a timestamped backup, the symlink replaces them, and divergence triggers a manual 3-way diff rather than silent overwrite.
Files involved
state/bin/sync/migrate-realdirs.sh— the migration script;
supports --dry, --apply, --runtime <name>.
state/log/migrations.ndjson— append-only completion record
(Phase 6 reads this to refuse running until it sees a row).
~/.claude/_backups/<ts>/<runtime>/<name>/— backup destination
for moved real dirs.
state/log/divergence-reports/<ts>-<runtime>-<name>.md— written
when SHA compare fails; surfaces the 3-way diff candidate.
Procedure
state/bin/sync/migrate-realdirs.sh --runtime gemini --dry
# For each ~/.gemini/skills/snappy-*/:
# - if real dir: read SKILL.md, find canonical at
# ~/projects/snappy-kernel/skills/<same-name>/SKILL.md
# - SHA-256 compare:
# - identical → safe to symlink
# - diverged → REPORT, do NOT touch
state/bin/sync/migrate-realdirs.sh --runtime gemini --apply
# Moves real dirs to ~/.claude/_backups/<ts>/gemini/<name>/
# Creates symlink to canonical
# Logs row to state/log/migrations.ndjson
Divergence resolution policy
When the runtime-local copy and the canonical copy differ, the script emits a 3-way diff candidate against:
- canonical —
~/projects/snappy-kernel/skills/<name>/SKILL.md - runtime-local —
~/.gemini/skills/snappy-<name>/SKILL.md - DO Spaces remote — last-pushed version of canonical
Robert picks one. The picker writes the chosen content to canonical, runs sync-runtimes.sh to fan out, and only THEN runs migrate-realdirs.sh --apply on the affected name. There is no auto-resolve; divergence implies a human-meaningful edit somewhere.
Operational gotchas
- Phase 11 must complete before Phase 6 on Robert's machine. Phase 6
reads state/log/migrations.ndjson; missing row = refuse. Otherwise Phase 6 captures stale canonical state from runtime-local copies that should have migrated first.
- Backup destination is shared across runtimes. The
<ts>prefix
prevents collisions across runs. Never delete a backup directory manually — the migration script's only safety net is that the original bytes still exist somewhere.
- The script handles broken symlinks by treating them as "no real
dir present" (safe to symlink). A symlink pointing somewhere else entirely (not snappy-os) is left alone with a warning.
- Cursor and Windsurf may not have a skills dir at all on first
bootstrap; the script no-ops gracefully.
- Per-runtime invocation is intentional. Migrating all runtimes in
one shot makes a divergence report unreadable. Run one runtime, resolve, then move to the next.
How to verify it's working
state/bin/sync/migrate-realdirs.sh --runtime <name> --drylists
every candidate with status safe-to-symlink, diverged, or already-symlink.
- After
--apply,readlink ~/.gemini/skills/snappy-<name>resolves
to canonical for every previously-real dir.
~/.claude/_backups/<ts>/<runtime>/<name>/SKILL.mdexists for every
moved dir.
state/log/migrations.ndjsongains one row per moved dir with
success: true.
- Phase 6 push proceeds without the migration-precondition refusal.