Page kind: Guide
A store as a remote: clone, push and sync another store#
After this page a store uses another store as its remote through swarmops serve: clone, fetch,
push and sync over ssh or HTTPS, and know what crosses between them.
You need: SwarmOps 0.12.0 or newer on both hosts; GitHub as a remote; a store served over ssh or over HTTP.
The commands are those of GitHub as a remote: so clone, so fetch,
so push, so sync. Only the remote differs: a store that swarmops serve serves over the git
wire. This page says what changes when the remote is a store.
host A host B host C
┌──────────────┐ so push, ┌──────────────────────────┐ so push, ┌──────────────┐
│ store │ so sync │ swarmops serve │ so sync │ store │
│ + git mirror│ ──── git ─▶ │ access.toml: the key's │ ◀─ git ─── │ + git mirror│
│ │ ◀── git ─── │ identity, its grants │ ─── git ─▶ │ │
└──────────────┘ so fetch │ the served store │ so fetch └──────────────┘
└──────────────────────────┘Each client store keeps a git mirror of the served store and moves packs with its own git; the served store records what each key pushed.
In short#
- Git commit ids are shared. A commit on the served
mainhas the same id and tree in every store that fetched it; changeset ids are each store's own.- The client lands.
so synclands the store's changes onto the remote's head and pushes them by compare-and-swap; the remote does not land a store's push.- The key is the identity. The served store knows the caller by its ssh key or web login, never by the client's
--as: every agent of one store is one identity there.- The op log, agents, workspaces and land keys stay local.
Before you start#
- A served store. The operator serves it as Serve over ssh or
Serve over HTTP describe. Branches other than
mainandrelease/<name>need aFORMAT 2store on both sides. - A grant for the key. The served store's
access.tomlnames the key's identity:readforcloneandfetch,pushonmainforso pushandso syncofmain,pushon a branch pattern such asfeature/**for that branch (Writeaccess.toml). - git 2.39 or later on the client host, and the key or web login git uses for the URL (Credentials).
- An identity for the commands that move
main(clone,sync):--as, elseSWARMOPS_AGENT. It names the agent in the client store only.
Clone#
export SWARMOPS_AGENT=operator/ada
so clone ssh://so-host/acme.git --into ~/.local/state/swarmops/repos/acme
export SWARMOPS_REPO=~/.local/state/swarmops/repos/acmeOver HTTPS the URL is the web server's prefix and the store's name,
https://so-host/so/acme.git. clone imports the served main into the new store's main,
commit by commit, and records the remote as origin. Each imported changeset keeps the served
commit's git id. --branch feature/m follows another served branch; a merge commit on it is
imported with both its parents.
Fetch, push and sync#
so fetch origin # the served store's new commits; no store ref moves
so sync status origin # ahead and behind, offline
so push origin # main to the served main, forward only
so push origin feature/x # a branch of its own (FORMAT 2), created or moved forward
so sync origin # land onto the served head, then pushThey behave as against GitHub (Fetch, Push, Sync). Against a served store:
- The remote records the key. Each push is a
ref_movein the served store's op log, whose actor is the key's identity (person/ada); the changesets it brought arecommitoperations with the agentunknown/unknown. No operation of the served store names the client's agent. pushsends branches.main,release/<name>and, in aFORMAT 2store, any branch. A tag, a remote-tracking ref or a target that starts withrefs/is refused before git runs:E_INVALID_ARGwithdetails.reasonsource_kindorbranch_target.main:refs/land/mainis one of them: over the git wire the remote does not land a store's push;so synclands on the client.- Nothing is rewritten.
rewrittenis 0 on a push of a history the store fetched whole.
Several hosts at once#
When two stores sync one served store at once, the second to push finds the remote moved: git's
lease refuses it, or the served store answers stale: refs/heads/main moved; fetch first. Either
reads as remote_moved, and so sync fetches, lands again onto the new head and pushes, up to
--max-attempts times (attempts counts them). Each change is then on the served main once.
A push held up after git read the served head can still lose to a push that moved main past
it: the served store then answers non-fast-forward: main moves along first parents; push to refs/land/main, which reads as E_REMOTE_REJECTED declined, not retried. main moved in the
client store already (details.main_moved): run so sync origin again, and it lands what is left
onto the winner's head (known limitations).
Refusals#
| The client sees | Why | What to do |
|---|---|---|
E_REMOTE_DIVERGED, remote_moved, exit 3 | The served branch moved since the last fetch | so sync origin, or fetch and push again |
E_REMOTE_DIVERGED, diverged, exit 3 | The fetched histories differ | so sync origin |
E_CONFLICT, exit 3 | A change does not merge onto the served head | Resolve as When sync stops says |
E_REMOTE_REJECTED, declined, exit 5, remote_messages holding E_POLICY_DENIED: person/bob may not push main | The key's grants do not cover the ref | Push to a branch the grant covers, or the operator widens it |
E_REMOTE_REJECTED, auth, exit 5 | No grant names the key, or the store has no access.toml | The operator adds a grant |
E_REMOTE_REJECTED, not_found, exit 5 | No store of that name is served | Check the name with the operator |
E_INVALID_ARG, source_kind or branch_target | A tag or a refs/ target | Push a branch |
The served store's own refusals are in Refusals a client sees; the store client reads each by the code at the start of its line.
When a transfer is cut#
A cut clone or fetch is E_REMOTE_UNREACHABLE (exit 4, retryable) and changes no store ref. A
cut clone leaves the store it made, with the remote and no changeset of the cut pack
(details.created): run the same command again into the same directory, and it imports the whole
history. git starts the pack again from its start.
A push whose answer was lost, a killed client or a dropped connection after the served store moved, leaves the remote holding the change. Run the push again:
so push originup_to_date with old different from new: the remote already held the commit, from a push
whose answer was lost; the tracking ref moved, nothing was sent. Run once more, old is new.
What crosses, and what does not#
| Over the git wire (this page) | Native sync (planned) | |
|---|---|---|
| Who lands | so sync: the client lands onto the remote's head, then pushes by compare-and-swap; the remote records a ref_move by the caller. A push to refs/land/<target> is refused before git | The remote lands every change, keyed |
| Ids | A commit on the remote's main has one git id and one tree in every store that fetched it. A change made in a store keeps its changeset id there; other stores import its commit under ids of their own, and stores are compared by git commit id. A change that loses a race gets a new git commit id and a new changeset id; the old id is dropped locally and only the SwarmOps-Changeset trailer names it. No lineage is recorded | New ids upstream, the origin recorded |
| Op log | Does not cross. The remote logs commit (agent unknown/unknown) and ref_move by the ssh or web identity | Crosses with identity, grant and acting records |
| Agent | Kept as the git author (authors.toml); not an agent on the other side | Kept |
| Access | The remote's access.toml binds the key or web login. All agents of one store are that one identity on the remote; per-agent rules, grants and makers do not reach through a store | Per agent |
| Approvals | Do not cross | Carried |
| Land keys | In the trailer; so sync drops what the remote holds. The remote does not index them for its own land --key: a keyed land retried against the remote is not a replay there | Exactly once by key on the upstream |
| Workspaces, unlanded commits | Local. A branch push (feature/x) shares unlanded work | Local |
| Checks | Run by the client (land-check, ws land --check); the remote runs none on a served push | The upstream's gate |
| Resume | git's: a cut clone or fetch starts its pack again | No object fetched twice |
| Offline | Every operation but fetch, push and sync | The same |
Native sync is on the roadmap.
Limits to know#
- Tags do not cross. A served store's tags stay in the client's mirror;
pushsends no tag. - No remote landing from a store.
refs/land/<target>takes a git client's push (Serve over ssh), not a store's. git push --atomic, push options and annotated tags are not served (Not served).- More in the known limitations.
Next#
Next: Serve over ssh: serve a store to other stores and to stock git.
- GitHub as a remote: the same commands against a git host.
GitHub as a remote: clone, fetch, push and sync
After this page a store keeps a git remote, GitHub or any git host: you can clone from it, fetch, push and sync, and know what to do when a sync stops.
Use SwarmOps from Claude Code over MCP
SwarmOps is version control for swarms of coding agents. After this page Claude Code agents read, commit and land in one store over MCP, one identity per…