gabriel / muse public
reserve.py python
141 lines 4.6 KB
a748f814 feat(code): Phase 7 — semantic versioning metadata on StructuredDelta +… Gabriel Cardona <cgcardona@gmail.com> 5d ago
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 )