commit_tree.py python
248 lines 7.3 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago
1 """muse plumbing commit-tree — create a commit from an explicit snapshot ID.
2
3 Low-level commit creation: takes a snapshot ID (which must already exist in the
4 store), optional parent commit IDs, and a message, and writes a new
5 ``CommitRecord`` to the store. Does not touch ``HEAD`` or any branch ref.
6
7 Analogous to ``git commit-tree``. Porcelain commands like ``muse commit`` call
8 this internally after staging changes and writing the snapshot.
9
10 Output::
11
12 {"commit_id": "<sha256>"}
13
14 Plumbing contract
15 -----------------
16
17 - Exit 0: commit written, commit_id printed.
18 - Exit 1: snapshot not found, parent commit not found, or repo.json unreadable.
19 - Exit 3: write failure.
20
21 Agent use
22 ---------
23
24 Agents must stamp their identity into every commit they create::
25
26 muse plumbing commit-tree \\
27 --snapshot <snap_id> \\
28 --message "feat: add melody" \\
29 --agent-id counterpoint-bot \\
30 --model-id claude-opus-4 \\
31 --toolchain-id cursor-agent-v2
32
33 Up to two parents are supported (for merge commits)::
34
35 muse plumbing commit-tree --snapshot <id> --parent <p1> --parent <p2>
36 """
37
38 from __future__ import annotations
39
40 import argparse
41 import datetime
42 import json
43 import logging
44 import sys
45
46 from muse.core.errors import ExitCode
47 from muse.core.repo import read_repo_id, require_repo
48 from muse.core.snapshot import compute_commit_id
49 from muse.core.store import (
50 CommitRecord,
51 read_commit,
52 read_current_branch,
53 read_snapshot,
54 write_commit,
55 )
56 from muse.core.validation import validate_object_id
57
58 logger = logging.getLogger(__name__)
59
60 _FORMAT_CHOICES = ("json", "text")
61
62
63 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
64 """Register the commit-tree subcommand."""
65 parser = subparsers.add_parser(
66 "commit-tree",
67 help="Create a commit from an explicit snapshot ID.",
68 description=__doc__,
69 formatter_class=argparse.RawDescriptionHelpFormatter,
70 )
71 parser.add_argument(
72 "--snapshot", "-s",
73 required=True,
74 dest="snapshot_id",
75 metavar="SNAPSHOT_ID",
76 help="SHA-256 snapshot ID.",
77 )
78 parser.add_argument(
79 "--parent", "-p",
80 action="append",
81 default=[],
82 dest="parent",
83 metavar="COMMIT_ID",
84 help=(
85 "Parent commit ID. Repeat once for merge commits. "
86 "At most two parents are supported."
87 ),
88 )
89 parser.add_argument(
90 "--message", "-m",
91 default="",
92 dest="message",
93 metavar="MESSAGE",
94 help="Commit message.",
95 )
96 parser.add_argument(
97 "--author", "-a",
98 default="",
99 dest="author",
100 metavar="AUTHOR",
101 help="Author name.",
102 )
103 parser.add_argument(
104 "--branch", "-b",
105 default=None,
106 dest="branch",
107 metavar="BRANCH",
108 help="Branch name to record (default: current branch).",
109 )
110 # Agent provenance — agents must set these so their identity is auditable.
111 parser.add_argument(
112 "--agent-id",
113 default="",
114 dest="agent_id",
115 metavar="AGENT_ID",
116 help="Stable agent identifier (e.g. 'counterpoint-bot').",
117 )
118 parser.add_argument(
119 "--model-id",
120 default="",
121 dest="model_id",
122 metavar="MODEL_ID",
123 help="Model identifier (e.g. 'claude-opus-4'). Empty for human authors.",
124 )
125 parser.add_argument(
126 "--toolchain-id",
127 default="",
128 dest="toolchain_id",
129 metavar="TOOLCHAIN_ID",
130 help="Toolchain that produced this commit (e.g. 'cursor-agent-v2').",
131 )
132 parser.add_argument(
133 "--format", "-f",
134 dest="fmt",
135 default="json",
136 metavar="FORMAT",
137 help="Output format: json (default) or text (bare commit_id).",
138 )
139 parser.add_argument(
140 "--json", action="store_const", const="json", dest="fmt",
141 help="Shorthand for --format json.",
142 )
143 parser.set_defaults(func=run)
144
145
146 def run(args: argparse.Namespace) -> None:
147 """Create a commit from an explicit snapshot ID.
148
149 The snapshot must already exist in ``.muse/snapshots/``. Each ``--parent``
150 flag adds a parent commit (use once for linear history, twice for merge
151 commits — at most two parents). The commit is written to ``.muse/commits/``
152 but no branch ref is updated — use ``muse plumbing update-ref`` to advance
153 a branch.
154
155 Agents should always pass ``--agent-id``, ``--model-id``, and
156 ``--toolchain-id`` so their provenance is embedded in the commit record
157 and auditable via ``muse plumbing read-commit``.
158
159 Output (``--format json``, default)::
160
161 {"commit_id": "<sha256>"}
162
163 Output (``--format text``)::
164
165 <sha256>
166 """
167 fmt: str = args.fmt
168 snapshot_id: str = args.snapshot_id
169 parent: list[str] = args.parent
170 message: str = args.message
171 author: str = args.author
172 branch: str | None = args.branch
173 agent_id: str = args.agent_id
174 model_id: str = args.model_id
175 toolchain_id: str = args.toolchain_id
176
177 if fmt not in _FORMAT_CHOICES:
178 print(
179 json.dumps({"error": f"Unknown format {fmt!r}. Valid: {', '.join(_FORMAT_CHOICES)}"}),
180 file=sys.stderr,
181 )
182 raise SystemExit(ExitCode.USER_ERROR)
183
184 # CommitRecord only supports two parents (regular and merge).
185 if len(parent) > 2:
186 print(
187 json.dumps({"error": f"At most 2 parents supported; got {len(parent)}."}),
188 file=sys.stderr,
189 )
190 raise SystemExit(ExitCode.USER_ERROR)
191
192 root = require_repo()
193
194 try:
195 validate_object_id(snapshot_id)
196 except ValueError as exc:
197 print(json.dumps({"error": f"Invalid snapshot ID: {exc}"}), file=sys.stderr)
198 raise SystemExit(ExitCode.USER_ERROR)
199
200 for pid in parent:
201 try:
202 validate_object_id(pid)
203 except ValueError as exc:
204 print(json.dumps({"error": f"Invalid parent commit ID: {exc}"}), file=sys.stderr)
205 raise SystemExit(ExitCode.USER_ERROR)
206
207 snap = read_snapshot(root, snapshot_id)
208 if snap is None:
209 print(json.dumps({"error": f"Snapshot not found: {snapshot_id}"}), file=sys.stderr)
210 raise SystemExit(ExitCode.USER_ERROR)
211
212 for pid in parent:
213 if read_commit(root, pid) is None:
214 print(json.dumps({"error": f"Parent commit not found: {pid}"}), file=sys.stderr)
215 raise SystemExit(ExitCode.USER_ERROR)
216
217 repo_id = read_repo_id(root)
218 branch_name = branch or read_current_branch(root)
219 committed_at = datetime.datetime.now(datetime.timezone.utc)
220
221 commit_id = compute_commit_id(
222 parent_ids=parent,
223 snapshot_id=snapshot_id,
224 message=message,
225 committed_at_iso=committed_at.isoformat(),
226 )
227
228 record = CommitRecord(
229 commit_id=commit_id,
230 repo_id=repo_id,
231 branch=branch_name,
232 snapshot_id=snapshot_id,
233 message=message,
234 committed_at=committed_at,
235 author=author,
236 parent_commit_id=parent[0] if len(parent) >= 1 else None,
237 parent2_commit_id=parent[1] if len(parent) >= 2 else None,
238 agent_id=agent_id,
239 model_id=model_id,
240 toolchain_id=toolchain_id,
241 )
242 write_commit(root, record)
243
244 if fmt == "text":
245 print(commit_id)
246 return
247
248 print(json.dumps({"commit_id": commit_id}))
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago