gabriel / muse public
harmony.py python
2,493 lines 90.4 KB
Raw
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 30 days ago
1 """muse harmony — Resolution Intelligence for domain-agnostic conflict resolution.
2
3 The harmony engine provides three-tier resolution intelligence:
4
5 1. Policy match — a declarative rule fires automatically.
6 2. Exact replay — blob fingerprint matches a saved resolution.
7 3. Semantic match — domain-plugin similarity score ≥ threshold.
8 4. Escalate — create a hub issue, flag for human or specialist agent.
9
10 Subcommands
11 -----------
12
13 ``muse harmony record`` Record a new conflict pattern.
14 ``muse harmony list`` List all recorded conflict patterns.
15 ``muse harmony show`` Show a pattern and all its resolutions.
16 ``muse harmony resolve`` Save a resolution for a conflict pattern.
17 ``muse harmony best`` Show the highest-quality resolution for a pattern.
18 ``muse harmony forget`` Remove a conflict pattern and all its resolutions.
19 ``muse harmony clear`` Remove all conflict patterns.
20 ``muse harmony gc`` Garbage-collect stale unresolved patterns.
21 ``muse harmony policy-add`` Add or replace a declarative resolution policy.
22 ``muse harmony policy-list`` List all policies (scope-sorted).
23 ``muse harmony policy-remove`` Remove a policy.
24 ``muse harmony audit`` Show the harmony audit log.
25
26 Storage
27 -------
28
29 ``.muse/harmony/patterns/<pattern_id>/pattern.json``
30 ``.muse/harmony/patterns/<pattern_id>/resolutions/<resolution_id>.json``
31 ``.muse/harmony/policies/<policy_id>.json``
32 ``.muse/harmony/audit/<YYYYMMDD>-<content-id>.json``
33
34 Security model
35 --------------
36
37 Pattern IDs and resolution IDs are validated as exactly 64 lowercase hex
38 characters before any filesystem path is constructed — preventing ``../``
39 path-traversal. Policy IDs are validated as URL-safe alphanumeric strings.
40 Error messages go to **stderr**; **stdout** carries only data.
41
42 Agent UX
43 --------
44
45 Pass ``--json`` to any subcommand for machine-readable output. JSON schemas
46 are defined by TypedDicts below — all fields are always present.
47
48 JSON schemas
49 ~~~~~~~~~~~~
50
51 ``muse harmony record --json``::
52
53 {"pattern_id": "<hex64>", "already_existed": <bool>}
54
55 ``muse harmony list --json``::
56
57 {
58 "total": <int>,
59 "patterns": [
60 {
61 "pattern_id": "<hex64>",
62 "path": "<str>",
63 "domain": "<str>",
64 "conflict_type": "<str>",
65 "resolution_count": <int>,
66 "recorded_at": "<iso8601>",
67 "recorded_by": "<str>"
68 }, ...
69 ]
70 }
71
72 ``muse harmony show <pattern_id> --json``::
73
74 {
75 "pattern": {
76 "pattern_id": "<hex64>", "path": "<str>", "domain": "<str>",
77 "conflict_type": "<str>", "blob_fingerprint": "<hex64>",
78 "semantic_fingerprint": "<hex64>", "ours_id": "<hex64>",
79 "theirs_id": "<hex64>", "description": {}, "recorded_at": "<iso8601>",
80 "recorded_by": "<str>"
81 },
82 "resolutions": [
83 {
84 "resolution_id": "<hex64>", "strategy": "<str>",
85 "confidence": <float>, "human_verified": <bool>,
86 "applied_count": <int>, "resolved_by": {"type": "...", ...},
87 "resolved_at": "<iso8601>", "rationale": "<str>"
88 }, ...
89 ]
90 }
91
92 ``muse harmony resolve --json``::
93
94 {"resolution_id": "<hex64>", "pattern_id": "<hex64>", "already_existed": <bool>}
95
96 ``muse harmony best <pattern_id> --json``::
97
98 {"pattern_id": "<hex64>", "resolution": {<resolution object> | null}}
99
100 ``muse harmony forget <pattern_id> --json``::
101
102 {"pattern_id": "<hex64>", "removed": <bool>}
103
104 ``muse harmony clear --json`` / ``muse harmony gc --json``::
105
106 {"removed": <int>} (gc also includes "age_days": <int>)
107
108 ``muse harmony policy-add --json``::
109
110 {"policy_id": "<str>", "action": "<str>", "scope": "<str>"}
111
112 ``muse harmony policy-list --json``::
113
114 {
115 "total": <int>,
116 "policies": [
117 {
118 "policy_id": "<str>", "description": "<str>", "scope": "<str>",
119 "action": "<str>", "confidence": <float>,
120 "conflict_type": "<str>|null", "domain": "<str>|null",
121 "path_pattern": "<str>|null", "created_at": "<iso8601>",
122 "created_by": "<str>"
123 }, ...
124 ]
125 }
126
127 ``muse harmony policy-remove <policy_id> --json``::
128
129 {"policy_id": "<str>", "removed": <bool>}
130
131 ``muse harmony audit --json``::
132
133 {"total": <int>, "entries": [{<AuditEvent>}, ...]}
134
135 Exit codes
136 ----------
137
138 - 0 — success
139 - 1 — invalid arguments, not-found, or validation failure
140 - 2 — not inside a Muse repository
141 """
142
143 import argparse
144 import json
145 import logging
146 import pathlib
147 import sys
148 from typing import Any, TypedDict
149
150 from muse.core.types import blob_id, short_id
151 from muse.core.envelope import EnvelopeJson, JsonValue, make_envelope
152 from muse.core.errors import ExitCode
153 from muse.core.harmony import (
154 AgentProvenance,
155 AuditEvent,
156 AuditEventType,
157 ConflictPattern,
158 EscalationRecord,
159 EscalationStatus,
160 Policy,
161 PolicyCondition,
162 Resolution,
163 ResolutionStrategy,
164 _validate_fingerprint,
165 _validate_id,
166 _validate_policy_id,
167 append_audit,
168 best_resolution,
169 blob_fingerprint,
170 clear_all,
171 compute_escalation_id,
172 compute_pattern_id,
173 compute_resolution_id,
174 forget_pattern,
175 gc_stale,
176 list_audit,
177 list_escalations,
178 list_patterns,
179 list_policies,
180 list_resolutions,
181 load_escalation,
182 load_pattern,
183 load_policy,
184 load_resolution,
185 record_escalation,
186 record_pattern,
187 remove_policy,
188 resolve_escalation,
189 save_policy,
190 save_resolution,
191 )
192 from muse.core.harmony_engine import EngineConfig, find_similar as _engine_find_similar, resolve as _engine_resolve
193 from muse.core.repo import require_repo
194 from muse.core.timing import start_timer
195 from muse.core.validation import clamp_int, sanitize_display
196
197 logger = logging.getLogger(__name__)
198
199 # ---------------------------------------------------------------------------
200 # JSON TypedDicts — stable machine-readable output schemas
201 # ---------------------------------------------------------------------------
202
203 class _HarmonyRecordJson(EnvelopeJson):
204 """JSON output for ``muse harmony record --json``.
205
206 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
207
208 Fields
209 ------
210 pattern_id 64-char hex SHA-256 pattern ID derived from the path, blob
211 fingerprint, and semantic fingerprint.
212 already_existed True when the pattern was already recorded — the existing
213 entry is returned unchanged (idempotent).
214 """
215
216 pattern_id: str
217 already_existed: bool
218
219 class _HarmonyListEntryJson(TypedDict):
220 """One conflict pattern entry in ``muse harmony list --json`` output.
221
222 Nested inside the ``patterns`` list of :class:`_HarmonyListJson`.
223
224 Fields
225 ------
226 pattern_id 64-char hex pattern ID.
227 path Workspace-relative POSIX path of the conflicting file.
228 domain Domain name the pattern belongs to (e.g. ``"midi"``).
229 conflict_type Conflict category (content, structural, metadata, …).
230 resolution_count Number of resolutions saved for this pattern.
231 recorded_at ISO-8601 UTC timestamp when the pattern was first recorded.
232 recorded_by Agent ID or ``"human"`` who recorded the pattern.
233 """
234
235 pattern_id: str
236 path: str
237 domain: str
238 conflict_type: str
239 resolution_count: int
240 recorded_at: str
241 recorded_by: str
242
243 class _HarmonyListJson(EnvelopeJson):
244 """JSON output for ``muse harmony list --json``.
245
246 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
247
248 Fields
249 ------
250 total Total number of patterns returned (after any domain/type filters).
251 patterns Ordered list of pattern entries; see :class:`_HarmonyListEntryJson`.
252 """
253
254 total: int
255 patterns: list[_HarmonyListEntryJson]
256
257 type _PatternDescription = dict[str, JsonValue]
258
259 class _HarmonyPatternDetailJson(TypedDict):
260 """Full conflict pattern detail nested inside :class:`_HarmonyShowJson`.
261
262 Fields
263 ------
264 pattern_id 64-char hex pattern ID.
265 path Workspace-relative POSIX path of the conflicting file.
266 domain Domain name (e.g. ``"midi"``, ``"code"``).
267 conflict_type Conflict category string.
268 blob_fingerprint SHA-256 of ``sorted(ours_id, theirs_id)`` — exact-replay key.
269 semantic_fingerprint Domain-plugin fingerprint — cross-content replay key.
270 Equals ``blob_fingerprint`` when no plugin is active.
271 ours_id 64-char hex object ID for the ours version.
272 theirs_id 64-char hex object ID for the theirs version.
273 description Domain-specific metadata dict (arbitrary JSON object).
274 recorded_at ISO-8601 UTC timestamp when the pattern was first recorded.
275 recorded_by Agent ID or ``"human"`` who recorded the pattern.
276 """
277
278 pattern_id: str
279 path: str
280 domain: str
281 conflict_type: str
282 blob_fingerprint: str
283 semantic_fingerprint: str
284 ours_id: str
285 theirs_id: str
286 description: _PatternDescription
287 recorded_at: str
288 recorded_by: str
289
290 class _HarmonyResolutionDetailJson(TypedDict):
291 """One resolution record — nested in show, best, and similar output.
292
293 Nested inside :class:`_HarmonyShowJson` (resolutions list) and
294 :class:`_HarmonyBestJson` (resolution field).
295
296 Fields
297 ------
298 resolution_id 64-char hex resolution ID.
299 strategy Resolution strategy: ``"manual"``, ``"exact-replay"``,
300 ``"semantic-proposal"``, or ``"policy"``.
301 confidence Reliability score 0.0–1.0 assigned when the resolution was saved.
302 human_verified True when a human confirmed this resolution is correct.
303 Human-verified resolutions always outrank unverified ones.
304 applied_count Number of times this resolution has been auto-applied.
305 resolved_by Provenance dict — ``{"type": "agent"|"human",
306 "agent_id": str|null, "model_id": str|null}``.
307 resolved_at ISO-8601 UTC timestamp when the resolution was saved.
308 rationale Human-readable explanation for why this resolution was chosen.
309 """
310
311 resolution_id: str
312 strategy: str
313 confidence: float
314 human_verified: bool
315 applied_count: int
316 resolved_by: dict[str, str | None]
317 resolved_at: str
318 rationale: str
319
320 class _HarmonyShowJson(EnvelopeJson):
321 """JSON output for ``muse harmony show <pattern_id> --json``.
322
323 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
324
325 Fields
326 ------
327 pattern Full pattern metadata; see :class:`_HarmonyPatternDetailJson`.
328 resolutions All resolutions for this pattern, sorted by quality
329 (human_verified → confidence → applied_count) descending.
330 """
331
332 pattern: _HarmonyPatternDetailJson
333 resolutions: list[_HarmonyResolutionDetailJson]
334
335 class _HarmonyResolveJson(EnvelopeJson):
336 """JSON output for ``muse harmony resolve --json``.
337
338 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
339
340 Fields
341 ------
342 resolution_id 64-char hex ID of the saved (or existing) resolution.
343 pattern_id 64-char hex ID of the parent conflict pattern.
344 already_existed True when this (outcome_blob, strategy, actor) triple was
345 already saved — idempotent; the existing ID is returned.
346 """
347
348 resolution_id: str
349 pattern_id: str
350 already_existed: bool
351
352 class _HarmonyBestJson(EnvelopeJson):
353 """JSON output for ``muse harmony best <pattern_id> --json``.
354
355 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
356
357 Fields
358 ------
359 pattern_id 64-char hex ID of the queried pattern.
360 resolution The highest-quality resolution (human_verified → confidence →
361 applied_count), or ``null`` when no resolutions exist yet.
362 """
363
364 pattern_id: str
365 resolution: _HarmonyResolutionDetailJson | None
366
367 class _HarmonyForgetJson(EnvelopeJson):
368 """JSON output for ``muse harmony forget <pattern_id> --json``.
369
370 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
371
372 Fields
373 ------
374 pattern_id 64-char hex ID of the pattern that was (or was not) removed.
375 removed True when the pattern and all its resolutions were deleted.
376 False when the pattern did not exist.
377 """
378
379 pattern_id: str
380 removed: bool
381
382 class _HarmonyScalarJson(EnvelopeJson):
383 """JSON output for ``muse harmony clear --json``.
384
385 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
386
387 Fields
388 ------
389 removed Total number of conflict patterns deleted from the store.
390 """
391
392 removed: int
393
394 class _HarmonyGcJson(EnvelopeJson):
395 """JSON output for ``muse harmony gc --json``.
396
397 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
398
399 Fields
400 ------
401 removed Number of stale unresolved patterns deleted.
402 age_days Age threshold used — patterns older than this with no resolution
403 were eligible for removal.
404 """
405
406 removed: int
407 age_days: int
408
409 class _HarmonyPolicyAddJson(EnvelopeJson):
410 """JSON output for ``muse harmony policy-add --json``.
411
412 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
413
414 Fields
415 ------
416 policy_id URL-safe identifier of the policy that was saved or replaced.
417 action The resolution action the policy fires: ``"prefer-ours"``,
418 ``"prefer-theirs"``, ``"escalate"``, ``"require-human"``,
419 or ``"delegate"``.
420 scope Policy scope: ``"workspace"``, ``"repo"``, ``"domain"``,
421 or ``"file"``.
422 """
423
424 policy_id: str
425 action: str
426 scope: str
427
428 class _HarmonyPolicyEntryJson(TypedDict):
429 """One policy record nested inside :class:`_HarmonyPolicyListJson`.
430
431 Fields
432 ------
433 policy_id URL-safe policy identifier.
434 description Human-readable explanation of what this policy does.
435 scope Scope level: ``"workspace"``, ``"repo"``, ``"domain"``,
436 or ``"file"``.
437 action Resolution action fired by this policy.
438 confidence Confidence score 0.0–1.0 assigned to policy-driven resolutions.
439 conflict_type Conflict type filter, or ``null`` (fires for all types).
440 domain Domain filter, or ``null`` (fires for all domains).
441 path_pattern fnmatch glob filter on file paths, or ``null``.
442 created_at ISO-8601 UTC timestamp when the policy was created.
443 created_by Creator attribution: agent ID or ``"human"``.
444 """
445
446 policy_id: str
447 description: str
448 scope: str
449 action: str
450 confidence: float
451 conflict_type: str | None
452 domain: str | None
453 path_pattern: str | None
454 created_at: str
455 created_by: str
456
457 class _HarmonyPolicyListJson(EnvelopeJson):
458 """JSON output for ``muse harmony policy-list --json``.
459
460 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
461
462 Fields
463 ------
464 total Total number of policies returned.
465 policies Scope-sorted policy list (workspace → repo → domain → file,
466 then by created_at ascending within each scope).
467 """
468
469 total: int
470 policies: list[_HarmonyPolicyEntryJson]
471
472 class _HarmonyPolicyRemoveJson(EnvelopeJson):
473 """JSON output for ``muse harmony policy-remove <policy_id> --json``.
474
475 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
476
477 Fields
478 ------
479 policy_id URL-safe identifier of the policy that was (or was not) removed.
480 removed True when the policy was deleted; False when it did not exist.
481 """
482
483 policy_id: str
484 removed: bool
485
486 class _HarmonyAuditJson(EnvelopeJson):
487 """JSON output for ``muse harmony audit --json``.
488
489 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
490
491 Fields
492 ------
493 total Total number of entries returned (capped by ``--limit``).
494 entries Audit log entries, newest first; each is a full AuditEvent dict
495 with event_type, occurred_at, actor, pattern_id, and metadata.
496 """
497
498 total: int
499 entries: list[AuditEvent]
500
501 class _HarmonyEscalateJson(EnvelopeJson):
502 """JSON output for ``muse harmony escalate <pattern_id> --json``.
503
504 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
505
506 Fields
507 ------
508 escalation_id 64-char hex ID of the escalation record (deterministic
509 from pattern_id + reason).
510 pattern_id 64-char hex ID of the conflict pattern being escalated.
511 already_existed True when an open escalation for this (pattern_id, reason)
512 pair already existed — idempotent.
513 """
514
515 escalation_id: str
516 pattern_id: str
517 already_existed: bool
518
519 class _HarmonyEscalationEntryJson(TypedDict):
520 """One escalation record nested inside :class:`_HarmonyEscalationsJson`.
521
522 Fields
523 ------
524 escalation_id 64-char hex escalation ID.
525 pattern_id 64-char hex pattern ID that was escalated.
526 reason Human-readable reason for escalation.
527 status ``"open"`` or ``"resolved"``.
528 escalated_at ISO-8601 UTC timestamp when the escalation was recorded.
529 escalated_by Provenance dict for the actor who created the escalation.
530 resolved_at ISO-8601 UTC timestamp when the escalation was closed,
531 or ``null`` if still open.
532 resolved_by Provenance dict for the actor who resolved it, or ``null``.
533 resolution_id 64-char hex resolution ID that closed this escalation,
534 or ``null`` if still open.
535 """
536
537 escalation_id: str
538 pattern_id: str
539 reason: str
540 status: str
541 escalated_at: str
542 escalated_by: dict[str, str | None]
543 resolved_at: str | None
544 resolved_by: dict[str, str | None] | None
545 resolution_id: str | None
546
547 class _HarmonyEscalationsJson(EnvelopeJson):
548 """JSON output for ``muse harmony escalations --json``.
549
550 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
551
552 Fields
553 ------
554 total Total number of escalation records returned.
555 escalations List of escalation entries; see :class:`_HarmonyEscalationEntryJson`.
556 """
557
558 total: int
559 escalations: list[_HarmonyEscalationEntryJson]
560
561 class _HarmonyResolveEscalationJson(EnvelopeJson):
562 """JSON output for ``muse harmony resolve-escalation <esc_id> --json``.
563
564 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
565
566 Fields
567 ------
568 escalation_id 64-char hex ID of the escalation that was closed.
569 resolved True when the escalation was successfully transitioned to
570 RESOLVED status; False when the escalation was not found.
571 """
572
573 escalation_id: str
574 resolved: bool
575
576 class _HarmonyProposalJson(TypedDict):
577 """A single resolution proposal from the engine or similar search.
578
579 Nested inside :class:`_HarmonyEngineJson` (proposal field) and
580 :class:`_HarmonySimilarJson` (proposals list).
581
582 Fields
583 ------
584 pattern_id 64-char hex ID of the base pattern being resolved.
585 strategy How this proposal was derived: ``"policy"``,
586 ``"exact-replay"``, or ``"semantic-proposal"``.
587 proposed_action The concrete action to take (e.g. ``"prefer-ours"``).
588 confidence Confidence score 0.0–1.0 for this proposal.
589 rationale Explanation of why this resolution was proposed.
590 policy_id If strategy is ``"policy"``, the ID of the matching
591 policy; ``null`` otherwise.
592 similar_pattern_id If strategy is ``"semantic-proposal"``, the ID of the
593 similar pattern; ``null`` otherwise.
594 similarity Similarity score 0.0–1.0 to the similar pattern,
595 or ``null`` when not a semantic proposal.
596 requires_confirmation True when the engine recommends human confirmation
597 before applying this resolution automatically.
598 """
599
600 pattern_id: str
601 strategy: str
602 proposed_action: str
603 confidence: float
604 rationale: str
605 policy_id: str | None
606 similar_pattern_id: str | None
607 similarity: float | None
608 requires_confirmation: bool
609
610 class _HarmonyEngineJson(EnvelopeJson):
611 """JSON output for ``muse harmony engine <pattern_id> --json``.
612
613 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
614
615 Fields
616 ------
617 status Engine outcome: ``"applied"`` (auto-applied a saved
618 resolution), ``"proposed"`` (returned a proposal for
619 human confirmation), or ``"escalated"`` (no match found).
620 pattern_id 64-char hex ID of the pattern evaluated.
621 proposal The resolution proposal, or ``null`` when the engine
622 applied or escalated without proposing.
623 applied_resolution_id 64-char hex ID of the auto-applied resolution,
624 or ``null`` when status is not ``"applied"``.
625 escalation_reason Explanation of why escalation was chosen,
626 or ``null`` when status is not ``"escalated"``.
627 """
628
629 status: str
630 pattern_id: str
631 proposal: _HarmonyProposalJson | None
632 applied_resolution_id: str | None
633 escalation_reason: str | None
634
635 class _HarmonySimilarJson(EnvelopeJson):
636 """JSON output for ``muse harmony similar <pattern_id> --json``.
637
638 Inherits the 6 standard envelope fields from :class:`~muse.core.envelope.EnvelopeJson`.
639
640 Fields
641 ------
642 pattern_id 64-char hex ID of the base pattern searched against.
643 total Total number of similar-pattern proposals returned.
644 proposals List of resolution proposals from semantically similar patterns;
645 see :class:`_HarmonyProposalJson`.
646 """
647
648 pattern_id: str
649 total: int
650 proposals: list[_HarmonyProposalJson]
651
652 # ---------------------------------------------------------------------------
653 # Serialisation helpers
654 # ---------------------------------------------------------------------------
655
656 def _pattern_to_list_entry(
657 p: ConflictPattern,
658 resolution_count: int,
659 ) -> _HarmonyListEntryJson:
660 return _HarmonyListEntryJson(
661 pattern_id=p.pattern_id,
662 path=sanitize_display(p.path),
663 domain=sanitize_display(p.domain),
664 conflict_type=p.conflict_type,
665 resolution_count=resolution_count,
666 recorded_at=p.recorded_at.isoformat(),
667 recorded_by=sanitize_display(p.recorded_by),
668 )
669
670 def _pattern_to_detail(p: ConflictPattern) -> _HarmonyPatternDetailJson:
671 return _HarmonyPatternDetailJson(
672 pattern_id=p.pattern_id,
673 path=sanitize_display(p.path),
674 domain=sanitize_display(p.domain),
675 conflict_type=p.conflict_type,
676 blob_fingerprint=p.blob_fingerprint,
677 semantic_fingerprint=p.semantic_fingerprint,
678 ours_id=p.ours_id,
679 theirs_id=p.theirs_id,
680 description=p.description,
681 recorded_at=p.recorded_at.isoformat(),
682 recorded_by=sanitize_display(p.recorded_by),
683 )
684
685 def _resolution_to_detail(r: Resolution) -> _HarmonyResolutionDetailJson:
686 return _HarmonyResolutionDetailJson(
687 resolution_id=r.resolution_id,
688 strategy=r.strategy,
689 confidence=r.confidence,
690 human_verified=r.human_verified,
691 applied_count=r.applied_count,
692 resolved_by=r.resolved_by.to_dict(),
693 resolved_at=r.resolved_at.isoformat(),
694 rationale=r.rationale,
695 )
696
697 def _policy_to_entry(p: Policy) -> _HarmonyPolicyEntryJson:
698 return _HarmonyPolicyEntryJson(
699 policy_id=p.policy_id,
700 description=p.description,
701 scope=p.scope,
702 action=p.action,
703 confidence=p.confidence,
704 conflict_type=p.when.conflict_type,
705 domain=p.when.domain,
706 path_pattern=p.when.path_pattern,
707 created_at=p.created_at.isoformat(),
708 created_by=p.created_by,
709 )
710
711 # ---------------------------------------------------------------------------
712 # Format check
713 # ---------------------------------------------------------------------------
714
715 # ---------------------------------------------------------------------------
716 # Registration
717 # ---------------------------------------------------------------------------
718
719 def register(
720 subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]",
721 ) -> None:
722 """Register the harmony subcommand group."""
723 parser = subparsers.add_parser(
724 "harmony",
725 help="Resolution Intelligence — record, resolve, and replay conflict patterns.",
726 description=__doc__,
727 formatter_class=argparse.RawDescriptionHelpFormatter,
728 )
729 subs = parser.add_subparsers(dest="subcommand", metavar="SUBCOMMAND")
730 subs.required = True
731
732 # -- record ---------------------------------------------------------------
733 record_p = subs.add_parser(
734 "record",
735 help="Record a new conflict pattern.",
736 description=(
737 "Persist a ConflictPattern to the harmony store.\n\n"
738 "The pattern is identified by a blob fingerprint (SHA-256 of sorted\n"
739 "ours_id:theirs_id) and an optional semantic fingerprint supplied by\n"
740 "a domain plugin. Recording the same pattern twice is idempotent.\n\n"
741 "Agent quickstart\n"
742 "----------------\n"
743 " muse harmony record --path track.mid --domain midi \\\n"
744 " --conflict-type content \\\n"
745 " --ours-id <hex64> --theirs-id <hex64> --json\n\n"
746 "JSON output schema\n"
747 "------------------\n"
748 ' {"pattern_id": "<hex64>", "already_existed": <bool>}\n\n'
749 "Exit codes\n"
750 "----------\n"
751 " 0 — pattern recorded (or already existed)\n"
752 " 1 — invalid arguments or validation failure\n"
753 " 2 — not inside a Muse repository\n"
754 ),
755 formatter_class=argparse.RawDescriptionHelpFormatter,
756 )
757 record_p.add_argument("--path", required=True,
758 help="Workspace-relative POSIX path of the conflicting file.")
759 record_p.add_argument("--domain", required=True,
760 help="Domain name, e.g. 'midi' or 'code'.")
761 record_p.add_argument("--conflict-type", required=True, dest="conflict_type",
762 help="Conflict category (content, structural, metadata, relational, ...).")
763 record_p.add_argument("--ours-id", required=True, dest="ours_id",
764 help="64-char hex SHA-256 object ID for the 'ours' version.")
765 record_p.add_argument("--theirs-id", required=True, dest="theirs_id",
766 help="64-char hex SHA-256 object ID for the 'theirs' version.")
767 record_p.add_argument("--semantic-fingerprint", dest="semantic_fingerprint", default=None,
768 help="64-char hex semantic fingerprint from a domain plugin. "
769 "Defaults to the blob fingerprint.")
770 record_p.add_argument("--description", default=None,
771 help="JSON-encoded domain-specific metadata (optional).")
772 record_p.add_argument("--recorded-by", dest="recorded_by", default="human",
773 help="Agent ID or 'human' (default: human).")
774 record_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
775 record_p.set_defaults(func=run_record)
776
777 # -- list -----------------------------------------------------------------
778 list_p = subs.add_parser(
779 "list",
780 help="List recorded conflict patterns.",
781 description=(
782 "List all ConflictPatterns in the harmony store, newest first.\n\n"
783 "Agent quickstart\n"
784 "----------------\n"
785 " muse harmony list --json\n"
786 " muse harmony list --domain midi --json\n\n"
787 "JSON output schema\n"
788 "------------------\n"
789 ' {"total": <int>, "patterns": [{...}, ...]}\n\n'
790 "Exit codes\n"
791 "----------\n"
792 " 0 — list returned (may be empty)\n"
793 " 2 — not inside a Muse repository\n"
794 ),
795 formatter_class=argparse.RawDescriptionHelpFormatter,
796 )
797 list_p.add_argument("--domain", default=None,
798 help="Filter to patterns in this domain only.")
799 list_p.add_argument("--conflict-type", dest="conflict_type", default=None,
800 help="Filter to patterns of this conflict type only.")
801 list_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
802 list_p.set_defaults(func=run_list)
803
804 # -- show -----------------------------------------------------------------
805 show_p = subs.add_parser(
806 "show",
807 help="Show a conflict pattern and all its resolutions.",
808 description=(
809 "Display the full ConflictPattern metadata and every saved Resolution,\n"
810 "sorted by quality (human_verified > confidence > applied_count).\n\n"
811 "Agent quickstart\n"
812 "----------------\n"
813 " muse harmony show <pattern_id> --json\n\n"
814 "JSON output schema\n"
815 "------------------\n"
816 ' {"pattern": {...}, "resolutions": [{...}, ...]}\n\n'
817 "Exit codes\n"
818 "----------\n"
819 " 0 — pattern found and shown\n"
820 " 1 — invalid pattern_id or pattern not found\n"
821 " 2 — not inside a Muse repository\n"
822 ),
823 formatter_class=argparse.RawDescriptionHelpFormatter,
824 )
825 show_p.add_argument("pattern_id", metavar="PATTERN_ID",
826 help="64-char hex pattern ID.")
827 show_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
828 show_p.set_defaults(func=run_show)
829
830 # -- resolve --------------------------------------------------------------
831 resolve_p = subs.add_parser(
832 "resolve",
833 help="Save a resolution for a conflict pattern.",
834 description=(
835 "Persist a Resolution to the harmony store. The parent pattern must\n"
836 "already exist (run 'muse harmony record' first). Saving the same\n"
837 "resolution twice is idempotent.\n\n"
838 "Agent quickstart\n"
839 "----------------\n"
840 " muse harmony resolve --pattern-id <hex64> \\\n"
841 " --strategy manual --outcome-blob <hex64> \\\n"
842 " --confidence 0.9 --json\n\n"
843 "JSON output schema\n"
844 "------------------\n"
845 ' {"resolution_id": "<hex64>", "pattern_id": "<hex64>",\n'
846 ' "already_existed": <bool>}\n\n'
847 "Exit codes\n"
848 "----------\n"
849 " 0 — resolution saved (or already existed)\n"
850 " 1 — validation failure, pattern not found, or confidence out of range\n"
851 " 2 — not inside a Muse repository\n"
852 ),
853 formatter_class=argparse.RawDescriptionHelpFormatter,
854 )
855 resolve_p.add_argument("--pattern-id", required=True, dest="pattern_id",
856 help="64-char hex pattern ID to resolve.")
857 resolve_p.add_argument("--strategy", required=True,
858 help="Resolution strategy (manual, exact-replay, "
859 "semantic-proposal, policy).")
860 resolve_p.add_argument("--outcome-blob", required=True, dest="outcome_blob",
861 help="64-char hex SHA-256 of the resolved object.")
862 resolve_p.add_argument("--confidence", required=True, type=float,
863 help="Resolution confidence 0.0–1.0.")
864 resolve_p.add_argument("--rationale", default="",
865 help="Human-readable reasoning for this resolution.")
866 resolve_p.add_argument("--agent-id", dest="agent_id", default=None,
867 help="Agent ID for provenance (omit for human).")
868 resolve_p.add_argument("--model-id", dest="model_id", default=None,
869 help="Model ID for agent provenance.")
870 resolve_p.add_argument("--human-verified", dest="human_verified",
871 action="store_true",
872 help="Mark this resolution as human-verified.")
873 resolve_p.add_argument("--policy-id", dest="policy_id", default=None,
874 help="Policy ID that triggered this resolution (if any).")
875 resolve_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
876 resolve_p.set_defaults(func=run_resolve)
877
878 # -- best -----------------------------------------------------------------
879 best_p = subs.add_parser(
880 "best",
881 help="Show the highest-quality resolution for a pattern.",
882 description=(
883 "Return the best Resolution for a pattern, ranked by\n"
884 "human_verified > confidence > applied_count. Returns null if\n"
885 "no resolutions exist yet.\n\n"
886 "Agent quickstart\n"
887 "----------------\n"
888 " muse harmony best <pattern_id> --json\n\n"
889 "JSON output schema\n"
890 "------------------\n"
891 ' {"pattern_id": "<hex64>", "resolution": {<resolution>|null}}\n\n'
892 "Exit codes\n"
893 "----------\n"
894 " 0 — result returned (resolution may be null)\n"
895 " 1 — invalid pattern_id\n"
896 " 2 — not inside a Muse repository\n"
897 ),
898 formatter_class=argparse.RawDescriptionHelpFormatter,
899 )
900 best_p.add_argument("pattern_id", metavar="PATTERN_ID",
901 help="64-char hex pattern ID.")
902 best_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
903 best_p.set_defaults(func=run_best)
904
905 # -- forget ---------------------------------------------------------------
906 forget_p = subs.add_parser(
907 "forget",
908 help="Remove a conflict pattern and all its resolutions.",
909 description=(
910 "Delete the pattern and every resolution stored under it. Returns\n"
911 "removed=false if the pattern does not exist.\n\n"
912 "Agent quickstart\n"
913 "----------------\n"
914 " muse harmony forget <pattern_id> --json\n\n"
915 "JSON output schema\n"
916 "------------------\n"
917 ' {"pattern_id": "<hex64>", "removed": <bool>}\n\n'
918 "Exit codes\n"
919 "----------\n"
920 " 0 — result returned\n"
921 " 1 — invalid pattern_id\n"
922 " 2 — not inside a Muse repository\n"
923 ),
924 formatter_class=argparse.RawDescriptionHelpFormatter,
925 )
926 forget_p.add_argument("pattern_id", metavar="PATTERN_ID",
927 help="64-char hex pattern ID.")
928 forget_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
929 forget_p.set_defaults(func=run_forget)
930
931 # -- clear ----------------------------------------------------------------
932 clear_p = subs.add_parser(
933 "clear",
934 help="Remove all conflict patterns and their resolutions.",
935 description=(
936 "Delete every pattern entry from the harmony store. This is\n"
937 "irreversible. Pass --yes to skip the confirmation prompt.\n\n"
938 "Agent quickstart\n"
939 "----------------\n"
940 " muse harmony clear --yes --json\n\n"
941 "JSON output schema\n"
942 "------------------\n"
943 ' {"removed": <int>}\n\n'
944 "Exit codes\n"
945 "----------\n"
946 " 0 — clear completed (removed may be 0)\n"
947 " 2 — not inside a Muse repository\n"
948 ),
949 formatter_class=argparse.RawDescriptionHelpFormatter,
950 )
951 clear_p.add_argument("--yes", "-y", action="store_true",
952 help="Skip the confirmation prompt.")
953 clear_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
954 clear_p.set_defaults(func=run_clear)
955
956 # -- gc -------------------------------------------------------------------
957 gc_p = subs.add_parser(
958 "gc",
959 help="Garbage-collect stale unresolved conflict patterns.",
960 description=(
961 "Remove patterns older than --age days that have no saved resolution.\n"
962 "Patterns with at least one resolution are always kept.\n\n"
963 "Agent quickstart\n"
964 "----------------\n"
965 " muse harmony gc --json\n"
966 " muse harmony gc --age 30 --json\n\n"
967 "JSON output schema\n"
968 "------------------\n"
969 ' {"removed": <int>, "age_days": <int>}\n\n'
970 "Exit codes\n"
971 "----------\n"
972 " 0 — gc completed (removed may be 0)\n"
973 " 1 — invalid --age value\n"
974 " 2 — not inside a Muse repository\n"
975 ),
976 formatter_class=argparse.RawDescriptionHelpFormatter,
977 )
978 gc_p.add_argument("--age", type=int, default=90, metavar="DAYS",
979 help="Age threshold in days (default: 90).")
980 gc_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
981 gc_p.set_defaults(func=run_gc)
982
983 # -- policy-add -----------------------------------------------------------
984 padd_p = subs.add_parser(
985 "policy-add",
986 help="Add or replace a declarative resolution policy.",
987 description=(
988 "Persist a Policy to the harmony store. Overwrites any existing\n"
989 "policy with the same policy_id.\n\n"
990 "Agent quickstart\n"
991 "----------------\n"
992 " muse harmony policy-add --policy-id prefer-ours \\\n"
993 " --description 'Always prefer ours for MIDI' \\\n"
994 " --scope domain --action prefer-ours --domain midi --json\n\n"
995 "JSON output schema\n"
996 "------------------\n"
997 ' {"policy_id": "<str>", "action": "<str>", "scope": "<str>"}\n\n'
998 "Exit codes\n"
999 "----------\n"
1000 " 0 — policy saved\n"
1001 " 1 — invalid policy_id or argument\n"
1002 " 2 — not inside a Muse repository\n"
1003 ),
1004 formatter_class=argparse.RawDescriptionHelpFormatter,
1005 )
1006 padd_p.add_argument("--policy-id", required=True, dest="policy_id",
1007 help="URL-safe alphanumeric policy identifier (1–128 chars).")
1008 padd_p.add_argument("--description", required=True,
1009 help="Human-readable explanation of what this policy does.")
1010 padd_p.add_argument("--scope", required=True,
1011 help="Scope: workspace, repo, domain, or file.")
1012 padd_p.add_argument("--action", required=True,
1013 help="Action: prefer-ours, prefer-theirs, escalate, "
1014 "require-human, or delegate.")
1015 padd_p.add_argument("--confidence", type=float, default=1.0,
1016 help="Confidence 0.0–1.0 assigned to policy-driven resolutions "
1017 "(default: 1.0).")
1018 padd_p.add_argument("--conflict-type", dest="conflict_type", default=None,
1019 help="Filter: only fire for this conflict type.")
1020 padd_p.add_argument("--domain", default=None,
1021 help="Filter: only fire for this domain.")
1022 padd_p.add_argument("--path-pattern", dest="path_pattern", default=None,
1023 help="Filter: fnmatch glob for file paths, e.g. '*.mid'.")
1024 padd_p.add_argument("--escalate-to", dest="escalate_to", default=None,
1025 help="Target for ESCALATE action: 'human' or an agent ID.")
1026 padd_p.add_argument("--delegate-to", dest="delegate_to", default=None,
1027 help="Agent ID for DELEGATE action.")
1028 padd_p.add_argument("--created-by", dest="created_by", default="human",
1029 help="Creator attribution (default: human).")
1030 padd_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1031 padd_p.set_defaults(func=run_policy_add)
1032
1033 # -- policy-list ----------------------------------------------------------
1034 plist_p = subs.add_parser(
1035 "policy-list",
1036 help="List all harmony policies, scope-sorted.",
1037 description=(
1038 "List every Policy from the harmony store. Sorted by scope order\n"
1039 "(workspace → repo → domain → file) then by created_at ascending.\n\n"
1040 "Agent quickstart\n"
1041 "----------------\n"
1042 " muse harmony policy-list --json\n\n"
1043 "JSON output schema\n"
1044 "------------------\n"
1045 ' {"total": <int>, "policies": [{...}, ...]}\n\n'
1046 "Exit codes\n"
1047 "----------\n"
1048 " 0 — list returned (may be empty)\n"
1049 " 2 — not inside a Muse repository\n"
1050 ),
1051 formatter_class=argparse.RawDescriptionHelpFormatter,
1052 )
1053 plist_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1054 plist_p.set_defaults(func=run_policy_list)
1055
1056 # -- policy-remove --------------------------------------------------------
1057 premove_p = subs.add_parser(
1058 "policy-remove",
1059 help="Remove a harmony policy.",
1060 description=(
1061 "Delete a policy by ID. Returns removed=false if the policy\n"
1062 "does not exist.\n\n"
1063 "Agent quickstart\n"
1064 "----------------\n"
1065 " muse harmony policy-remove <policy_id> --json\n\n"
1066 "JSON output schema\n"
1067 "------------------\n"
1068 ' {"policy_id": "<str>", "removed": <bool>}\n\n'
1069 "Exit codes\n"
1070 "----------\n"
1071 " 0 — result returned\n"
1072 " 1 — invalid policy_id\n"
1073 " 2 — not inside a Muse repository\n"
1074 ),
1075 formatter_class=argparse.RawDescriptionHelpFormatter,
1076 )
1077 premove_p.add_argument("policy_id", metavar="POLICY_ID",
1078 help="URL-safe policy identifier.")
1079 premove_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1080 premove_p.set_defaults(func=run_policy_remove)
1081
1082 # -- audit ----------------------------------------------------------------
1083 audit_p = subs.add_parser(
1084 "audit",
1085 help="Show the harmony audit log.",
1086 description=(
1087 "List recent audit log entries (newest first). The audit log is\n"
1088 "append-only — a tamper-evident record of all harmony engine actions.\n\n"
1089 "Agent quickstart\n"
1090 "----------------\n"
1091 " muse harmony audit --json\n"
1092 " muse harmony audit --limit 50 --json\n\n"
1093 "JSON output schema\n"
1094 "------------------\n"
1095 ' {"total": <int>, "entries": [{<AuditEvent>}, ...]}\n\n'
1096 "Exit codes\n"
1097 "----------\n"
1098 " 0 — audit log returned (may be empty)\n"
1099 " 2 — not inside a Muse repository\n"
1100 ),
1101 formatter_class=argparse.RawDescriptionHelpFormatter,
1102 )
1103 audit_p.add_argument("--limit", type=int, default=100, metavar="N",
1104 help="Maximum entries to return (default: 100).")
1105 audit_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1106 audit_p.set_defaults(func=run_audit)
1107
1108 # -- engine ---------------------------------------------------------------
1109 engine_p = subs.add_parser(
1110 "engine",
1111 help="Run the three-tier resolution engine for a conflict pattern.",
1112 description=(
1113 "Evaluate a ConflictPattern through the resolution pipeline:\n"
1114 " 1. Policy match — declarative rule fires automatically.\n"
1115 " 2. Exact replay — saved resolution above confidence threshold.\n"
1116 " 3. Semantic match — similar pattern found via domain plugin.\n"
1117 " 4. Escalate — no match; human or specialist-agent required.\n\n"
1118 "Agent quickstart\n"
1119 "----------------\n"
1120 " muse harmony engine <pattern_id> --json\n\n"
1121 "JSON output schema\n"
1122 "------------------\n"
1123 ' {"status": "applied|proposed|escalated",\n'
1124 ' "pattern_id": "<hex64>",\n'
1125 ' "proposal": {<proposal>|null},\n'
1126 ' "applied_resolution_id": "<hex64>|null",\n'
1127 ' "escalation_reason": "<str>|null"}\n\n'
1128 "Exit codes\n"
1129 "----------\n"
1130 " 0 — engine ran successfully (check status field)\n"
1131 " 1 — invalid pattern_id or invalid threshold\n"
1132 " 2 — not inside a Muse repository\n"
1133 ),
1134 formatter_class=argparse.RawDescriptionHelpFormatter,
1135 )
1136 engine_p.add_argument("pattern_id", metavar="PATTERN_ID",
1137 help="64-char hex pattern ID to resolve.")
1138 engine_p.add_argument("--auto-apply-threshold", dest="auto_apply_threshold",
1139 type=float, default=None,
1140 help="Override auto-apply confidence threshold (0.0–1.0). "
1141 "Default: 0.85.")
1142 engine_p.add_argument("--auto-escalate", dest="auto_escalate", action="store_true",
1143 help="Automatically record an EscalationRecord when the engine "
1144 "returns status=escalated.")
1145 engine_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1146 engine_p.set_defaults(func=run_engine)
1147
1148 # -- similar --------------------------------------------------------------
1149 similar_p = subs.add_parser(
1150 "similar",
1151 help="Find conflict patterns semantically similar to the given pattern.",
1152 description=(
1153 "Search the harmony store for patterns whose semantic fingerprint is\n"
1154 "similar to the given pattern. Uses DefaultPlugin (exact fingerprint\n"
1155 "match) unless a domain plugin is injected at the engine level.\n\n"
1156 "Agent quickstart\n"
1157 "----------------\n"
1158 " muse harmony similar <pattern_id> --json\n\n"
1159 "JSON output schema\n"
1160 "------------------\n"
1161 ' {"pattern_id": "<hex64>",\n'
1162 ' "total": <int>,\n'
1163 ' "proposals": [{<proposal>}, ...]}\n\n'
1164 "Exit codes\n"
1165 "----------\n"
1166 " 0 — result returned (proposals may be empty)\n"
1167 " 1 — invalid pattern_id\n"
1168 " 2 — not inside a Muse repository\n"
1169 ),
1170 formatter_class=argparse.RawDescriptionHelpFormatter,
1171 )
1172 similar_p.add_argument("pattern_id", metavar="PATTERN_ID",
1173 help="64-char hex pattern ID to find similar patterns for.")
1174 similar_p.add_argument("--limit", type=int, default=None, metavar="N",
1175 help="Maximum proposals to return (default: engine max_proposals=5).")
1176 similar_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1177 similar_p.set_defaults(func=run_similar)
1178
1179 # -- escalate -------------------------------------------------------------
1180 escalate_p = subs.add_parser(
1181 "escalate",
1182 help="Record an escalation for a conflict pattern requiring human attention.",
1183 description=(
1184 "Persist an EscalationRecord marking a conflict pattern as open and\n"
1185 "awaiting human or specialist-agent resolution. Recording the same\n"
1186 "(pattern_id, reason) pair twice is idempotent.\n\n"
1187 "Agent quickstart\n"
1188 "----------------\n"
1189 " muse harmony escalate <pattern_id> --json\n"
1190 " muse harmony escalate <pattern_id> --reason 'No policy found' --json\n\n"
1191 "JSON output schema\n"
1192 "------------------\n"
1193 ' {"escalation_id": "<hex64>", "pattern_id": "<hex64>",\n'
1194 ' "already_existed": <bool>}\n\n'
1195 "Exit codes\n"
1196 "----------\n"
1197 " 0 — escalation recorded (or already existed)\n"
1198 " 1 — invalid pattern_id\n"
1199 " 2 — not inside a Muse repository\n"
1200 ),
1201 formatter_class=argparse.RawDescriptionHelpFormatter,
1202 )
1203 escalate_p.add_argument("pattern_id", metavar="PATTERN_ID",
1204 help="64-char hex pattern ID to escalate.")
1205 escalate_p.add_argument("--reason", default="Escalated for human review",
1206 help="Human-readable reason for escalation.")
1207 escalate_p.add_argument("--agent-id", dest="agent_id", default=None,
1208 help="Agent ID for provenance (omit for human).")
1209 escalate_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1210 escalate_p.set_defaults(func=run_escalate)
1211
1212 # -- escalations ----------------------------------------------------------
1213 escalations_p = subs.add_parser(
1214 "escalations",
1215 help="List conflict pattern escalations (operator dashboard).",
1216 description=(
1217 "List EscalationRecords — the operator dashboard of unresolved conflicts.\n"
1218 "Filter by --status to see open, resolved, or all escalations.\n\n"
1219 "Agent quickstart\n"
1220 "----------------\n"
1221 " muse harmony escalations --json\n"
1222 " muse harmony escalations --status open --json\n\n"
1223 "JSON output schema\n"
1224 "------------------\n"
1225 ' {"total": <int>, "escalations": [{...}, ...]}\n\n'
1226 "Exit codes\n"
1227 "----------\n"
1228 " 0 — list returned (may be empty)\n"
1229 " 2 — not inside a Muse repository\n"
1230 ),
1231 formatter_class=argparse.RawDescriptionHelpFormatter,
1232 )
1233 escalations_p.add_argument("--status", default=None,
1234 choices=["open", "resolved"],
1235 help="Filter by escalation status.")
1236 escalations_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1237 escalations_p.set_defaults(func=run_escalations)
1238
1239 # -- resolve-escalation ---------------------------------------------------
1240 resolve_esc_p = subs.add_parser(
1241 "resolve-escalation",
1242 help="Close an escalation after a conflict is manually resolved.",
1243 description=(
1244 "Transition an EscalationRecord from OPEN to RESOLVED. Links the\n"
1245 "escalation to the resolution that closed it. Returns resolved=false\n"
1246 "if the escalation does not exist.\n\n"
1247 "Agent quickstart\n"
1248 "----------------\n"
1249 " muse harmony resolve-escalation <esc_id> --resolution-id <hex64> --json\n\n"
1250 "JSON output schema\n"
1251 "------------------\n"
1252 ' {"escalation_id": "<hex64>", "resolved": <bool>}\n\n'
1253 "Exit codes\n"
1254 "----------\n"
1255 " 0 — result returned (resolved may be false)\n"
1256 " 1 — invalid escalation_id or resolution_id\n"
1257 " 2 — not inside a Muse repository\n"
1258 ),
1259 formatter_class=argparse.RawDescriptionHelpFormatter,
1260 )
1261 resolve_esc_p.add_argument("escalation_id", metavar="ESCALATION_ID",
1262 help="64-char hex escalation ID to close.")
1263 resolve_esc_p.add_argument("--resolution-id", required=True, dest="resolution_id",
1264 help="64-char hex resolution ID that closes this escalation.")
1265 resolve_esc_p.add_argument("--agent-id", dest="agent_id", default=None,
1266 help="Agent ID for resolved_by provenance (omit for human).")
1267 resolve_esc_p.add_argument("--json", "-j", action="store_true", dest="json_out", help="Emit JSON output.")
1268 resolve_esc_p.set_defaults(func=run_resolve_escalation)
1269
1270 # ---------------------------------------------------------------------------
1271 # record
1272 # ---------------------------------------------------------------------------
1273
1274 def run_record(args: argparse.Namespace) -> None:
1275 """Record a new conflict pattern in the harmony store.
1276
1277 Computes a blob fingerprint from the sorted (ours_id, theirs_id) pair.
1278 If ``--semantic-fingerprint`` is not supplied the blob fingerprint is reused
1279 as the semantic fingerprint. Recording the same pattern twice is idempotent
1280 — the existing entry is returned unchanged. Writes an audit entry on first record.
1281
1282 Agent quickstart
1283 ----------------
1284 ::
1285
1286 muse harmony record <path> --ours-id <hex> --theirs-id <hex> --format json
1287 muse harmony record config.py --ours-id <hex> --theirs-id <hex> --domain code --format json
1288
1289 JSON fields
1290 -----------
1291 pattern_id Hex-64 fingerprint identifying the pattern.
1292 already_existed ``true`` if the pattern was already in the store.
1293
1294 Exit codes
1295 ----------
1296 0 Success.
1297 1 Invalid IDs, fingerprints, or description.
1298 2 Not inside a Muse repository.
1299 """
1300 elapsed = start_timer()
1301 path: str = args.path
1302 domain: str = args.domain
1303 conflict_type: str = args.conflict_type
1304 ours_id: str = args.ours_id
1305 theirs_id: str = args.theirs_id
1306 semantic_fp_arg: str | None = args.semantic_fingerprint
1307 description_raw: str | None = args.description
1308 recorded_by: str = args.recorded_by
1309 json_out: bool = args.json_out
1310
1311 # Validate hex IDs before touching the filesystem.
1312 for label, value in (("ours_id", ours_id), ("theirs_id", theirs_id)):
1313 try:
1314 _validate_id(value, label)
1315 except ValueError as exc:
1316 if json_out:
1317 print(json.dumps({"error": str(exc)}))
1318 else:
1319 print(f"❌ {exc}", file=sys.stderr)
1320 raise SystemExit(ExitCode.USER_ERROR)
1321
1322 if semantic_fp_arg is not None:
1323 try:
1324 _validate_fingerprint(semantic_fp_arg, "semantic_fingerprint")
1325 except ValueError as exc:
1326 if json_out:
1327 print(json.dumps({"error": str(exc)}))
1328 else:
1329 print(f"❌ {exc}", file=sys.stderr)
1330 raise SystemExit(ExitCode.USER_ERROR)
1331
1332 description = {}
1333 if description_raw is not None:
1334 try:
1335 description = json.loads(description_raw)
1336 if not isinstance(description, dict):
1337 raise ValueError("description must be a JSON object")
1338 except (json.JSONDecodeError, ValueError) as exc:
1339 if json_out:
1340 print(json.dumps({"error": f"Invalid --description: {exc}"}))
1341 else:
1342 print(f"❌ Invalid --description: {exc}", file=sys.stderr)
1343 raise SystemExit(ExitCode.USER_ERROR)
1344
1345 root = require_repo()
1346
1347 blob_fp = blob_fingerprint(ours_id, theirs_id)
1348 semantic_fp = semantic_fp_arg if semantic_fp_arg is not None else blob_fp
1349 pattern_id = compute_pattern_id(path, blob_fp, semantic_fp)
1350
1351 # Check idempotency before building the full object.
1352 existing = load_pattern(root, pattern_id)
1353 already_existed = existing is not None
1354
1355 if not already_existed:
1356 import datetime
1357 pattern = ConflictPattern(
1358 pattern_id=pattern_id,
1359 path=path,
1360 domain=domain,
1361 conflict_type=conflict_type,
1362 blob_fingerprint=blob_fp,
1363 semantic_fingerprint=semantic_fp,
1364 ours_id=ours_id,
1365 theirs_id=theirs_id,
1366 description=description,
1367 recorded_at=datetime.datetime.now(datetime.timezone.utc),
1368 recorded_by=recorded_by,
1369 )
1370 record_pattern(root, pattern)
1371 append_audit(
1372 root,
1373 AuditEventType.PATTERN_RECORDED,
1374 AgentProvenance.human() if recorded_by == "human"
1375 else AgentProvenance.agent(recorded_by),
1376 pattern_id=pattern_id,
1377 )
1378 logger.debug("harmony: recorded pattern %s for '%s'", short_id(pattern_id), path)
1379
1380 if json_out:
1381 print(json.dumps(_HarmonyRecordJson(
1382 **make_envelope(elapsed),
1383 pattern_id=pattern_id,
1384 already_existed=already_existed,
1385 )))
1386 return
1387
1388 if already_existed:
1389 print(f" ℹ pattern {pattern_id} already recorded for '{sanitize_display(path)}'")
1390 else:
1391 print(f" ✅ recorded pattern {pattern_id} '{sanitize_display(path)}'")
1392
1393 # ---------------------------------------------------------------------------
1394 # list
1395 # ---------------------------------------------------------------------------
1396
1397 def run_list(args: argparse.Namespace) -> None:
1398 """List all conflict patterns in the harmony store.
1399
1400 Returns patterns sorted newest-first. Use ``--domain`` and
1401 ``--conflict-type`` to narrow the results.
1402
1403 Agent quickstart
1404 ----------------
1405 ::
1406
1407 muse harmony patterns --format json
1408 muse harmony patterns --domain code --format json
1409
1410 JSON fields
1411 -----------
1412 total Total number of patterns matching the filter.
1413 patterns List of pattern objects: ``pattern_id``, ``path``, ``domain``,
1414 ``conflict_type``, ``resolution_count``, ``recorded_at``.
1415
1416 Exit codes
1417 ----------
1418 0 Success (may be empty).
1419 2 Not inside a Muse repository.
1420 """
1421 elapsed = start_timer()
1422 domain_filter: str | None = args.domain
1423 ct_filter: str | None = args.conflict_type
1424 json_out: bool = args.json_out
1425
1426 root = require_repo()
1427 patterns = list_patterns(root)
1428
1429 if domain_filter is not None:
1430 patterns = [p for p in patterns if p.domain == domain_filter]
1431 if ct_filter is not None:
1432 patterns = [p for p in patterns if p.conflict_type == ct_filter]
1433
1434 entries: list[_HarmonyListEntryJson] = []
1435 for p in patterns:
1436 resolutions = list_resolutions(root, p.pattern_id)
1437 entries.append(_pattern_to_list_entry(p, len(resolutions)))
1438
1439 if json_out:
1440 print(json.dumps(_HarmonyListJson(
1441 **make_envelope(elapsed),
1442 total=len(entries),
1443 patterns=entries,
1444 )))
1445 return
1446
1447 if not entries:
1448 print("No harmony conflict patterns recorded.")
1449 return
1450
1451 print(f"{'pattern_id':14} {'domain':8} {'type':12} {'res':>3} path")
1452 print("─" * 70)
1453 for e in entries:
1454 pid_short = short_id(e["pattern_id"])
1455 print(
1456 f" {pid_short} {e['domain']:8} {e['conflict_type']:12} "
1457 f"{e['resolution_count']:>3} {e['path']}"
1458 )
1459
1460 # ---------------------------------------------------------------------------
1461 # show
1462 # ---------------------------------------------------------------------------
1463
1464 def run_show(args: argparse.Namespace) -> None:
1465 """Show a conflict pattern and all its resolutions.
1466
1467 Exits 1 if the pattern_id is invalid or the pattern does not exist.
1468
1469 JSON schema::
1470
1471 {"pattern": {...}, "resolutions": [{...}, ...]}
1472
1473 Exit codes:
1474 0 — pattern found
1475 1 — invalid pattern_id or pattern not found
1476 2 — not inside a Muse repository
1477 """
1478 elapsed = start_timer()
1479 pattern_id: str = args.pattern_id
1480 json_out: bool = args.json_out
1481
1482 try:
1483 _validate_id(pattern_id, "pattern_id")
1484 except ValueError as exc:
1485 if json_out:
1486 print(json.dumps({"error": str(exc)}))
1487 else:
1488 print(f"❌ {exc}", file=sys.stderr)
1489 raise SystemExit(ExitCode.USER_ERROR)
1490
1491 root = require_repo()
1492 pattern = load_pattern(root, pattern_id)
1493 if pattern is None:
1494 msg = f"Pattern {pattern_id} not found."
1495 if json_out:
1496 print(json.dumps({"error": msg}))
1497 else:
1498 print(f"❌ {msg}", file=sys.stderr)
1499 raise SystemExit(ExitCode.USER_ERROR)
1500
1501 resolutions = list_resolutions(root, pattern_id)
1502
1503 if json_out:
1504 print(json.dumps(_HarmonyShowJson(
1505 **make_envelope(elapsed),
1506 pattern=_pattern_to_detail(pattern),
1507 resolutions=[_resolution_to_detail(r) for r in resolutions],
1508 )))
1509 return
1510
1511 print(f"\nPattern {pattern_id} '{sanitize_display(pattern.path)}'")
1512 print(f" domain: {pattern.domain}")
1513 print(f" type: {pattern.conflict_type}")
1514 print(f" blob_fp: {short_id(pattern.blob_fingerprint)}…")
1515 print(f" recorded_at: {pattern.recorded_at.strftime('%Y-%m-%d %H:%M')}")
1516 print(f" recorded_by: {sanitize_display(pattern.recorded_by)}")
1517 if resolutions:
1518 print(f"\n Resolutions ({len(resolutions)}):")
1519 for r in resolutions:
1520 hv = " ✅ verified" if r.human_verified else ""
1521 print(
1522 f" {short_id(r.resolution_id)} {r.strategy:18} "
1523 f"conf={r.confidence:.2f} applied={r.applied_count}{hv}"
1524 )
1525 else:
1526 print("\n No resolutions saved yet.")
1527
1528 # ---------------------------------------------------------------------------
1529 # resolve
1530 # ---------------------------------------------------------------------------
1531
1532 def run_resolve(args: argparse.Namespace) -> None:
1533 """Save a resolution for a conflict pattern.
1534
1535 The parent pattern must already exist. Saving the same resolution_id
1536 twice is idempotent. Writes an audit entry on first save.
1537
1538 JSON schema::
1539
1540 {"resolution_id": "<hex64>", "pattern_id": "<hex64>",
1541 "already_existed": <bool>}
1542
1543 Exit codes:
1544 0 — resolution saved (or already existed)
1545 1 — invalid ID, pattern not found, or confidence out of range
1546 2 — not inside a Muse repository
1547 """
1548 elapsed = start_timer()
1549 pattern_id: str = args.pattern_id
1550 strategy: str = args.strategy
1551 outcome_blob: str = args.outcome_blob
1552 confidence: float = args.confidence
1553 rationale: str = args.rationale
1554 agent_id: str | None = args.agent_id
1555 model_id: str | None = args.model_id
1556 human_verified: bool = args.human_verified
1557 policy_id: str | None = args.policy_id
1558 json_out: bool = args.json_out
1559
1560 # Validate IDs.
1561 for label, value in (("pattern_id", pattern_id), ("outcome_blob", outcome_blob)):
1562 try:
1563 _validate_id(value, label)
1564 except ValueError as exc:
1565 if json_out:
1566 print(json.dumps({"error": str(exc)}))
1567 else:
1568 print(f"❌ {exc}", file=sys.stderr)
1569 raise SystemExit(ExitCode.USER_ERROR)
1570
1571 if not (0.0 <= confidence <= 1.0):
1572 msg = f"--confidence must be between 0.0 and 1.0, got {confidence}"
1573 if json_out:
1574 print(json.dumps({"error": msg}))
1575 else:
1576 print(f"❌ {msg}", file=sys.stderr)
1577 raise SystemExit(ExitCode.USER_ERROR)
1578
1579 root = require_repo()
1580
1581 provenance = (
1582 AgentProvenance.agent(agent_id, model_id)
1583 if agent_id is not None
1584 else AgentProvenance.human()
1585 )
1586
1587 actor = provenance.agent_id or "human"
1588
1589 # Check idempotency: scan existing resolutions for one that matches the
1590 # key fields (outcome_blob, strategy, actor). resolution_id encodes
1591 # resolved_at so two calls at different times would otherwise produce
1592 # distinct IDs for semantically identical resolutions.
1593 existing_resolutions = list_resolutions(root, pattern_id)
1594 already_existed = False
1595 resolution_id: str | None = None
1596 for existing in existing_resolutions:
1597 existing_actor = existing.resolved_by.agent_id or "human"
1598 if (
1599 existing.outcome_blob == outcome_blob
1600 and existing.strategy == strategy
1601 and existing_actor == actor
1602 ):
1603 resolution_id = existing.resolution_id
1604 already_existed = True
1605 break
1606
1607 import datetime
1608 resolved_at = datetime.datetime.now(datetime.timezone.utc)
1609 if resolution_id is None:
1610 resolution_id = compute_resolution_id(
1611 pattern_id, outcome_blob, strategy, provenance, resolved_at
1612 )
1613
1614 if not already_existed:
1615 try:
1616 resolution = Resolution(
1617 resolution_id=resolution_id,
1618 pattern_id=pattern_id,
1619 strategy=strategy,
1620 policy_id=policy_id,
1621 outcome_blob=outcome_blob,
1622 resolved_by=provenance,
1623 human_verified=human_verified,
1624 confidence=confidence,
1625 rationale=rationale,
1626 resolved_at=resolved_at,
1627 )
1628 save_resolution(root, resolution)
1629 except FileNotFoundError as exc:
1630 msg = str(exc)
1631 if json_out:
1632 print(json.dumps({"error": msg}))
1633 else:
1634 print(f"❌ {msg}", file=sys.stderr)
1635 raise SystemExit(ExitCode.USER_ERROR)
1636
1637 append_audit(
1638 root,
1639 AuditEventType.RESOLUTION_SAVED,
1640 provenance,
1641 pattern_id=pattern_id,
1642 resolution_id=resolution_id,
1643 )
1644
1645 if json_out:
1646 print(json.dumps(_HarmonyResolveJson(
1647 **make_envelope(elapsed),
1648 resolution_id=resolution_id,
1649 pattern_id=pattern_id,
1650 already_existed=already_existed,
1651 )))
1652 return
1653
1654 if already_existed:
1655 print(f" ℹ resolution {resolution_id} already saved.")
1656 else:
1657 print(f" ✅ saved resolution {resolution_id} for pattern {pattern_id}")
1658
1659 # ---------------------------------------------------------------------------
1660 # best
1661 # ---------------------------------------------------------------------------
1662
1663 def run_best(args: argparse.Namespace) -> None:
1664 """Show the highest-quality resolution for a pattern.
1665
1666 Quality ranking: human_verified > confidence > applied_count.
1667 Returns null if no resolutions exist.
1668
1669 JSON schema::
1670
1671 {"pattern_id": "<hex64>", "resolution": {<resolution>|null}}
1672
1673 Exit codes:
1674 0 — result returned
1675 1 — invalid pattern_id
1676 2 — not inside a Muse repository
1677 """
1678 elapsed = start_timer()
1679 pattern_id: str = args.pattern_id
1680 json_out: bool = args.json_out
1681
1682 try:
1683 _validate_id(pattern_id, "pattern_id")
1684 except ValueError as exc:
1685 if json_out:
1686 print(json.dumps({"error": str(exc)}))
1687 else:
1688 print(f"❌ {exc}", file=sys.stderr)
1689 raise SystemExit(ExitCode.USER_ERROR)
1690
1691 root = require_repo()
1692 best = best_resolution(root, pattern_id)
1693
1694 if json_out:
1695 print(json.dumps(_HarmonyBestJson(
1696 **make_envelope(elapsed),
1697 pattern_id=pattern_id,
1698 resolution=_resolution_to_detail(best) if best is not None else None,
1699 )))
1700 return
1701
1702 if best is None:
1703 print(f" ℹ No resolutions for pattern {pattern_id}.")
1704 return
1705
1706 hv = " ✅ verified" if best.human_verified else ""
1707 print(
1708 f" best for {pattern_id}:\n"
1709 f" {best.resolution_id} {best.strategy} "
1710 f"conf={best.confidence:.2f} applied={best.applied_count}{hv}\n"
1711 f" rationale: {sanitize_display(best.rationale)}"
1712 )
1713
1714 # ---------------------------------------------------------------------------
1715 # forget
1716 # ---------------------------------------------------------------------------
1717
1718 def run_forget(args: argparse.Namespace) -> None:
1719 """Remove a conflict pattern and all its resolutions.
1720
1721 JSON schema::
1722
1723 {"pattern_id": "<hex64>", "removed": <bool>}
1724
1725 Exit codes:
1726 0 — result returned
1727 1 — invalid pattern_id
1728 2 — not inside a Muse repository
1729 """
1730 elapsed = start_timer()
1731 pattern_id: str = args.pattern_id
1732 json_out: bool = args.json_out
1733
1734 try:
1735 _validate_id(pattern_id, "pattern_id")
1736 except ValueError as exc:
1737 if json_out:
1738 print(json.dumps({"error": str(exc)}))
1739 else:
1740 print(f"❌ {exc}", file=sys.stderr)
1741 raise SystemExit(ExitCode.USER_ERROR)
1742
1743 root = require_repo()
1744 removed = forget_pattern(root, pattern_id)
1745
1746 if json_out:
1747 print(json.dumps(_HarmonyForgetJson(
1748 **make_envelope(elapsed),
1749 pattern_id=pattern_id,
1750 removed=removed,
1751 )))
1752 return
1753
1754 if removed:
1755 print(f" 🗑 forgot pattern {pattern_id}")
1756 else:
1757 print(f" ℹ pattern {pattern_id} not found.")
1758
1759 # ---------------------------------------------------------------------------
1760 # clear
1761 # ---------------------------------------------------------------------------
1762
1763 def run_clear(args: argparse.Namespace) -> None:
1764 """Remove all conflict patterns and their resolutions.
1765
1766 JSON schema::
1767
1768 {"removed": <int>}
1769
1770 Exit codes:
1771 0 — clear completed (removed may be 0)
1772 2 — not inside a Muse repository
1773 """
1774 elapsed = start_timer()
1775 yes: bool = args.yes
1776 json_out: bool = args.json_out
1777
1778 root = require_repo()
1779
1780 if not yes:
1781 from muse.core.harmony import patterns_dir
1782 pdir = patterns_dir(root)
1783 count = (
1784 sum(1 for e in pdir.iterdir() if not e.is_symlink() and e.is_dir())
1785 if pdir.exists()
1786 else 0
1787 )
1788 if count == 0:
1789 if json_out:
1790 print(json.dumps(_HarmonyScalarJson(**make_envelope(elapsed), removed=0)))
1791 else:
1792 print("Harmony store is already empty.")
1793 return
1794 confirmed = input(
1795 f"This will permanently delete {count} harmony pattern(s). Continue? [y/N]: "
1796 ).strip().lower() in ("y", "yes")
1797 if not confirmed:
1798 print("Aborted.")
1799 return
1800
1801 removed = clear_all(root)
1802
1803 if json_out:
1804 print(json.dumps(_HarmonyScalarJson(**make_envelope(elapsed), removed=removed)))
1805 return
1806
1807 print(f"✅ Cleared {removed} harmony pattern(s).")
1808
1809 # ---------------------------------------------------------------------------
1810 # gc
1811 # ---------------------------------------------------------------------------
1812
1813 def run_gc(args: argparse.Namespace) -> None:
1814 """Remove stale unresolved patterns older than --age days.
1815
1816 Patterns with at least one resolution are always kept.
1817
1818 JSON schema::
1819
1820 {"removed": <int>, "age_days": <int>}
1821
1822 Exit codes:
1823 0 — gc completed (removed may be 0)
1824 1 — invalid --age value
1825 2 — not inside a Muse repository
1826 """
1827 elapsed = start_timer()
1828 json_out: bool = args.json_out
1829 try:
1830 age_days: int = clamp_int(args.age, 1, 36_500, "age")
1831 except ValueError as exc:
1832 if json_out:
1833 print(json.dumps({"error": str(exc)}))
1834 else:
1835 print(f"❌ {exc}", file=sys.stderr)
1836 raise SystemExit(ExitCode.USER_ERROR)
1837
1838 root = require_repo()
1839 removed = gc_stale(root, age_days=age_days)
1840
1841 if json_out:
1842 print(json.dumps(_HarmonyGcJson(
1843 **make_envelope(elapsed),
1844 removed=removed,
1845 age_days=age_days,
1846 )))
1847 return
1848
1849 if removed:
1850 print(
1851 f"✅ gc: removed {removed} stale unresolved pattern(s) "
1852 f"older than {age_days} day(s)."
1853 )
1854 else:
1855 print(f"gc: nothing to remove (threshold: {age_days} day(s)).")
1856
1857 # ---------------------------------------------------------------------------
1858 # policy-add
1859 # ---------------------------------------------------------------------------
1860
1861 def run_policy_add(args: argparse.Namespace) -> None:
1862 """Add or replace a declarative resolution policy.
1863
1864 Overwrites any existing policy with the same policy_id.
1865
1866 JSON schema::
1867
1868 {"policy_id": "<str>", "action": "<str>", "scope": "<str>"}
1869
1870 Exit codes:
1871 0 — policy saved
1872 1 — invalid policy_id
1873 2 — not inside a Muse repository
1874 """
1875 elapsed = start_timer()
1876 policy_id: str = args.policy_id
1877 description: str = args.description
1878 scope: str = args.scope
1879 action: str = args.action
1880 confidence: float = args.confidence
1881 conflict_type: str | None = args.conflict_type
1882 domain: str | None = args.domain
1883 path_pattern: str | None = args.path_pattern
1884 escalate_to: str | None = args.escalate_to
1885 delegate_to: str | None = args.delegate_to
1886 created_by: str = args.created_by
1887 json_out: bool = args.json_out
1888
1889 try:
1890 _validate_policy_id(policy_id)
1891 except ValueError as exc:
1892 if json_out:
1893 print(json.dumps({"error": str(exc)}))
1894 else:
1895 print(f"❌ {exc}", file=sys.stderr)
1896 raise SystemExit(ExitCode.USER_ERROR)
1897
1898 root = require_repo()
1899
1900 import datetime
1901 policy = Policy(
1902 policy_id=policy_id,
1903 description=description,
1904 when=PolicyCondition(
1905 conflict_type=conflict_type,
1906 domain=domain,
1907 path_pattern=path_pattern,
1908 ),
1909 action=action,
1910 confidence=confidence,
1911 escalate_to=escalate_to,
1912 delegate_to=delegate_to,
1913 scope=scope,
1914 created_at=datetime.datetime.now(datetime.timezone.utc),
1915 created_by=created_by,
1916 )
1917 save_policy(root, policy)
1918 append_audit(
1919 root,
1920 AuditEventType.POLICY_SAVED,
1921 AgentProvenance.human() if created_by == "human"
1922 else AgentProvenance.agent(created_by),
1923 policy_id=policy_id,
1924 )
1925
1926 if json_out:
1927 print(json.dumps(_HarmonyPolicyAddJson(
1928 **make_envelope(elapsed),
1929 policy_id=policy_id,
1930 action=action,
1931 scope=scope,
1932 )))
1933 return
1934
1935 print(f" ✅ saved policy '{policy_id}' scope={scope} action={action}")
1936
1937 # ---------------------------------------------------------------------------
1938 # policy-list
1939 # ---------------------------------------------------------------------------
1940
1941 def run_policy_list(args: argparse.Namespace) -> None:
1942 """List all policies from the harmony store, scope-sorted.
1943
1944 JSON schema::
1945
1946 {"total": <int>, "policies": [{...}, ...]}
1947
1948 Exit codes:
1949 0 — list returned (may be empty)
1950 2 — not inside a Muse repository
1951 """
1952 elapsed = start_timer()
1953 json_out: bool = args.json_out
1954
1955 root = require_repo()
1956 policies = list_policies(root)
1957
1958 entries = [_policy_to_entry(p) for p in policies]
1959
1960 if json_out:
1961 print(json.dumps(_HarmonyPolicyListJson(
1962 **make_envelope(elapsed),
1963 total=len(entries),
1964 policies=entries,
1965 )))
1966 return
1967
1968 if not entries:
1969 print("No harmony policies defined.")
1970 return
1971
1972 print(f"{'policy_id':24} {'scope':10} {'action':14} conf")
1973 print("─" * 60)
1974 for e in entries:
1975 print(
1976 f" {e['policy_id']:<22} {e['scope']:10} "
1977 f"{e['action']:14} {e['confidence']:.2f}"
1978 )
1979
1980 # ---------------------------------------------------------------------------
1981 # policy-remove
1982 # ---------------------------------------------------------------------------
1983
1984 def run_policy_remove(args: argparse.Namespace) -> None:
1985 """Remove a policy from the harmony store.
1986
1987 JSON schema::
1988
1989 {"policy_id": "<str>", "removed": <bool>}
1990
1991 Exit codes:
1992 0 — result returned
1993 1 — invalid policy_id
1994 2 — not inside a Muse repository
1995 """
1996 elapsed = start_timer()
1997 policy_id: str = args.policy_id
1998 json_out: bool = args.json_out
1999
2000 try:
2001 _validate_policy_id(policy_id)
2002 except ValueError as exc:
2003 if json_out:
2004 print(json.dumps({"error": str(exc)}))
2005 else:
2006 print(f"❌ {exc}", file=sys.stderr)
2007 raise SystemExit(ExitCode.USER_ERROR)
2008
2009 root = require_repo()
2010 removed = remove_policy(root, policy_id)
2011
2012 if json_out:
2013 print(json.dumps(_HarmonyPolicyRemoveJson(
2014 **make_envelope(elapsed),
2015 policy_id=policy_id,
2016 removed=removed,
2017 )))
2018 return
2019
2020 if removed:
2021 print(f" 🗑 removed policy '{policy_id}'")
2022 else:
2023 print(f" ℹ policy '{policy_id}' not found.")
2024
2025 # ---------------------------------------------------------------------------
2026 # audit
2027 # ---------------------------------------------------------------------------
2028
2029 def run_audit(args: argparse.Namespace) -> None:
2030 """Show the harmony audit log (newest first).
2031
2032 JSON schema::
2033
2034 {"total": <int>, "entries": [{<AuditEvent>}, ...]}
2035
2036 Exit codes:
2037 0 — audit log returned (may be empty)
2038 2 — not inside a Muse repository
2039 """
2040 elapsed = start_timer()
2041 limit: int = args.limit
2042 json_out: bool = args.json_out
2043
2044 root = require_repo()
2045 entries = list_audit(root, limit=limit)
2046
2047 if json_out:
2048 print(json.dumps(_HarmonyAuditJson(
2049 **make_envelope(elapsed),
2050 total=len(entries),
2051 entries=entries,
2052 )))
2053 return
2054
2055 if not entries:
2056 print("Harmony audit log is empty.")
2057 return
2058
2059 print(f"{'occurred_at':22} {'event_type':28} {'pattern_id':14}")
2060 print("─" * 72)
2061 for e in entries:
2062 pid_short = short_id(e["pattern_id"] or "") or "-"
2063 ts = e["occurred_at"][:19]
2064 print(f" {ts} {e['event_type']:28} {pid_short}")
2065
2066 # ---------------------------------------------------------------------------
2067 # engine
2068 # ---------------------------------------------------------------------------
2069
2070 def _proposal_to_json(p: "Any") -> _HarmonyProposalJson:
2071 """Serialise a ResolutionProposal to its JSON TypedDict form."""
2072 from muse.core.harmony import ResolutionProposal # local to avoid circular
2073 return _HarmonyProposalJson(
2074 pattern_id=p.pattern_id,
2075 strategy=p.strategy,
2076 proposed_action=p.proposed_action,
2077 confidence=p.confidence,
2078 rationale=p.rationale,
2079 policy_id=p.policy_id,
2080 similar_pattern_id=p.similar_pattern_id,
2081 similarity=p.similarity,
2082 requires_confirmation=p.requires_confirmation,
2083 )
2084
2085 def run_engine(args: argparse.Namespace) -> None:
2086 """Run the three-tier resolution engine for a conflict pattern.
2087
2088 The engine evaluates the pattern against policies, saved resolutions, and
2089 semantically similar patterns. Returns status=applied|proposed|escalated.
2090
2091 JSON schema::
2092
2093 {
2094 "status": "applied|proposed|escalated",
2095 "pattern_id": "<hex64>",
2096 "proposal": {<proposal>|null},
2097 "applied_resolution_id": "<hex64>|null",
2098 "escalation_reason": "<str>|null"
2099 }
2100
2101 Exit codes:
2102 0 — engine ran successfully
2103 1 — invalid pattern_id or invalid threshold
2104 2 — not inside a Muse repository
2105 """
2106 elapsed = start_timer()
2107 import datetime
2108 from muse.core.harmony import AgentProvenance, load_pattern, ConflictPattern
2109 from muse.core.harmony_engine import EngineConfig, resolve as _engine_resolve
2110
2111 pattern_id: str = args.pattern_id
2112 threshold: float | None = args.auto_apply_threshold
2113 auto_escalate: bool = getattr(args, "auto_escalate", False)
2114 json_out: bool = args.json_out
2115
2116 try:
2117 _validate_id(pattern_id, "pattern_id")
2118 except ValueError as exc:
2119 if json_out:
2120 print(json.dumps({"error": str(exc)}))
2121 else:
2122 print(f"❌ {exc}", file=sys.stderr)
2123 raise SystemExit(ExitCode.USER_ERROR)
2124
2125 if threshold is not None and not (0.0 <= threshold <= 1.0):
2126 msg = f"--auto-apply-threshold must be between 0.0 and 1.0, got {threshold}"
2127 if json_out:
2128 print(json.dumps({"error": msg}))
2129 else:
2130 print(f"❌ {msg}", file=sys.stderr)
2131 raise SystemExit(ExitCode.USER_ERROR)
2132
2133 root = require_repo()
2134
2135 # Build a ConflictPattern from the stored pattern, or synthesize a
2136 # minimal stub if it does not exist (engine escalates gracefully).
2137 pattern = load_pattern(root, pattern_id)
2138 if pattern is None:
2139 blob_fp = _stub_id(pattern_id)
2140 pattern = ConflictPattern(
2141 pattern_id=pattern_id,
2142 path="<unknown>",
2143 domain="<unknown>",
2144 conflict_type="<unknown>",
2145 blob_fingerprint=blob_fp,
2146 semantic_fingerprint=blob_fp,
2147 ours_id=blob_fp,
2148 theirs_id=blob_fp,
2149 description={},
2150 recorded_at=datetime.datetime.now(datetime.timezone.utc),
2151 recorded_by="engine-cli",
2152 )
2153
2154 cfg_kwargs = {}
2155 if threshold is not None:
2156 cfg_kwargs["auto_apply_threshold"] = threshold
2157 cfg = EngineConfig(**cfg_kwargs)
2158
2159 result = _engine_resolve(root, pattern, cfg)
2160
2161 proposal_json: _HarmonyProposalJson | None = (
2162 _proposal_to_json(result.proposal) if result.proposal is not None else None
2163 )
2164
2165 # Auto-escalate: record an EscalationRecord when the engine escalates.
2166 if auto_escalate and result.status == "escalated":
2167 reason = result.escalation_reason or "Engine escalated — no automatic resolution found"
2168 esc_id = compute_escalation_id(pattern_id, reason)
2169 esc_rec = EscalationRecord(
2170 escalation_id=esc_id,
2171 pattern_id=pattern_id,
2172 reason=reason,
2173 escalated_at=datetime.datetime.now(datetime.timezone.utc),
2174 escalated_by=AgentProvenance.agent("harmony-engine"),
2175 )
2176 record_escalation(root, esc_rec)
2177 append_audit(
2178 root,
2179 AuditEventType.ESCALATION_RECORDED,
2180 AgentProvenance.agent("harmony-engine"),
2181 pattern_id=pattern_id,
2182 metadata={"escalation_id": esc_id},
2183 )
2184
2185 if json_out:
2186 print(json.dumps(_HarmonyEngineJson(
2187 **make_envelope(elapsed),
2188 status=result.status,
2189 pattern_id=result.pattern_id,
2190 proposal=proposal_json,
2191 applied_resolution_id=result.applied_resolution_id,
2192 escalation_reason=result.escalation_reason,
2193 )))
2194 return
2195
2196 status_icon = {"applied": "✅", "proposed": "💡", "escalated": "⚠️"}.get(result.status, "")
2197 print(f" {status_icon} [{result.status.upper()}] pattern {pattern_id}")
2198 if result.proposal is not None:
2199 p = result.proposal
2200 print(f" strategy: {p.strategy}")
2201 print(f" confidence: {p.confidence:.3f}")
2202 print(f" rationale: {sanitize_display(p.rationale)}")
2203 if result.escalation_reason:
2204 print(f" reason: {sanitize_display(result.escalation_reason)}")
2205
2206 def _stub_id(seed: str) -> str:
2207 """Return a deterministic ``sha256:``-prefixed ID derived from seed (for stub patterns)."""
2208 return blob_id(seed.encode())
2209
2210 # ---------------------------------------------------------------------------
2211 # similar
2212 # ---------------------------------------------------------------------------
2213
2214 def run_similar(args: argparse.Namespace) -> None:
2215 """Find conflict patterns semantically similar to the given pattern.
2216
2217 Uses DefaultPlugin (exact fingerprint match) at the CLI level.
2218
2219 JSON schema::
2220
2221 {
2222 "pattern_id": "<hex64>",
2223 "total": <int>,
2224 "proposals": [{<proposal>}, ...]
2225 }
2226
2227 Exit codes:
2228 0 — result returned (proposals may be empty)
2229 1 — invalid pattern_id
2230 2 — not inside a Muse repository
2231 """
2232 elapsed = start_timer()
2233 from muse.core.harmony import load_pattern, ConflictPattern
2234 from muse.core.harmony_engine import EngineConfig, find_similar as _find_similar
2235
2236 pattern_id: str = args.pattern_id
2237 limit: int | None = args.limit
2238 json_out: bool = args.json_out
2239
2240 try:
2241 _validate_id(pattern_id, "pattern_id")
2242 except ValueError as exc:
2243 if json_out:
2244 print(json.dumps({"error": str(exc)}))
2245 else:
2246 print(f"❌ {exc}", file=sys.stderr)
2247 raise SystemExit(ExitCode.USER_ERROR)
2248
2249 root = require_repo()
2250
2251 pattern = load_pattern(root, pattern_id)
2252 if pattern is None:
2253 # Pattern not found — return empty results gracefully.
2254 if json_out:
2255 print(json.dumps(_HarmonySimilarJson(**make_envelope(elapsed), pattern_id=pattern_id, total=0, proposals=[])))
2256 else:
2257 print(f" ℹ pattern {pattern_id} not found — no similar patterns.")
2258 return
2259
2260 cfg_kwargs = {}
2261 if limit is not None:
2262 cfg_kwargs["max_proposals"] = limit
2263 cfg = EngineConfig(**cfg_kwargs)
2264
2265 proposals = _find_similar(root, pattern, config=cfg)
2266 proposal_jsons = [_proposal_to_json(p) for p in proposals]
2267
2268 if json_out:
2269 print(json.dumps(_HarmonySimilarJson(
2270 **make_envelope(elapsed),
2271 pattern_id=pattern_id,
2272 total=len(proposal_jsons),
2273 proposals=proposal_jsons,
2274 )))
2275 return
2276
2277 if not proposal_jsons:
2278 print(f" ℹ no similar patterns found for {pattern_id}.")
2279 return
2280
2281 print(f" Similar patterns for {pattern_id} ({len(proposal_jsons)} proposal(s)):")
2282 for p in proposal_jsons:
2283 sim_str = f"{p['similarity']:.2f}" if p["similarity"] is not None else "n/a"
2284 print(
2285 f" {short_id(p['similar_pattern_id'] or '')} "
2286 f"sim={sim_str} conf={p['confidence']:.3f} {p['strategy']}"
2287 )
2288
2289 # ---------------------------------------------------------------------------
2290 # escalate
2291 # ---------------------------------------------------------------------------
2292
2293 def _escalation_to_entry(rec: EscalationRecord) -> _HarmonyEscalationEntryJson:
2294 return _HarmonyEscalationEntryJson(
2295 escalation_id=rec.escalation_id,
2296 pattern_id=rec.pattern_id,
2297 reason=sanitize_display(rec.reason),
2298 status=rec.status,
2299 escalated_at=rec.escalated_at.isoformat(),
2300 escalated_by=rec.escalated_by.to_dict(),
2301 resolved_at=rec.resolved_at.isoformat() if rec.resolved_at is not None else None,
2302 resolved_by=rec.resolved_by.to_dict() if rec.resolved_by is not None else None,
2303 resolution_id=rec.resolution_id,
2304 )
2305
2306 def run_escalate(args: argparse.Namespace) -> None:
2307 """Record an escalation for a conflict pattern.
2308
2309 Creates an EscalationRecord with status=open. Idempotent — the same
2310 (pattern_id, reason) pair always maps to the same escalation_id.
2311
2312 JSON schema::
2313
2314 {"escalation_id": "<hex64>", "pattern_id": "<hex64>",
2315 "already_existed": <bool>}
2316
2317 Exit codes:
2318 0 — escalation recorded (or already existed)
2319 1 — invalid pattern_id
2320 2 — not inside a Muse repository
2321 """
2322 elapsed = start_timer()
2323 import datetime
2324
2325 pattern_id: str = args.pattern_id
2326 reason: str = args.reason
2327 agent_id: str | None = args.agent_id
2328 json_out: bool = args.json_out
2329
2330 try:
2331 _validate_id(pattern_id, "pattern_id")
2332 except ValueError as exc:
2333 if json_out:
2334 print(json.dumps({"error": str(exc)}))
2335 else:
2336 print(f"❌ {exc}", file=sys.stderr)
2337 raise SystemExit(ExitCode.USER_ERROR)
2338
2339 root = require_repo()
2340
2341 provenance = (
2342 AgentProvenance.agent(agent_id) if agent_id is not None
2343 else AgentProvenance.human()
2344 )
2345
2346 esc_id = compute_escalation_id(pattern_id, reason)
2347 rec = EscalationRecord(
2348 escalation_id=esc_id,
2349 pattern_id=pattern_id,
2350 reason=reason,
2351 escalated_at=datetime.datetime.now(datetime.timezone.utc),
2352 escalated_by=provenance,
2353 )
2354 newly_created = record_escalation(root, rec)
2355 already_existed = not newly_created
2356
2357 if newly_created:
2358 append_audit(
2359 root,
2360 AuditEventType.ESCALATION_RECORDED,
2361 provenance,
2362 pattern_id=pattern_id,
2363 metadata={"escalation_id": esc_id},
2364 )
2365
2366 if json_out:
2367 print(json.dumps(_HarmonyEscalateJson(
2368 **make_envelope(elapsed),
2369 escalation_id=esc_id,
2370 pattern_id=pattern_id,
2371 already_existed=already_existed,
2372 )))
2373 return
2374
2375 if already_existed:
2376 print(f" ℹ escalation {esc_id} already open for pattern {pattern_id}")
2377 else:
2378 print(f" ⚠️ escalated pattern {pattern_id} (esc={esc_id})")
2379
2380 # ---------------------------------------------------------------------------
2381 # escalations
2382 # ---------------------------------------------------------------------------
2383
2384 def run_escalations(args: argparse.Namespace) -> None:
2385 """List conflict pattern escalations.
2386
2387 The operator dashboard: lists all open (or resolved, or all) escalations.
2388
2389 JSON schema::
2390
2391 {"total": <int>, "escalations": [{...}, ...]}
2392
2393 Exit codes:
2394 0 — list returned (may be empty)
2395 2 — not inside a Muse repository
2396 """
2397 elapsed = start_timer()
2398 status_filter: str | None = args.status
2399 json_out: bool = args.json_out
2400
2401 root = require_repo()
2402 records = list_escalations(root, status=status_filter)
2403 entries = [_escalation_to_entry(r) for r in records]
2404
2405 if json_out:
2406 print(json.dumps(_HarmonyEscalationsJson(
2407 **make_envelope(elapsed),
2408 total=len(entries),
2409 escalations=entries,
2410 )))
2411 return
2412
2413 if not entries:
2414 label = f" ({status_filter})" if status_filter else ""
2415 print(f"No harmony escalations{label}.")
2416 return
2417
2418 print(f"{'escalation_id':14} {'status':8} {'pattern_id':14} reason")
2419 print("─" * 76)
2420 for e in entries:
2421 eid_short = short_id(e["escalation_id"])
2422 pid_short = short_id(e["pattern_id"])
2423 print(
2424 f" {eid_short} {e['status']:8} {pid_short} "
2425 f"{e['reason'][:40]}"
2426 )
2427
2428 # ---------------------------------------------------------------------------
2429 # resolve-escalation
2430 # ---------------------------------------------------------------------------
2431
2432 def run_resolve_escalation(args: argparse.Namespace) -> None:
2433 """Close an escalation after a conflict is manually resolved.
2434
2435 JSON schema::
2436
2437 {"escalation_id": "<hex64>", "resolved": <bool>}
2438
2439 Exit codes:
2440 0 — result returned (resolved may be false if escalation not found)
2441 1 — invalid escalation_id or resolution_id
2442 2 — not inside a Muse repository
2443 """
2444 elapsed = start_timer()
2445 import datetime
2446
2447 escalation_id: str = args.escalation_id
2448 resolution_id: str = args.resolution_id
2449 agent_id: str | None = args.agent_id
2450 json_out: bool = args.json_out
2451
2452 for label, value in (("escalation_id", escalation_id), ("resolution_id", resolution_id)):
2453 try:
2454 _validate_id(value, label)
2455 except ValueError as exc:
2456 if json_out:
2457 print(json.dumps({"error": str(exc)}))
2458 else:
2459 print(f"❌ {exc}", file=sys.stderr)
2460 raise SystemExit(ExitCode.USER_ERROR)
2461
2462 root = require_repo()
2463
2464 provenance = (
2465 AgentProvenance.agent(agent_id) if agent_id is not None
2466 else AgentProvenance.human()
2467 )
2468 now = datetime.datetime.now(datetime.timezone.utc)
2469 resolved = resolve_escalation(root, escalation_id, resolution_id, provenance, now)
2470
2471 if resolved:
2472 append_audit(
2473 root,
2474 AuditEventType.ESCALATION_RESOLVED,
2475 provenance,
2476 metadata={
2477 "escalation_id": escalation_id,
2478 "resolution_id": resolution_id,
2479 },
2480 )
2481
2482 if json_out:
2483 print(json.dumps(_HarmonyResolveEscalationJson(
2484 **make_envelope(elapsed),
2485 escalation_id=escalation_id,
2486 resolved=resolved,
2487 )))
2488 return
2489
2490 if resolved:
2491 print(f" ✅ resolved escalation {escalation_id}")
2492 else:
2493 print(f" ℹ escalation {escalation_id} not found.")
File History 2 commits
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 30 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 30 days ago