gabriel / muse public
plumbing.md markdown
1,610 lines 48.6 KB
Raw
sha256:bd58aa96f2ed23218e823e8bb60da938ce95c3ec8bf6658bb1317873a460720b docs: fix systemic --format→--json drift and broken plumbin… Sonnet 5 6 days ago

Muse Low-level Commands Reference

Low-level commands are the machine-readable layer of the Muse CLI. They output JSON, stream bytes without size limits, use predictable exit codes, and compose cleanly in shell pipelines and agent scripts.

If you want to automate Muse — write a script, build an agent workflow, or integrate Muse into another tool — these commands are the right entry point. The higher-level porcelain commands (muse commit, muse merge, etc.) call these internally.

These commands were previously namespaced under muse plumbing <cmd> and are now top-level: muse <cmd>. No sub-namespace needed.


Quick Index

Command Purpose
hash-object Compute SHA-256 of a file; optionally store it
cat-object Stream raw bytes or metadata for a stored object
verify-object Re-hash stored objects to detect corruption
rev-parse Resolve branch name / HEAD / prefix → full commit ID
read-commit Print full commit JSON record
read-snapshot Print full snapshot JSON record
ls-files List tracked files and their object IDs
commit-tree Create a commit from an existing snapshot
update-ref Move or delete a branch ref
commit-graph BFS walk of the commit DAG
merge-base Find the lowest common ancestor of two commits
snapshot-diff Diff two snapshots: added / modified / deleted
pack-objects Pack commits, snapshots, and objects into an MPack
unpack-objects Apply an MPack to the local store
verify-pack Three-tier integrity check for an MPack
show-ref List all branch refs and their commit IDs
symbolic-ref Read or write the HEAD symbolic reference
for-each-ref Iterate refs with full commit metadata; sort and filter
name-rev Map commit IDs to <branch>~N names
check-ref-format Validate branch/ref names against naming rules
check-ignore Test whether paths match .museignore rules
check-attr Query .museattributes for merge strategies
domain-info Inspect the active domain plugin and its schema
ls-remote List refs on a remote without changing local state

The Plumbing Contract

Every plumbing command follows the same rules:

Property Guarantee
Output Text (or binary, for pack-objects) by default; JSON with --json
Exit 0 Success — output is valid and complete
Exit 1 User error — bad input, ref not found, invalid ID
Exit 3 Internal error — I/O failure, integrity check failed
Idempotent reads Reading commands never modify state
Idempotent writes Writing the same object twice is a no-op
Encoding All text I/O is UTF-8
Object IDs Always the canonical sha256:<64 lowercase hex> form
Short flags Every flag has a -x short form

Every command accepts a boolean --json / -j flag. There is no --format flag anywhere in the CLI — flags are booleans, not choice-style value flags. Passing --json switches output from plain text (the default for most commands) to structured JSON; omitting it prints the human-readable default shown in each command's "default output" example below. Check each command's own section — some (ls-remote, show-ref, read-snapshot, verify-object, domain-info, symbolic-ref, for-each-ref, name-rev, check-ref-format, check-ignore, check-attr) default to plain text and require --json for structured output, matching git-style plumbing conventions.

JSON output is always printed to stdout. When an error occurs in --json mode, the error is emitted as a {"error": "..."} (or {"status": "error", "error": "..."}) object on stdout — not stderr — so scripts that parse stdout can detect the failure without inspecting exit codes separately.

Every --json response is wrapped in a standard envelope carrying muse_version, schema, exit_code, duration_ms, timestamp, and warnings, in addition to the command-specific fields shown in each example below. The examples in this document omit the envelope for brevity and show only the command-specific fields — the envelope keys are always present alongside them. For example, muse rev-parse HEAD --json actually prints:

{"muse_version": "0.2.1rc4", "schema": 1, "exit_code": 0, "duration_ms": 0.9,
 "timestamp": "2026-03-18T12:00:00.000Z", "warnings": [],
 "ref": "HEAD", "commit_id": "sha256:a3f2...c8d1"}

Command Reference

hash-object — compute a content ID

muse hash-object <file> [--stdin] [-w] [-j]

Computes the SHA-256 content address of a file or stdin stream. Identical bytes always produce the same ID; this is how Muse deduplicates storage. With --write (-w) the object is also stored in .muse/objects/ so it can be referenced by future snapshots and commits. The file is streamed at 64 KiB at a time — arbitrarily large blobs never spike memory.

Flags

Flag Short Default Description
--stdin off Read content from stdin instead of a file path
--write -w off Store the object after hashing
--json -j off Emit machine-readable JSON (default: plain text object ID)

Output — default (plain text)

sha256:a3f2...c8d1

Output — --json

{"object_id": "sha256:a3f2...c8d1", "stored": false, "size_bytes": 5}

stored is true only when --write is passed and the object was not already in the store.

Exit codes: 0 success · 1 path not found or is a directory · 3 I/O write error or integrity check failed


cat-object — read a stored object

muse cat-object <object-id> [-j] [--inline]
muse cat-object --batch | --batch-check

Reads a content-addressed object from .muse/objects/. By default the raw bytes are streamed to stdout at 64 KiB at a time — pipe to a file, another process, or a network socket without any size ceiling. With --json a JSON metadata summary is printed instead of the content.

Object IDs must be given in the canonical sha256:<64-hex> form — bare hex is rejected.

Flags

Flag Short Default Description
--json -j off Emit JSON metadata instead of raw bytes
--inline off With --json, embed the object content as base64 in content_b64 (requires --json)
--batch off Batch mode: read object IDs from stdin, one per line; emit <oid> blob <size>\n<content>\n per object (<oid> missing\n for absent/invalid ones)
--batch-check off Like --batch but header-only — no content bytes

Output — default (raw bytes)

hello

Output — --json

{"object_id": "sha256:a3f2...c8d1", "present": true, "size_bytes": 4096}

With --inline:

{"object_id": "sha256:a3f2...c8d1", "present": true, "size_bytes": 5,
 "content_b64": "aGVsbG8="}

When the object is absent and --json is used, present is false and size_bytes is 0 (exit 1). When raw mode is used and the object is absent, the error goes to stderr (exit 1).

Exit codes: 0 found · 1 not found or invalid ID format · 3 I/O read error. Batch mode (--batch/--batch-check) always exits 0 — missing objects are reported inline, not as failures.


rev-parse — resolve a ref to a commit ID

muse rev-parse <ref> [--abbrev-ref] [-j]

Resolves a branch name, HEAD, or an abbreviated SHA prefix to the full 64-character commit ID. Use this to canonicalise any ref before passing it to other commands.

Arguments

Argument Description
<ref> Branch name, HEAD, full commit ID, or unique prefix

Flags

Flag Short Default Description
--abbrev-ref off Resolve to the branch name instead of the commit ID
--json -j off Emit machine-readable JSON (default: plain text commit ID)

Output — default (plain text)

sha256:a3f2...c8d1

Output — --json

{"ref": "main", "commit_id": "sha256:a3f2...c8d1"}

Output — --abbrev-ref

{"ref": "HEAD", "branch": "main"}

Text mode with --abbrev-ref prints just main.

Ambiguous or unresolvable prefixes return a JSON error object on stdout (exit 1): {"ref": "...", "commit_id": null, "error": "not found"}.

Exit codes: 0 resolved · 1 not found, ambiguous, or empty ref


ls-files — list files in a snapshot

muse ls-files [--commit <id>] [--path-prefix <prefix>] [-j]

Lists every file tracked in a commit's snapshot together with its content object ID. Defaults to the HEAD commit of the current branch.

Flags

Flag Short Default Description
--commit -c HEAD Commit ID to inspect
--path-prefix -p Only list files whose path starts with this prefix
--json -j off Emit machine-readable JSON (default: tab-separated text)

Output — default (tab-separated, suitable for awk / cut)

sha256:c1d2...a3b4	tracks/bass.mid
sha256:e5f6...b7c8	tracks/drums.mid
sha256:09ab...cd10	tracks/piano.mid

Output — --json

{
  "status":      "ok",
  "error":       "",
  "commit_id":   "sha256:a3f2...c8d1",
  "snapshot_id": "sha256:b7e4...f912",
  "branch":      "main",
  "path_prefix": null,
  "file_count":  3,
  "files": [
    {"path": "tracks/bass.mid",  "object_id": "sha256:c1d2...a3b4"},
    {"path": "tracks/drums.mid", "object_id": "sha256:e5f6...b7c8"},
    {"path": "tracks/piano.mid", "object_id": "sha256:09ab...cd10"}
  ]
}

All keys are always present so agents can read them without dict.get guards. "branch" is null when --commit is given explicitly (no branch resolution occurs). "path_prefix" is null when no --path-prefix filter was applied. Files are sorted by path.

On error, --json mode emits {"status": "error", "error": "...", "exit_code": 1} to stdout.

Exit codes: 0 listed · 1 commit or snapshot not found, or invalid argument


read-commit — print full commit metadata

muse read-commit <commit-id> [--fields FIELD,...] [-j]

Emits the complete JSON record for a commit. Accepts a full 64-character ID or a unique prefix. Default output (no --json) is a compact one-line summary; the full structured record requires --json.

Flags

Flag Short Default Description
--fields (all) Comma-separated list of fields to include in JSON output — reduces response size for agent pipelines
--json -j off Emit machine-readable JSON (default: compact one-line text)

Output — default (plain text)

sha256:a3f2...c8d1  main  gabriel  2026-03-21T12:00:00+00:00  Add verse melody

Output — --json

{
  "commit_id": "sha256:a3f2...c8d1",
  "branch": "main",
  "snapshot_id": "sha256:b7e4...f912",
  "message": "Add verse melody",
  "committed_at": "2026-03-18T12:00:00+00:00",
  "parent_commit_id": "sha256:ff01...23ab",
  "parent2_commit_id": null,
  "author": "gabriel",
  "agent_id": "",
  "model_id": "",
  "toolchain_id": "",
  "sem_ver_bump": "none",
  "breaking_changes": [],
  "reviewed_by": [],
  "test_runs": 0
}

Use --fields to trim the response, e.g. muse read-commit <id> --fields commit_id,branch,message,committed_at --json.

Error conditions always produce JSON on stdout so scripts can parse them without inspecting stderr.

Exit codes: 0 found · 1 not found, ambiguous prefix, or invalid ID format


read-snapshot — print full snapshot metadata

muse read-snapshot <snapshot-id> [--no-manifest] [--path-prefix <prefix>] [-j]

Emits the complete JSON record for a snapshot. Every commit references exactly one snapshot. Use ls-files --commit <id> if you want to look up a snapshot from a commit ID rather than the snapshot ID directly.

Flags

Flag Short Default Description
--no-manifest off Omit the file manifest, returning only snapshot_id, created_at, file_count. Only valid with --json
--path-prefix -p Filter the manifest to paths starting with this prefix
--json -j off Emit machine-readable JSON (default: compact one-line text)

Output — default (plain text)

sha256:b7e4...f912  3 files  2026-03-21T12:00:00+00:00

Output — --json

{
  "snapshot_id": "sha256:b7e4...f912",
  "created_at": "2026-03-18T12:00:00+00:00",
  "file_count": 3,
  "manifest": {
    "tracks/bass.mid":  "sha256:c1d2...a3b4",
    "tracks/drums.mid": "sha256:e5f6...b7c8",
    "tracks/piano.mid": "sha256:09ab...cd10"
  }
}

With --no-manifest --json:

{"snapshot_id": "sha256:b7e4...f912", "created_at": "2026-03-18T12:00:00+00:00", "file_count": 3}

Exit codes: 0 found · 1 not found, invalid ID format, or --no-manifest used without --json


commit-tree — create a commit from a snapshot ID

muse commit-tree -s <snapshot-id> [-p <parent-id>]... [-m <message>] [-a <author>] [-b <branch>] [--agent-id <id>] [--model-id <id>] [--toolchain-id <id>] [-j]

Low-level commit creation. The snapshot must already exist in the store. Both the snapshot ID and any parent IDs are validated as proper 64-character SHA-256 hex strings before any I/O is attempted. Use --parent / -p once for a linear commit and twice for a merge commit (at most two parents are supported). The commit is written to .muse/commits/ but no branch ref is updated — use update-ref to advance a branch to the new commit.

Flags

Flag Short Required Description
--snapshot -s SHA-256 snapshot ID
--parent -p Parent commit ID (repeat once for merges)
--message -m Commit message
--author -a Author name
--branch -b Branch name to record (default: current branch)
--agent-id Stable agent identifier (e.g. counterpoint-bot)
--model-id Model identifier (e.g. claude-opus-4); empty for human authors
--toolchain-id Toolchain that produced this commit (e.g. cursor-agent-v2)
--json -j Emit machine-readable JSON (default: bare commit ID as text)

Output — default (plain text)

sha256:a3f2...c8d1

Output — --json

{
  "commit_id":          "sha256:a3f2...c8d1",
  "snapshot_id":        "sha256:b7e4...f912",
  "branch":             "main",
  "message":            "feat: add melody",
  "committed_at":       "2026-03-18T12:00:00+00:00",
  "author":             "gabriel",
  "agent_id":           "counterpoint-bot",
  "model_id":           "claude-opus-4",
  "toolchain_id":       "cursor-agent-v2",
  "parent_commit_id":   "sha256:ff01...23ab",
  "parent2_commit_id":  null
}

The text form is ideal for shell pipelines where you want to capture the ID directly without a jq call: NEW=$(muse commit-tree -s "$SNAP" -m "msg"). Agents should always pass --agent-id / --model-id (and --toolchain-id where applicable) so their identity is auditable in the resulting commit.

Exit codes: 0 commit written · 1 snapshot or parent not found, invalid ID format, or repo.json unreadable · 3 write failure


update-ref — move a branch to a commit

muse update-ref <branch> <commit-id> [--no-verify] [--old-value <commit-id>] [-j]
muse update-ref <branch> --delete [-j]

Directly writes (or deletes) a branch reference file under .muse/refs/heads/. The branch name is validated with the same rules as check-ref-format before any file is written — path-traversal via crafted branch names is not possible. The commit ID format is always validated regardless of --no-verify, so a malformed ID can never corrupt the ref file.

By default, the commit must already exist in the local store (--verify is on); pass --no-verify to write the ref before the commit is stored — useful after an unpack-objects pipeline where objects arrive in dependency order.

Use --old-value for compare-and-swap semantics in multi-agent environments: the update succeeds only if the current ref value matches the expected value. Pass --old-value null to require that the ref does not currently exist.

Flags

Flag Short Default Description
--delete -d off Delete the branch ref instead of updating it
--no-verify off (verify is on) Skip verifying the commit exists in the store before updating
--old-value Compare-and-swap guard: only update if the current ref matches this commit ID (null = ref must not exist)
--json -j off Emit machine-readable JSON (default: silent on success)

Output — default, update or delete (silent on success)

Nothing is printed; the command exits 0. This mirrors git update-ref, making it drop-in compatible with shell scripts that use exit code only.

Output — --json, update

{"branch": "main", "commit_id": "sha256:a3f2...c8d1", "previous": "sha256:ff01...23ab"}

previous is null when the branch had no prior commit.

Output — --json, delete

{"branch": "feat/x", "deleted": true}

CAS failures include the conflicting values:

{"error": "cas_mismatch", "message": "CAS mismatch: ...",
 "current": "sha256:...", "expected": "sha256:..."}

Exit codes: 0 done · 1 commit not in store (with verify on), invalid branch or commit ID, --delete on non-existent ref, or CAS mismatch · 3 file write failure


commit-graph — emit the commit DAG

muse commit-graph [--tip <id>] [--stop-at <id>] [-n <max>] [-c] [-1] [-a] [-j]

Performs a BFS walk from a tip commit (defaulting to HEAD), following both parent_commit_id and parent2_commit_id pointers. Returns every reachable commit. Useful for building visualisations, computing reachability sets, or finding the commits on a branch since it diverged from another.

Flags

Flag Short Default Description
--tip HEAD Commit to start from
--stop-at Stop BFS at this commit (exclusive)
--max -n 10 000 Maximum commits to traverse
--count -c off Emit only the integer count, not the full node list
--first-parent -1 off Follow only first-parent links — linear history, no merge parents
--ancestry-path -a off With --stop-at: restrict to commits on the direct path between tip and stop-at
--json -j off Emit machine-readable JSON (default: one commit ID per line, prefixed with a # TRUNCATED comment line if --max cut the walk short)

Output — default (plain text)

sha256:a3f2...c8d1
sha256:ff01...23ab

Output — --json

{
  "tip": "sha256:a3f2...c8d1",
  "count": 42,
  "truncated": false,
  "commits": [
    {
      "commit_id":        "sha256:a3f2...c8d1",
      "parent_commit_id": "sha256:ff01...23ab",
      "parent2_commit_id": null,
      "message":          "Add verse melody",
      "branch":           "main",
      "committed_at":     "2026-03-18T12:00:00+00:00",
      "snapshot_id":      "sha256:b7e4...f912",
      "author":           "gabriel",
      "agent_id":         "claude-code",
      "model_id":         "claude-sonnet-4-6",
      "sem_ver_bump":     "minor",
      "breaking_changes": []
    }
  ]
}

truncated is true when the graph was cut off by --max.

Output — --count --json

{"tip": "sha256:a3f2...c8d1", "count": 42, "truncated": false}

--count suppresses the commits array entirely, making it suitable for fast cardinality checks without loading commit metadata.

Examples

Commits on a feature branch since it diverged from main:

BASE=$(muse merge-base feat/x main)
muse commit-graph --tip feat/x --stop-at "$BASE"

Count commits in a feature branch:

muse commit-graph \
  --tip "$(muse rev-parse feat/x)" \
  --stop-at "$(muse merge-base feat/x dev)" \
  --count --json

Linear history only (skip merge parents):

muse commit-graph --first-parent

Exit codes: 0 graph emitted · 1 tip commit not found, or --ancestry-path without --stop-at


pack-objects — pack commits for transport

muse pack-objects <want>... [--have <id>...] [--dry-run] [-j]

Collects a set of commits — and all their referenced snapshots and objects — into a single MPack (msgpack binary) written to stdout. Pass --have to tell the packer which commits the receiver already has; objects reachable only from --have ancestors are excluded, minimising transfer size.

<want> may be a full commit ID or HEAD.

Flags

Flag Short Description
--have Commits the receiver already has (repeat for multiple)
--dry-run -n Print a JSON summary of what would be packed instead of writing binary msgpack
--json -j Shorthand equivalent used with --dry-run to emit the JSON summary

Output — default — an MPack msgpack binary (pipe to a file or unpack-objects)

Output — --dry-run

{"want": ["sha256:a3f2...c8d1"], "have": [], "commits": 3, "snapshots": 3, "blobs": 12, "object_bytes": 40960}

object_bytes is the total uncompressed byte size of all object payloads in the pack — use it to decide whether to buffer the full mpack in memory or stream it directly to the remote.

Exit codes: 0 pack written (or dry-run summary printed) · 1 a wanted commit not found or HEAD has no commits · 3 I/O error reading from the local store


unpack-objects — apply an mpack to the local store

cat pack.muse | muse unpack-objects [-j]
muse pack-objects HEAD | muse unpack-objects

Reads an MPack from stdin and writes its commits, snapshots, blobs, and tags into .muse/. Idempotent: objects already present in the store are silently skipped. Partial packs from interrupted transfers are safe to re-apply.

Flags

Flag Short Default Description
--json -j off Emit machine-readable JSON (default: human-readable summary)

Output — default (plain text)

Wrote 12 commits, 12 snapshots, 47 blobs (3 skipped), 2 tags.

Output — --json

{
  "commits_written":   12,
  "snapshots_written": 12,
  "blobs_written":     47,
  "blobs_skipped":      3,
  "tags_written":       2
}

Exit codes: 0 unpacked (all objects stored) · 1 invalid or malformed msgpack input, or input is not a top-level map · 3 write failure


ls-remote — list refs on a remote

muse ls-remote [<remote-or-url>] [-j]

Contacts a remote and lists every branch HEAD without altering local state. The <remote-or-url> argument is either a remote name configured with muse remote add (defaults to origin) or a full https:// URL.

Flags

Flag Short Default Description
--json -j off Emit machine-readable JSON (default: tab-separated text)

Output — default (plain text)

One line per branch, tab-separated. The default branch is marked with *.

sha256:a3f2...c8d1	main *
sha256:b7e4...f912	feat/experiment

Output — --json

{
  "status":         "ok",
  "error":          "",
  "repo_id":        "sha256:550e8400...",
  "domain":         "midi",
  "default_branch": "main",
  "branches": {
    "main":             "sha256:a3f2...c8d1",
    "feat/experiment":  "sha256:b7e4...f912"
  },
  "remote":         "origin",
  "url":            "https://musehub.ai/org/repo"
}

All keys are always present so agents can read them without dict.get guards. "remote" is null when the caller passed a full URL directly instead of a configured remote name. Branch commit IDs are always sha256:-prefixed regardless of what the remote returns.

Exit codes: 0 remote contacted · 1 remote not configured or URL invalid · 3 transport error (network, HTTP error)


Composability Patterns

Export a history range

# All commits on feat/x that are not on main
BASE=$(muse rev-parse main)
TIP=$(muse rev-parse feat/x)
muse commit-graph --tip "$TIP" --stop-at "$BASE"

Ship commits between two machines

# On the sender — pack everything the receiver doesn't have
HAVE=$(muse ls-remote origin | awk '{print "--have " $1}' | tr '\n' ' ')
muse pack-objects HEAD $HAVE > bundle.muse

# On the receiver
cat bundle.muse | muse unpack-objects
muse update-ref main <commit-id>

Verify a stored object

ID=$(muse hash-object tracks/drums.mid)
muse cat-object "$ID" --json

Inspect what changed in the last commit

muse read-commit "$(muse rev-parse HEAD)" --json | \
  python3 -c "import sys, json; d=json.load(sys.stdin); print(d['message'])"

Script a bare commit (advanced)

# 1. Hash and store the files
OID=$(muse hash-object -w tracks/drums.mid)

# 2. Build a snapshot manifest and write it (via muse commit is easier,
#    but for full control use commit-tree after writing the snapshot)
SNAP=$(muse rev-parse HEAD | \
  xargs -I{} muse read-commit {} --json | \
  python3 -c "import sys,json; print(json.load(sys.stdin)['snapshot_id'])")

# 3. Create a commit on top of HEAD
PARENT=$(muse rev-parse HEAD)
NEW=$(muse commit-tree -s "$SNAP" -p "$PARENT" -m "scripted commit" --json | \
  python3 -c "import sys,json; print(json.load(sys.stdin)['commit_id'])")

# 4. Advance the branch
muse update-ref main "$NEW"

merge-base — find the common ancestor of two commits

Find the lowest common ancestor of two commits — the point at which two branches diverged.

muse merge-base <commit-a> <commit-b> [-j]
Flag Short Default Description
--json -j off Emit machine-readable JSON (default: plain text commit ID)

Arguments accept full SHA-256 commit IDs, branch names, or HEAD.

Default output (plain text):

sha256:<sha256>

--json output:

{
  "commit_a":   "sha256:<sha256>",
  "commit_b":   "sha256:<sha256>",
  "merge_base": "sha256:<sha256>"
}

When no common ancestor exists, merge_base is null and error is set.

Exit Meaning
0 Result computed (check merge_base for null vs. found)
1 A commit ID or ref cannot be resolved
3 DAG walk failed

snapshot-diff — diff two snapshot manifests

Compare two snapshots and categorise every changed path as added, modified, or deleted.

muse snapshot-diff <ref-a> <ref-b> [-j] [-s] [--raw] [--only CATEGORY] [--path-prefix PREFIX]
muse snapshot-diff --stdin [-j]
Flag Short Default Description
--json -j off Emit JSON output (default: human-readable text)
--stat -s off Append a summary line in text mode
--raw off Include object IDs in text-format output (no effect on JSON, which always includes them)
--only Restrict output to one category: added, modified, or deleted
--path-prefix Filter the diff to paths starting with this prefix
--stdin off Batch mode: read <ref-a> <ref-b> pairs from stdin, one per line

Arguments accept snapshot IDs, commit IDs, branch names, or HEAD.

Default output (text):

A  new.mid
M  main.mid
D  old.mid

With --raw:

A  sha256:<oid_b>  new.mid
M  sha256:<oid_a>  sha256:<oid_b>  main.mid
D  sha256:<oid_a>  old.mid

--json output:

{
  "snapshot_a":     "sha256:<sha256>",
  "snapshot_b":     "sha256:<sha256>",
  "added":          [{"path": "new.mid",  "object_id": "sha256:<sha256>"}],
  "modified":       [{"path": "main.mid", "object_id_a": "sha256:<sha256>", "object_id_b": "sha256:<sha256>"}],
  "deleted":        [{"path": "old.mid",  "object_id": "sha256:<sha256>"}],
  "added_count":    1,
  "modified_count": 1,
  "deleted_count":  1,
  "total_changes":  3
}

--only restricts which category is populated (the other two are emitted as empty arrays in JSON mode, or simply not printed in text mode). --path-prefix is applied before --only, and counts/total_changes reflect the filtered view.

Exit Meaning
0 Diff computed (zero changes is a valid result)
1 Ref cannot be resolved, or bad --only value
3 I/O error reading snapshot records

Batch mode (--stdin) always exits 0; individual pair errors are reported inline.


domain-info — inspect the active domain plugin

Inspect the domain plugin active for this repository — its name, class, optional protocol capabilities, and full structural schema.

muse domain-info [-j] [-a] [--domain DOMAIN] [--capabilities-only]
Flag Short Default Description
--json -j off Emit machine-readable JSON (default: human-readable text)
--all-domains -a off List every registered domain; no repo required
--domain Inspect a specific domain by name without requiring an active repo
--capabilities-only off Emit only the capabilities dict

Default output (text):

Domain:       midi
Plugin:       MidiPlugin
Merge mode:   three_way
Capabilities: addressed_merge

--json output:

{
  "domain":       "midi",
  "plugin_class": "MidiPlugin",
  "capabilities": {
    "addressed_merge": true,
    "crdt":             false,
    "harmony":          false
  },
  "schema": {
    "domain": "midi", "merge_mode": "three_way",
    "dimensions": [...], "top_level": {...}
  },
  "registered_domains": ["bitcoin", "code", "midi", "scaffold"]
}
Exit Meaning
0 Domain resolved and schema emitted
1 No repository found, or domain not registered
3 Plugin raised an error computing its schema

show-ref — list all branch refs

List all branch refs and the commit IDs they point to.

muse show-ref [-j] [-p PATTERN] [-H] [-v REF] [--count]
Flag Short Default Description
--json -j off Emit machine-readable JSON (default: human-readable text)
--pattern -p "" fnmatch glob to filter ref names
--head -H off Print only HEAD ref and commit ID
--verify -v "" Silent existence check — exit 0 if found, 1 if not
--count off Emit only the branch count

Default output (text):

sha256:<sha256>  refs/heads/dev
* sha256:<sha256>  refs/heads/main  (HEAD)

--json output:

{
  "refs": [
    {"ref": "refs/heads/dev",  "commit_id": "sha256:<sha256>"},
    {"ref": "refs/heads/main", "commit_id": "sha256:<sha256>"}
  ],
  "head":  {"ref": "refs/heads/main", "branch": "main", "commit_id": "sha256:<sha256>"},
  "count": 2
}

Use --verify in shell conditionals:

muse show-ref --verify refs/heads/my-branch && echo "branch exists"
Exit Meaning
0 Refs enumerated (or --verify ref exists)
1 --verify ref absent
3 I/O error reading refs directory

check-ignore — test whether paths are excluded by .museignore

Test whether workspace paths are excluded by .museignore rules.

muse check-ignore <path>... [-j] [-q] [-V] [--stdin] [--patterns-only] [--ignored-only]
Flag Short Default Description
--json -j off Emit machine-readable JSON (default: human-readable text)
--quiet -q off No output; exit 0 if all ignored, 1 otherwise
--verbose -V off Include matching pattern in text output (JSON always includes it)
--stdin off Read additional paths from stdin, one per line
--patterns-only off Emit the resolved pattern list and exit; no path arguments needed
--ignored-only off Show only paths that are ignored

Default output (text):

ignored  build/output.bin  [build/]
ok       tracks/dr.mid

--json output:

{
  "domain":          "midi",
  "patterns_loaded": 4,
  "summary": {"total": 2, "ignored": 1, "not_ignored": 1},
  "results": [
    {"path": "build/out.bin", "ignored": true,  "matching_pattern": "build/"},
    {"path": "tracks/dr.mid", "ignored": false, "matching_pattern": null}
  ]
}

With --patterns-only:

{"domain": "midi", "patterns_loaded": 3, "patterns": ["build/", "*.bin", "!tracks/*.mid"]}

Last-match-wins: a negation rule (!important.mid) can un-ignore a path matched by an earlier rule.

Exit Meaning
0 Results emitted (or --quiet with all ignored)
1 --quiet with any non-ignored path; missing args; --ignored-only combined with --patterns-only or --quiet
3 I/O or TOML parse error reading .museignore

check-attr — query merge-strategy attributes for paths

Query merge-strategy attributes for workspace paths from .museattributes.

muse check-attr <path>... [-j] [-d DIMENSION] [-A] [--stdin] [--rules-only] [--unmatched-only]
Flag Short Default Description
--json -j off Emit machine-readable JSON (default: human-readable text)
--dimension -d * Domain axis to query (e.g. notes, tempo)
--all-rules -A off Return every matching rule, not just first-match
--stdin off Read additional paths from stdin, one per line
--rules-only off Emit the loaded rule list without testing any paths
--unmatched-only off Show only paths with no matching rule (strategy=auto)

--json output (default: first-match):

{
  "domain":       "midi",
  "rules_loaded": 3,
  "dimension":    "*",
  "summary": {"total": 2, "matched": 1, "unmatched": 1, "by_strategy": {"ours": 1, "auto": 1}},
  "results": [
    {
      "path":      "drums/kit.mid",
      "dimension": "*",
      "strategy":  "ours",
      "rule": {"path_pattern": "drums/*", "strategy": "ours", "priority": 10, ...}
    }
  ]
}

When no rule matches, strategy is "auto" and rule is null.

Exit Meaning
0 Attributes resolved and emitted
1 Missing args; --unmatched-only combined with --rules-only
3 TOML parse error in .museattributes

verify-object — re-hash stored objects to detect corruption

Re-hash stored objects to detect silent data corruption.

muse verify-object <object-id>... [-j] [-q] [--all] [--stdin] [--fail-fast]
Flag Short Default Description
--json -j off Emit machine-readable JSON (default: human-readable text)
--quiet -q off No output; exit 0 if all OK, 1 otherwise
--all -a off Verify every object in the store — the fsck equivalent
--stdin off Read additional object IDs from stdin, one per line
--fail-fast off Stop after the first failed object and exit 1 immediately

Objects are streamed in 64 KiB chunks — safe for very large blobs.

Default output (text):

OK    sha256:<sha256>  (4096 bytes)
FAIL  sha256:<sha256>  object not found in store
---
Checked: 2  Failed: 1

--json output:

{
  "results": [
    {"object_id": "sha256:<sha256>", "ok": true,  "size_bytes": 4096, "error": null},
    {"object_id": "sha256:<sha256>", "ok": false, "size_bytes": null,
     "error": "object not found in store"}
  ],
  "all_ok":  false,
  "checked": 2,
  "failed":  1
}

Compose with show-ref to verify every commit in a repo:

muse show-ref --json \
  | jq -r '.refs[].commit_id' \
  | xargs muse verify-object
Exit Meaning
0 All objects verified successfully
1 One or more objects failed; object not found; bad args
3 Unexpected I/O error (disk read failure)

symbolic-ref — read or write HEAD's symbolic reference

In Muse, HEAD is normally a symbolic reference — it points to a branch — but can also be detached, pointing directly at a commit. symbolic-ref reads which branch HEAD tracks or, with --set, points HEAD at a different branch.

# Read mode
muse symbolic-ref HEAD [-j] [--short]

# Write mode
muse symbolic-ref HEAD --set <branch> [--create-branch] [-j]
Flag Short Default Description
--set -s "" Branch name to point HEAD at
--create-branch off With --set, create the branch pointer even if it has no commits yet
--short -S off Emit branch name only (not the full refs/heads/… path)
--json -j off Emit machine-readable JSON (default: human-readable text)

Default output (text, read mode):

refs/heads/main

With --short: main.

--json output (read mode, normal branch HEAD):

{
  "ref":             "HEAD",
  "symbolic_target": "refs/heads/main",
  "branch":          "main",
  "commit_id":       "sha256:<sha256>",
  "detached":        false
}

--json output (read mode, detached HEAD):

{
  "ref":             "HEAD",
  "symbolic_target": null,
  "branch":          null,
  "commit_id":       "sha256:<sha256>",
  "detached":        true
}

commit_id is null when the branch has no commits yet. --set without --create-branch requires the target branch to already exist.

Exit Meaning
0 Ref read or written
1 --set target branch does not exist (without --create-branch); unsupported ref name; invalid branch name
3 I/O error reading or writing HEAD

for-each-ref — iterate all refs with rich commit metadata

Enumerates every branch ref together with the full commit metadata it points to. Supports sorting by any commit field and glob-pattern filtering, making it ideal for agent pipelines that need to slice the ref list without post-processing.

muse for-each-ref [-p <pattern>] [-s <field>] [-d] [--count <count>] [--no-commits] [-j]
Flag Short Default Description
--pattern -p "" fnmatch glob on the full ref name, e.g. refs/heads/feat/*
--sort -s ref Sort field: ref, branch, commit_id, author, committed_at, message, snapshot_id
--desc off Reverse sort order (descending)
--count 0 Limit to first N refs after sorting (0 = unlimited)
--no-commits off Skip loading commit records; emit only ref, branch, commit_id
--json -j off Emit machine-readable JSON (default: human-readable text)

Default output (text): <commit_id> <ref> <committed_at> <author> (or just <commit_id> <ref> with --no-commits)

--json output:

{
  "refs": [
    {
      "ref":          "refs/heads/dev",
      "branch":       "dev",
      "commit_id":    "sha256:<sha256>",
      "author":       "gabriel",
      "message":      "Add verse melody",
      "committed_at": "2026-01-01T00:00:00+00:00",
      "snapshot_id":  "sha256:<sha256>"
    }
  ],
  "count": 1,
  "current_branch": "dev"
}

With --no-commits, author / message / committed_at / snapshot_id are omitted.

Example — three most recently committed branches:

muse for-each-ref --sort committed_at --desc --count 3
Exit Meaning
0 Refs emitted (list may be empty)
1 Unknown --sort field, or negative --count
3 I/O error reading refs or commit records

name-rev — map commit IDs to branch-relative names

For each supplied commit ID, performs a single multi-source BFS from all branch tips and reports the closest branch and hop distance. Results are expressed as <branch>~N — where N is the number of parent hops from the tip. When N is 0 (the commit is the exact branch tip) the name is the bare branch name with no ~0 suffix.

muse name-rev <commit-id>... [-j] [--name-only] [-u <string>] [--stdin] [--branches GLOB] [--max-walk N]
Flag Short Default Description
--name-only off Emit only the name (or the undefined string), not the commit ID
--undefined -u "undefined" String to emit for unreachable commits
--stdin off Read additional commit IDs from stdin, one per line
--branches Restrict BFS seeds to branch names matching this fnmatch glob
--max-walk 50 000 Maximum BFS steps before stopping
--json -j off Emit machine-readable JSON (default: human-readable text)

Default output (text): <sha256> main~3 (or main~3 with --name-only)

--json output:

{
  "results": [
    {
      "commit_id": "sha256:<sha256>",
      "input":     "<sha256>",
      "name":      "main~3",
      "branch":    "main",
      "distance":  3,
      "undefined": false,
      "ambiguous": false
    },
    {
      "commit_id": null,
      "input":     "deadbeef",
      "name":      null,
      "branch":    null,
      "distance":  null,
      "undefined": true,
      "ambiguous": false
    }
  ]
}

Performance: A single O(total-commits) BFS from all branch tips simultaneously. Every commit is visited at most once regardless of how many input IDs are supplied.

Exit Meaning
0 All results computed (some may be undefined or ambiguous)
1 No commit IDs supplied, or non-hex input rejected
3 I/O error reading commit records

check-ref-format — validate branch and ref names

Tests one or more names against Muse's branch-naming rules — the same validation used by muse branch and muse update-ref. Use in scripts to pre-validate names before attempting to create a branch.

muse check-ref-format <name>... [-q] [-j] [--stdin] [--rules] [--invalid-only]
Flag Short Default Description
--stdin off Read additional names from stdin, one per line
--rules off Emit the validation ruleset as JSON and exit
--quiet -q off No output — exit 0 if all valid, exit 1 otherwise
--json -j off Emit machine-readable JSON (default: human-readable text)
--invalid-only off Show only invalid names in results

Rules enforced: 1–255 chars; no C0 control characters, space, or DEL; no backslash; no Git-banned punctuation (~ ^ : ? * [); no leading/trailing dot; no consecutive dots (..); no leading/trailing or consecutive slashes; no single-dot path component; no component ending in .lock; no @{ sequence; not the bare string @.

--json output:

{
  "results": [
    {"name": "feat/my-branch", "valid": true,  "error": null},
    {"name": "bad..name",      "valid": false, "error": "..."}
  ],
  "all_valid": false,
  "valid_count": 1,
  "invalid_count": 1
}

Default output (text):

ok    feat/my-branch
FAIL  bad..name  →  Branch name 'bad..name' contains forbidden characters

With --rules:

{"max_length": 255, "forbidden_chars": [...], "forbidden_patterns": [...], "notes": "..."}

Shell conditional:

muse check-ref-format -q "$BRANCH" && muse checkout -b "$BRANCH"
Exit Meaning
0 All names are valid, or --rules was used
1 One or more names are invalid; no names supplied; --invalid-only combined with --quiet or --rules

(No exit 3 — this command is pure CPU, no I/O.)


verify-pack — verify MPack integrity

Reads an MPack from stdin or --file and performs three-tier integrity checking:

  1. Blob integrity — every blob payload's SHA-256 is recomputed from the raw bytes. The digest must match the declared object_id.
  2. Snapshot consistency — every snapshot's manifest entries reference objects present in the bundle or already in the local store.
  3. Commit consistency — every commit's snapshot_id is present in the bundle or already in the local store.
muse pack-objects main | muse verify-pack
muse verify-pack --file bundle.muse
Flag Short Default Description
--file -i "" Path to bundle file (reads stdin when omitted)
--stat off Fast structural inspection — counts only, no hashing
--quiet -q off No output — exit 0 if clean, exit 1 on any failure
--no-local -L off Skip local store checks (verify bundle in isolation)
--strict off Treat promised objects (absent locally but covered by a promisor remote) as integrity failures
--json -j off Emit machine-readable JSON (default: human-readable text)

Default output (text):

blobs=42  snapshots=5  commits=5  all_ok=True

--json output:

{
  "blobs_checked":     42,
  "snapshots_checked": 5,
  "commits_checked":   5,
  "all_ok":            true,
  "failures":          [],
  "promised_objects":  0,
  "base_objects":      0,
  "bundle_mode":       "full",
  "base_commits":      []
}

With failures:

{
  "all_ok": false,
  "failures": [
    {"kind": "object",   "id": "sha256:<sha256>", "error": "hash mismatch"},
    {"kind": "snapshot", "id": "sha256:<sha256>", "error": "missing object: ..."}
  ]
}

Objects absent from the mpack are resolved against the local store using a three-state model: PRESENT (verified, not a failure), PROMISED (absent locally but a promisor remote is configured — counted separately, not a failure unless --strict), MISSING (absent with no promisor — always a failure).

Validate before upload:

muse pack-objects main | muse verify-pack -q \
  && echo "mpack is clean — safe to push"
Exit Meaning
0 MPack is fully intact (or --stat completed)
1 One or more integrity failures; malformed msgpack; bad args
3 I/O error reading stdin or the mpack file


Composability Patterns — Advanced

Name every commit reachable from a branch

# Get all commit IDs on feat/x since it diverged from dev
BASE=$(muse merge-base feat/x dev)
muse commit-graph --tip feat/x --stop-at "$BASE" \
  | xargs muse name-rev --name-only

Audit all refs with full metadata and filter by recency

# List all branches modified in 2026, sorted newest-first
muse for-each-ref --sort committed_at --desc --json \
  | jq '.refs[] | select(.committed_at | startswith("2026"))'

Validate a branch name before creating it

BRANCH="feat/my-feature"
muse check-ref-format -q "$BRANCH" \
  && echo "Name is valid — safe to branch" \
  || echo "Invalid branch name"

Verify a bundle before shipping

muse pack-objects main | tee bundle.muse | muse verify-pack -q \
  && echo "bundle is clean — safe to push" \
  || echo "bundle has integrity failures — do not push"

Switch active branch via plumbing

# Check where HEAD is now
muse symbolic-ref HEAD        # → refs/heads/main
# Redirect HEAD to dev
muse symbolic-ref HEAD --set dev
muse rev-parse HEAD           # → tip of dev

Find stale branches (no commits in the last 30 days)

# Requires `date` and `jq`
CUTOFF=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
         || date -u --date="30 days ago" +%Y-%m-%dT%H:%M:%SZ)
muse for-each-ref --json \
  | jq --arg c "$CUTOFF" '.refs[] | select(.committed_at < $c) | .branch'

Check which files changed between two branches

BASE=$(muse merge-base main feat/x)
muse snapshot-diff "$BASE" feat/x --stat

Object ID Quick Reference

All IDs in Muse are canonical sha256:<64-char lowercase hex> digests. There are three kinds:

Kind Computed from Used by
Object ID File bytes hash-object, cat-object, snapshot manifests
Snapshot ID Sorted path:object_id pairs read-snapshot, commit-tree
Commit ID Parent IDs + snapshot ID + message + timestamp read-commit, rev-parse, update-ref

Every ID is deterministic and content-addressed. The same input always produces the same ID; two different inputs never produce the same ID in practice.


Exit Code Summary

Code Constant Meaning
0 SUCCESS Command completed successfully
1 USER_ERROR Bad input, ref not found, invalid format
3 INTERNAL_ERROR I/O failure, integrity check, transport error
File History 1 commit
sha256:bd58aa96f2ed23218e823e8bb60da938ce95c3ec8bf6658bb1317873a460720b docs: fix systemic --format→--json drift and broken plumbin… Sonnet 5 6 days ago