gabriel / muse public
pack_objects.py python
196 lines 6.5 KB
Raw
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago
1 """muse pack-objects — build a MPack and write to stdout.
2
3 Collects a set of commits (and all referenced snapshots and objects) into a
4 single msgpack MPack suitable for transport to a remote. Efficient binary
5 encoding with raw bytes for object content (no base64 overhead).
6
7 Usage::
8
9 muse pack-objects <want_id>... [--have <id>...]
10
11 The ``--have`` IDs are commits the receiver already has. Objects reachable
12 exclusively from ``--have`` ancestors are pruned from the mpack.
13
14 Output: a MPack msgpack binary written to stdout (pipe to a file or HTTP
15 request body).
16
17 Output contract
18 ---------------
19
20 - Exit 0: pack written to stdout.
21 - Exit 1: a wanted commit not found or HEAD has no commits.
22 - Exit 3: I/O error reading objects or snapshots from the local store.
23
24 Agent use — dry-run inspection
25 -------------------------------
26
27 Agents can inspect what *would* be packed without producing binary output::
28
29 muse pack-objects HEAD --dry-run
30 # → {
31 # "want": [...], "have": [...],
32 # "commits": 3, "snapshots": 3, "blobs": 12, "object_bytes": 40960,
33 # "duration_ms": 4.2, "exit_code": 0
34 # }
35
36 ``object_bytes`` is the total uncompressed byte size of all object payloads in
37 the pack. Agents use it to decide whether to buffer the full mpack in memory
38 or stream it directly to the remote.
39 """
40
41 import argparse
42 import json
43 import logging
44 import sys
45 from typing import TypedDict
46
47 import msgpack
48
49 from muse.core.errors import ExitCode
50 from muse.core.mpack import build_mpack
51 from muse.core.envelope import EnvelopeJson, make_envelope
52 from muse.core.repo import require_repo
53 from muse.core.refs import (
54 get_head_commit_id,
55 read_current_branch,
56 )
57 from muse.core.validation import validate_object_id
58 from muse.core.timing import start_timer
59
60 logger = logging.getLogger(__name__)
61
62 # ---------------------------------------------------------------------------
63 # Wire-format TypedDicts
64 # ---------------------------------------------------------------------------
65
66 class _PackObjectsDryRunJson(EnvelopeJson):
67 """Stable JSON envelope for ``pack-objects --dry-run`` output."""
68 want: list[str]
69 have: list[str]
70 commits: int
71 snapshots: int
72 blobs: int
73 object_bytes: int
74
75 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
76 """Register the pack-objects subcommand."""
77 parser = subparsers.add_parser(
78 "pack-objects",
79 help="Build a MPack from wanted commits and write to stdout.",
80 description=__doc__,
81 formatter_class=argparse.RawDescriptionHelpFormatter,
82 )
83 parser.add_argument(
84 "want",
85 nargs="+",
86 help="Commit IDs to pack. May be full hex IDs or 'HEAD'.",
87 )
88 parser.add_argument(
89 "--have",
90 action="append",
91 default=[],
92 dest="have",
93 metavar="COMMIT_ID",
94 help="Commits the receiver already has (pruned from pack). Repeat for multiple.",
95 )
96 parser.add_argument(
97 "--dry-run", "-n",
98 action="store_true",
99 dest="dry_run",
100 help=(
101 "Print pack summary as JSON instead of writing binary msgpack. "
102 "Includes elapsed(), exit_code, and object_bytes for agent pipelines."
103 ),
104 )
105 parser.add_argument(
106 "--json", "-j",
107 action="store_true",
108 dest="json_out",
109 help="Emit machine-readable JSON.",
110 )
111 parser.set_defaults(func=run)
112
113 def run(args: argparse.Namespace) -> None:
114 """Build a MPack from wanted commits and write to stdout.
115
116 Traverses the commit graph from each ``want`` ID, collecting all commits,
117 snapshots, and objects not already reachable from ``--have`` ancestors.
118 The resulting binary mpack can be piped to ``muse unpack-objects`` or sent
119 to a MuseHub endpoint. Use ``--dry-run`` for a JSON summary without binary
120 output — shows ``object_bytes`` so agents can size the transfer first.
121
122 Agent quickstart
123 ----------------
124 ::
125
126 muse pack-objects HEAD --dry-run --json
127 muse pack-objects sha256:<id> --dry-run --json
128 muse pack-objects HEAD --have sha256:<base> --dry-run --json
129
130 JSON fields (``--dry-run`` only — binary output otherwise)
131 -----------------------------------------------------------
132 want Resolved want commit IDs.
133 have Have commit IDs excluded from the pack.
134 commits Number of commits included.
135 snapshots Number of snapshots included.
136 objects Number of content objects included.
137 object_bytes Total uncompressed payload bytes.
138
139 Exit codes
140 ----------
141 0 Success.
142 1 Invalid want/have IDs, or HEAD has no commits.
143 2 Not inside a Muse repository.
144 """
145 elapsed = start_timer()
146 want: list[str] = args.want
147 have: list[str] = args.have
148 dry_run: bool = args.dry_run
149
150 root = require_repo()
151
152 # Resolve "HEAD" → commit ID and validate all other IDs upfront so
153 # we fail loudly instead of silently producing empty packs.
154 resolved_wants: list[str] = []
155 for w in want:
156 if w.upper() == "HEAD":
157 branch = read_current_branch(root)
158 cid = get_head_commit_id(root, branch)
159 if cid is None:
160 print(json.dumps({"error": "HEAD has no commits"}), file=sys.stderr)
161 raise SystemExit(ExitCode.USER_ERROR)
162 resolved_wants.append(cid)
163 else:
164 try:
165 validate_object_id(w)
166 except ValueError as exc:
167 print(json.dumps({"error": f"Invalid want ID: {exc}"}), file=sys.stderr)
168 raise SystemExit(ExitCode.USER_ERROR)
169 resolved_wants.append(w)
170
171 for h in have:
172 try:
173 validate_object_id(h)
174 except ValueError as exc:
175 print(json.dumps({"error": f"Invalid --have ID: {exc}"}), file=sys.stderr)
176 raise SystemExit(ExitCode.USER_ERROR)
177
178 mpack = build_mpack(root, commit_ids=resolved_wants, have=have)
179
180 if dry_run:
181 object_bytes = sum(
182 len(obj["content"]) if isinstance(obj.get("content"), (bytes, bytearray)) else 0
183 for obj in mpack["blobs"]
184 )
185 print(json.dumps(_PackObjectsDryRunJson(
186 **make_envelope(elapsed),
187 want=resolved_wants,
188 have=have,
189 commits=len(mpack["commits"]),
190 snapshots=len(mpack["snapshots"]),
191 blobs=len(mpack["blobs"]),
192 object_bytes=object_bytes,
193 )))
194 return
195
196 sys.stdout.buffer.write(msgpack.packb(mpack, use_bin_type=True))
File History 1 commit
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago