gabriel / muse public
plumbing.md markdown
583 lines 15.7 KB
7e374cd5 docs: fix plumbing docstring gaps + add plumbing reference doc Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 # Muse Plumbing Commands Reference
2
3 Plumbing commands are the low-level, machine-readable layer of the Muse CLI.
4 They output JSON, stream bytes without size limits, use predictable exit codes,
5 and compose cleanly in shell pipelines and agent scripts.
6
7 If you want to automate Muse — write a script, build an agent workflow, or
8 integrate Muse into another tool — plumbing commands are the right entry point.
9 The higher-level porcelain commands (`muse commit`, `muse merge`, etc.) call
10 these internally.
11
12 ---
13
14 ## 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 ## Object ID Quick Reference
561
562 All IDs in Muse are 64-character lowercase hex SHA-256 digests. There are
563 three kinds:
564
565 | Kind | Computed from | Used by |
566 |---|---|---|
567 | **Object ID** | File bytes | `hash-object`, `cat-object`, snapshot manifests |
568 | **Snapshot ID** | Sorted `path:object_id` pairs | `read-snapshot`, `commit-tree` |
569 | **Commit ID** | Parent IDs + snapshot ID + message + timestamp | `read-commit`, `rev-parse`, `update-ref` |
570
571 Every ID is deterministic and content-addressed. The same input always
572 produces the same ID; two different inputs never produce the same ID in
573 practice.
574
575 ---
576
577 ## Exit Code Summary
578
579 | Code | Constant | Meaning |
580 |---|---|---|
581 | 0 | `SUCCESS` | Command completed successfully |
582 | 1 | `USER_ERROR` | Bad input, ref not found, invalid format |
583 | 3 | `INTERNAL_ERROR` | I/O failure, integrity check, transport error |