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