intent.py
python
| 1 | """muse 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 intent`` says |
| 5 | "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 intent "src/billing.py::compute_total" \\ |
| 17 | --op rename --detail "rename to compute_invoice_total" \\ |
| 18 | --reservation-id <UUID> |
| 19 | |
| 20 | muse intent "src/auth.py::validate_token" \\ |
| 21 | --op extract --detail "extract into src/auth/validators.py" \\ |
| 22 | --run-id agent-42 |
| 23 | |
| 24 | muse intent "src/core.py::hash_content" --op delete --run-id refactor-bot |
| 25 | |
| 26 | Flags: |
| 27 | |
| 28 | ``--op OPERATION`` |
| 29 | Required. The operation being declared: |
| 30 | rename | move | modify | extract | delete | inline | split | merge. |
| 31 | |
| 32 | ``--detail TEXT`` |
| 33 | Human-readable description of the intended change. |
| 34 | |
| 35 | ``--reservation-id UUID`` |
| 36 | Link to an existing reservation (optional; creates standalone intent if omitted). |
| 37 | |
| 38 | ``--run-id ID`` |
| 39 | Agent identifier (used when --reservation-id is not provided). |
| 40 | |
| 41 | ``--json`` |
| 42 | Emit intent details as JSON. |
| 43 | """ |
| 44 | from __future__ import annotations |
| 45 | |
| 46 | import json |
| 47 | import logging |
| 48 | |
| 49 | import typer |
| 50 | |
| 51 | from muse.core.coordination import create_intent |
| 52 | from muse.core.errors import ExitCode |
| 53 | from muse.core.repo import require_repo |
| 54 | |
| 55 | logger = logging.getLogger(__name__) |
| 56 | |
| 57 | app = typer.Typer() |
| 58 | |
| 59 | _VALID_OPS = frozenset({ |
| 60 | "rename", "move", "modify", "extract", "delete", "inline", "split", "merge", |
| 61 | }) |
| 62 | |
| 63 | |
| 64 | @app.callback(invoke_without_command=True) |
| 65 | def intent( |
| 66 | ctx: typer.Context, |
| 67 | addresses: list[str] = typer.Argument( |
| 68 | ..., metavar="ADDRESS...", |
| 69 | help='Symbol addresses this intent applies to.', |
| 70 | ), |
| 71 | operation: str = typer.Option( |
| 72 | ..., "--op", metavar="OPERATION", |
| 73 | help="Operation to declare: rename, move, modify, extract, delete, inline, split, merge.", |
| 74 | ), |
| 75 | detail: str = typer.Option( |
| 76 | "", "--detail", metavar="TEXT", |
| 77 | help="Human-readable description of the intended change.", |
| 78 | ), |
| 79 | reservation_id: str = typer.Option( |
| 80 | "", "--reservation-id", metavar="UUID", |
| 81 | help="Link to an existing reservation.", |
| 82 | ), |
| 83 | run_id: str = typer.Option( |
| 84 | "unknown", "--run-id", metavar="ID", |
| 85 | help="Agent identifier (used when --reservation-id is not provided).", |
| 86 | ), |
| 87 | as_json: bool = typer.Option(False, "--json", help="Emit intent details as JSON."), |
| 88 | ) -> None: |
| 89 | """Declare a specific operation before executing it. |
| 90 | |
| 91 | ``muse intent`` extends a reservation with operational detail. The |
| 92 | operation type enables ``muse forecast`` to compute more precise conflict |
| 93 | predictions — a rename conflicts differently from a delete. |
| 94 | |
| 95 | Intents are write-once records stored under ``.muse/coordination/intents/``. |
| 96 | They are purely advisory and never affect VCS correctness. |
| 97 | """ |
| 98 | root = require_repo() |
| 99 | |
| 100 | if operation not in _VALID_OPS: |
| 101 | typer.echo( |
| 102 | f"❌ Unknown operation '{operation}'. " |
| 103 | f"Valid: {', '.join(sorted(_VALID_OPS))}", |
| 104 | err=True, |
| 105 | ) |
| 106 | raise typer.Exit(code=ExitCode.USER_ERROR) |
| 107 | |
| 108 | head_ref = (root / ".muse" / "HEAD").read_text().strip() |
| 109 | branch = head_ref.removeprefix("refs/heads/").strip() |
| 110 | |
| 111 | intent_record = create_intent( |
| 112 | root=root, |
| 113 | reservation_id=reservation_id, |
| 114 | run_id=run_id, |
| 115 | branch=branch, |
| 116 | addresses=addresses, |
| 117 | operation=operation, |
| 118 | detail=detail, |
| 119 | ) |
| 120 | |
| 121 | if as_json: |
| 122 | typer.echo(json.dumps(intent_record.to_dict(), indent=2)) |
| 123 | return |
| 124 | |
| 125 | typer.echo( |
| 126 | f"\n✅ Intent recorded\n" |
| 127 | f" Intent ID: {intent_record.intent_id}\n" |
| 128 | f" Operation: {operation}\n" |
| 129 | f" Addresses: {len(addresses)}\n" |
| 130 | f" Run ID: {intent_record.run_id}" |
| 131 | ) |
| 132 | if detail: |
| 133 | typer.echo(f" Detail: {detail}") |
| 134 | if reservation_id: |
| 135 | typer.echo(f" Reservation: {reservation_id}") |
| 136 | typer.echo("\nRun 'muse forecast' to check for predicted conflicts.") |