gabriel / muse public
agent_map.py python
160 lines 6.0 KB
Raw
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago
1 """muse agent-map — show which agents have edited which bars of a MIDI track.
2
3 Walks the commit graph and annotates each bar of the composition with the
4 agent (commit author) that last touched it. The musical equivalent of
5 ``git blame`` at the bar level — essential in a multi-agent swarm to
6 understand who owns what section.
7
8 Usage::
9
10 muse agent-map tracks/melody.mid
11 muse agent-map tracks/bass.mid --depth 20
12 muse agent-map tracks/piano.mid --json
13
14 Output::
15
16 Agent map: tracks/melody.mid
17
18 Bar Last author Commit Message
19 ──────────────────────────────────────────────────────────────
20 1 agent-melody-composer cb4afaed feat: add intro melody
21 2 agent-melody-composer cb4afaed feat: add intro melody
22 3 agent-harmoniser 9f3a12e7 feat: harmonise verse
23 4 agent-harmoniser 9f3a12e7 feat: harmonise verse
24 5 agent-arranger 1b2c3d4e refactor: restructure bridge
25 ...
26 """
27
28 import argparse
29 import json
30 import logging
31 import pathlib
32 import sys
33 from typing import TypedDict
34
35 from muse.core.types import short_id
36 from muse.core.envelope import EnvelopeJson, make_envelope
37 from muse.core.errors import ExitCode
38 from muse.core.repo import require_repo
39 from muse.core.timing import start_timer
40 from muse.core.refs import read_current_branch
41 from muse.core.commits import resolve_commit_ref
42 from muse.core.validation import clamp_int, sanitize_display
43 from muse.plugins.midi._query import (
44 NoteInfo,
45 load_track,
46 notes_by_bar,
47 walk_commits_for_track,
48 )
49
50 logger = logging.getLogger(__name__)
51
52 class BarAttribution(TypedDict):
53 """Attribution record for one bar."""
54
55 bar: int
56 author: str
57 commit_id: str
58 message: str
59
60 class _AgentMapJson(EnvelopeJson):
61 """JSON envelope for ``muse agent-map --json``."""
62
63 mode: str # always "agent-map"
64 track: str # path to the MIDI track
65 attributions: list[BarAttribution]
66
67 def _read_branch(root: pathlib.Path) -> str:
68 return read_current_branch(root)
69
70 def _bar_set(notes: list[NoteInfo]) -> frozenset[int]:
71 return frozenset(notes_by_bar(notes).keys())
72
73 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
74 """Register the agent-map subcommand."""
75 parser = subparsers.add_parser("agent-map", help="Show which agent last edited each bar of a MIDI track.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
76 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
77 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Start walking from this commit (default: HEAD).")
78 parser.add_argument("--depth", "-d", metavar="N", type=int, default=50, help="Maximum commits to walk back (default 50).")
79 parser.add_argument("--json", "-j", action="store_true", dest="as_json", help="Emit results as JSON.")
80 parser.set_defaults(func=run)
81
82 def run(args: argparse.Namespace) -> None:
83 """Show which agent last edited each bar of a MIDI track.
84
85 ``muse agent-map`` walks the commit graph from HEAD (or ``--commit``)
86 backward and annotates each bar with the commit that introduced or last
87 modified it. When multiple agents work on different sections of a
88 composition, this shows the ownership map at a glance.
89
90 Git cannot do this: it has no model of bars or note-level changes.
91 Muse tracks note-level diffs at every commit, enabling per-bar blame.
92 """
93 elapsed = start_timer()
94 track: str = args.track
95 ref: str | None = args.ref
96 depth: int = clamp_int(args.depth, 1, 50, 'depth')
97 as_json: bool = args.as_json
98
99 if depth < 1 or depth > 10_000:
100 print(f"❌ --depth must be between 1 and 10,000 (got {depth}).", file=sys.stderr)
101 raise SystemExit(ExitCode.USER_ERROR)
102 root = require_repo()
103 branch = _read_branch(root)
104
105 start_ref = ref or "HEAD"
106 start_commit = resolve_commit_ref(root, branch, start_ref)
107 if start_commit is None:
108 print(f"❌ Commit '{start_ref}' not found.", file=sys.stderr)
109 raise SystemExit(ExitCode.USER_ERROR)
110
111 history = walk_commits_for_track(root, start_commit.commit_id, track, max_commits=depth)
112
113 # For each bar, find the most recent commit that contains it
114 bar_attr: dict[int, BarAttribution] = {}
115 prev_bars: frozenset[int] = frozenset()
116
117 for commit, manifest in history:
118 if manifest is None or track not in manifest:
119 continue
120 result = load_track(root, commit.commit_id, track)
121 if result is None:
122 continue
123 notes, _tpb = result
124 cur_bars = _bar_set(notes)
125
126 # Bars that appear now but not in the previous (newer) snapshot
127 new_bars = cur_bars - prev_bars if prev_bars else cur_bars
128
129 for bar in new_bars:
130 if bar not in bar_attr:
131 bar_attr[bar] = BarAttribution(
132 bar=bar,
133 author=sanitize_display(commit.author or "unknown"),
134 commit_id=commit.commit_id,
135 message=sanitize_display((commit.message or "").splitlines()[0][:60]),
136 )
137 prev_bars = cur_bars
138
139 if not bar_attr:
140 print(f" (no bar attribution data found for '{track}')")
141 return
142
143 attributions = sorted(bar_attr.values(), key=lambda a: a["bar"])
144
145 if as_json:
146 print(json.dumps(_AgentMapJson(
147 **make_envelope(elapsed),
148 mode="agent-map",
149 track=track,
150 attributions=list(attributions),
151 )))
152 return
153
154 print(f"\nAgent map: {track}\n")
155 print(f" {'Bar':>4} {'Last author':<28} {'Commit':<10} Message")
156 print(f" {'─' * 76}")
157 for attr in attributions:
158 print(
159 f" {attr['bar']:>4} {attr['author']:<28} {short_id(attr['commit_id']):<10} {attr['message']}"
160 )
File History 1 commit
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago