| name | sync |
| description | Bidirectionally sync ALL git repos across two machines — this MacBook and the always-on Mac mini — over SSH, using GitHub/GitLab as the transport. Commits and pushes uncommitted work on both sides, then rebases/fast-forwards each so they converge, with per-repo subagents resolving rebases and merge conflicts. This is a MULTI-repo, TWO-machine operation — distinct from /commit, /ship, or /git-cleanup, which act on a single repo. Use whenever the user wants to sync their machines/repos, "catch up" the laptop to the Mac mini or push laptop work back, is about to travel or just got back, has uncommitted work scattered across many repos, or wants to reconcile branches that diverged between the two machines. Trigger even on a bare "sync", "sync my machines", or "get my repos up to date". |
sync — MacBook ⇄ Mac mini repo reconciliation
Keep every git repo consistent across the two machines. The MacBook is always
the orchestrator (this skill runs here); the Mac mini is always the remote
peer, reached with ssh mini. GitHub/GitLab is the transport — nothing is
copied machine-to-machine directly, so the flow is: commit local work → push →
pull the other side → reconcile.
The user has chosen these defaults (do not re-litigate them):
- Uncommitted work becomes a plain real commit —
git commit --no-verify -m "wip(sync): snapshot from <host> <YYYY-MM-DD>" — normal push, normal rebase-pull.
Never force-push. Always use --no-verify: a wip snapshot is a transport
commit, not a quality-gated one, and pre-commit hooks (lefthook/husky calling
bunx/node) both waste time AND fail outright on the mini, where ssh mini '<cmd>' runs a non-login shell with a bare PATH (no fnm/bun shims) → bunx: command not found (exit 127). --no-verify sidesteps that entirely. These wip
commits accumulate in history; squash later with /git-cleanup or gback when
resuming. Keep the message uniform and fast — no clever per-file messages.
- Bidirectional by default — reconcile both directions in one run; per repo,
figure out who is ahead.
- Different branches per machine → preserve both. Commit+push both branches
(nothing is ever lost — both end up on the remote), report the mismatch, and
then ask whether to check one branch out on both machines. Never drop a branch
without asking.
- Plan → one confirmation → execute. Always print the full reconcile plan and
get a single go-ahead before doing anything with side effects. Then run it all,
stopping only on a genuine merge conflict a subagent can't confidently resolve.
Scoping (args)
If the user passes repo name(s) as arguments (e.g. /sync basalt-ui or
/sync epos.student-enrolment free-planning-poker), still run recon across
everything (so the full report is accurate), but in Phase 3 only execute on
the named repos — list the rest as "out of scope this run." No args = reconcile all
in-scope repos. This is the safe way to validate one repo at a time before trusting
a full sweep.
Phase 0 — Preflight
- Confirm you are on the MacBook (
hostname — the mini is Minivonohannes3…).
If run on the mini, stop and tell the user this skill orchestrates from the
MacBook.
- Confirm the mini is reachable:
ssh -o ConnectTimeout=8 mini 'echo ok'.
If it fails, report and stop — no half-sync.
Phase 1 — Recon (read-only, safe)
Gather git state on both machines with the bundled script. It takes root names
relative to $HOME so the same call works despite different usernames
(johannes.krumm here, jkrumm on the mini).
This is a dotfiles-local skill — it only loads when Claude is started inside
~/SourceRoot/dotfiles, so paths below are relative to that repo root.
SKILL=~/SourceRoot/dotfiles/.claude/skills/sync
bash "$SKILL/scripts/recon.sh" SourceRoot IuRoot > /tmp/sync-macbook.jsonl
ssh mini 'bash -s -- SourceRoot IuRoot' < "$SKILL/scripts/recon.sh" > /tmp/sync-mini.jsonl
The default does a parallel git fetch --prune per repo first, so ahead/behind
are accurate. Add --no-fetch (before the root names) for a quick offline peek.
Abort guard — do not skip this. If the remote recon exits non-zero or produces
zero lines, STOP. Never build a plan from a one-sided recon: an empty mini file
makes every repo look "MacBook-only," and acting on that could push over or ignore
real remote state. A common cause is a transient 1Password SSH-agent failure
(signing failed … communication with agent failed) — retry the SSH command once
or twice; if it keeps failing, report it and stop. [ -s /tmp/sync-mini.jsonl ]
must be true before proceeding.
Each line is JSON: {root, repo, path, branch, detached, dirty, upstream, ahead, behind, remote, head, head_date, head_msg, unpushed_branches, local_only}.
upstream — string enum for the checked-out branch:
"true" — tracks a remote branch that exists → normal reconcile.
"gone" — HAD an upstream but the remote ref is deleted (merged MR, pruned
branch). The branch is stale/dead — do NOT push -u to resurrect it. The
right action is to move off it onto the live main branch (see reconcile.md).
"false" — never tracked anything → a genuinely new local branch → push -u.
unpushed_branches — comma list of branch:N: non-current branches that
already track a remote and are N commits ahead. This is real unpushed work on a
side branch — push it.
local_only — a count of no-upstream local branches. These are almost always
the stale graveyard (old, merged, remote-deleted branches). Never mass-push
them. Surface the number ("repo has 250 local-only branches") and only act if
the user explicitly asks to review/prune them. Mass-pushing hundreds of dead
branches to pollute the remote is the classic wrong move here.
The checked-out branch is synced according to its upstream value above (new
"false" branch → push -u; "gone" → move off, don't resurrect). The graveyard
rule only applies to non-current branches.
Staleness — recon is a snapshot, not a lock. On active work repos (IuRoot
especially: teammates, CI, MRs merging), remote state can move between recon and
execution. Immediately before executing a repo's plan, re-fetch and re-check that
one repo's ahead/behind/upstream. If it moved materially from the plan, re-plan
that repo (and re-confirm if the action changed). Never trust a minutes-old snapshot
for the mutating step.
Read both files. Match repos by (root, repo) (same relative path on both
machines). This handles that folder names, not remotes, define identity —
duplicate clones like argo vs argo-old are treated as separate repos.
Phase 2 — Classify and build the plan
Classify every repo, then print a plan grouped by action. See
references/reconcile.md for the full decision table — the summary:
- Both sides, clean, in sync → skip (list briefly under "already synced").
- Both sides, work on the same branch → the reconcile case. Sequence pushes so
the second pusher rebases onto the first. Subagent handles it.
- Different branch per machine → push both branches, flag mismatch, queue an
"align?" question for after execution.
- Current branch
upstream:"false" (new local branch) → push -u to create it.
- Current branch
upstream:"gone" (merged/deleted upstream — stale branch) → do
NOT push it. The action is to move off it onto the live main branch
(git switch <main> or git switch -C <main> origin/<main>), then reconcile that.
Flag it in the report; if the branch might hold unmerged local work, confirm before
leaving it. This reads identically to "false" in older recon output — the enum
now disambiguates them, so treat them oppositely.
- Non-current side branches (
unpushed_branches) → each gets its own plan line
item, even on a repo whose current branch is "trivial"/clean. It's easy to
under-scope a repo as "just a fast-forward" and silently drop its side-branch
pushes — don't. A side-branch push is a plain push; on a PR-required repo it is
unaffected by the master-protection hook (only pushes to master/main are).
- No remote at all → report, skip (can't sync without a bus).
- PR-required repo (
config/pr-required-repos.json) with work on master/main
→ the branch-protection hook blocks pushing master. Move the work to a
sync/<host>-<YYYY-MM-DD> branch, push that, and report it instead. Then ask which
landing the user wants — a real PR to origin/master, or local-sync-only (rebase
the branch onto master, fast-forward both machines' local master to it, delete
the branch, remote master untouched). Don't assume; see references/reconcile.md.
- Detached HEAD → report, skip (never auto-reconcile a detached head).
- Repo on only one machine → report under "MacBook-only" / "mini-only" and do
NOT touch it (user's explicit call: sync is strictly machine-to-machine). Still
flag the ones with uncommitted work (
[uncommitted]) so the user knows that
work isn't backed up — they'll commit it themselves. Offer to clone to the other
side only if the user asks.
Print the plan like the example the user approved:
PLAN — MacBook ⇄ Mac mini
RECONCILE (subagent per repo):
free-planning-poker rebase 1 local commit onto 48 remote, then push wip
argo mini has 16 dirty + feat/argo-voice → commit, push, MacBook pulls
student-enrolment push BOTH branches (feat/enable-reimport ⇄ fix/gasthoerer-…) — ALIGN?
TRIVIAL (inline):
dotfiles MacBook 1 dirty → wip commit + push; mini ff
SIDE BRANCHES (plain push, own line even on "trivial" repos):
basalt-ui push feat/new-theme:20, feat/pencil:1, master:1 (PR-req: side pushes OK)
STALE / MOVE OFF:
student-enrolment MacBook on feat/enable-reimport [upstream gone] → switch to main
SKIP (already synced): homelab, vps, modelpick, …
MacBook-only: photo-flow, busplan, open-news, snow-finder, …
mini-only: hermes-agent, audio-gateway, research-gateway, argo-old(detached), …
QUESTIONS AFTER EXECUTE: align student-enrolment branch?
Then ask for the single confirmation. Nothing with side effects runs before this.
Phase 3 — Execute
After confirmation, split the work:
- Trivial repos (one side clean, other has only a linear push, or a pure
fast-forward) — handle inline with plain git. Fast, no subagent overhead.
- Non-trivial repos (rebase needed, both sides have work, conflicts possible,
PR-required-master, no-upstream) — spawn one
implementer subagent per repo,
in parallel across disjoint repos (each repo is independent, so this is safe
fan-out). Give each the brief from references/reconcile.md (the "Subagent brief"
template): both-sides state, the house rules above, and the specific reconcile it
must perform. Each subagent runs git locally and ssh mini for the mini
side, resolves rebases/conflicts with judgment, and returns a one-line result.
Conflict policy for subagents: auto-resolve the obvious mechanical stuff
(regenerated lockfiles, both-added-same-import, formatting-only clashes). Escalate
to the user only a genuine semantic conflict where either resolution could be
wrong — with the file, both hunks, and a recommendation. Don't guess on meaning.
File-ownership caveat: a repo is owned exclusively by its subagent while it
runs. Never run your own git/validation over a repo a subagent is mid-reconcile on.
Parallelize on disjoint repos only.
Phase 4 — Report and follow-ups
Collect the subagent result lines and print a final summary: what synced, what wip
commits were created (and where to squash them), what was skipped and why. Then ask
any queued questions (branch alignment for mismatched repos). Do not silently
converge branches — that was the user's explicit call.
If wip commits were pushed, remind: "resume with gback (soft reset) on the
receiving machine to get your working tree back, or /git-cleanup to squash."
Notes
- IuRoot is in scope (GitLab). Git ops need no 1Password account; the transport
is SSH-key git.
student-enrolment is the flagship mismatch case.
- Keep the orchestrator context clean: recon output goes to
/tmp, per-repo grind
goes to subagents. The orchestrator holds the plan and the verdicts.
- This skill lives at
dotfiles/.claude/skills/sync/ (committed, not symlinked) and
loads only in dotfiles sessions. Edit it there.