gabriel / muse public
plumbing.md markdown
1263 lines 35.2 KB
99746394 feat(tests+docs): supercharge plumbing test suite and update reference doc Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 # Muse Plumbing Commands Reference
2
3 Plumbing commands are the low-level, machine-readable layer of the Muse CLI.
4 They output JSON, stream bytes without size limits, use predictable exit codes,
5 and compose cleanly in shell pipelines and agent scripts.
6
7 If you want to automate Muse — write a script, build an agent workflow, or
8 integrate Muse into another tool — plumbing commands are the right entry point.
9 The higher-level porcelain commands (`muse commit`, `muse merge`, etc.) call
10 these internally.
11
12 ---
13
14 ## Quick Index
15
16 | Command | Purpose |
17 |---|---|
18 | [`hash-object`](#hash-object----compute-a-content-id) | Compute SHA-256 of a file; optionally store it |
19 | [`cat-object`](#cat-object----read-a-stored-object) | Stream raw bytes or metadata for a stored object |
20 | [`verify-object`](#verify-object----re-hash-stored-objects-to-detect-corruption) | Re-hash stored objects to detect corruption |
21 | [`rev-parse`](#rev-parse----resolve-a-ref-to-a-commit-id) | Resolve branch name / HEAD / prefix → full commit ID |
22 | [`read-commit`](#read-commit----print-full-commit-metadata) | Print full commit JSON record |
23 | [`read-snapshot`](#read-snapshot----print-full-snapshot-metadata) | Print full snapshot JSON record |
24 | [`ls-files`](#ls-files----list-files-in-a-snapshot) | List tracked files and their object IDs |
25 | [`commit-tree`](#commit-tree----create-a-commit-from-a-snapshot-id) | Create a commit from an existing snapshot |
26 | [`update-ref`](#update-ref----move-a-branch-to-a-commit) | Move or delete a branch ref |
27 | [`commit-graph`](#commit-graph----emit-the-commit-dag) | BFS walk of the commit DAG |
28 | [`merge-base`](#merge-base----find-the-common-ancestor-of-two-commits) | Find the lowest common ancestor of two commits |
29 | [`snapshot-diff`](#snapshot-diff----diff-two-snapshot-manifests) | Diff two snapshots: added / modified / deleted |
30 | [`pack-objects`](#pack-objects----bundle-commits-for-transport) | Bundle commits, snapshots, and objects into a PackBundle |
31 | [`unpack-objects`](#unpack-objects----apply-a-bundle-to-the-local-store) | Apply a PackBundle to the local store |
32 | [`verify-pack`](#verify-pack----verify-packbundle-integrity) | Three-tier integrity check for a PackBundle |
33 | [`show-ref`](#show-ref----list-all-branch-refs) | List all branch refs and their commit IDs |
34 | [`symbolic-ref`](#symbolic-ref----read-or-write-heads-symbolic-reference) | Read or write the HEAD symbolic reference |
35 | [`for-each-ref`](#for-each-ref----iterate-all-refs-with-rich-commit-metadata) | Iterate refs with full commit metadata; sort and filter |
36 | [`name-rev`](#name-rev----map-commit-ids-to-branch-relative-names) | Map commit IDs to `<branch>~N` names |
37 | [`check-ref-format`](#check-ref-format----validate-branch-and-ref-names) | Validate branch/ref names against naming rules |
38 | [`check-ignore`](#check-ignore----test-whether-paths-are-excluded-by-museignore) | Test whether paths match `.museignore` rules |
39 | [`check-attr`](#check-attr----query-merge-strategy-attributes-for-paths) | Query `.museattributes` for merge strategies |
40 | [`domain-info`](#domain-info----inspect-the-active-domain-plugin) | Inspect the active domain plugin and its schema |
41 | [`ls-remote`](#ls-remote----list-refs-on-a-remote) | List refs on a remote without changing local state |
42
43 ---
44
45 ## The Plumbing Contract
46
47 Every plumbing command follows the same rules:
48
49 | Property | Guarantee |
50 |---|---|
51 | **Output** | JSON to `stdout`, errors to `stderr` |
52 | **Exit 0** | Success — output is valid and complete |
53 | **Exit 1** | User error — bad input, ref not found, invalid ID |
54 | **Exit 3** | Internal error — I/O failure, integrity check failed |
55 | **Idempotent reads** | Reading commands never modify state |
56 | **Idempotent writes** | Writing the same object twice is a no-op |
57 | **Encoding** | All text I/O is UTF-8 |
58 | **Object IDs** | Always 64 lowercase hex characters (SHA-256) |
59 | **Short flags** | Every flag has a `-x` short form |
60
61 JSON output is always printed to `stdout`. When an error occurs, the message
62 goes to `stderr`; some commands also write a machine-readable `{"error": "..."}`
63 object to `stdout` so scripts that parse `stdout` can detect the failure
64 without inspecting exit codes.
65
66 ---
67
68 ## Command Reference
69
70 ### `hash-object` — compute a content ID
71
72 ```
73 muse plumbing hash-object <file> [-w] [-f json|text]
74 ```
75
76 Computes the SHA-256 content address of a file. Identical bytes always produce
77 the same ID; this is how Muse deduplicates storage. With `--write` (`-w`) the
78 object is also stored in `.muse/objects/` so it can be referenced by future
79 snapshots and commits. The file is streamed at 64 KiB at a time — arbitrarily
80 large blobs never spike memory.
81
82 **Flags**
83
84 | Flag | Short | Default | Description |
85 |---|---|---|---|
86 | `--write` | `-w` | off | Store the object after hashing |
87 | `--format` | `-f` | `json` | Output format: `json` or `text` |
88
89 **Output — JSON (default)**
90
91 ```json
92 {"object_id": "a3f2...c8d1", "stored": false}
93 ```
94
95 `stored` is `true` only when `--write` is passed and the object was not already
96 in the store.
97
98 **Output — `--format text`**
99
100 ```
101 a3f2...c8d1
102 ```
103
104 **Exit codes:** 0 success · 1 path not found, is a directory, or bad `--format` · 3 I/O write error or integrity check failed
105
106 ---
107
108 ### `cat-object` — read a stored object
109
110 ```
111 muse plumbing cat-object <object-id> [-f raw|info]
112 ```
113
114 Reads a content-addressed object from `.muse/objects/`. With `--format raw`
115 (the default) the raw bytes are streamed to `stdout` at 64 KiB at a time —
116 pipe to a file, another process, or a network socket without any size ceiling.
117 With `--format info` a JSON summary is printed instead of the content.
118
119 **Flags**
120
121 | Flag | Short | Default | Description |
122 |---|---|---|---|
123 | `--format` | `-f` | `raw` | `raw` (stream bytes) or `info` (JSON metadata) |
124
125 **Output — `--format info`**
126
127 ```json
128 {"object_id": "a3f2...c8d1", "present": true, "size_bytes": 4096}
129 ```
130
131 When the object is absent and `--format info` is used, `present` is `false`
132 and `size_bytes` is `0` (exit 1). When `--format raw` is used and the object
133 is absent, the error goes to `stderr` (exit 1).
134
135 **Exit codes:** 0 found · 1 not found or invalid ID format · 3 I/O read error
136
137 ---
138
139 ### `rev-parse` — resolve a ref to a commit ID
140
141 ```
142 muse plumbing rev-parse <ref> [-f json|text]
143 ```
144
145 Resolves a branch name, `HEAD`, or an abbreviated SHA prefix to the full
146 64-character commit ID. Use this to canonicalise any ref before passing it to
147 other commands.
148
149 **Arguments**
150
151 | Argument | Description |
152 |---|---|
153 | `<ref>` | Branch name, `HEAD`, full commit ID, or unique prefix |
154
155 **Flags**
156
157 | Flag | Short | Default | Description |
158 |---|---|---|---|
159 | `--format` | `-f` | `json` | `json` or `text` |
160
161 **Output — JSON**
162
163 ```json
164 {"ref": "main", "commit_id": "a3f2...c8d1"}
165 ```
166
167 Ambiguous prefixes return an error object with a `candidates` list (exit 1).
168
169 **Output — `--format text`**
170
171 ```
172 a3f2...c8d1
173 ```
174
175 **Exit codes:** 0 resolved · 1 not found, ambiguous, or bad `--format`
176
177 ---
178
179 ### `ls-files` — list files in a snapshot
180
181 ```
182 muse plumbing ls-files [--commit <id>] [-f json|text]
183 ```
184
185 Lists every file tracked in a commit's snapshot together with its content
186 object ID. Defaults to the HEAD commit of the current branch.
187
188 **Flags**
189
190 | Flag | Short | Default | Description |
191 |---|---|---|---|
192 | `--commit` | `-c` | HEAD | Commit ID to inspect |
193 | `--format` | `-f` | `json` | `json` or `text` |
194
195 **Output — JSON**
196
197 ```json
198 {
199 "commit_id": "a3f2...c8d1",
200 "snapshot_id": "b7e4...f912",
201 "file_count": 3,
202 "files": [
203 {"path": "tracks/bass.mid", "object_id": "c1d2...a3b4"},
204 {"path": "tracks/drums.mid", "object_id": "e5f6...b7c8"},
205 {"path": "tracks/piano.mid", "object_id": "09ab...cd10"}
206 ]
207 }
208 ```
209
210 Files are sorted by path.
211
212 **Output — `--format text`** (tab-separated, suitable for `awk` / `cut`)
213
214 ```
215 c1d2...a3b4 tracks/bass.mid
216 e5f6...b7c8 tracks/drums.mid
217 09ab...cd10 tracks/piano.mid
218 ```
219
220 **Exit codes:** 0 listed · 1 commit or snapshot not found, or bad `--format`
221
222 ---
223
224 ### `read-commit` — print full commit metadata
225
226 ```
227 muse plumbing read-commit <commit-id>
228 ```
229
230 Emits the complete JSON record for a commit. Accepts a full 64-character ID
231 or a unique prefix. The schema is stable across Muse versions; use
232 `format_version` to detect any future schema changes.
233
234 **Output**
235
236 ```json
237 {
238 "format_version": 5,
239 "commit_id": "a3f2...c8d1",
240 "repo_id": "550e8400-e29b-41d4-a716-446655440000",
241 "branch": "main",
242 "snapshot_id": "b7e4...f912",
243 "message": "Add verse melody",
244 "committed_at": "2026-03-18T12:00:00+00:00",
245 "parent_commit_id": "ff01...23ab",
246 "parent2_commit_id": null,
247 "author": "gabriel",
248 "agent_id": "",
249 "model_id": "",
250 "toolchain_id": "",
251 "prompt_hash": "",
252 "signature": "",
253 "signer_key_id": "",
254 "sem_ver_bump": "none",
255 "breaking_changes": [],
256 "reviewed_by": [],
257 "test_runs": 0,
258 "metadata": {}
259 }
260 ```
261
262 Error conditions also produce JSON on `stdout` so scripts can parse them
263 without inspecting `stderr`.
264
265 **Exit codes:** 0 found · 1 not found, ambiguous prefix, or invalid ID format
266
267 ---
268
269 ### `read-snapshot` — print full snapshot metadata
270
271 ```
272 muse plumbing read-snapshot <snapshot-id>
273 ```
274
275 Emits the complete JSON record for a snapshot. Every commit references exactly
276 one snapshot. Use `ls-files --commit <id>` if you want to look up a snapshot
277 from a commit ID rather than the snapshot ID directly.
278
279 **Output**
280
281 ```json
282 {
283 "snapshot_id": "b7e4...f912",
284 "created_at": "2026-03-18T12:00:00+00:00",
285 "file_count": 3,
286 "manifest": {
287 "tracks/bass.mid": "c1d2...a3b4",
288 "tracks/drums.mid": "e5f6...b7c8",
289 "tracks/piano.mid": "09ab...cd10"
290 }
291 }
292 ```
293
294 **Exit codes:** 0 found · 1 not found or invalid ID format
295
296 ---
297
298 ### `commit-tree` — create a commit from a snapshot ID
299
300 ```
301 muse plumbing commit-tree -s <snapshot-id> [-p <parent-id>]... [-m <message>] [-a <author>] [-b <branch>]
302 ```
303
304 Low-level commit creation. The snapshot must already exist in the store. Use
305 `--parent` / `-p` once for a linear commit and twice for a merge commit. The
306 commit is written to `.muse/commits/` but **no branch ref is updated** — use
307 `update-ref` to advance a branch to the new commit.
308
309 **Flags**
310
311 | Flag | Short | Required | Description |
312 |---|---|---|---|
313 | `--snapshot` | `-s` | ✅ | SHA-256 snapshot ID |
314 | `--parent` | `-p` | — | Parent commit ID (repeat for merges) |
315 | `--message` | `-m` | — | Commit message |
316 | `--author` | `-a` | — | Author name |
317 | `--branch` | `-b` | — | Branch name (default: current branch) |
318
319 **Output**
320
321 ```json
322 {"commit_id": "a3f2...c8d1"}
323 ```
324
325 **Exit codes:** 0 commit written · 1 snapshot or parent not found, or `repo.json` unreadable · 3 write failure
326
327 ---
328
329 ### `update-ref` — move a branch to a commit
330
331 ```
332 muse plumbing update-ref <branch> <commit-id> [--no-verify]
333 muse plumbing update-ref <branch> --delete
334 ```
335
336 Directly writes (or deletes) a branch reference file under `.muse/refs/heads/`.
337 By default, the commit must already exist in the local store (`--verify` is
338 on); pass `--no-verify` to write the ref before the commit is stored — useful
339 after an `unpack-objects` pipeline where objects arrive in dependency order.
340
341 The commit ID format is always validated regardless of `--no-verify`, so a
342 malformed ID can never corrupt the ref file.
343
344 **Flags**
345
346 | Flag | Short | Default | Description |
347 |---|---|---|---|
348 | `--delete` | `-d` | off | Delete the branch ref instead of updating it |
349 | `--verify/--no-verify` | — | `--verify` | Require commit to exist in store |
350
351 **Output — update**
352
353 ```json
354 {"branch": "main", "commit_id": "a3f2...c8d1", "previous": "ff01...23ab"}
355 ```
356
357 `previous` is `null` when the branch had no prior commit.
358
359 **Output — delete**
360
361 ```json
362 {"branch": "feat/x", "deleted": true}
363 ```
364
365 **Exit codes:** 0 done · 1 commit not in store (with `--verify`), invalid ID, or `--delete` on non-existent ref · 3 file write failure
366
367 ---
368
369 ### `commit-graph` — emit the commit DAG
370
371 ```
372 muse plumbing commit-graph [--tip <id>] [--stop-at <id>] [-n <max>] [-c] [-1] [-a] [-f json|text]
373 ```
374
375 Performs a BFS walk from a tip commit (defaulting to HEAD), following both
376 `parent_commit_id` and `parent2_commit_id` pointers. Returns every reachable
377 commit as a JSON array. Useful for building visualisations, computing
378 reachability sets, or finding the commits on a branch since it diverged from
379 another.
380
381 **Flags**
382
383 | Flag | Short | Default | Description |
384 |---|---|---|---|
385 | `--tip` | — | HEAD | Commit to start from |
386 | `--stop-at` | — | — | Stop BFS at this commit (exclusive) |
387 | `--max` | `-n` | 10 000 | Maximum commits to traverse |
388 | `--count` | `-c` | off | Emit only the integer count, not the full node list |
389 | `--first-parent` | `-1` | off | Follow only first-parent links — linear history, no merge parents |
390 | `--ancestry-path` | `-a` | off | With `--stop-at`: restrict to commits on the direct path between tip and stop-at |
391 | `--format` | `-f` | `json` | `json` or `text` (one ID per line) |
392
393 **Output — JSON**
394
395 ```json
396 {
397 "tip": "a3f2...c8d1",
398 "count": 42,
399 "truncated": false,
400 "commits": [
401 {
402 "commit_id": "a3f2...c8d1",
403 "parent_commit_id": "ff01...23ab",
404 "parent2_commit_id": null,
405 "message": "Add verse melody",
406 "branch": "main",
407 "committed_at": "2026-03-18T12:00:00+00:00",
408 "snapshot_id": "b7e4...f912",
409 "author": "gabriel"
410 }
411 ]
412 }
413 ```
414
415 `truncated` is `true` when the graph was cut off by `--max`.
416
417 **Output — `--count`**
418
419 ```json
420 {"tip": "a3f2...c8d1", "count": 42}
421 ```
422
423 `--count` suppresses the `commits` array entirely, making it suitable for fast
424 cardinality checks without loading commit metadata.
425
426 **Examples**
427
428 Commits on a feature branch since it diverged from `main`:
429
430 ```sh
431 BASE=$(muse plumbing merge-base feat/x main -f text)
432 muse plumbing commit-graph --tip feat/x --stop-at "$BASE" -f text
433 ```
434
435 Count commits in a feature branch:
436
437 ```sh
438 muse plumbing commit-graph \
439 --tip $(muse plumbing rev-parse feat/x -f text) \
440 --stop-at $(muse plumbing merge-base feat/x dev -f text) \
441 --count
442 ```
443
444 Linear history only (skip merge parents):
445
446 ```sh
447 muse plumbing commit-graph --first-parent -f text
448 ```
449
450 **Exit codes:** 0 graph emitted · 1 tip commit not found, `--ancestry-path` without `--stop-at`, or bad `--format`
451
452 ---
453
454 ### `pack-objects` — bundle commits for transport
455
456 ```
457 muse plumbing pack-objects <want>... [--have <id>...]
458 ```
459
460 Collects a set of commits — and all their referenced snapshots and objects —
461 into a single JSON `PackBundle` written to `stdout`. Pass `--have` to tell
462 the packer which commits the receiver already has; objects reachable only from
463 `--have` ancestors are excluded, minimising transfer size.
464
465 `<want>` may be a full commit ID or `HEAD`.
466
467 **Flags**
468
469 | Flag | Short | Description |
470 |---|---|---|
471 | `--have` | — | Commits the receiver already has (repeat for multiple) |
472
473 **Output** — a JSON `PackBundle` object (pipe to a file or `unpack-objects`)
474
475 ```json
476 {
477 "commits": [...],
478 "snapshots": [...],
479 "objects": [{"object_id": "...", "content_b64": "..."}],
480 "branch_heads": {"main": "a3f2...c8d1"}
481 }
482 ```
483
484 `objects` entries are base64-encoded so the bundle is safe for any JSON-capable
485 transport (HTTP body, agent message, file).
486
487 **Exit codes:** 0 pack written · 1 a wanted commit not found or HEAD has no commits · 3 I/O error reading from the local store
488
489 ---
490
491 ### `unpack-objects` — apply a bundle to the local store
492
493 ```
494 cat pack.json | muse plumbing unpack-objects
495 muse plumbing pack-objects HEAD | muse plumbing unpack-objects
496 ```
497
498 Reads a `PackBundle` JSON document from `stdin` and writes its commits,
499 snapshots, and objects into `.muse/`. Idempotent: objects already present in
500 the store are silently skipped. Partial packs from interrupted transfers are
501 safe to re-apply.
502
503 **Output**
504
505 ```json
506 {
507 "commits_written": 12,
508 "snapshots_written": 12,
509 "objects_written": 47,
510 "objects_skipped": 3
511 }
512 ```
513
514 **Exit codes:** 0 unpacked (all objects stored) · 1 invalid JSON from stdin · 3 write failure
515
516 ---
517
518 ### `ls-remote` — list refs on a remote
519
520 ```
521 muse plumbing ls-remote [<remote-or-url>] [-j]
522 ```
523
524 Contacts a remote and lists every branch HEAD without altering local state.
525 The `<remote-or-url>` argument is either a remote name configured with
526 `muse remote add` (defaults to `origin`) or a full `https://` URL.
527
528 **Flags**
529
530 | Flag | Short | Default | Description |
531 |---|---|---|---|
532 | `--json` | `-j` | off | Emit structured JSON instead of tab-separated text |
533
534 **Output — text (default)**
535
536 One line per branch, tab-separated. The default branch is marked with ` *`.
537
538 ```
539 a3f2...c8d1 main *
540 b7e4...f912 feat/experiment
541 ```
542
543 **Output — `--json`**
544
545 ```json
546 {
547 "repo_id": "550e8400-e29b-41d4-a716-446655440000",
548 "domain": "midi",
549 "default_branch": "main",
550 "branches": {
551 "main": "a3f2...c8d1",
552 "feat/experiment": "b7e4...f912"
553 }
554 }
555 ```
556
557 **Exit codes:** 0 remote contacted · 1 remote not configured or URL invalid · 3 transport error (network, HTTP error)
558
559 ---
560
561 ## Composability Patterns
562
563 ### Export a history range
564
565 ```sh
566 # All commits on feat/x that are not on main
567 BASE=$(muse plumbing rev-parse main -f text)
568 TIP=$(muse plumbing rev-parse feat/x -f text)
569 muse plumbing commit-graph --tip "$TIP" --stop-at "$BASE" -f text
570 ```
571
572 ### Ship commits between two machines
573
574 ```sh
575 # On the sender — pack everything the receiver doesn't have
576 HAVE=$(muse plumbing ls-remote origin -f text | awk '{print "--have " $1}' | tr '\n' ' ')
577 muse plumbing pack-objects HEAD $HAVE > bundle.json
578
579 # On the receiver
580 cat bundle.json | muse plumbing unpack-objects
581 muse plumbing update-ref main <commit-id>
582 ```
583
584 ### Verify a stored object
585
586 ```sh
587 ID=$(muse plumbing hash-object tracks/drums.mid -f text)
588 muse plumbing cat-object "$ID" -f info
589 ```
590
591 ### Inspect what changed in the last commit
592
593 ```sh
594 muse plumbing read-commit $(muse plumbing rev-parse HEAD -f text) | \
595 python3 -c "import sys, json; d=json.load(sys.stdin); print(d['message'])"
596 ```
597
598 ### Script a bare commit (advanced)
599
600 ```sh
601 # 1. Hash and store the files
602 OID=$(muse plumbing hash-object -w tracks/drums.mid -f text)
603
604 # 2. Build a snapshot manifest and write it (via muse commit is easier,
605 # but for full control use commit-tree after writing the snapshot)
606 SNAP=$(muse plumbing rev-parse HEAD -f text | \
607 xargs -I{} muse plumbing read-commit {} | \
608 python3 -c "import sys,json; print(json.load(sys.stdin)['snapshot_id'])")
609
610 # 3. Create a commit on top of HEAD
611 PARENT=$(muse plumbing rev-parse HEAD -f text)
612 NEW=$(muse plumbing commit-tree -s "$SNAP" -p "$PARENT" -m "scripted commit" | \
613 python3 -c "import sys,json; print(json.load(sys.stdin)['commit_id'])")
614
615 # 4. Advance the branch
616 muse plumbing update-ref main "$NEW"
617 ```
618
619 ---
620
621 ### `merge-base` — find the common ancestor of two commits
622
623 Find the lowest common ancestor of two commits — the point at which two
624 branches diverged.
625
626 ```sh
627 muse plumbing merge-base <commit-a> <commit-b> [-f json|text]
628 ```
629
630 | Flag | Short | Default | Description |
631 |---|---|---|---|
632 | `--format` | `-f` | `json` | Output format: `json` or `text` |
633
634 Arguments accept full SHA-256 commit IDs, branch names, or `HEAD`.
635
636 **JSON output:**
637
638 ```json
639 {
640 "commit_a": "<sha256>",
641 "commit_b": "<sha256>",
642 "merge_base": "<sha256>"
643 }
644 ```
645
646 When no common ancestor exists, `merge_base` is `null` and `error` is set.
647
648 | Exit | Meaning |
649 |---|---|
650 | 0 | Result computed (check `merge_base` for null vs. found) |
651 | 1 | Ref cannot be resolved; bad `--format` |
652 | 3 | DAG walk failed |
653
654 ---
655
656 ### `snapshot-diff` — diff two snapshot manifests
657
658 Compare two snapshots and categorise every changed path as added, modified,
659 or deleted.
660
661 ```sh
662 muse plumbing snapshot-diff <ref-a> <ref-b> [-f json|text] [-s]
663 ```
664
665 | Flag | Short | Default | Description |
666 |---|---|---|---|
667 | `--format` | `-f` | `json` | Output format: `json` or `text` |
668 | `--stat` | `-s` | false | Append a summary line in text mode |
669
670 Arguments accept snapshot IDs, commit IDs, branch names, or `HEAD`.
671
672 **JSON output:**
673
674 ```json
675 {
676 "snapshot_a": "<sha256>",
677 "snapshot_b": "<sha256>",
678 "added": [{"path": "new.mid", "object_id": "<sha256>"}],
679 "modified": [{"path": "main.mid", "object_id_a": "<sha256>", "object_id_b": "<sha256>"}],
680 "deleted": [{"path": "old.mid", "object_id": "<sha256>"}],
681 "total_changes": 3
682 }
683 ```
684
685 **Text output:**
686
687 ```
688 A new.mid
689 M main.mid
690 D old.mid
691 ```
692
693 | Exit | Meaning |
694 |---|---|
695 | 0 | Diff computed (zero changes is a valid result) |
696 | 1 | Ref cannot be resolved; bad `--format` |
697 | 3 | I/O error reading snapshot records |
698
699 ---
700
701 ### `domain-info` — inspect the active domain plugin
702
703 Inspect the domain plugin active for this repository — its name, class,
704 optional protocol capabilities, and full structural schema.
705
706 ```sh
707 muse plumbing domain-info [-f json|text] [-a]
708 ```
709
710 | Flag | Short | Default | Description |
711 |---|---|---|---|
712 | `--format` | `-f` | `json` | Output format: `json` or `text` |
713 | `--all-domains` | `-a` | false | List every registered domain; no repo required |
714
715 **JSON output:**
716
717 ```json
718 {
719 "domain": "midi",
720 "plugin_class": "MidiPlugin",
721 "capabilities": {
722 "structured_merge": true,
723 "crdt": false,
724 "rerere": false
725 },
726 "schema": {
727 "domain": "midi", "merge_mode": "three_way",
728 "dimensions": [...], "top_level": {...}
729 },
730 "registered_domains": ["bitcoin", "code", "midi", "scaffold"]
731 }
732 ```
733
734 | Exit | Meaning |
735 |---|---|
736 | 0 | Domain resolved and schema emitted |
737 | 1 | Domain not registered; bad `--format` |
738 | 3 | Plugin raised an error computing its schema |
739
740 ---
741
742 ### `show-ref` — list all branch refs
743
744 List all branch refs and the commit IDs they point to.
745
746 ```sh
747 muse plumbing show-ref [-f json|text] [-p PATTERN] [-H] [-v REF]
748 ```
749
750 | Flag | Short | Default | Description |
751 |---|---|---|---|
752 | `--format` | `-f` | `json` | Output format: `json` or `text` |
753 | `--pattern` | `-p` | `""` | fnmatch glob to filter ref names |
754 | `--head` | `-H` | false | Print only HEAD ref and commit ID |
755 | `--verify` | `-v` | `""` | Silent existence check — exit 0 if found, 1 if not |
756
757 **JSON output:**
758
759 ```json
760 {
761 "refs": [
762 {"ref": "refs/heads/dev", "commit_id": "<sha256>"},
763 {"ref": "refs/heads/main", "commit_id": "<sha256>"}
764 ],
765 "head": {"ref": "refs/heads/main", "branch": "main", "commit_id": "<sha256>"},
766 "count": 2
767 }
768 ```
769
770 Use `--verify` in shell conditionals:
771
772 ```sh
773 muse plumbing show-ref --verify refs/heads/my-branch && echo "branch exists"
774 ```
775
776 | Exit | Meaning |
777 |---|---|
778 | 0 | Refs enumerated (or `--verify` ref exists) |
779 | 1 | `--verify` ref absent; bad `--format` |
780 | 3 | I/O error reading refs directory |
781
782 ---
783
784 ### `check-ignore` — test whether paths are excluded by `.museignore`
785
786 Test whether workspace paths are excluded by `.museignore` rules.
787
788 ```sh
789 muse plumbing check-ignore <path>... [-f json|text] [-q] [-V]
790 ```
791
792 | Flag | Short | Default | Description |
793 |---|---|---|---|
794 | `--format` | `-f` | `json` | Output format: `json` or `text` |
795 | `--quiet` | `-q` | false | No output; exit 0 if all ignored, 1 otherwise |
796 | `--verbose` | `-V` | false | Include matching pattern in text output |
797
798 **JSON output:**
799
800 ```json
801 {
802 "domain": "midi",
803 "patterns_loaded": 4,
804 "results": [
805 {"path": "build/out.bin", "ignored": true, "matching_pattern": "build/"},
806 {"path": "tracks/dr.mid", "ignored": false, "matching_pattern": null}
807 ]
808 }
809 ```
810
811 Last-match-wins: a negation rule (`!important.mid`) can un-ignore a path
812 matched by an earlier rule.
813
814 | Exit | Meaning |
815 |---|---|
816 | 0 | Results emitted (or `--quiet` with all ignored) |
817 | 1 | `--quiet` with any non-ignored path; missing args |
818 | 3 | TOML parse error in `.museignore` |
819
820 ---
821
822 ### `check-attr` — query merge-strategy attributes for paths
823
824 Query merge-strategy attributes for workspace paths from `.museattributes`.
825
826 ```sh
827 muse plumbing check-attr <path>... [-f json|text] [-d DIMENSION] [-A]
828 ```
829
830 | Flag | Short | Default | Description |
831 |---|---|---|---|
832 | `--format` | `-f` | `json` | Output format: `json` or `text` |
833 | `--dimension` | `-d` | `*` | Domain axis to query (e.g. `notes`, `tempo`) |
834 | `--all-rules` | `-A` | false | Return every matching rule, not just first-match |
835
836 **JSON output (default: first-match):**
837
838 ```json
839 {
840 "domain": "midi",
841 "rules_loaded": 3,
842 "dimension": "*",
843 "results": [
844 {
845 "path": "drums/kit.mid",
846 "dimension": "*",
847 "strategy": "ours",
848 "rule": {"path_pattern": "drums/*", "strategy": "ours", "priority": 10, ...}
849 }
850 ]
851 }
852 ```
853
854 When no rule matches, `strategy` is `"auto"` and `rule` is `null`.
855
856 | Exit | Meaning |
857 |---|---|
858 | 0 | Attributes resolved and emitted |
859 | 1 | Missing args; bad `--format` |
860 | 3 | TOML parse error in `.museattributes` |
861
862 ---
863
864 ### `verify-object` — re-hash stored objects to detect corruption
865
866 Re-hash stored objects to detect silent data corruption.
867
868 ```sh
869 muse plumbing verify-object <object-id>... [-f json|text] [-q]
870 ```
871
872 | Flag | Short | Default | Description |
873 |---|---|---|---|
874 | `--format` | `-f` | `json` | Output format: `json` or `text` |
875 | `--quiet` | `-q` | false | No output; exit 0 if all OK, 1 otherwise |
876
877 Objects are streamed in 64 KiB chunks — safe for very large blobs.
878
879 **JSON output:**
880
881 ```json
882 {
883 "results": [
884 {"object_id": "<sha256>", "ok": true, "size_bytes": 4096, "error": null},
885 {"object_id": "<sha256>", "ok": false, "size_bytes": null,
886 "error": "object not found in store"}
887 ],
888 "all_ok": false,
889 "checked": 2,
890 "failed": 1
891 }
892 ```
893
894 Compose with `show-ref` to verify every commit in a repo:
895
896 ```sh
897 muse plumbing show-ref -f json \
898 | jq -r '.refs[].commit_id' \
899 | xargs muse plumbing verify-object
900 ```
901
902 | Exit | Meaning |
903 |---|---|
904 | 0 | All objects verified successfully |
905 | 1 | One or more objects failed; object not found; bad args |
906 | 3 | Unexpected I/O error (disk read failure) |
907
908 ---
909
910 ### `symbolic-ref` — read or write HEAD's symbolic reference
911
912 In Muse, HEAD is always a symbolic reference — it always points to a branch,
913 never directly to a commit. `symbolic-ref` reads which branch HEAD tracks or,
914 with `--set`, points HEAD at a different branch.
915
916 ```sh
917 # Read mode
918 muse plumbing symbolic-ref HEAD [-f json|text] [--short]
919
920 # Write mode
921 muse plumbing symbolic-ref HEAD --set <branch> [-f json|text]
922 ```
923
924 | Flag | Short | Default | Description |
925 |---|---|---|---|
926 | `--set` | `-s` | `""` | Branch name to point HEAD at |
927 | `--short` | `-S` | false | Emit branch name only (not the full `refs/heads/…` path) |
928 | `--format` | `-f` | `json` | Output format: `json` or `text` |
929
930 **JSON output (read mode):**
931
932 ```json
933 {
934 "ref": "HEAD",
935 "symbolic_target": "refs/heads/main",
936 "branch": "main",
937 "commit_id": "<sha256>"
938 }
939 ```
940
941 `commit_id` is `null` when the branch has no commits yet.
942
943 **Text output:** `refs/heads/main` (or just `main` with `--short`)
944
945 | Exit | Meaning |
946 |---|---|
947 | 0 | Ref read or written |
948 | 1 | `--set` target branch does not exist; bad `--format` |
949 | 3 | I/O error reading or writing HEAD |
950
951 ---
952
953 ### `for-each-ref` — iterate all refs with rich commit metadata
954
955 Enumerates every branch ref together with the full commit metadata it points to.
956 Supports sorting by any commit field and glob-pattern filtering, making it
957 ideal for agent pipelines that need to slice the ref list without post-processing.
958
959 ```sh
960 muse plumbing for-each-ref [-p <pattern>] [-s <field>] [-d] [-n <count>] [-f json|text]
961 ```
962
963 | Flag | Short | Default | Description |
964 |---|---|---|---|
965 | `--pattern` | `-p` | `""` | fnmatch glob on the full ref name, e.g. `refs/heads/feat/*` |
966 | `--sort` | `-s` | `ref` | Sort field: `ref`, `branch`, `commit_id`, `author`, `committed_at`, `message` |
967 | `--desc` | `-d` | false | Reverse sort order (descending) |
968 | `--count` | `-n` | `0` | Limit to first N refs after sorting (0 = unlimited) |
969 | `--format` | `-f` | `json` | Output format: `json` or `text` |
970
971 **JSON output:**
972
973 ```json
974 {
975 "refs": [
976 {
977 "ref": "refs/heads/dev",
978 "branch": "dev",
979 "commit_id": "<sha256>",
980 "author": "gabriel",
981 "message": "Add verse melody",
982 "committed_at": "2026-01-01T00:00:00+00:00",
983 "snapshot_id": "<sha256>"
984 }
985 ],
986 "count": 1
987 }
988 ```
989
990 **Text output:** `<commit_id> <ref> <committed_at> <author>`
991
992 **Example — three most recently committed branches:**
993
994 ```sh
995 muse plumbing for-each-ref --sort committed_at --desc --count 3
996 ```
997
998 | Exit | Meaning |
999 |---|---|
1000 | 0 | Refs emitted (list may be empty) |
1001 | 1 | Bad `--sort` field; bad `--format` |
1002 | 3 | I/O error reading refs or commit records |
1003
1004 ---
1005
1006 ### `name-rev` — map commit IDs to branch-relative names
1007
1008 For each supplied commit ID, performs a single multi-source BFS from all branch
1009 tips and reports the closest branch and hop distance. Results are expressed as
1010 `<branch>~N` — where N is the number of parent hops from the tip. When N is 0
1011 (the commit is the exact branch tip) the name is the bare branch name with no
1012 `~0` suffix.
1013
1014 ```sh
1015 muse plumbing name-rev <commit-id>... [-n] [-u <string>] [-f json|text]
1016 ```
1017
1018 | Flag | Short | Default | Description |
1019 |---|---|---|---|
1020 | `--name-only` | `-n` | false | Emit only the name (or the undefined string), not the commit ID |
1021 | `--undefined` | `-u` | `"undefined"` | String to emit for unreachable commits |
1022 | `--format` | `-f` | `json` | Output format: `json` or `text` |
1023
1024 **JSON output:**
1025
1026 ```json
1027 {
1028 "results": [
1029 {
1030 "commit_id": "<sha256>",
1031 "name": "main~3",
1032 "branch": "main",
1033 "distance": 3,
1034 "undefined": false
1035 },
1036 {
1037 "commit_id": "<sha256>",
1038 "name": null,
1039 "branch": null,
1040 "distance": null,
1041 "undefined": true
1042 }
1043 ]
1044 }
1045 ```
1046
1047 **Text output:** `<sha256> main~3` (or `main~3` with `--name-only`)
1048
1049 **Performance:** A single O(total-commits) BFS from all branch tips simultaneously.
1050 Every commit is visited at most once regardless of how many input IDs are supplied.
1051
1052 | Exit | Meaning |
1053 |---|---|
1054 | 0 | All results computed (some may be `undefined`) |
1055 | 1 | Bad `--format`; no commit IDs supplied |
1056 | 3 | I/O error reading commit records |
1057
1058 ---
1059
1060 ### `check-ref-format` — validate branch and ref names
1061
1062 Tests one or more names against Muse's branch-naming rules — the same
1063 validation used by `muse branch` and `muse plumbing update-ref`. Use in
1064 scripts to pre-validate names before attempting to create a branch.
1065
1066 ```sh
1067 muse plumbing check-ref-format <name>... [-q] [-f json|text]
1068 ```
1069
1070 | Flag | Short | Default | Description |
1071 |---|---|---|---|
1072 | `--quiet` | `-q` | false | No output — exit 0 if all valid, exit 1 otherwise |
1073 | `--format` | `-f` | `json` | Output format: `json` or `text` |
1074
1075 **Rules enforced:** 1–255 chars; no backslash, null bytes, CR, LF, or tab;
1076 no leading/trailing dot; no consecutive dots (`..`); no leading/trailing or
1077 consecutive slashes.
1078
1079 **JSON output:**
1080
1081 ```json
1082 {
1083 "results": [
1084 {"name": "feat/my-branch", "valid": true, "error": null},
1085 {"name": "bad..name", "valid": false, "error": "..."}
1086 ],
1087 "all_valid": false
1088 }
1089 ```
1090
1091 **Text output:**
1092 ```
1093 ok feat/my-branch
1094 FAIL bad..name → Branch name 'bad..name' contains forbidden characters
1095 ```
1096
1097 **Shell conditional:**
1098
1099 ```sh
1100 muse plumbing check-ref-format -q "$BRANCH" && git checkout -b "$BRANCH"
1101 ```
1102
1103 | Exit | Meaning |
1104 |---|---|
1105 | 0 | All names are valid |
1106 | 1 | One or more names are invalid; no names supplied |
1107
1108 ---
1109
1110 ### `verify-pack` — verify PackBundle integrity
1111
1112 Reads a PackBundle JSON from stdin or `--file` and performs three-tier integrity
1113 checking:
1114
1115 1. **Object integrity** — every object payload is base64-decoded and its SHA-256
1116 is recomputed. The digest must match the declared `object_id`.
1117 2. **Snapshot consistency** — every snapshot's manifest entries reference objects
1118 present in the bundle or already in the local store.
1119 3. **Commit consistency** — every commit's `snapshot_id` is present in the bundle
1120 or already in the local store.
1121
1122 ```sh
1123 muse plumbing pack-objects main | muse plumbing verify-pack
1124 muse plumbing verify-pack --file bundle.json
1125 ```
1126
1127 | Flag | Short | Default | Description |
1128 |---|---|---|---|
1129 | `--file` | `-i` | `""` | Path to bundle file (reads stdin when omitted) |
1130 | `--quiet` | `-q` | false | No output — exit 0 if clean, exit 1 on any failure |
1131 | `--no-local` | `-L` | false | Skip local store checks (verify bundle in isolation) |
1132 | `--format` | `-f` | `json` | Output format: `json` or `text` |
1133
1134 **JSON output:**
1135
1136 ```json
1137 {
1138 "objects_checked": 42,
1139 "snapshots_checked": 5,
1140 "commits_checked": 5,
1141 "all_ok": true,
1142 "failures": []
1143 }
1144 ```
1145
1146 **With failures:**
1147
1148 ```json
1149 {
1150 "all_ok": false,
1151 "failures": [
1152 {"kind": "object", "id": "<sha256>", "error": "hash mismatch"},
1153 {"kind": "snapshot", "id": "<sha256>", "error": "missing object: ..."}
1154 ]
1155 }
1156 ```
1157
1158 **Validate before upload:**
1159
1160 ```sh
1161 muse plumbing pack-objects main | muse plumbing verify-pack -q \
1162 && echo "bundle is clean — safe to push"
1163 ```
1164
1165 | Exit | Meaning |
1166 |---|---|
1167 | 0 | Bundle is fully intact |
1168 | 1 | One or more integrity failures; malformed JSON; bad args |
1169 | 3 | I/O error reading stdin or the bundle file |
1170
1171 ---
1172
1173 ---
1174
1175 ## Composability Patterns — Advanced
1176
1177 ### Name every commit reachable from a branch
1178
1179 ```sh
1180 # Get all commit IDs on feat/x since it diverged from dev
1181 BASE=$(muse plumbing merge-base feat/x dev -f text)
1182 muse plumbing commit-graph --tip feat/x --stop-at "$BASE" -f text \
1183 | xargs muse plumbing name-rev --name-only
1184 ```
1185
1186 ### Audit all refs with full metadata and filter by recency
1187
1188 ```sh
1189 # List all branches modified in 2026, sorted newest-first
1190 muse plumbing for-each-ref --sort committed_at --desc \
1191 | jq '.refs[] | select(.committed_at | startswith("2026"))'
1192 ```
1193
1194 ### Validate a branch name before creating it
1195
1196 ```sh
1197 BRANCH="feat/my-feature"
1198 muse plumbing check-ref-format -q "$BRANCH" \
1199 && echo "Name is valid — safe to branch" \
1200 || echo "Invalid branch name"
1201 ```
1202
1203 ### Verify a bundle before shipping
1204
1205 ```sh
1206 muse plumbing pack-objects main | tee bundle.json | muse plumbing verify-pack -q \
1207 && echo "bundle is clean — safe to push" \
1208 || echo "bundle has integrity failures — do not push"
1209 ```
1210
1211 ### Switch active branch via plumbing
1212
1213 ```sh
1214 # Check where HEAD is now
1215 muse plumbing symbolic-ref HEAD -f text # → refs/heads/main
1216 # Redirect HEAD to dev
1217 muse plumbing symbolic-ref HEAD --set dev
1218 muse plumbing rev-parse HEAD -f text # → tip of dev
1219 ```
1220
1221 ### Find stale branches (no commits in the last 30 days)
1222
1223 ```sh
1224 # Requires `date` and `jq`
1225 CUTOFF=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
1226 || date -u --date="30 days ago" +%Y-%m-%dT%H:%M:%SZ)
1227 muse plumbing for-each-ref -f json \
1228 | jq --arg c "$CUTOFF" '.refs[] | select(.committed_at < $c) | .branch'
1229 ```
1230
1231 ### Check which files changed between two branches
1232
1233 ```sh
1234 BASE=$(muse plumbing merge-base main feat/x -f text)
1235 muse plumbing snapshot-diff "$BASE" feat/x --format text --stat
1236 ```
1237
1238 ---
1239
1240 ## Object ID Quick Reference
1241
1242 All IDs in Muse are 64-character lowercase hex SHA-256 digests. There are
1243 three kinds:
1244
1245 | Kind | Computed from | Used by |
1246 |---|---|---|
1247 | **Object ID** | File bytes | `hash-object`, `cat-object`, snapshot manifests |
1248 | **Snapshot ID** | Sorted `path:object_id` pairs | `read-snapshot`, `commit-tree` |
1249 | **Commit ID** | Parent IDs + snapshot ID + message + timestamp | `read-commit`, `rev-parse`, `update-ref` |
1250
1251 Every ID is deterministic and content-addressed. The same input always
1252 produces the same ID; two different inputs never produce the same ID in
1253 practice.
1254
1255 ---
1256
1257 ## Exit Code Summary
1258
1259 | Code | Constant | Meaning |
1260 |---|---|---|
1261 | 0 | `SUCCESS` | Command completed successfully |
1262 | 1 | `USER_ERROR` | Bad input, ref not found, invalid format |
1263 | 3 | `INTERNAL_ERROR` | I/O failure, integrity check, transport error |