intent.py
python
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