gabriel / muse public
reconcile.py python
389 lines 13.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 136 days ago
1 """``muse coord reconcile`` — recommend merge ordering and integration strategy.
2
3 Reads active reservations, intents, and branch divergence to recommend:
4
5 1. **Merge ordering** — which branches should be merged first to minimize
6 downstream conflicts.
7 2. **Integration strategy** — fast-forward, squash, or rebase for each branch.
8 3. **Conflict hotspots** — symbols reserved by multiple agents that need
9 special attention.
10
11 ``muse coord reconcile`` is a *read-only* planning command. It does not write
12 to branches, commit history, or the coordination layer. It provides the plan;
13 agents execute it.
14
15 Why this exists
16 ---------------
17 In a system with millions of concurrent agents, merges happen constantly.
18 Without coordination, every merge introduces friction. ``muse coord reconcile``
19 gives an orchestration agent a complete picture of the current coordination
20 state and a recommended action plan.
21
22 Usage::
23
24 muse coord reconcile
25 muse coord reconcile --json
26
27 Output (text)::
28
29 Reconciliation report
30 ──────────────────────────────────────────────────────────────
31
32 Active reservations: 3 Active intents: 2 Conflict hotspots: 1
33
34 Recommended merge order:
35 1. feature/billing (3 addresses, 0 conflict(s))
36 2. feature/auth (5 addresses, 1 conflict(s))
37
38 Conflict hotspot(s):
39 src/billing.py::compute_total
40 reserved by: agent-41, agent-42
41 → resolve 'feature/billing' first; feature/auth must rebase
42
43 Integration strategies:
44 feature/billing → fast-forward (no conflicts predicted)
45 feature/auth → rebase onto main before merging
46
47 (0.001s)
48
49 JSON output schema::
50
51 {
52 "active_reservations": int,
53 "active_intents": int,
54 "conflict_hotspots": int,
55 "branches": [
56 {
57 "branch": str,
58 "reserved_addresses": [str, ...],
59 "intents": [str, ...],
60 "run_ids": [str, ...],
61 "predicted_conflicts": int
62 },
63 ...
64 ],
65 "recommended_merge_order": [str, ...],
66 "strategies": {branch: str, ...},
67 "hotspots": [
68 {"address": str, "branches": [str, ...]}
69 ],
70 "duration_ms": float,
71 "exit_code": int
72 }
73
74 Exit codes::
75
76 0 — success (no active data is also success)
77 1 — unexpected error loading coordination state
78
79 Flags:
80
81 ``--json`` / ``--format json``
82 Emit the reconciliation report as compact JSON on stdout.
83 """
84
85 from __future__ import annotations
86
87 import argparse
88 import json
89 import logging
90 import sys
91 from typing import TypedDict
92
93 from muse.core._types import Metadata
94 from muse.core.coordination import active_reservations, load_all_intents
95 from muse.core.envelope import EnvelopeJson, make_envelope
96 from muse.core.errors import ExitCode
97 from muse.core.repo import require_repo
98 from muse.core.validation import sanitize_display
99 from muse.core.timing import start_timer
100
101 logger = logging.getLogger(__name__)
102
103 type _ReconcileDict = dict[str, str | int | list[str]]
104 type _BranchMap = dict[str, "_BranchSummary"]
105 type _AddrBranchMap = dict[str, list[str]]
106 type _StrategyMap = dict[str, str]
107
108
109 class _BranchSummaryJson(TypedDict):
110 branch: str
111 reserved_addresses: list[str]
112 intents: list[str]
113 run_ids: list[str]
114 predicted_conflicts: int
115
116
117 class _HotspotEntry(TypedDict):
118 address: str
119 branches: list[str]
120
121
122 class _ReconcileJson(EnvelopeJson):
123 """JSON output schema for ``muse coord reconcile --json``."""
124
125 active_reservations: int
126 active_intents: int
127 conflict_hotspots: int
128 branches: list[_BranchSummaryJson]
129 recommended_merge_order: list[str]
130 strategies: _StrategyMap
131 hotspots: list[_HotspotEntry]
132
133
134 # ── Error helper ──────────────────────────────────────────────────────────────
135
136
137 def _err(msg: str, as_json: bool, status: str = "error") -> None:
138 """Print an error and return. Caller raises SystemExit."""
139 if as_json:
140 print(json.dumps({"error": msg, "status": status}))
141 else:
142 print(f"❌ {msg}", file=sys.stderr)
143
144
145 # ── Internal types ────────────────────────────────────────────────────────────
146
147
148 class _BranchSummary:
149 """Aggregated coordination state for a single branch.
150
151 Collects reserved addresses, declared intents, participating agent run-IDs,
152 and a predicted conflict count (populated after hotspot detection).
153
154 Attributes:
155 branch: Branch name this summary covers.
156 reserved_addresses: Symbol addresses reserved on this branch.
157 intents: Operation names declared via ``muse coord intent``.
158 run_ids: Set of agent run-IDs that have reservations or intents here.
159 conflict_count: Number of hotspot addresses this branch participates in.
160 """
161
162 def __init__(self, branch: str) -> None:
163 self.branch = branch
164 self.reserved_addresses: list[str] = []
165 self.intents: list[str] = []
166 self.run_ids: set[str] = set()
167 self.conflict_count: int = 0
168
169 def to_dict(self) -> _ReconcileDict:
170 """Serialise to a plain dict suitable for JSON output."""
171 return {
172 "branch": self.branch,
173 "reserved_addresses": self.reserved_addresses,
174 "intents": self.intents,
175 "run_ids": sorted(self.run_ids),
176 "predicted_conflicts": self.conflict_count,
177 }
178
179
180 # ── CLI registration ──────────────────────────────────────────────────────────
181
182
183 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
184 """Register the ``reconcile`` subcommand on *subparsers* (under ``muse coord``).
185
186 Wires all flags with their defaults, choices, and help text so that
187 ``--help`` output is accurate. Sets ``func`` to :func:`run`.
188 """
189 parser = subparsers.add_parser(
190 "reconcile",
191 help="Recommend merge ordering and integration strategy.",
192 description=__doc__,
193 formatter_class=argparse.RawDescriptionHelpFormatter,
194 )
195 parser.add_argument(
196 "--json", "-j",
197 action="store_true",
198 dest="json_out",
199 help="Emit machine-readable JSON.",
200 )
201 parser.set_defaults(func=run)
202
203
204 # ── Command implementation ────────────────────────────────────────────────────
205
206
207 def run(args: argparse.Namespace) -> None:
208 """Recommend merge ordering and integration strategy.
209
210 Reads coordination state (reservations + intents) and produces a recommended
211 action plan: which branches to merge first, what strategy to use, and which
212 conflict hotspots need manual attention.
213
214 Execution order
215 ---------------
216 1. **Resolve repo** — :func:`~muse.core.repo.require_repo`.
217 2. **Load state** — :func:`~muse.core.coordination.active_reservations` and
218 :func:`~muse.core.coordination.load_all_intents`. Any unexpected I/O
219 error exits :attr:`~muse.core.errors.ExitCode.USER_ERROR` (1) with a
220 message on *stderr* (or compact JSON on *stdout* when ``--format json``).
221 3. **Aggregate by branch** — build a :class:`_BranchSummary` per branch.
222 4. **Detect hotspots** — addresses reserved across >1 branch.
223 5. **Score branches** — increment ``conflict_count`` for each hotspot
224 participation.
225 6. **Order branches** — ascending by ``(conflict_count, address_count)``
226 so cleanest branches merge first.
227 7. **Assign strategies** — fast-forward (0 conflicts), rebase (1–2), or
228 manual (3+).
229 8. **Emit output** — compact JSON or human-readable text.
230
231 This command is *read-only*. It never writes to branches, commit history,
232 or the coordination layer.
233
234 Security
235 --------
236 * All branch names, addresses, and run-IDs in text output are passed through
237 :func:`~muse.core.validation.sanitize_display` to prevent ANSI injection.
238 * No user-supplied path strings are used to construct file paths.
239
240 Performance
241 -----------
242 * O(R + I) to load reservations and intents (one directory scan each).
243 * O(A) to build the hotspot map where A is the total number of addresses.
244 * O(B log B) to sort branches where B is the number of distinct branches.
245
246 Agent quickstart::
247
248 muse coord reconcile --json
249 muse coord reconcile --format json
250
251 JSON fields::
252
253 active_reservations int number of active reservation records
254 active_intents int number of active intent records
255 conflict_hotspots int addresses reserved by >1 branch
256 branches list per-branch summary (address/intent/conflict counts)
257 recommended_merge_order list branches sorted cleanest-first
258 strategies dict branch → recommended integration strategy string
259 hotspots list [{address, branches}] conflict hotspot details
260
261 Exit codes::
262
263 0 Success.
264 1 Error loading coordination state.
265 """
266 as_json: bool = args.json_out
267 elapsed = start_timer()
268
269 root = require_repo()
270
271 try:
272 reservations = active_reservations(root)
273 except Exception as exc: # noqa: BLE001
274 _err(str(exc), as_json, "load_error")
275 raise SystemExit(ExitCode.USER_ERROR)
276
277 try:
278 intents = load_all_intents(root)
279 except Exception as exc: # noqa: BLE001
280 _err(str(exc), as_json, "load_error")
281 raise SystemExit(ExitCode.USER_ERROR)
282
283 # Aggregate by branch.
284 branch_map: _BranchMap = {}
285 for res in reservations:
286 b = res.branch
287 if b not in branch_map:
288 branch_map[b] = _BranchSummary(b)
289 branch_map[b].reserved_addresses.extend(res.addresses)
290 branch_map[b].run_ids.add(res.run_id)
291
292 for it in intents:
293 b = it.branch
294 if b not in branch_map:
295 branch_map[b] = _BranchSummary(b)
296 branch_map[b].intents.append(it.operation)
297 branch_map[b].run_ids.add(it.run_id)
298
299 # Detect conflict hotspots.
300 addr_branches: _AddrBranchMap = {}
301 for res in reservations:
302 for addr in res.addresses:
303 addr_branches.setdefault(addr, []).append(res.branch)
304
305 hotspots: _AddrBranchMap = {
306 addr: branches
307 for addr, branches in addr_branches.items()
308 if len(set(branches)) > 1
309 }
310
311 # Compute conflict counts per branch based on hotspot participation.
312 for addr, branches in hotspots.items():
313 unique_branches = list(dict.fromkeys(branches))
314 for b in unique_branches:
315 if b in branch_map:
316 branch_map[b].conflict_count += 1
317
318 # Recommend merge order: fewer conflicts → merge first.
319 ordered = sorted(
320 branch_map.values(),
321 key=lambda bs: (bs.conflict_count, len(bs.reserved_addresses)),
322 )
323
324 # Recommend integration strategies.
325 strategies: Metadata = {}
326 for bs in ordered:
327 if bs.conflict_count == 0:
328 strategies[bs.branch] = "fast-forward (no conflicts predicted)"
329 elif bs.conflict_count <= 2:
330 strategies[bs.branch] = "rebase onto main before merging"
331 else:
332 strategies[bs.branch] = "manual conflict resolution required"
333
334
335 if as_json:
336 print(json.dumps(_ReconcileJson(
337 **make_envelope(elapsed),
338 active_reservations=len(reservations),
339 active_intents=len(intents),
340 conflict_hotspots=len(hotspots),
341 branches=[bs.to_dict() for bs in ordered],
342 recommended_merge_order=[bs.branch for bs in ordered],
343 strategies=strategies,
344 hotspots=[
345 {"address": addr, "branches": list(dict.fromkeys(brs))}
346 for addr, brs in sorted(hotspots.items())
347 ],
348 )))
349 return
350
351 # ── Text output ───────────────────────────────────────────────────────────
352 print("\nReconciliation report")
353 print("─" * 62)
354 print(
355 f" Active reservations: {len(reservations)} "
356 f"Active intents: {len(intents)} "
357 f"Conflict hotspots: {len(hotspots)}"
358 )
359
360 if not reservations and not intents:
361 print(
362 "\n (no active coordination data — run 'muse reserve' or 'muse intent' first)"
363 )
364 return
365
366 if ordered:
367 print(f"\n Recommended merge order:")
368 for rank, bs in enumerate(ordered, 1):
369 c = bs.conflict_count
370 print(
371 f" {rank}. {sanitize_display(bs.branch):<30} "
372 f"({len(bs.reserved_addresses)} addresses, {c} conflict(s))"
373 )
374
375 if hotspots:
376 print(f"\n Conflict hotspot(s):")
377 for addr, branches in sorted(hotspots.items()):
378 unique = list(dict.fromkeys(branches))
379 print(f" {sanitize_display(addr)}")
380 print(f" reserved by: {', '.join(sanitize_display(b) for b in unique)}")
381 first = unique[0]
382 rest = ", ".join(sanitize_display(b) for b in unique[1:])
383 print(f" → resolve {sanitize_display(first)!r} first; {rest} must rebase")
384
385 print(f"\n Integration strategies:")
386 for bs in ordered:
387 print(f" {sanitize_display(bs.branch):<30} → {strategies[bs.branch]}")
388
389 print(f"\n ({elapsed():.3f}s)")
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 136 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 142 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 145 days ago