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