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