gabriel / muse public
intent.py python
293 lines 10.6 KB
Raw
sha256:c0eba5ad689cec79f4a3fcdc4f5da78556cb4b8cb7b330f944634356c10379ed chore: pivot to nightly channel — bump version to 0.2.0.dev… Sonnet 5 patch 12 days ago
1 """``muse coord intent`` — declare a specific operation before executing it.
2
3 Records a structured intent extending an existing reservation. Whereas
4 ``muse reserve`` says "I will touch these symbols", ``muse coord intent``
5 says "I will rename src/billing.py::compute_total to compute_invoice_total".
6
7 This additional detail enables:
8
9 * ``muse forecast`` to compute more precise conflict predictions
10 (a rename conflicts differently with a delete than a modify does).
11 * ``muse plan-merge`` to classify conflicts by a semantic taxonomy.
12 * Audit trail of what each agent intended before committing.
13
14 Usage::
15
16 muse coord intent "src/billing.py::compute_total" \\
17 --op rename --detail "rename to compute_invoice_total" \\
18 --reservation-id <sha256:...>
19
20 muse coord intent "src/auth.py::validate_token" \\
21 --op extract --detail "extract into src/auth/validators.py" \\
22 --run-id agent-42
23
24 muse coord intent "src/core.py::hash_content" --op delete --run-id refactor-bot
25
26 JSON output schema::
27
28 {
29 "schema_version": str,
30 "intent_id": str, // content-addressed ID
31 "reservation_id": str, // content-addressed ID or "" if standalone
32 "run_id": str,
33 "branch": str,
34 "addresses": [str, ...],
35 "operation": str,
36 "created_at": str, // ISO 8601
37 "detail": str
38 }
39
40 Exit codes::
41
42 0 — intent recorded successfully
43 1 — bad arguments (unknown --op, --run-id too long, --detail too long,
44 too many addresses, --reservation-id not a valid sha256: content ID)
45
46 Flags:
47
48 ``--op OPERATION``
49 Required. The operation being declared:
50 delete | extract | inline | merge | modify | move | rename | split.
51
52 ``--detail TEXT``
53 Human-readable description of the intended change.
54 Maximum length: 4096 characters.
55
56 ``--reservation-id <sha256:...>``
57 Link to an existing reservation (optional; creates a standalone intent
58 if omitted). When provided, must be a valid sha256: content ID.
59
60 ``--run-id ID``
61 Agent identifier (used when --reservation-id is not provided).
62 Maximum length: 256 characters.
63
64 ``--json`` / ``-j``
65 Emit intent details as compact JSON on stdout.
66 """
67
68 import argparse
69 import json
70 import logging
71 import sys
72
73 from muse.core.coordination import _validate_reservation_id, create_intent
74 from muse.core.errors import ExitCode
75 from muse.core.repo import require_repo
76 from muse.core.refs import read_current_branch
77 from muse.core.validation import sanitize_display
78
79 logger = logging.getLogger(__name__)
80
81 # ── Input constraints ─────────────────────────────────────────────────────────
82
83 #: Allowed values for ``--op``. Intent supports more granular operations
84 #: than ``reserve`` (which only tracks which symbols will change).
85 _VALID_OPS: frozenset[str] = frozenset({
86 "rename", "move", "modify", "extract", "delete", "inline", "split", "merge",
87 })
88
89 #: Maximum byte-length of the ``--run-id`` value. Mirrors all other
90 #: coordination commands so audit records stay bounded in size.
91 _MAX_RUN_ID_LEN: int = 256
92
93 #: Maximum character-length of the ``--detail`` value. Bounds intent file
94 #: sizes and prevents pathological memory use when loading large intent sets.
95 _MAX_DETAIL_LEN: int = 4096
96
97 #: Maximum number of symbol addresses per intent call. Mirrors reserve.py.
98 _MAX_ADDRESSES: int = 1000
99
100 # ── CLI registration ──────────────────────────────────────────────────────────
101
102 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
103 """Register the ``intent`` subcommand on *subparsers* (under ``muse coord``).
104
105 Wires all flags with their defaults, choices, and help text so that
106 ``--help`` output is accurate. Sets ``func`` to :func:`run`.
107 """
108 parser = subparsers.add_parser(
109 "intent",
110 help="Declare a specific operation before executing it.",
111 description=__doc__,
112 formatter_class=argparse.RawDescriptionHelpFormatter,
113 )
114 parser.add_argument(
115 "addresses",
116 nargs="+",
117 metavar="ADDRESS",
118 help=(
119 "Symbol address(es) this intent applies to. "
120 f"Maximum {_MAX_ADDRESSES} per call."
121 ),
122 )
123 parser.add_argument(
124 "--op",
125 dest="operation",
126 required=True,
127 choices=sorted(_VALID_OPS),
128 metavar="OPERATION",
129 help=(
130 f"Operation to declare — one of: {', '.join(sorted(_VALID_OPS))}."
131 ),
132 )
133 parser.add_argument(
134 "--detail",
135 default="",
136 metavar="TEXT",
137 help=(
138 "Human-readable description of the intended change. "
139 f"Maximum {_MAX_DETAIL_LEN} characters."
140 ),
141 )
142 parser.add_argument(
143 "--reservation-id",
144 dest="reservation_id",
145 default="",
146 metavar="ID",
147 help=(
148 "Link to an existing reservation. When provided, must be a valid "
149 "sha256: content ID. Omit to create a standalone intent."
150 ),
151 )
152 parser.add_argument(
153 "--run-id",
154 dest="run_id",
155 default="unknown",
156 metavar="ID",
157 help=(
158 "Agent identifier (used when --reservation-id is not provided). "
159 f"Maximum {_MAX_RUN_ID_LEN} characters."
160 ),
161 )
162 parser.add_argument(
163 "--json", "-j",
164 action="store_true",
165 dest="json_out",
166 help="Emit intent details as compact JSON on stdout.",
167 )
168 parser.set_defaults(func=run)
169
170 # ── Command implementation ────────────────────────────────────────────────────
171
172 def run(args: argparse.Namespace) -> None:
173 """Declare a specific operation before executing it.
174
175 Execution order
176 ---------------
177 1. **Validate inputs** — ``--run-id`` length, ``--detail`` length, address
178 count, and ``--reservation-id`` content ID syntax (when non-empty). All
179 validation fires before any file I/O; any failure exits
180 :attr:`~muse.core.errors.ExitCode.USER_ERROR` (1) with a message to
181 *stderr*. ``--op`` is validated by argparse ``choices`` before this
182 function is called.
183 2. **Resolve repo** — calls :func:`~muse.core.repo.require_repo` and reads
184 the current branch from the ref store.
185 3. **Write intent** — calls :func:`~muse.core.coordination.create_intent`,
186 which atomically writes ``.muse/coordination/intents/<id>.json``.
187 4. **Emit output** — compact JSON to *stdout* when ``--json``,
188 or human-readable text otherwise.
189
190 What intents enable
191 -------------------
192 ``muse coord intent`` extends a reservation with operational detail. The
193 operation type enables ``muse forecast`` to compute more precise conflict
194 predictions — a rename conflicts differently from a delete. Intents are
195 write-once audit records and never affect VCS correctness.
196
197 Security
198 --------
199 * ``--run-id`` is capped at :data:`_MAX_RUN_ID_LEN` to bound audit record
200 size.
201 * ``--detail`` is capped at :data:`_MAX_DETAIL_LEN` to bound intent file
202 sizes and memory use when loading large intent sets.
203 * Address count is capped at :data:`_MAX_ADDRESSES`.
204 * ``--reservation-id``, when non-empty, is validated as a sha256: content ID before any
205 path construction (even though intent files reference, not construct,
206 the reservation path).
207 * All display strings in text output are passed through
208 :func:`~muse.core.validation.sanitize_display`.
209
210 Args:
211 args: Parsed ``argparse.Namespace`` with attributes ``addresses``,
212 ``operation``, ``detail``, ``reservation_id``, ``run_id``,
213 and ``json_out``.
214
215 Exit codes:
216 0 — intent recorded successfully.
217 1 — bad arguments.
218 """
219 addresses: list[str] = args.addresses
220 operation: str = args.operation
221 detail: str = args.detail
222 reservation_id: str = args.reservation_id
223 run_id: str = args.run_id
224 as_json: bool = args.json_out
225
226 # ── Input validation (before any file I/O) ────────────────────────────────
227
228 if len(run_id) > _MAX_RUN_ID_LEN:
229 msg = f"--run-id is too long ({len(run_id)} chars; max {_MAX_RUN_ID_LEN})"
230 if as_json:
231 print(json.dumps({"error": msg, "status": "bad_args"}))
232 else:
233 print(f"❌ {msg}", file=sys.stderr)
234 raise SystemExit(ExitCode.USER_ERROR)
235
236 if len(detail) > _MAX_DETAIL_LEN:
237 msg = f"--detail is too long ({len(detail)} chars; max {_MAX_DETAIL_LEN})"
238 if as_json:
239 print(json.dumps({"error": msg, "status": "bad_args"}))
240 else:
241 print(f"❌ {msg}", file=sys.stderr)
242 raise SystemExit(ExitCode.USER_ERROR)
243
244 if len(addresses) > _MAX_ADDRESSES:
245 msg = f"Too many addresses: {len(addresses)} (max {_MAX_ADDRESSES})"
246 if as_json:
247 print(json.dumps({"error": msg, "status": "bad_args"}))
248 else:
249 print(f"❌ {msg}", file=sys.stderr)
250 raise SystemExit(ExitCode.USER_ERROR)
251
252 if reservation_id:
253 try:
254 _validate_reservation_id(reservation_id)
255 except ValueError as exc:
256 msg = str(exc)
257 if as_json:
258 print(json.dumps({"error": msg, "status": "bad_reservation_id"}))
259 else:
260 print(f"❌ --reservation-id: {msg}", file=sys.stderr)
261 raise SystemExit(ExitCode.USER_ERROR)
262
263 root = require_repo()
264 branch = read_current_branch(root)
265
266 intent_record = create_intent(
267 root=root,
268 reservation_id=reservation_id,
269 run_id=run_id,
270 branch=branch,
271 addresses=addresses,
272 operation=operation,
273 detail=detail,
274 )
275
276 if as_json:
277 print(json.dumps(intent_record.to_dict()))
278 return
279
280 safe_intent_id = sanitize_display(intent_record.intent_id)
281 safe_run_id = sanitize_display(intent_record.run_id)
282 print(
283 f"\n✅ Intent recorded\n"
284 f" Intent ID: {safe_intent_id}\n"
285 f" Operation: {operation}\n"
286 f" Addresses: {len(addresses)}\n"
287 f" Run ID: {safe_run_id}"
288 )
289 if detail:
290 print(f" Detail: {sanitize_display(detail)}")
291 if reservation_id:
292 print(f" Reservation: {sanitize_display(reservation_id)}")
293 print("\nRun 'muse forecast' to check for predicted conflicts.")
File History 10 commits
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6 chore: bump version to 0.2.0.dev2 — nightly.2 Sonnet 4.6 patch 11 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 14 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9 docs: add issue docs for push have-negotiation bug (#55) an… Sonnet 4.6 18 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74 chore: trigger push to surface null-OID paths Sonnet 4.6 23 days ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8 chore: prebuild timing test Sonnet 4.6 33 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1 merge: pull staging/dev — advance to 0.2.0rc12 Sonnet 4.6 patch 35 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 47 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 49 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 56 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 56 days ago