agent_map.py python
155 lines 5.6 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 12 days 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 from __future__ import annotations
29
30 import argparse
31 import json
32 import logging
33 import pathlib
34 import sys
35 from typing import TypedDict
36
37 from muse.core.errors import ExitCode
38 from muse.core.repo import read_repo_id, require_repo
39 from muse.core.store import read_current_branch, resolve_commit_ref
40 from muse.core.validation import clamp_int, sanitize_display
41 from muse.plugins.midi._query import (
42 NoteInfo,
43 load_track,
44 notes_by_bar,
45 walk_commits_for_track,
46 )
47
48 logger = logging.getLogger(__name__)
49
50
51 class BarAttribution(TypedDict):
52 """Attribution record for one bar."""
53
54 bar: int
55 author: str
56 commit_id: str
57 message: str
58
59
60
61 def _read_branch(root: pathlib.Path) -> str:
62 return read_current_branch(root)
63
64
65 def _bar_set(notes: list[NoteInfo]) -> frozenset[int]:
66 return frozenset(notes_by_bar(notes).keys())
67
68
69 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
70 """Register the agent-map subcommand."""
71 parser = subparsers.add_parser("agent-map", help="Show which agent last edited each bar of a MIDI track.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
72 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
73 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Start walking from this commit (default: HEAD).")
74 parser.add_argument("--depth", "-d", metavar="N", type=int, default=50, help="Maximum commits to walk back (default 50).")
75 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
76 parser.set_defaults(func=run)
77
78
79 def run(args: argparse.Namespace) -> None:
80 """Show which agent last edited each bar of a MIDI track.
81
82 ``muse agent-map`` walks the commit graph from HEAD (or ``--commit``)
83 backward and annotates each bar with the commit that introduced or last
84 modified it. When multiple agents work on different sections of a
85 composition, this shows the ownership map at a glance.
86
87 Git cannot do this: it has no model of bars or note-level changes.
88 Muse tracks note-level diffs at every commit, enabling per-bar blame.
89 """
90 track: str = args.track
91 ref: str | None = args.ref
92 depth: int = clamp_int(args.depth, 1, 50, 'depth')
93 as_json: bool = args.as_json
94
95 if depth < 1 or depth > 10_000:
96 print(f"❌ --depth must be between 1 and 10,000 (got {depth}).", file=sys.stderr)
97 raise SystemExit(ExitCode.USER_ERROR)
98 root = require_repo()
99 repo_id = read_repo_id(root)
100 branch = _read_branch(root)
101
102 start_ref = ref or "HEAD"
103 start_commit = resolve_commit_ref(root, repo_id, branch, start_ref)
104 if start_commit is None:
105 print(f"❌ Commit '{start_ref}' not found.", file=sys.stderr)
106 raise SystemExit(ExitCode.USER_ERROR)
107
108 history = walk_commits_for_track(root, start_commit.commit_id, track, max_commits=depth)
109
110 # For each bar, find the most recent commit that contains it
111 bar_attr: dict[int, BarAttribution] = {}
112 prev_bars: frozenset[int] = frozenset()
113
114 for commit, manifest in history:
115 if manifest is None or track not in manifest:
116 continue
117 result = load_track(root, commit.commit_id, track)
118 if result is None:
119 continue
120 notes, _tpb = result
121 cur_bars = _bar_set(notes)
122
123 # Bars that appear now but not in the previous (newer) snapshot
124 new_bars = cur_bars - prev_bars if prev_bars else cur_bars
125
126 for bar in new_bars:
127 if bar not in bar_attr:
128 bar_attr[bar] = BarAttribution(
129 bar=bar,
130 author=sanitize_display(commit.author or "unknown"),
131 commit_id=commit.commit_id[:8],
132 message=sanitize_display((commit.message or "").splitlines()[0][:60]),
133 )
134 prev_bars = cur_bars
135
136 if not bar_attr:
137 print(f" (no bar attribution data found for '{track}')")
138 return
139
140 attributions = sorted(bar_attr.values(), key=lambda a: a["bar"])
141
142 if as_json:
143 print(json.dumps(
144 {"track": track, "start_ref": start_ref, "attributions": list(attributions)},
145 indent=2,
146 ))
147 return
148
149 print(f"\nAgent map: {track}\n")
150 print(f" {'Bar':>4} {'Last author':<28} {'Commit':<10} Message")
151 print(" " + "─" * 76)
152 for attr in attributions:
153 print(
154 f" {attr['bar']:>4} {attr['author']:<28} {attr['commit_id']:<10} {attr['message']}"
155 )
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 12 days ago