reserve.py
python
| 1 | """muse reserve — advisory symbol reservation for parallel agents. |
| 2 | |
| 3 | Places an advisory lock on one or more symbol addresses. This does NOT block |
| 4 | other agents from editing those symbols — it is a coordination signal, not an |
| 5 | enforcement mechanism. Other agents can check existing reservations via |
| 6 | ``muse forecast`` or ``muse reconcile`` before starting work. |
| 7 | |
| 8 | Why reservations? |
| 9 | ----------------- |
| 10 | When millions of agents operate on a codebase simultaneously, merge conflicts |
| 11 | are inevitable *if* agents don't communicate intent. Reservations give agents |
| 12 | a low-cost way to say "I'm about to touch this function" before they do it, |
| 13 | so that: |
| 14 | |
| 15 | 1. Other agents can check with ``muse forecast`` and re-route if needed. |
| 16 | 2. ``muse plan-merge`` can predict conflicts with higher accuracy. |
| 17 | 3. ``muse reconcile`` can recommend merge ordering. |
| 18 | |
| 19 | A reservation expires after ``--ttl`` seconds (default: 1 hour) and is never |
| 20 | enforced — the VCS engine ignores them for correctness. They are purely |
| 21 | advisory. |
| 22 | |
| 23 | Usage:: |
| 24 | |
| 25 | muse reserve "src/billing.py::compute_total" --run-id agent-42 |
| 26 | muse reserve "src/auth.py::validate_token" "src/auth.py::refresh_token" \\ |
| 27 | --run-id pipeline-7 --ttl 7200 |
| 28 | muse reserve "src/core.py::hash_content" --op rename --run-id refactor-bot |
| 29 | |
| 30 | Output:: |
| 31 | |
| 32 | ✅ Reserved 1 address(es) for run-id agent-42 |
| 33 | Expires: 2026-03-18T13:00:00+00:00 |
| 34 | |
| 35 | ⚠️ Conflict: src/billing.py::compute_total is already reserved |
| 36 | by run-id agent-41 (expires 2026-03-18T12:30:00+00:00) |
| 37 | |
| 38 | Flags: |
| 39 | |
| 40 | ``--run-id ID`` |
| 41 | Agent/pipeline identifier (required for conflict detection). |
| 42 | |
| 43 | ``--ttl N`` |
| 44 | Reservation duration in seconds (default: 3600). |
| 45 | |
| 46 | ``--op OPERATION`` |
| 47 | Declared operation: rename, move, modify, extract, delete. |
| 48 | |
| 49 | ``--json`` |
| 50 | Emit reservation details as JSON. |
| 51 | """ |
| 52 | from __future__ import annotations |
| 53 | |
| 54 | import json |
| 55 | import logging |
| 56 | |
| 57 | import typer |
| 58 | |
| 59 | from muse.core.coordination import active_reservations, create_reservation |
| 60 | from muse.core.repo import require_repo |
| 61 | |
| 62 | logger = logging.getLogger(__name__) |
| 63 | |
| 64 | app = typer.Typer() |
| 65 | |
| 66 | |
| 67 | @app.callback(invoke_without_command=True) |
| 68 | def reserve( |
| 69 | ctx: typer.Context, |
| 70 | addresses: list[str] = typer.Argument( |
| 71 | ..., metavar="ADDRESS...", |
| 72 | help='Symbol addresses to reserve, e.g. "src/billing.py::compute_total".', |
| 73 | ), |
| 74 | run_id: str = typer.Option( |
| 75 | "unknown", "--run-id", metavar="ID", |
| 76 | help="Agent/pipeline identifier.", |
| 77 | ), |
| 78 | ttl: int = typer.Option( |
| 79 | 3600, "--ttl", metavar="SECONDS", |
| 80 | help="Reservation duration in seconds (default: 3600).", |
| 81 | ), |
| 82 | operation: str | None = typer.Option( |
| 83 | None, "--op", metavar="OPERATION", |
| 84 | help="Declared operation: rename, move, modify, extract, delete.", |
| 85 | ), |
| 86 | as_json: bool = typer.Option(False, "--json", help="Emit reservation details as JSON."), |
| 87 | ) -> None: |
| 88 | """Place advisory reservations on symbol addresses. |
| 89 | |
| 90 | Reservations are write-once, expiry-based advisory signals. They do not |
| 91 | block other agents or affect VCS correctness — they enable conflict |
| 92 | *prediction* via ``muse forecast`` and ``muse reconcile``. |
| 93 | |
| 94 | Multiple addresses can be reserved in one call. Active reservations by |
| 95 | other agents on the same addresses are reported as warnings. |
| 96 | """ |
| 97 | root = require_repo() |
| 98 | |
| 99 | head_ref = (root / ".muse" / "HEAD").read_text().strip() |
| 100 | branch = head_ref.removeprefix("refs/heads/").strip() |
| 101 | |
| 102 | # Check for conflicts with existing active reservations. |
| 103 | existing = active_reservations(root) |
| 104 | conflicts: list[str] = [] |
| 105 | for addr in addresses: |
| 106 | for res in existing: |
| 107 | if addr in res.addresses and res.run_id != run_id: |
| 108 | conflicts.append( |
| 109 | f" ⚠️ {addr}\n" |
| 110 | f" already reserved by run-id {res.run_id!r}" |
| 111 | f" (expires {res.expires_at.isoformat()[:19]})" |
| 112 | ) |
| 113 | |
| 114 | res = create_reservation(root, run_id, branch, addresses, ttl, operation) |
| 115 | |
| 116 | if as_json: |
| 117 | typer.echo(json.dumps( |
| 118 | { |
| 119 | **res.to_dict(), |
| 120 | "conflicts": conflicts, |
| 121 | }, |
| 122 | indent=2, |
| 123 | )) |
| 124 | return |
| 125 | |
| 126 | if conflicts: |
| 127 | for c in conflicts: |
| 128 | typer.echo(c) |
| 129 | |
| 130 | typer.echo( |
| 131 | f"\n✅ Reserved {len(addresses)} address(es) for run-id {run_id!r}\n" |
| 132 | f" Reservation ID: {res.reservation_id}\n" |
| 133 | f" Expires: {res.expires_at.isoformat()[:19]}" |
| 134 | ) |
| 135 | if operation: |
| 136 | typer.echo(f" Operation: {operation}") |
| 137 | if conflicts: |
| 138 | typer.echo( |
| 139 | f"\n ⚠️ {len(conflicts)} conflict(s) detected. " |
| 140 | "Run 'muse forecast' for details." |
| 141 | ) |