gabriel / muse public
reconcile.py python
375 lines 13.7 KB
Raw
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 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 import argparse
86 import json
87 import logging
88 import sys
89 from typing import TypedDict
90
91 from muse.core.types import Metadata
92 from muse.core.coordination import active_reservations, load_all_intents
93 from muse.core.envelope import EnvelopeJson, make_envelope
94 from muse.core.errors import ExitCode
95 from muse.core.repo import require_repo
96 from muse.core.validation import sanitize_display
97 from muse.core.timing import start_timer
98
99 logger = logging.getLogger(__name__)
100
101 type _ReconcileDict = dict[str, str | int | list[str]]
102 type _BranchMap = dict[str, "_BranchSummary"]
103 type _AddrBranchMap = dict[str, list[str]]
104 type _StrategyMap = dict[str, str]
105
106 class _BranchSummaryJson(TypedDict):
107 branch: str
108 reserved_addresses: list[str]
109 intents: list[str]
110 run_ids: list[str]
111 predicted_conflicts: int
112
113 class _HotspotEntry(TypedDict):
114 address: str
115 branches: list[str]
116
117 class _ReconcileJson(EnvelopeJson):
118 """JSON output schema for ``muse coord reconcile --json``."""
119
120 active_reservations: int
121 active_intents: int
122 conflict_hotspots: int
123 branches: list[_BranchSummaryJson]
124 recommended_merge_order: list[str]
125 strategies: _StrategyMap
126 hotspots: list[_HotspotEntry]
127
128 # ── Error helper ──────────────────────────────────────────────────────────────
129
130 def _err(msg: str, as_json: bool, status: str = "error") -> None:
131 """Print an error and return. Caller raises SystemExit."""
132 if as_json:
133 print(json.dumps({"error": msg, "status": status}))
134 else:
135 print(f"❌ {msg}", file=sys.stderr)
136
137 # ── Internal types ────────────────────────────────────────────────────────────
138
139 class _BranchSummary:
140 """Aggregated coordination state for a single branch.
141
142 Collects reserved addresses, declared intents, participating agent run-IDs,
143 and a predicted conflict count (populated after hotspot detection).
144
145 Attributes:
146 branch: Branch name this summary covers.
147 reserved_addresses: Symbol addresses reserved on this branch.
148 intents: Operation names declared via ``muse coord intent``.
149 run_ids: Set of agent run-IDs that have reservations or intents here.
150 conflict_count: Number of hotspot addresses this branch participates in.
151 """
152
153 def __init__(self, branch: str) -> None:
154 self.branch = branch
155 self.reserved_addresses: list[str] = []
156 self.intents: list[str] = []
157 self.run_ids: set[str] = set()
158 self.conflict_count: int = 0
159
160 def to_dict(self) -> _ReconcileDict:
161 """Serialise to a plain dict suitable for JSON output."""
162 return {
163 "branch": self.branch,
164 "reserved_addresses": self.reserved_addresses,
165 "intents": self.intents,
166 "run_ids": sorted(self.run_ids),
167 "predicted_conflicts": self.conflict_count,
168 }
169
170 # ── CLI registration ──────────────────────────────────────────────────────────
171
172 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
173 """Register the ``reconcile`` subcommand on *subparsers* (under ``muse coord``).
174
175 Wires all flags with their defaults, choices, and help text so that
176 ``--help`` output is accurate. Sets ``func`` to :func:`run`.
177 """
178 parser = subparsers.add_parser(
179 "reconcile",
180 help="Recommend merge ordering and integration strategy.",
181 description=__doc__,
182 formatter_class=argparse.RawDescriptionHelpFormatter,
183 )
184 parser.add_argument(
185 "--json", "-j",
186 action="store_true",
187 dest="json_out",
188 help="Emit machine-readable JSON.",
189 )
190 parser.set_defaults(func=run)
191
192 # ── Command implementation ────────────────────────────────────────────────────
193
194 def run(args: argparse.Namespace) -> None:
195 """Recommend merge ordering and integration strategy.
196
197 Reads coordination state (reservations + intents) and produces a recommended
198 action plan: which branches to merge first, what strategy to use, and which
199 conflict hotspots need manual attention.
200
201 Execution order
202 ---------------
203 1. **Resolve repo** — :func:`~muse.core.repo.require_repo`.
204 2. **Load state** — :func:`~muse.core.coordination.active_reservations` and
205 :func:`~muse.core.coordination.load_all_intents`. Any unexpected I/O
206 error exits :attr:`~muse.core.errors.ExitCode.USER_ERROR` (1) with a
207 message on *stderr* (or compact JSON on *stdout* when ``--format json``).
208 3. **Aggregate by branch** — build a :class:`_BranchSummary` per branch.
209 4. **Detect hotspots** — addresses reserved across >1 branch.
210 5. **Score branches** — increment ``conflict_count`` for each hotspot
211 participation.
212 6. **Order branches** — ascending by ``(conflict_count, address_count)``
213 so cleanest branches merge first.
214 7. **Assign strategies** — fast-forward (0 conflicts), rebase (1–2), or
215 manual (3+).
216 8. **Emit output** — compact JSON or human-readable text.
217
218 This command is *read-only*. It never writes to branches, commit history,
219 or the coordination layer.
220
221 Security
222 --------
223 * All branch names, addresses, and run-IDs in text output are passed through
224 :func:`~muse.core.validation.sanitize_display` to prevent ANSI injection.
225 * No user-supplied path strings are used to construct file paths.
226
227 Performance
228 -----------
229 * O(R + I) to load reservations and intents (one directory scan each).
230 * O(A) to build the hotspot map where A is the total number of addresses.
231 * O(B log B) to sort branches where B is the number of distinct branches.
232
233 Agent quickstart::
234
235 muse coord reconcile --json
236 muse coord reconcile --format json
237
238 JSON fields::
239
240 active_reservations int number of active reservation records
241 active_intents int number of active intent records
242 conflict_hotspots int addresses reserved by >1 branch
243 branches list per-branch summary (address/intent/conflict counts)
244 recommended_merge_order list branches sorted cleanest-first
245 strategies dict branch → recommended integration strategy string
246 hotspots list [{address, branches}] conflict hotspot details
247
248 Exit codes::
249
250 0 Success.
251 1 Error loading coordination state.
252 """
253 as_json: bool = args.json_out
254 elapsed = start_timer()
255
256 root = require_repo()
257
258 try:
259 reservations = active_reservations(root)
260 except Exception as exc: # noqa: BLE001
261 _err(str(exc), as_json, "load_error")
262 raise SystemExit(ExitCode.USER_ERROR)
263
264 try:
265 intents = load_all_intents(root)
266 except Exception as exc: # noqa: BLE001
267 _err(str(exc), as_json, "load_error")
268 raise SystemExit(ExitCode.USER_ERROR)
269
270 # Aggregate by branch.
271 branch_map: _BranchMap = {}
272 for res in reservations:
273 b = res.branch
274 if b not in branch_map:
275 branch_map[b] = _BranchSummary(b)
276 branch_map[b].reserved_addresses.extend(res.addresses)
277 branch_map[b].run_ids.add(res.run_id)
278
279 for it in intents:
280 b = it.branch
281 if b not in branch_map:
282 branch_map[b] = _BranchSummary(b)
283 branch_map[b].intents.append(it.operation)
284 branch_map[b].run_ids.add(it.run_id)
285
286 # Detect conflict hotspots.
287 addr_branches: _AddrBranchMap = {}
288 for res in reservations:
289 for addr in res.addresses:
290 addr_branches.setdefault(addr, []).append(res.branch)
291
292 hotspots: _AddrBranchMap = {
293 addr: branches
294 for addr, branches in addr_branches.items()
295 if len(set(branches)) > 1
296 }
297
298 # Compute conflict counts per branch based on hotspot participation.
299 for addr, branches in hotspots.items():
300 unique_branches = list(dict.fromkeys(branches))
301 for b in unique_branches:
302 if b in branch_map:
303 branch_map[b].conflict_count += 1
304
305 # Recommend merge order: fewer conflicts → merge first.
306 ordered = sorted(
307 branch_map.values(),
308 key=lambda bs: (bs.conflict_count, len(bs.reserved_addresses)),
309 )
310
311 # Recommend integration strategies.
312 strategies: Metadata = {}
313 for bs in ordered:
314 if bs.conflict_count == 0:
315 strategies[bs.branch] = "fast-forward (no conflicts predicted)"
316 elif bs.conflict_count <= 2:
317 strategies[bs.branch] = "rebase onto main before merging"
318 else:
319 strategies[bs.branch] = "manual conflict resolution required"
320
321 if as_json:
322 print(json.dumps(_ReconcileJson(
323 **make_envelope(elapsed),
324 active_reservations=len(reservations),
325 active_intents=len(intents),
326 conflict_hotspots=len(hotspots),
327 branches=[bs.to_dict() for bs in ordered],
328 recommended_merge_order=[bs.branch for bs in ordered],
329 strategies=strategies,
330 hotspots=[
331 {"address": addr, "branches": list(dict.fromkeys(brs))}
332 for addr, brs in sorted(hotspots.items())
333 ],
334 )))
335 return
336
337 # ── Text output ───────────────────────────────────────────────────────────
338 print("\nReconciliation report")
339 print("─" * 62)
340 print(
341 f" Active reservations: {len(reservations)} "
342 f"Active intents: {len(intents)} "
343 f"Conflict hotspots: {len(hotspots)}"
344 )
345
346 if not reservations and not intents:
347 print(
348 "\n (no active coordination data — run 'muse reserve' or 'muse intent' first)"
349 )
350 return
351
352 if ordered:
353 print(f"\n Recommended merge order:")
354 for rank, bs in enumerate(ordered, 1):
355 c = bs.conflict_count
356 print(
357 f" {rank}. {sanitize_display(bs.branch):<30} "
358 f"({len(bs.reserved_addresses)} addresses, {c} conflict(s))"
359 )
360
361 if hotspots:
362 print(f"\n Conflict hotspot(s):")
363 for addr, branches in sorted(hotspots.items()):
364 unique = list(dict.fromkeys(branches))
365 print(f" {sanitize_display(addr)}")
366 print(f" reserved by: {', '.join(sanitize_display(b) for b in unique)}")
367 first = unique[0]
368 rest = ", ".join(sanitize_display(b) for b in unique[1:])
369 print(f" → resolve {sanitize_display(first)!r} first; {rest} must rebase")
370
371 print(f"\n Integration strategies:")
372 for bs in ordered:
373 print(f" {sanitize_display(bs.branch):<30} → {strategies[bs.branch]}")
374
375 print(f"\n ({elapsed():.3f}s)")
File History 11 commits
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 days ago
sha256:b7be56ec091919a612cffe7f3c8b600209d5155517443fdac0e16954c8c7d81f Merge 'task/git-export-ignored-file-deletion' into 'dev' — … Human 12 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 15 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9 docs: add issue docs for push have-negotiation bug (#55) an… Sonnet 4.6 19 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74 chore: trigger push to surface null-OID paths Sonnet 4.6 24 days ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8 chore: prebuild timing test Sonnet 4.6 34 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1 merge: pull staging/dev — advance to 0.2.0rc12 Sonnet 4.6 patch 36 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 48 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 50 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 57 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 57 days ago