Troubleshooting#
This page goes from what you see to why it happens and what to run. It covers the installer, the
MCP server, where a store may live, a busy store, a damaged store, git import and export, and every
E_* error code that a command or a tool call can return.
Every example was run on 2026-10-22: the commands with the binary built from this repository
(swarmops 0.1.0), in a scratch directory that holds the store acme (SWARMOPS_REPO=acme); the
installer from get.swarmops.cc; and Claude Code's claude mcp with a configuration directory of
its own. Every output is what it printed, with three cuts: an id is cut to its first 8 hex
digits and …, the scratch directory in an absolute path is …, and … alone marks a cut of a
long output. The examples run the long name, swarmops; so is the same program.
Read the error first#
Every command prints one JSON envelope on stdout, and every MCP tool result holds the same
envelope. A failure has "ok": false and an error object:
| Field | What it tells you |
|---|---|
code | The stable E_* code. The table below links each one to its section. |
exit | The process exit code: 1 failed, 2 malformed request, 3 conflict, 4 retryable, 5 identity, 6 not found. |
retryable | true when running the operation again can succeed: as it is, or after re-reading what the error names. |
details | Machine-readable context: the path, the ref, what the ref holds now. A store file is named relative to the store's root, never by a host path. |
hint | The next command to run, when there is an obvious one; else null. |
So the first step is the same for every failure: read hint, then details. Two commands help
with any of them:
swarmops schema OpError # the error object's schema
swarmops verify # is the store itself sound?The README's Output and exit codes says more about the envelope. A run that meets one of the store's four documented aborts prints no envelope at all: the process aborts, and says why on stderr (The server exits).
Every error code#
ErrorCode in crates/swarmops-api/src/error.rs is the one table of codes, exit codes and
retry advice; this is that table. Four codes are in the schema but no operation raises them yet.
| Code | Exit | Retryable | Means | Section |
|---|---|---|---|---|
E_EXISTS | 1 | no | init found a store there already. | Where a store may live |
E_NOT_A_REPO | 6 | no | No store at that path, or no store named. | Where a store may live |
E_UNSUPPORTED_FS | 1 | no | The store's path leads under /mnt/. | Where a store may live |
E_IDENTITY_REQUIRED | 5 | no | A write without an identity. | Identity |
E_INVALID_ARG | 2 | no | A malformed argument, request or command line. | Arguments and revisions |
E_REV_NOT_FOUND | 6 | no | A revision names no changeset. | Arguments and revisions |
E_AMBIGUOUS_ID | 1 | no | An id prefix matches more than one changeset. | Arguments and revisions |
E_PATH_NOT_FOUND | 6 | no | Nothing is at that path at that revision. | Arguments and revisions |
E_IS_DIR | 1 | no | A file read named a directory. | Arguments and revisions |
E_REF_NAME_INVALID | 1 | no | A ref name that is not main or release/<name>. | Arguments and revisions |
E_OP_NOT_FOUND | 6 | no | No operation by that number. | Arguments and revisions |
E_EMPTY_COMMIT | 1 | no | The change list changes nothing. | Commits |
E_TYPE_CONFLICT | 1 | no | A file and a directory at the same path. | Commits |
E_TOO_LARGE | 1 | no | Content past a size limit. | Commits |
E_UNKNOWN_PARENT | 6 | no | The parent left the store before the commit recorded. | Commits |
E_CAS_MISMATCH | 4 | yes | The ref no longer holds what you expected. | Commits |
E_CONFLICT | 3 | no | The land conflicts with the target; nothing recorded. | Landing and undo |
E_NOTHING_TO_LAND | 1 | no | The target holds the change already. | Landing and undo |
E_CAS_RETRY_EXHAUSTED | 4 | yes | The target moved before every land attempt. | Landing and undo |
E_NOTHING_TO_UNDO | 1 | no | The operation moved no ref. | Landing and undo |
E_BUSY | 4 | yes | Another writer held the store's lock for the whole 10 s wait. | A busy store |
E_STORE_CORRUPT | 1 | no | Stored data failed verification. | A damaged store |
E_IO | 1 | no | The filesystem refused an operation. | A damaged store |
E_IO_TRANSIENT | 4 | yes | A journal append failed and was cut back; nothing committed. | A damaged store |
E_INTERNAL | 1 | no | A bug in SwarmOps. | A damaged store |
E_GIT_NOT_A_REPO | 6 | no | No git repository at that path. | Git import |
E_GIT_REF_NOT_FOUND | 6 | no | The git ref names no commit. | Git import |
E_GIT_UNSUPPORTED | 1 | no | A repository the import does not read (shallow, SHA-256, …). | Git import |
E_GIT_READ | 1 | no | An object is missing or damaged in the git repository. | Git import |
E_GIT_IMPORT_LOSSY | 1 | no | Content a changeset cannot hold as git has it. | Git import |
E_GIT_IMPORT_DIVERGED | 3 | no | The store's ref and the git history went separate ways. | Git import |
E_GIT_EXPORT_DIVERGED | 3 | no | The git branch holds commits that are no export of the ref. | Git export |
E_GIT_WRITE | 1 | no | The git repository could not be written. | Git export |
E_REF_NOT_FOUND | 6 | no | Not raised yet. | Codes not raised yet |
E_AUTH | 5 | no | Not raised yet. | Codes not raised yet |
E_POLICY_DENIED | 5 | no | Not raised yet. | Codes not raised yet |
E_MAINTENANCE_BUSY | 4 | yes | Not raised yet. | Codes not raised yet |
git import and git export are commands only, so the E_GIT_* codes never come from a tool
call; init is a command only too. Every other code can come from both.
The installer#
curl -fsSL https://get.swarmops.cc/install.sh | bash runs scripts/install.sh. It prints its
progress and its errors on stderr, each line starting with swarmops-install:, and stops with
status 1 on a failure and 2 on a usage error. It checks the host before it downloads anything, and
installs nothing when a check after the download fails. Options go after bash -s --;
bash install.sh --help lists them.
A good run, for reference (into a directory not on PATH):
curl -fsSL https://get.swarmops.cc/install.sh | bash -s -- --version 0.1.0 --dir instswarmops-install: Installing SwarmOps 0.1.0 (x86_64-unknown-linux-gnu) into inst.
swarmops-install: Downloading https://github.com/KeiaiLab/SwarmOps/releases/download/v0.1.0/swarmops-0.1.0-x86_64-unknown-linux-gnu.tar.gz
swarmops-install: swarmops-0.1.0-x86_64-unknown-linux-gnu.tar.gz matches SHA256SUMS.
swarmops 0.1.0
Installed swarmops and so (its short form) in …/inst.
…/inst is not on PATH. Add this line to your shell startup file (~/.profile, ~/.bashrc or ~/.zshrc):
export PATH="…/inst:$PATH"
Quick start: https://github.com/KeiaiLab/SwarmOps#quick-start
Website: https://swarmops.ccThe installer stops#
Each message below is one the script prints; X, V and DIR stand for what it names.
| Message | Cause | What to do |
|---|---|---|
Unknown option 'X'; see --help. (exit 2) | A misspelt option. | … | bash -s -- --version 0.1.0 |
X needs a value; see --help. (exit 2) | --version or --dir last on the line. | Give the value: --dir ~/bin. |
SwarmOps runs on Linux today; macOS and Windows are next. then This system is X. | Not Linux. | No release build runs there yet. |
No release build runs on X, only on x86_64 and aarch64. | Another CPU architecture. | Build from source (the README's Install). |
This system's C library is musl; … | Alpine and other musl systems. | Build from source. |
This system has glibc V; the release binaries need 2.34 or newer. | An older distribution. | Build from source, or a newer host. |
Cannot tell this system's glibc version … | Neither ldd --version nor getconf GNU_LIBC_VERSION gave one. | Build from source. |
Neither curl nor wget is installed: … | No downloader. | Install one, then run it again. |
'V' is not a release version: X.Y.Z or X.Y.Z-pre, such as 0.1.0. | --version 0.1 or another short form. | Give all three numbers; a leading v is fine. |
Cannot find the latest release: … | The redirect from the releases page did not name a tag (network, proxy). | Pin --version, or check that https://github.com/KeiaiLab/SwarmOps/releases/latest opens. |
Cannot download …: is V a release? … | No such release, or the download failed. | Check the releases page. |
Cannot download …/SHA256SUMS. | The checksum file did not download. | Run it again. |
SHA256SUMS of vV has no line for …: nothing was installed. | The release has no archive for this target. | Pick another release. |
Neither sha256sum nor shasum is installed, … | The download cannot be checked. | Install coreutils or perl's shasum. |
… does not match its line in SHA256SUMS: nothing was installed. | A damaged or altered download. | Run it again; if it repeats, report it (SECURITY.md). |
Cannot write in DIR. | DIR belongs to another user, such as /usr/local/bin for a normal user. | Run as root (… | sudo bash), or pick a DIR you own. |
DIR/swarmops is a directory: nothing was installed. | Something else holds the name. | Move it, or pick another DIR. |
Installed, but 'DIR/swarmops --version' failed. | The binary does not start here. | Run DIR/swarmops --version to see why. |
DIR holds no SwarmOps binary: nothing was removed. | --uninstall with the wrong DIR. | Give the DIR you installed into, with --dir. |
HOME is not set: name the install directory with --dir. | A bare environment, such as some containers. | --dir /usr/local/bin as root, or another DIR. |
Three of these, as they were printed (install.sh is a downloaded copy of the script):
curl -fsSL https://get.swarmops.cc/install.sh | bash -s -- --frob
bash install.sh --version 9.9.9 --dir inst
bash install.sh --version 0.1.0 --dir /usr/local/binswarmops-install: Unknown option '--frob'; see --help.
swarmops-install: Installing SwarmOps 9.9.9 (x86_64-unknown-linux-gnu) into inst.
swarmops-install: Downloading https://github.com/KeiaiLab/SwarmOps/releases/download/v9.9.9/swarmops-9.9.9-x86_64-unknown-linux-gnu.tar.gz
curl: (22) The requested URL returned error: 404
swarmops-install: Cannot download https://github.com/KeiaiLab/SwarmOps/releases/download/v9.9.9/swarmops-9.9.9-x86_64-unknown-linux-gnu.tar.gz: is 9.9.9 a release? https://github.com/KeiaiLab/SwarmOps/releases
swarmops-install: Installing SwarmOps 0.1.0 (x86_64-unknown-linux-gnu) into /usr/local/bin.
swarmops-install: Downloading https://github.com/KeiaiLab/SwarmOps/releases/download/v0.1.0/swarmops-0.1.0-x86_64-unknown-linux-gnu.tar.gz
swarmops-install: swarmops-0.1.0-x86_64-unknown-linux-gnu.tar.gz matches SHA256SUMS.
mktemp: failed to create file via template '/usr/local/bin/.swarmops.XXXXXX': Permission denied
swarmops-install: Cannot write in /usr/local/bin.An interrupted run leaves the old binaries in place: each one is copied under a temporary name in DIR and renamed over its own name, and the temporary files go when the script ends.
swarmops: command not found after the install#
The installer edits no shell startup file. When DIR is not on PATH it prints the line to add,
as in the good run above (fish_add_path "DIR" under fish). Add it, then open a new shell. Under
the default, DIR is ~/.local/bin for a normal user and /usr/local/bin for root.
A weak version 'GLIBC_2.39' not found line on every start#
On glibc 2.34 to 2.38 (Ubuntu 22.04, Debian 12, RHEL 9), every start of the release binaries
prints this one ld.so line on stderr, and the installer says so before it downloads. Exit codes,
stdout and the MCP protocol are unaffected, so nothing needs fixing. On glibc 2.39 nothing is
printed.
Update and uninstall#
Run the same command again to update: it says Replaced swarmops <version>.. --uninstall
removes swarmops and so from DIR when they are SwarmOps' own, and leaves every store alone:
Removed swarmops so from inst. Stores are left as they are.The MCP server#
swarmops mcp is started by the client (Claude Code), not by you, so its failures show in the
client. claude mcp list starts each approved server once and reports on it.
claude mcp list says Failed to connect#
Three servers registered in one project, one good, one with a typo in the command, one with a
flag mcp does not take:
claude mcp add swarmops -e SWARMOPS_AGENT=agent/worker-1 -- swarmops mcp --repo …/acme
claude mcp add typo -- swarmopz mcp --repo …/acme
claude mcp add refused -- swarmops mcp --repo …/acme --frob
claude mcp listChecking MCP server health…
swarmops: swarmops mcp --repo …/acme - ✔ Connected
typo: swarmopz mcp --repo …/acme - ✘ Failed to connect — ENOENT: Executable not found in $PATH: "swarmopz"
refused: swarmops mcp --repo …/acme --frob - ✘ Failed to connect — CONNECTION_CLOSED: Connection closedENOENT: Executable not found in $PATH: the client cannot find the program. Check the name, runcommand -v swarmops, or register the binary by its absolute path, in the directory the installer named (~/.local/bin/swarmopsby default).CONNECTION_CLOSED: the program started and exited at once. A command line that parses asswarmops mcpbut is refused starts no server: its envelope goes to stderr, since stdout belongs to the protocol. Run the registered command yourself to read it:
swarmops mcp --repo "$PWD/acme" --frob < /dev/null{"ok":false,"op":"mcp","api":1,"error":{"code":"E_INVALID_ARG","message":"unexpected argument '--frob' found","retryable":false,"exit":2,"details":{},"hint":"Usage: swarmops mcp --repo <STORE>\nFor more information, try '--help'."},"meta":{"elapsed_ms":0,"resolved":{},"auth":"trust"}}That line is on stderr, and the exit code is 2. A SWARMOPS_FORMAT that is neither json nor
human is refused the same way (SWARMOPS_FORMAT: "yaml" is not a format: json or human). A
usage error before the word mcp (swarmops --frob mcp) is an ordinary command error, printed on
stdout with exit code 2; no server starts either.
A store that does not exist does not stop the server: it starts and serves, and every call then
fails with E_NOT_A_REPO (below).
⏸ Pending approval#
A server named in a project's .mcp.json is not started until you approve it once in that project:
claude mcp list shows ⏸ Pending approval (run `claude` to approve). Start claude in the
project and approve it.
Missing environment variables: SWARMOPS_AGENT#
This .mcp.json passes "SWARMOPS_AGENT": "${SWARMOPS_AGENT}", without a default, in a shell
where the variable is not set:
Checking MCP server health…
swarmops: swarmops mcp --repo …/acme - ⏸ Pending approval (run `claude` to approve)
MCP config diagnostics ⚠
For help configuring MCP servers, see: https://code.claude.com/docs/en/mcp
[Contains warnings] Project config (shared via .mcp.json)
Location: …/proj/.mcp.json
└ [Warning] [swarmops] mcpServers.swarmops: Missing environment variables: SWARMOPS_AGENTClaude Code then passes the text ${SWARMOPS_AGENT} on to the server as it is (the README's
Register the server says so). A server started with that value
serves reads as usual, but every write fails, because that text is no agent name (the text of a
commit tool result):
{"ok":false,"op":"commit","api":1,"error":{"code":"E_INVALID_ARG","message":"SWARMOPS_AGENT: invalid agent name \"${SWARMOPS_AGENT}\": expected <family>/<instance> of at most 255 bytes","retryable":false,"exit":2,"details":{"source":"SWARMOPS_AGENT"},"hint":"agent names look like agent/worker-17: family [a-z0-9-]+, instance [A-Za-z0-9._-]+ not starting with '.'"},"meta":{"repo":"…/acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Write the entry with an empty default, "${SWARMOPS_AGENT:-}", as the README does: the warning
goes, and an agent
started without an identity gets the clearer E_IDENTITY_REQUIRED on a write instead. Then give
each agent session its own SWARMOPS_AGENT, such as agent/worker-1, before it starts claude.
Every write fails with E_IDENTITY_REQUIRED#
The server has no identity: no --as and an empty or unset SWARMOPS_AGENT. Reads still work.
Give the session an identity, as above, and start the session again; a call cannot name one.
Every call fails with E_NOT_A_REPO#
The server opens the store for each call, so a wrong --repo shows on every call, not at start
(the text of a repo_info tool result):
{"ok":false,"op":"repo_info","api":1,"error":{"code":"E_NOT_A_REPO","message":"…/nowhere is not a store: no FORMAT file","retryable":false,"exit":6,"details":{},"hint":"swarmops init …/nowhere"},"meta":{"repo":"…/nowhere","elapsed_ms":0,"resolved":{},"auth":"trust"}}Without --repo and SWARMOPS_REPO, the message is no store named: neither --repo nor SWARMOPS_REPO gives one. Fix the path in the registration. Give it absolute: the server resolves
a relative one against the directory the client started it in.
A tool call fails with E_INVALID_ARG#
The server adds rules to the request types, so a call the CLI would take can be refused here. Each message says what to send instead:
expected_oldis not a full id.ref_setandref_deletetake the 64-hex id the ref held when the agent read it, nevermainor a prefix:expected_old "main" is not a full changeset id, which a compare-and-swap needs: …. The same rule holds for aland'sresolutions.against.- A relative
filepath.{"file": "a.json"}is refused, since the server would read it from its own working directory:the client file "a.json" is a relative path, which the server would read from its own working directory, not yours: …. Give an absolute path, or send the bytes asinlineorbase64. A path that is the server's own stdin or stdout, or that is no regular file (a FIFO, a socket, a device, a directory), is refused too. - Arguments that do not fit the type, such as a missing field:
the arguments are not a RefSetReq: missing field `new`. The tool'sinputSchemalists the fields;swarmops schema RefSetReqprints the type.
A message the server cannot serve as a request gets a JSON-RPC error instead of a tool result:
-32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params,
-32603 internal error (a bug), -32022 unsupported protocol version. The module documentation
of crates/swarmops-cli/src/mcp.rs says when each is sent, and the README's
MCP section gives the protocol versions the server speaks.
The server exits during a session#
The server exits 0 when the client closes its stdin and 1 when reading stdin or writing stdout
fails. It also ends, by SIGABRT, on any of the store's four documented aborts: a failed journal
write or sync whose cut-back fails too, frames that repair writes afresh landing anywhere but
where they were and not cut back, or an init that cannot take its FORMAT back (the README's
Store, identity and lifetime lists them). The reason is
on the server's stderr. The client sees the server exit and has to start it again; start a new
session, run swarmops verify, and look at the filesystem's free space and health before you go
on.
Where a store may live#
A store must live on a local, native Linux filesystem: ext4 with its journal (what SwarmOps is
tested and measured on), XFS or btrfs. A store directory of mode 0700, owned by the one user
every agent, server and command runs as, is the layout operations.md
recommends:
install -d -m 0700 ~/.local/state/swarmops/repos
swarmops init ~/.local/state/swarmops/repos/acme --name acmeE_UNSUPPORTED_FS#
Exit 1, not retryable. The path leads under /mnt/, where WSL mounts Windows drives. It is
checked where the path leads, on init and on every open, so a symlink into /mnt/ is refused
too:
swarmops init /mnt/d/acme
ln -s /mnt/d/stores/acme linked
swarmops --repo linked info{"ok":false,"op":"repo_init","api":1,"error":{"code":"E_UNSUPPORTED_FS","message":"store path /mnt/d/acme is not on a native Linux filesystem","retryable":false,"exit":1,"details":{"path":"/mnt/d/acme"},"hint":"put the store on a native Linux filesystem, such as under ~/.local/state/swarmops/repos/; /mnt/* holds Windows drives under WSL"},"meta":{"repo":"/mnt/d/acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}
{"ok":false,"op":"repo_info","api":1,"error":{"code":"E_UNSUPPORTED_FS","message":"store path /mnt/d/stores/acme is not on a native Linux filesystem","retryable":false,"exit":1,"details":{"path":"/mnt/d/stores/acme"},"hint":"put the store on a native Linux filesystem, such as under ~/.local/state/swarmops/repos/; /mnt/* holds Windows drives under WSL"},"meta":{"repo":"linked","elapsed_ms":0,"resolved":{},"auth":"trust"}}Put the store in the Linux filesystem, as the hint says. The check knows only /mnt/: NFS, SMB
and FUSE mounts, Docker Desktop's and virtiofs bind mounts among them, are neither detected nor
supported.
E_NOT_A_REPO#
Exit 6, not retryable. No store at that path, no store named, or a store of a layout this build does not read:
swarmops --repo nowhere info
env -u SWARMOPS_REPO swarmops info{"ok":false,"op":"repo_info","api":1,"error":{"code":"E_NOT_A_REPO","message":"…/nowhere is not a store: no FORMAT file","retryable":false,"exit":6,"details":{},"hint":"swarmops init nowhere"},"meta":{"repo":"nowhere","elapsed_ms":0,"resolved":{},"auth":"trust"}}
{"ok":false,"op":"repo_info","api":1,"error":{"code":"E_NOT_A_REPO","message":"no store named: neither --repo nor SWARMOPS_REPO gives one","retryable":false,"exit":6,"details":{},"hint":"pass --repo <store> or set SWARMOPS_REPO; swarmops init <dir> creates a store"},"meta":{"elapsed_ms":0,"resolved":{},"auth":"trust"}}- No
FORMATfile: a typo in the path, or a store not made yet. Check the path;swarmops initmakes one. - No store named: set
SWARMOPS_REPO, or pass--repo. There is no default location. FORMAT names a store layout this build does not read: the store was made by another release. operations.md says what an upgrade across aFORMATtakes.
E_EXISTS#
Exit 1, not retryable. init found a store there already:
{"ok":false,"op":"repo_init","api":1,"error":{"code":"E_EXISTS","message":"a store exists at acme already","retryable":false,"exit":1,"details":{},"hint":"swarmops --repo acme info"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Use the store (the hint describes it), or pick another directory.
Permission refused#
The store's objects, refs and checkpoint are created readable by their owner only (mode 0600).
A command that cannot read a file of the store fails with E_IO, details.fs_op and
details.store_file naming it. Here the checkpoint of a copy was made unreadable
(chmod 000 chk/meta/journal.ckpt):
{"ok":false,"op":"repo_info","api":1,"error":{"code":"E_IO","message":"i/o error: read meta/journal.ckpt: Permission denied (os error 13)","retryable":false,"exit":1,"details":{"fs_op":"read","store_file":"meta/journal.ckpt"},"hint":null},"meta":{"repo":"chk","elapsed_ms":0,"resolved":{},"auth":"trust"}}A command run as another Unix user meets the same refusal. Run every agent, server and command as
the user who owns the store, and check the mode of the file store_file names.
Identity#
Reads need no identity. A commit, a land, a ref move, an undo and a git import need one:
--as <family>/<instance>, else SWARMOPS_AGENT.
E_IDENTITY_REQUIRED#
Exit 5, not retryable.
swarmops commit -m c1 --changes c1.json{"ok":false,"op":"commit","api":1,"error":{"code":"E_IDENTITY_REQUIRED","message":"this operation changes the store, so it needs the caller's identity","retryable":false,"exit":5,"details":{},"hint":"pass --as <family>/<instance> or set SWARMOPS_AGENT, e.g. --as agent/worker-1"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Pass --as, or export SWARMOPS_AGENT. Nothing was written. For an MCP server, see
above.
An invalid agent name#
An identity that is not <family>/<instance> is E_INVALID_ARG (exit 2), with
details.source naming where it came from:
SWARMOPS_AGENT=worker-1 swarmops commit -m c1 --changes c1.json{"ok":false,"op":"commit","api":1,"error":{"code":"E_INVALID_ARG","message":"SWARMOPS_AGENT: invalid agent name \"worker-1\": expected <family>/<instance> of at most 255 bytes","retryable":false,"exit":2,"details":{"source":"SWARMOPS_AGENT"},"hint":"agent names look like agent/worker-17: family [a-z0-9-]+, instance [A-Za-z0-9._-]+ not starting with '.'"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}The family is [a-z0-9-], the instance [A-Za-z0-9._-] not starting with .: agent/worker-1,
operator/ada.
Arguments and revisions#
A revision is main, release/<name>, a changeset id or 12 or more of its first hex digits, then
any number of ~N (the N-th first parent). The examples run on a store where main holds
src/lib.rs.
E_INVALID_ARG#
Exit 2, not retryable. A malformed argument, request or command line. The message names what is wrong, and the hint often gives the grammar:
swarmops show 'not a rev'
swarmops log -n 0{"ok":false,"op":"show","api":1,"error":{"code":"E_INVALID_ARG","message":"\"not a rev\" is not a revision","retryable":false,"exit":2,"details":{"rev":"not a rev"},"hint":"a revision is main, release/<name>, a changeset id or 12 or more of its first hex digits, then optionally ~N for the N-th first parent"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}
{"ok":false,"op":"log","api":1,"error":{"code":"E_INVALID_ARG","message":"limit must be at least 1","retryable":false,"exit":2,"details":{"limit":0},"hint":null},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}A usage error of the command line is this code too, with the usage line as its hint. Other causes
met in practice: a change list that is not a JSON array of changes (the hint gives an example, and
swarmops schema ChangeInput the rest), a change that names by id a blob the store does not hold
(the hint is swarmops blob put <file>), a land --key that landed another change already, a
git export to a branch checked out in a work tree (Git export),
and the MCP server's own rules.
E_REV_NOT_FOUND#
Exit 6, not retryable. The revision names no changeset: no such ref, an id the store does not
hold, or a ~N past the first changeset.
{"ok":false,"op":"show","api":1,"error":{"code":"E_REV_NOT_FOUND","message":"release/none: not found","retryable":false,"exit":6,"details":{"rev":"release/none"},"hint":"swarmops ref list"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Run the hint: swarmops ref list for a ref name, swarmops log for an id or a ~N walk.
E_AMBIGUOUS_ID#
Exit 1, not retryable. An id prefix of 12 or more hex digits matches more than one changeset. The
hint says to give more digits of the id; swarmops log shows whole ids. Scripts and agents should
pass the full 64-hex id, which never becomes ambiguous.
E_PATH_NOT_FOUND#
Exit 6, not retryable. Nothing is at that path at that revision.
swarmops cat main:src/none.rs{"ok":false,"op":"read_file","api":1,"error":{"code":"E_PATH_NOT_FOUND","message":"nothing is at \"src/none.rs\" in main","retryable":false,"exit":6,"details":{"path":"src/none.rs","rev":"main"},"hint":"swarmops ls main:src"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{"main":"82a6c2d1…"},"auth":"trust"}}List the directory, as the hint does. A commit's delete of a path that holds no file fails the
same way.
E_IS_DIR#
Exit 1, not retryable. A file read named a directory:
{"ok":false,"op":"read_file","api":1,"error":{"code":"E_IS_DIR","message":"\"src\" is a directory at main","retryable":false,"exit":1,"details":{"path":"src","rev":"main"},"hint":"swarmops ls main:src"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{"main":"82a6c2d1…"},"auth":"trust"}}Use swarmops ls for a directory and swarmops cat for a file.
E_REF_NAME_INVALID#
Exit 1, not retryable. Refs are main and release/<name>; there are no other branches.
swarmops ref set feature/x main --create{"ok":false,"op":"ref_set","api":1,"error":{"code":"E_REF_NAME_INVALID","message":"invalid ref name \"feature/x\"","retryable":false,"exit":1,"details":{"name":"feature/x"},"hint":"a ref name is main or release/<name>, the name of letters, digits, '.', '_' and '-', not starting with '.'"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{"main":"82a6c2d1…"},"auth":"trust"}}An agent's own line of work needs no ref: it commits on a parent and lands the head. Use
release/<name> for a ref of your own, such as release/feature-x. git export gives this code
too for a branch name git does not take; its hint is git check-ref-format --branch.
E_OP_NOT_FOUND#
Exit 6, not retryable. op undo of an operation the store does not have:
{"ok":false,"op":"undo","api":1,"error":{"code":"E_OP_NOT_FOUND","message":"there is no operation 99 (the last is 1)","retryable":false,"exit":6,"details":{"last":1,"op":99},"hint":"swarmops op log"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Operations are numbered from 0; swarmops op log lists them, newest first.
Commits#
E_EMPTY_COMMIT#
Exit 1, not retryable. The change list changes nothing: it is empty, or every put writes what the path holds already.
{"ok":false,"op":"commit","api":1,"error":{"code":"E_EMPTY_COMMIT","message":"the change list changes nothing","retryable":false,"exit":1,"details":{},"hint":"pass --allow-empty to record a changeset that changes nothing"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{"main":"82a6c2d1…"},"auth":"trust"}}Check the change list against the parent (swarmops cat <parent>:<path>), or pass
--allow-empty when an empty changeset is what you want.
E_TYPE_CONFLICT#
Exit 1, not retryable. A change puts an entry below a file, or a file where a directory is.
Here src/lib.rs is a file, and the change puts src/lib.rs/x:
{"ok":false,"op":"commit","api":1,"error":{"code":"E_TYPE_CONFLICT","message":"src/lib.rs/x: a file and a directory conflict there","retryable":false,"exit":1,"details":{"path":"src/lib.rs/x"},"hint":null},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{"main":"82a6c2d1…"},"auth":"trust"}}Delete the file in the same change list first ({"path": "src/lib.rs", "op": "delete"}), or
choose another path.
E_TOO_LARGE#
Exit 1, not retryable. A put's content over 512 MiB, a commit's contents over 1 GiB together, or a
change list's text over 64 MiB. details names the change, its size and the limit:
{"ok":false,"op":"commit","api":1,"error":{"code":"E_TOO_LARGE","message":"change 0 (\"huge.bin\"): the content is 629145600 bytes, more than the 536870912 a put may hold","retryable":false,"exit":1,"details":{"file":"…/huge.bin","index":0,"limit":536870912,"path":"huge.bin","scope":"put","size":629145600},"hint":null},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{"main":"82a6c2d1…"},"auth":"trust"}}A file over 512 MiB cannot be stored. Send large content as {"file": <path>} rather than
inline or base64, which keeps the change list's text small. For a commit whose contents pass
1 GiB together, store some of them first with swarmops blob put and name them by id
({"blob": "<id>"}): a blob named by id counts nothing against that total. A git import of a
blob longer than an object holds fails with this code too.
E_UNKNOWN_PARENT#
Exit 6, not retryable. Rare: the commit's parent was in the store when the commit started, and
was gone when it recorded, because a failed journal sync took the frame that held it back. Nothing
was recorded. A parent the store never held is E_REV_NOT_FOUND instead, since the parent is
resolved first. Run swarmops log, then commit on a changeset it shows.
E_CAS_MISMATCH#
Exit 4, retryable after a re-read. A ref moves only by compare-and-swap, and another writer moved
it first. Agent 2 advanced main while agent 1 held the old head:
swarmops ref set main 03b168a3… --expect 82a6c2d1…
swarmops commit -m ours --changes ours.json --parent 82a6c2d1… --advance main{"ok":false,"op":"ref_set","api":1,"error":{"code":"E_CAS_MISMATCH","message":"ref main moved to a2c75f68…: commit your change on top of it (--parent a2c75f68…), then retry with --expect a2c75f68…; re-apply your edits to the content at a2c75f68… rather than re-sending your old files","retryable":true,"exit":4,"details":{"actual":"a2c75f68…","name":"main"},"hint":"swarmops show a2c75f68…"},"meta":{"repo":"acme","elapsed_ms":0,…}}
{"ok":false,"op":"commit","api":1,"error":{"code":"E_CAS_MISMATCH","message":"ref main moved to a2c75f68…, so this commit recorded nothing: commit your change on top of it (--parent a2c75f68… --advance main), re-applying your edits to the content at a2c75f68… rather than re-sending your old files","retryable":true,"exit":4,"details":{"actual":"a2c75f68…","committed":false,"name":"main"},"hint":"swarmops show a2c75f68…"},"meta":{"repo":"acme","elapsed_ms":3,…}}details.actual is what the ref holds now (null when it does not exist). Do not re-send your
old whole files on top of it: that reverts the other writer's edits. Land your change instead, and
the land merges it three-way onto the new head (here it conflicts, as the next section shows):
swarmops land 03b168a3…To publish without a land, read the files you changed at actual, apply your edits to them, and
commit with --parent <actual> --advance main. An op undo that finds a ref moved since the
operation fails with this code as well, and writes nothing: undo the later operations first,
newest first, or land a change that reverses the operation.
Landing and undo#
E_CONFLICT#
Exit 3, not retryable as it is. The land's change and the target both changed the same lines, and
nothing was recorded. Continuing the example above, the land of 03b168a3… (--pretty):
{
"ok": false,
"op": "land",
"api": 1,
"error": {
"code": "E_CONFLICT",
"message": "1 path(s) of the change conflict with the target's head a2c75f68…: nothing was recorded",
"retryable": false,
"exit": 3,
"details": {
"report": {
"base": "82a6c2d1…",
"files": [
{
"base_blob": "f5f5220a…",
"hunks": [
{
"base": {
"first": 1,
"last": 1,
"text": "pub fn f() {}\n"
},
"id": 0,
"ours": {
"first": 1,
"last": 1,
"text": "pub fn ours() {}\n"
},
"theirs": {
"first": 1,
"last": 1,
"text": "pub fn theirs() {}\n"
}
}
],
"kind": "content",
"ours_blob": "d0ddd9c1…",
"path": "src/lib.rs",
"theirs_agent": "agent/worker-2",
"theirs_blob": "7504e69e…",
"theirs_changeset": "a2c75f68…"
}
],
"kinds": [
"content"
],
"ours": "03b168a3…",
"theirs": "a2c75f68…",
"truncated": false
}
},
"hint": "swarmops cat a2c75f68…:src/lib.rs, then swarmops land 03b168a3… --resolve <file> with each path's resolution and against = a2c75f68…"
},
…
}The report names each conflicting file, its lines on each side and the agent that last changed it
on the target. Resolve without a checkout: write a resolutions file against the report's
theirs, then land again with --resolve. Each path takes {"take": "ours"},
{"take": "theirs"}, {"content": …} or {"delete": true} (swarmops land --help has the
format, and operations.md a full run):
{"against": "<the report's theirs, all 64 hex digits>",
"files": {"src/lib.rs": {"content": {"inline": "pub fn both() {}\n"}}}}A path the target changed again since the report is stale: the land fails with E_CONFLICT
again, naming it in stale. Read the new theirs and resolve against it. A report whose hunk
texts pass --max-conflict-bytes (32768 by default) leaves the texts out and says truncated;
read the sides with swarmops cat.
E_NOTHING_TO_LAND#
Exit 1, not retryable. The land has nothing to add: its head is on the target (on_target), or the
target holds its change already (no_changes).
swarmops land main{"ok":false,"op":"land","api":1,"error":{"code":"E_NOTHING_TO_LAND","message":"nothing to land: the head is on the target","retryable":false,"exit":1,"details":{"reason":"on_target"},"hint":null},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{"main":"a2c75f68…"},"auth":"trust"}}Usually the change landed already, for example by an earlier run of the same land. To make such a
retry report the first land instead, give each land a --key (a uuid v7, say): run again with the
same key, it replays (status: replayed).
E_CAS_RETRY_EXHAUSTED#
Exit 4, retryable. The target moved before each of the land's attempts could publish, and nothing
was recorded. A land merges again and retries up to --max-attempts times (8 by default). This
one was forced with --max-attempts 1 while two other writers advanced main in a loop:
{"ok":false,"op":"land","api":1,"error":{"code":"E_CAS_RETRY_EXHAUSTED","message":"the target moved before each of the land's 1 attempt(s) could publish (it names bcc33bb8…): nothing was recorded","retryable":true,"exit":4,"details":{"actual":"bcc33bb8…","attempts":1},"hint":"run the land again: nothing of this one was recorded"},"meta":{"repo":"race","elapsed_ms":40,…}}Run the land again, as the hint says. If it keeps coming at the default of 8 attempts, too many writers publish to one ref at once; see A busy store.
E_NOTHING_TO_UNDO#
Exit 1, not retryable. op undo of an operation that moved no ref, such as a plain commit without
--advance:
{"ok":false,"op":"undo","api":1,"error":{"code":"E_NOTHING_TO_UNDO","message":"operation 0 moved no ref, so there is nothing to undo","retryable":false,"exit":1,"details":{"op":0},"hint":"swarmops op log"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Find the operation that moved the ref with swarmops op log (its ref_moves) and undo that one.
A busy store#
E_BUSY#
Exit 4, retryable. Every journal, index and ref write holds the store's lock, meta/lock (an
flock). A writer waits up to 10 s for it, then gives up; nothing was committed. Here
flock acme/meta/lock sleep 14, standing in for a backup, held the lock for longer than that:
swarmops commit -m notes --changes notes.json --advance main{"ok":false,"op":"commit","api":1,"error":{"code":"E_BUSY","message":"the metadata store is busy: another writer holds meta/lock","retryable":true,"exit":4,"details":{},"hint":"retry: another writer held the store's lock for the whole wait"},"meta":{"repo":"acme","elapsed_ms":10009,"resolved":{"main":"a2c75f68…"},"auth":"trust"}}Reads take no lock: a swarmops log run while the lock was still held answered at once. A read
can still get E_BUSY in one case: when commits land during every one of its 32 attempts to
take a snapshot. Its hint says which kind of wait ran out.
What to run:
- Run the same command again. Nothing of the failed one was recorded (a commit's objects may remain, unnamed, and the retry reuses them).
- If it repeats, find what holds the lock. In
lsof'sFDcolumn,Wmarks the holder (here a second hold of the same kind,flock acme/meta/lock sleep 5):
lsof acme/meta/lockCOMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
flock 816551 evo 3rW REG 9,0 0 63116628 acme/meta/lock
sleep 816581 evo 3r REG 9,0 0 63116628 acme/meta/lock- A backup under
flockis the usual holder: hold the lock briefly, or copy the objects first without it (operations.md). The kernel releases the lock when a process dies, so a crash never leaves a stale lock behind.
Under load alone, E_BUSY is rare. In the v0.1.0 measurement on the maintainers' host, 32
processes made 32,000 commits through the binary on one store, at 71.7 commits/s: none got
E_BUSY, and the slowest commit took 712 ms (perf.md). A rising count means more
contention than the lock serves: too many writers on one store, a slow disk, or a long lock
holder. Watch it as What to monitor shows. The same holds for
E_CAS_RETRY_EXHAUSTED.
A damaged store#
A crash at any point leaves a store that the next writer repairs. Damage that no writer repairs,
such as a changed object file, is what verify reports. There is no repair tool yet (M6): the
remedy is a restore from a backup.
What repairs itself at the next write#
A writer that dies mid-write can leave a torn tail on the journal: the bytes of a transaction that
never reached its commit point. That is no damage. Reads pass over it, verify reports nothing,
and the next write cuts it off and says so in meta.recovered. Four stray bytes appended to a
copy's journal, then a commit:
printf 'torn' >> torn/meta/journal.log
swarmops --repo torn verify
swarmops --repo torn commit -m notes --changes notes.json --advance main --pretty{"ok":true,"op":"verify","api":1,"result":{"objects_checked":12,"changesets_checked":3,"problems":[],"findings":[]},"meta":{"repo":"torn","elapsed_ms":1,"resolved":{},"auth":"trust"}}
{
"ok": true,
"op": "commit",
…
"meta": {
"repo": "torn",
"elapsed_ms": 50,
"resolved": {
"main": "a2c75f68…"
},
"auth": "trust",
"recovered": {
"log_cut": {
"at": 484,
"bytes": 4
},
"frames_listed": 0,
"frames_rewritten": 0,
"off_bytes_dropped": 0,
"index_records_dropped": 0,
"views_rebuilt": false,
"records_replayed": 0,
"refs_replayed": 0
}
}
}The same repair lists frames a crashed writer committed but never listed, drops the torn end of
the index and of journal.off, finishes an interrupted index compaction, and replays changesets
and ref files from the journal; each count in meta.recovered says how much of which. A commit,
ref set and ref delete report it. It is no warning and needs no action. Seen now and then, it
means agents get killed; seen often, something keeps killing writers.
Two kinds of leftover are not repaired, and are safe to remove by hand with every writer stopped:
the files of interrupted writes in objects/tmp/ and meta/derived/tmp/, and the derived values
in meta/derived/ (operations.md).
E_STORE_CORRUPT#
Exit 1, not retryable. Stored data failed verification. One byte of one object file changed, in a copy of the store:
swarmops --repo flipped verify
swarmops --repo flipped verify --deep --pretty{"ok":false,"op":"verify","api":1,"error":{"code":"E_STORE_CORRUPT","message":"verify found a problem: object 03b168a3… is corrupt","retryable":false,"exit":1,"details":{"changesets_checked":3,"findings":[{"kind":"corrupt_object","object":"03b168a3…"}],"objects_checked":9,"problems":["object 03b168a3… is corrupt"]},"hint":"swarmops verify --deep"},"meta":{"repo":"flipped","elapsed_ms":2,"resolved":{},"auth":"trust"}}
{
"ok": false,
"op": "verify",
"api": 1,
"error": {
"code": "E_STORE_CORRUPT",
"message": "verify found a problem: object 03b168a3… is corrupt",
"retryable": false,
"exit": 1,
"details": {
"changesets_checked": 3,
"findings": [
{
"kind": "corrupt_object",
"object": "03b168a3…"
}
],
"objects_checked": 15,
"problems": [
"object 03b168a3… is corrupt"
]
},
"hint": "details.findings names each damaged object, record or ref by kind and id: restore them from a backup of the store"
},
…
}The damaged object here is a changeset, which plain verify reads; damage to a blob shows only
under --deep, which re-hashes every object file. details.findings names each damaged object,
record or ref by kind (corrupt_object, missing_object, …) and id. Any other command that
meets the damage fails with this code too, and a metadata file that does not parse also names
details.fs_op and details.store_file, such as {"fs_op": "read", "store_file": "meta/journal.ckpt"}.
What to run: stop every writer, keep the damaged store aside, and restore it
(below). Run verify --deep after a crash or a power loss, on every
backup, before an upgrade and on a schedule. known-limitations.md
lists the kinds of damage no writer repairs.
E_IO#
Exit 1, not retryable. The filesystem refused an operation. details.fs_op names the operation,
and details.store_file the file, relative to the store's root:
swarmops --repo nooff info{"ok":false,"op":"repo_info","api":1,"error":{"code":"E_IO","message":"i/o error: open meta/journal.off: No such file or directory (os error 2)","retryable":false,"exit":1,"details":{"fs_op":"open","store_file":"meta/journal.off"},"hint":null},"meta":{"repo":"nooff","elapsed_ms":0,"resolved":{},"auth":"trust"}}A file of the store is gone (here meta/journal.off, removed by hand): restore the store. When
a journal append was refused for want of permission or on a read-only filesystem,
details.committed is false and the hint says what to fix first. The journal was made
read-only in a copy:
{
"ok": false,
"op": "commit",
"api": 1,
"error": {
"code": "E_IO",
"message": "the transaction did not commit: a journal append failed, and what it wrote was cut back off the journal: append_durable meta/journal.log: Permission denied (os error 13)",
"retryable": false,
"exit": 1,
"details": {
"committed": false,
"fs_op": "append_durable",
"store_file": "meta/journal.log"
},
"hint": "make the store's files writable for this user, then run the same command again: nothing of this one was committed"
},
…
}After chmod u+w on the file, the same command committed. Check the owner and mode of the
store's files (one Unix user, above), whether the filesystem went
read-only (dmesg, mount), and its free space (df).
E_IO_TRANSIENT#
Exit 4, retryable. A journal append failed for another reason, and the store cut what it wrote
back off the journal: details.committed is false, and details.fs_op and
details.store_file name the append. Nothing was committed, so run the same command again. If it
keeps failing, look at the disk as for E_IO. A failure after the commit point is no error: the
operation succeeds with a warning in warnings, and the next write finishes the work.
Restore from a backup#
operations.md gives the whole procedure. In short, with every writer stopped (the agents' sessions and their MCP servers, and any script):
mv "$SWARMOPS_REPO" "$SWARMOPS_REPO.damaged"
cp -a "$BACKUP" "$SWARMOPS_REPO"
swarmops verify --deep
swarmops git import "$GIT_SIDE" --ref review --into main
swarmops git export "$GIT_SIDE" --ref main --branch reviewThe import takes back what was exported after the backup. What was written after the backup and
never exported is lost. Without any backup, a new store made from the git side (swarmops init,
then swarmops git import of each branch) exports to the same commits.
E_INTERNAL#
Exit 1, not retryable. A bug in SwarmOps, such as a caught panic. Run swarmops verify --deep to
check the store, then report it as a GitHub issue through the bug report template, which asks
for the commands, their envelopes, the verify --deep envelope and the SwarmOps version.
Git import#
swarmops git import PATH reads a git repository on this host. The examples import from
acme.git, a bare clone of a repository with the trunk stable.
E_GIT_NOT_A_REPO#
Exit 6, not retryable. No git repository at that path:
{"ok":false,"op":"git_import","api":1,"error":{"code":"E_GIT_NOT_A_REPO","message":"no git repository it can open there: \"nowhere.git\" does not appear to be a git repository: Could not retrieve metadata of \"nowhere.git\": No such file or directory (os error 2)","retryable":false,"exit":6,"details":{"git_path":"nowhere.git"},"hint":null},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Give the path of a work tree or of a bare repository; a relative path is read from the working
directory. git export gives the same code, with the hint git init --bare <path>.
E_GIT_REF_NOT_FOUND#
Exit 6, not retryable. --ref names nothing in the repository, or an object that is no commit:
{"ok":false,"op":"git_import","api":1,"error":{"code":"E_GIT_REF_NOT_FOUND","message":"no ref and no commit is named \"no-such-branch\"","retryable":false,"exit":6,"details":{"git_ref":"no-such-branch"},"hint":"git -C acme.git for-each-ref"},"meta":{"repo":"acme","elapsed_ms":1,"resolved":{},"auth":"trust"}}--ref is HEAD by default. The hint lists the repository's refs.
E_GIT_UNSUPPORTED#
Exit 1, not retryable. A repository the import does not read as it is; details.reason says why:
reason | Means | What to do |
|---|---|---|
shallow | A shallow clone, whose oldest commits lack their parents. | git -C <path> fetch --unshallow (the hint) |
object_format | A SHA-256 repository. | Not supported. |
ref_storage | reftable refs. | Not supported. |
special_file | A FIFO, a socket or a device where a file of the git directory should be (details.file). | Replace it with the file. |
file_too_long | A ref, config or alternates file over 16 MiB (details.file, size, max). | Check the file. |
parents | A commit of more than 255 parents (details.commit). | Not supported. |
mode | An entry whose mode is no kind of entry (details.commit). | Not supported. |
{"ok":false,"op":"git_import","api":1,"error":{"code":"E_GIT_UNSUPPORTED","message":"the repository is not read as it is: it is a shallow clone, whose oldest commits lack their parents","retryable":false,"exit":1,"details":{"reason":"shallow"},"hint":"git -C shallow.git fetch --unshallow"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}git export refuses the same first five for the repository it writes.
E_GIT_READ#
Exit 1, not retryable. An object the import needs is missing (missing, as in a partial clone,
which lacks blobs), does not hash to its id (corrupt) or does not parse (malformed), or a read
failed (io). A --filter=blob:none clone, into a new store:
{"ok":false,"op":"git_import","api":1,"error":{"code":"E_GIT_READ","message":"the repository lacks object 5626abf0…","retryable":false,"exit":1,"details":{"object":"5626abf0…","reason":"missing"},"hint":"git -C partial.git fsck"},"meta":{"repo":"fresh","elapsed_ms":1,"resolved":{},"auth":"trust"}}Import from a full clone. For damage, git fsck (the hint) says what git thinks of it.
E_GIT_IMPORT_LOSSY#
Exit 1, not retryable. A commit holds content a changeset cannot keep as git has it. The import
stops before that commit, and the target ref does not move. details names the commit, the
path and the class:
swarmops git import withsub --into release/sub
swarmops git import withsub --into release/sub --lossy gitlinks{"ok":false,"op":"git_import","api":1,"error":{"code":"E_GIT_IMPORT_LOSSY","message":"commit 40ef66a7… holds gitlinks content at lib/sub: a gitlink, to commit 8a314e05…; allowing the class drops or normalizes it","retryable":false,"exit":1,"details":{"class":"gitlinks","commit":"40ef66a7…","path":"lib/sub"},"hint":"run it again with --lossy gitlinks to drop or normalize it and mark the changeset (git.lossy)"},"meta":{"repo":"acme","elapsed_ms":1,"resolved":{},"auth":"trust"}}
{"ok":true,"op":"git_import","api":1,"result":{"imported":1,"skipped":0,"head_changeset":"d2cac5ec…","git_head_sha":"40ef66a7…","target_ref":"release/sub","ref_moved":true,"lossy":[{"commit":"40ef66a7…","changeset":"d2cac5ec…","classes":["gitlinks"]}],"lossy_total":1},"meta":{"repo":"acme","elapsed_ms":176,"resolved":{},"auth":"trust"}}The classes are gitlinks (a submodule's entry), paths (a name that is not UTF-8, or . or
..), modes, tree_order, empty_trees and nonstandard (a commit whose bytes its fields
cannot give back). Allow a class with --lossy <class> (repeatable, or all) only when losing it
is fine: the changeset records the loss in its git.lossy metadata, and a history imported with
a loss exports as new commits, not as the commits it came from.
E_GIT_IMPORT_DIVERGED#
Exit 3, not retryable as it is. The target's head is not the import of --ref's commit or of an
ancestor of it, so nothing was written; there is no silent merge. It happens when the git history
was rewritten, or when a changeset was committed on the target that git export never wrote.
Here a native commit went onto main while git gained the commit three:
swarmops commit -m native --changes b.json --advance main
swarmops git import acme.git --ref stable{"ok":false,"op":"git_import","api":1,"error":{"code":"E_GIT_IMPORT_DIVERGED","message":"main has diverged from the git history: its head ed56eaaf… stands for git commit cc5b5431…, which the tip 4dd39525… does not reach","retryable":false,"exit":3,"details":{"base":"cc5b5431…","git_head":"4dd39525…","target_head":"ed56eaaf…","target_ref":"main"},"hint":"swarmops log main"},"meta":{"repo":"acme","elapsed_ms":1,"resolved":{},"auth":"trust"}}Merge the two sides in git, where a person can review the merge, then import the result:
- Export the store's side to a branch of its own:
swarmops git export acme.git --branch review. - Merge the trunk into it with git, in a clone of the git side, and push it back.
- Import the merged branch: an import continues a head that
git exportwrote.
git -C work merge -q --no-edit origin/stable && git -C work push -q origin review
git -C acme.git log --oneline --graph review
swarmops git import acme.git --ref review --into main* e15933b Merge remote-tracking branch 'origin/stable' into review
|\
| * 4dd3952 three
* | c1c374f native two
* | cc5b543 native
|/
* 8a314e0 two
* 222d9ba one
{"ok":true,"op":"git_import","api":1,"result":{"imported":2,"skipped":2,"head_changeset":"7ac0ad8a…","git_head_sha":"e15933bc…","target_ref":"main","ref_moved":true,"old_head":"ca467904…","lossy":[],"lossy_total":0},"meta":{"repo":"acme","elapsed_ms":153,"resolved":{},"auth":"trust"}}The daily loop avoids this: it imports what git gained before
agents land, and exports to a review branch rather than to the trunk. When the git history was
rewritten instead (a force-push), details.base names the commit the store stands on. To keep
both, import the rewritten history into a new ref (--into release/<name>); to follow git, create
the store again from it.
Git export#
swarmops git export PATH writes into a git repository on this host. It needs no identity.
E_GIT_EXPORT_DIVERGED#
Exit 3, not retryable as it is. The branch holds commits that are no export of the ref's head or
of an ancestor of it, so the branch did not move. details.reason says why, and the hint what
to do:
reason | Means | What to do |
|---|---|---|
branch_ahead | Commits made in git on top of the export. | Import them first: swarmops git import PATH --ref <branch>. |
both_moved | Git and the store both moved past the last export. | Merge in git, as for E_GIT_IMPORT_DIVERGED. |
branch_moved | The branch names a commit that is no export of this history. | Export to another branch, or --force. |
source_moved_back | The ref is behind the export the branch holds, after an undo or a ref set backwards. | swarmops op log, then decide; --force moves the branch back. |
lossy_history | The branch holds the original commits of a history imported lossily. | Export to another branch, or --force. |
moved_during_export | Another writer moved the branch while this export wrote. | Run the export again; what it wrote stays. |
{"ok":false,"op":"git_export","api":1,"error":{"code":"E_GIT_EXPORT_DIVERGED","message":"the branch stable has diverged from ed56eaaf… (both_moved): it names 4dd39525…","retryable":false,"exit":3,"details":{"branch":"stable","exported_changeset":"9f4cb058…","git_head":"4dd39525…","head_changeset":"ed56eaaf…","reason":"both_moved"},"hint":"both git and main moved past 9f4cb058…, and nothing is merged: git -C acme.git log refs/heads/stable shows the git side, and --force replaces it"},"meta":{"repo":"acme","elapsed_ms":1,"resolved":{"main":"ed56eaaf…"},"auth":"trust"}}--force moves the branch all the same and replaces what git holds there; read the git side
first (the hint's git log). Exporting to a review branch rather than to the trunk keeps commits
made in git and exports apart.
Exporting to a checked-out branch#
E_INVALID_ARG (exit 2), with or without --force: an export does not move the branch a work
tree has checked out, as git push does not.
{"ok":false,"op":"git_export","api":1,"error":{"code":"E_INVALID_ARG","message":"the branch stable is checked out in the repository's work tree, which an export does not move","retryable":false,"exit":2,"details":{"branch":"stable","reason":"checked_out"},"hint":"detach the work tree's HEAD (git -C src switch --detach), or export to another branch (--branch <name>)"},"meta":{"repo":"acme","elapsed_ms":0,"resolved":{},"auth":"trust"}}Export into a bare repository (git init --bare), or to another branch.
E_GIT_WRITE#
Exit 1, not retryable as it is. The repository could not be written; details.reason says what
failed: ref_locked (the branch's lock file exists), ref_name_conflict (a ref stands where the
branch's name needs a directory, or the other way round) or io (details.path names the file).
The branch is as it was, and the objects written stay. A lock file left in place:
swarmops git export acme.git --branch review
rm acme.git/refs/heads/review.lock
swarmops git export acme.git --branch review{"ok":false,"op":"git_export","api":1,"error":{"code":"E_GIT_WRITE","message":"refs/heads/review.lock is there: another writer holds the branch, or one that died left it","retryable":false,"exit":1,"details":{"path":"refs/heads/review.lock","reason":"ref_locked"},"hint":"once no git process runs in the repository, remove refs/heads/review.lock from its git directory (git -C acme.git rev-parse --git-path refs/heads/review.lock names it), as git itself says, then run the export again"},"meta":{"repo":"acme","elapsed_ms":149,"resolved":{},"auth":"trust"}}
{"ok":true,"op":"git_export","api":1,"result":{"exported":0,"objects_written":0,"head_changeset":"ca467904…","head_sha":"c1c374f6…","branch":"review","old_sha":"cc5b5431…","branch_moved":true,"incremental":true,"rewritten":0},"meta":{"repo":"acme","elapsed_ms":32,"resolved":{"main":"ca467904…"},"auth":"trust"}}Remove the lock file only once no git process runs in the repository, as the hint says. The run
after it wrote no object (objects_written: 0), since the failed run had written them, and moved
the branch. An export that dies while it moves the branch leaves such a file too.
Codes not raised yet#
E_REF_NOT_FOUND (exit 6), E_AUTH (exit 5), E_POLICY_DENIED (exit 5) and
E_MAINTENANCE_BUSY (exit 4, retryable) are in the schema so that a client can match them now, but
no operation raises them: no operation looks a ref up by name (a missing ref is
E_REV_NOT_FOUND), and tokens, policy.toml and gc's maintenance lease come later
(known-limitations.md). A client that meets a code newer
than its own table reads exit and retryable from the error itself.
Operating SwarmOps
This guide is for whoever runs SwarmOps for a swarm of coding agents on one host. It installs the binary, creates a store, gives agents the store through MCP,…
Give agents the store
swarmops mcp serves the store's operations to an agent as an MCP server on stdin and stdout. This page registers it with Claude Code, lists what agents can…