cgcardona / muse public
muse-vcs.md markdown
8751 lines 365.8 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 # Muse VCS — Musical Version Control System
2
3 > **Status:** Canonical Implementation Reference
4 > **E2E demo:** [`muse_e2e_demo.md`](muse_e2e_demo.md)
5
6 ---
7
8 ## What Muse Is
9
10 Muse is a persistent, Git-style version control system for musical compositions. It tracks every committed change as a variation in a DAG (directed acyclic graph), enabling:
11
12 - **Commit history** — every accepted variation is recorded with parent lineage
13 - **Branching** — multiple variations can diverge from the same parent
14 - **Three-way merge** — auto-merges non-conflicting changes, reports conflicts
15 - **Drift detection** — compares HEAD snapshot against the live DAW state (`git status`)
16 - **Checkout / time travel** — reconstruct any historical state via deterministic tool calls
17 - **Log graph** — serialize the full commit DAG as Swift-ready JSON
18
19 ---
20
21 ## Why Muse and not Git?
22
23 > *"Can't we just commit MIDI files to Git?"*
24
25 You can. And you'll immediately discover everything Git cannot tell you about music.
26
27 ### The core problem: Git sees music as bytes, not music
28
29 `git diff` on a MIDI file produces binary noise. `git log` tells you "file changed." That's it. Git is a filesystem historian — it records *which bytes* changed, not *what happened musically*.
30
31 Music is **multidimensional** and **happens in time**. A single session commit might simultaneously change the key, the groove, the instrumentation, the dynamic arc, and the emotional character — dimensions that share zero representation in Git's diff model.
32
33 ### What Muse can do that Git categorically cannot
34
35 | Question | Git | Muse |
36 |----------|-----|------|
37 | What key is this arrangement in? | ❌ | ✅ `muse key HEAD` |
38 | How did the chord progression change between commit 12 and commit 47? | ❌ | ✅ `muse diff HEAD~35 HEAD --harmonic` |
39 | When did the song modulate from Eb major to F minor? | ❌ | ✅ `muse find --harmony "key=F minor"` |
40 | Did the groove get tighter or looser over 200 commits? | ❌ | ✅ `muse groove-check HEAD~200 HEAD` |
41 | Find me all versions where the chorus had a string layer | ❌ | ✅ `muse find --structure "has=strings" --structure "section=chorus"` |
42 | Where does the main motif first appear, and how was it transformed? | ❌ | ✅ `muse motif track "main-theme"` |
43 | What was the arrangement before we cut the bridge? | ❌ | ✅ `muse arrange HEAD~10` |
44 | How musically similar are these two alternative mixes? | ❌ | ✅ `muse similarity mix-a mix-b` |
45 | "Find a melancholic minor-key version with sparse texture" | ❌ | ✅ `muse recall "melancholic minor sparse"` |
46 | What is the full musical state of this project for AI generation? | ❌ | ✅ `muse context --json` |
47
48 ### Music is multidimensional — diffs should be too
49
50 When a producer changes a session, five things may change at once:
51
52 - **Harmonic** — a new chord substitution shifts the tension profile
53 - **Rhythmic** — the drummer's part gets slightly more swing
54 - **Structural** — a breakdown section is added before the final chorus
55 - **Dynamic** — the overall level is pushed 6dB louder in the chorus
56 - **Melodic** — the piano melody gets a new phrase in bar 7
57
58 Git records all of this as: *"beat.mid changed."*
59
60 Muse records all of this as five orthogonal dimensions, each independently queryable, diffable, and searchable across the full commit history.
61
62 ### Muse as AI musical memory
63
64 This is where the difference is sharpest. An AI agent generating music needs to answer:
65
66 - What key are we in right now?
67 - What's the established chord progression?
68 - Which sections already have strings? Which don't?
69 - Has the energy been building or falling across the last 10 commits?
70 - What emotional arc are we maintaining?
71
72 `muse context --json` answers all of this in one call — a structured document containing the key, tempo, mode, chord progression, arrangement matrix, dynamic arc, emotional state, and 10-commit evolutionary history. An agent with this context makes musically coherent decisions. An agent without it is generating blind.
73
74 Git provides zero of this. Muse was built because musical creativity is multidimensional, happens in time, and deserves version control that understands music — not just files.
75
76 ---
77
78 ## Module Map
79
80 ### CLI Entry Point
81
82 ```
83 maestro/muse_cli/
84 ├── __init__.py — Package marker
85 ├── app.py — Typer application root (console script: `muse`)
86 ├── errors.py — Exit-code enum (0 success / 1 user / 2 repo / 3 internal) + exceptions
87 │ MuseNotARepoError = RepoNotFoundError (public alias, issue #46)
88 ├── _repo.py — Repository detection (.muse/ directory walker)
89 │ find_repo_root(), require_repo(), require_repo_root alias
90 ├── repo.py — Public re-export of _repo.py (canonical import surface, issue #46)
91 └── commands/
92 ├── __init__.py
93 ├── init.py — muse init ✅ fully implemented (--bare, --template, --default-branch added in issue #85)
94 ├── status.py — muse status ✅ fully implemented (issue #44)
95 ├── commit.py — muse commit ✅ fully implemented (issue #32)
96 ├── log.py — muse log ✅ fully implemented (issue #33)
97 ├── snapshot.py — walk_workdir, hash_file, build_snapshot_manifest, compute IDs,
98 │ diff_workdir_vs_snapshot (added/modified/deleted/untracked sets)
99 ├── models.py — MuseCliCommit, MuseCliSnapshot, MuseCliObject, MuseCliTag (SQLAlchemy)
100 ├── db.py — open_session, upsert/get helpers, get_head_snapshot_manifest, find_commits_by_prefix
101 ├── tag.py — muse tag ✅ add/remove/list/search (issue #123)
102 ├── merge_engine.py — find_merge_base(), diff_snapshots(), detect_conflicts(),
103 │ apply_merge(), read/write_merge_state(), MergeState dataclass
104 ├── checkout.py — muse checkout (stub — issue #34)
105 ├── merge.py — muse merge ✅ fast-forward + 3-way merge (issue #35)
106 ├── remote.py — muse remote (add, remove, rename, set-url, -v)
107 ├── fetch.py — muse fetch
108 ├── push.py — muse push
109 ├── pull.py — muse pull
110 ├── clone.py — muse clone
111 ├── open_cmd.py — muse open ✅ macOS artifact preview (issue #45)
112 ├── play.py — muse play ✅ macOS audio playback via afplay (issue #45)
113 ├── export.py — muse export ✅ snapshot export to MIDI/JSON/MusicXML/ABC/WAV (issue #112)
114 ├── find.py — muse find ✅ search commit history by musical properties (issue #114)
115 └── ask.py — muse ask ✅ natural language query over commit history (issue #126)
116 ```
117
118 `maestro/muse_cli/export_engine.py` — `ExportFormat`, `MuseExportOptions`, `MuseExportResult`,
119 `StorpheusUnavailableError`, `filter_manifest`, `export_snapshot`, and per-format handlers
120 (`export_midi`, `export_json`, `export_musicxml`, `export_abc`, `export_wav`). See
121 `## muse export` section below.
122
123 `maestro/muse_cli/artifact_resolver.py` — `resolve_artifact_async()` / `resolve_artifact()`:
124 resolves a user-supplied path-or-commit-ID to a concrete `pathlib.Path` (see below).
125
126 The CLI delegates to existing `maestro/services/muse_*.py` service modules. Stub subcommands print "not yet implemented" and exit 0.
127
128 ---
129
130 ## `muse tag` — Music-Semantic Tagging
131
132 `muse tag` attaches free-form music-semantic labels to commits, enabling expressive search across
133 the composition history.
134
135 ### Subcommands
136
137 | Command | Description |
138 |---------|-------------|
139 | `muse tag add <tag> [<commit>]` | Attach a tag (defaults to HEAD) |
140 | `muse tag remove <tag> [<commit>]` | Remove a tag (defaults to HEAD) |
141 | `muse tag list [<commit>]` | List all tags on a commit (defaults to HEAD) |
142 | `muse tag search <tag>` | Find commits carrying the tag; use trailing `:` for namespace prefix search |
143
144 ### Tag namespaces
145
146 Tags are free-form strings. Conventional namespace prefixes aid search:
147
148 | Namespace | Example | Meaning |
149 |-----------|---------|---------|
150 | `emotion:` | `emotion:melancholic` | Emotional character |
151 | `stage:` | `stage:rough-mix` | Production stage |
152 | `ref:` | `ref:beatles` | Reference track or source |
153 | `key:` | `key:Am` | Musical key |
154 | `tempo:` | `tempo:120bpm` | Tempo annotation |
155 | *(free-form)* | `lo-fi` | Any other label |
156
157 ### Storage
158
159 Tags are stored in the `muse_cli_tags` table (PostgreSQL):
160
161 ```
162 muse_cli_tags
163 tag_id UUID PK
164 repo_id String(36) — scoped per local repo
165 commit_id String(64) — FK → muse_cli_commits.commit_id (CASCADE DELETE)
166 tag Text
167 created_at DateTime
168 ```
169
170 Tags are scoped to a `repo_id` so independent local repositories use separate tag spaces.
171 A commit can carry multiple tags. Adding the same tag twice is a no-op (idempotent).
172
173 ---
174
175 ## `muse merge` — Fast-Forward and 3-Way Merge
176
177 `muse merge <branch>` integrates another branch into the current branch.
178
179 **Usage:**
180 ```bash
181 muse merge <branch> [OPTIONS]
182 ```
183
184 **Flags:**
185
186 | Flag | Type | Default | Description |
187 |------|------|---------|-------------|
188 | `--no-ff` | flag | off | Force a merge commit even when fast-forward is possible. Preserves branch topology in the history graph. |
189 | `--squash` | flag | off | Collapse all commits from `<branch>` into one new commit on the current branch. The result has a single parent and no `parent2_commit_id` — not a merge commit in the DAG. |
190 | `--strategy TEXT` | string | none | Resolution shortcut: `ours` keeps all files from the current branch; `theirs` takes all files from the target branch. Both skip conflict detection. |
191 | `--continue` | flag | off | Finalize a paused merge after resolving all conflicts with `muse resolve`. |
192
193 ### Algorithm
194
195 1. **Guard** — If `.muse/MERGE_STATE.json` exists, a merge is already in progress. Exit 1 with: *"Merge in progress. Resolve conflicts and run `muse merge --continue`."*
196 2. **Resolve commits** — Read HEAD commit ID for the current branch and the target branch from their `.muse/refs/heads/<branch>` ref files.
197 3. **Find merge base** — BFS over the commit graph to find the LCA (Lowest Common Ancestor) of the two HEAD commits. Both `parent_commit_id` and `parent2_commit_id` are traversed (supporting existing merge commits).
198 4. **Fast-forward** — If `base == ours` *and* `--no-ff` is not set *and* `--squash` is not set, the target is strictly ahead of current HEAD. Move the current branch pointer to `theirs` without creating a new commit.
199 5. **Already up-to-date** — If `base == theirs`, current branch is already ahead. Exit 0.
200 6. **Strategy shortcut** — If `--strategy ours` or `--strategy theirs` is set, apply the resolution immediately before conflict detection and proceed to create a merge commit. No conflict state is written.
201 7. **3-way merge** — When branches have diverged and no strategy is set:
202 - Compute `diff(base → ours)` and `diff(base → theirs)` at file-path granularity.
203 - Detect conflicts: paths changed on *both* sides since the base.
204 - If **no conflicts**: auto-merge (take the changed side for each path), create a merge commit with two parent IDs, advance the branch pointer.
205 - If **conflicts**: write `.muse/MERGE_STATE.json` and exit 1 with a conflict summary.
206 8. **Squash** — If `--squash` is set, create a single commit with a combined tree but only `parent_commit_id` = current HEAD. `parent2_commit_id` is `None`.
207
208 ### `MERGE_STATE.json` Schema
209
210 Written on conflict, read by `muse status` and `muse commit` to block further operations:
211
212 ```json
213 {
214 "base_commit": "abc123...",
215 "ours_commit": "def456...",
216 "theirs_commit": "789abc...",
217 "conflict_paths": ["beat.mid", "lead.mp3"],
218 "other_branch": "feature/experiment"
219 }
220 ```
221
222 All fields except `other_branch` are required. `conflict_paths` is sorted alphabetically.
223
224 ### Merge Commit
225
226 A successful 3-way merge (or `--no-ff` or `--strategy`) creates a commit with:
227 - `parent_commit_id` = `ours_commit_id` (current branch HEAD at merge time)
228 - `parent2_commit_id` = `theirs_commit_id` (target branch HEAD)
229 - `snapshot_id` = merged manifest (non-conflicting changes from both sides)
230 - `message` = `"Merge branch '<branch>' into <current_branch>"` (strategy appended if set)
231
232 ### Squash Commit
233
234 `--squash` creates a commit with:
235 - `parent_commit_id` = `ours_commit_id` (current branch HEAD)
236 - `parent2_commit_id` = `None` — not a merge commit in the graph
237 - `snapshot_id` = same merged manifest as a regular merge would produce
238 - `message` = `"Squash merge branch '<branch>' into <current_branch>"`
239
240 Use squash when you want to land a feature branch as one clean commit without
241 polluting `muse log` with intermediate work-in-progress commits.
242
243 ### Path-Level Granularity (MVP)
244
245 This merge implementation operates at **file-path level**. Two commits that modify the same file path (even if the changes are disjoint within the file) are treated as a conflict. Note-level merging (music-aware diffs inside MIDI files) is a future enhancement reserved for the existing `maestro/services/muse_merge.py` engine.
246
247 ### Agent Use Case
248
249 - **`--no-ff`**: Use when building a structured session history is important (e.g., preserving that a feature branch existed). The branch topology is visible in `muse log --graph`.
250 - **`--squash`**: Use after iterative experimentation on a feature branch to produce one atomic commit for review. Equivalent to "clean up before sharing."
251 - **`--strategy ours`**: Use to quickly resolve a conflict situation where the current branch's version is definitively correct (e.g., a hotfix already applied to main).
252 - **`--strategy theirs`**: Use to accept all incoming changes wholesale (e.g., adopting a new arrangement from a collaborator).
253
254 ---
255
256 ## `.museattributes` — Per-Repo Merge Strategy Configuration
257
258 `.museattributes` is an optional configuration file placed in the repository root (next to `.muse/`). It encodes per-track, per-dimension merge strategy rules so that `muse merge` can skip conflict detection for well-understood cases.
259
260 ### File Format
261
262 ```
263 # one rule per line: <track-pattern> <dimension> <strategy>
264 drums/* * ours
265 keys/* harmonic theirs
266 * * auto
267 ```
268
269 - **`track-pattern`**: `fnmatch` glob against the track name.
270 - **`dimension`**: one of `harmonic`, `rhythmic`, `melodic`, `structural`, `dynamic`, or `*` (all).
271 - **`strategy`**: `ours` | `theirs` | `union` | `auto` | `manual`.
272
273 First matching rule wins. If no rule matches, `auto` is used.
274
275 ### Integration with `muse merge`
276
277 When `build_merge_checkout_plan` is called with a `repo_path`, it loads `.museattributes` automatically and passes the parsed rules to `build_merge_result`. For each region:
278
279 1. The track name is resolved from `track_regions`.
280 2. `resolve_strategy(attributes, track, dimension)` returns the configured strategy.
281 3. `ours` → take the left snapshot, no conflict detection.
282 4. `theirs` → take the right snapshot, no conflict detection.
283 5. All other strategies → normal three-way merge.
284
285 ### CLI
286
287 ```
288 muse attributes [--json]
289 ```
290
291 Displays the parsed rules in a human-readable table or JSON.
292
293 ### Reference
294
295 Full reference: [`docs/reference/museattributes.md`](../reference/museattributes.md)
296
297 ---
298
299 ## Artifact Resolution (`artifact_resolver.py`)
300
301 `resolve_artifact_async(path_or_commit_id, root, session)` resolves a user-supplied
302 string to a concrete `pathlib.Path` in this priority order:
303
304 1. **Direct filesystem path** — if the argument exists on disk, return it as-is.
305 No DB query is needed.
306 2. **Relative to `muse-work/`** — if `<root>/muse-work/<arg>` exists, return that.
307 3. **Commit-ID prefix** — if the argument is 4–64 lowercase hex characters:
308 - Query `muse_cli_commits` for commits whose `commit_id` starts with the prefix.
309 - If exactly one match: load its `MuseCliSnapshot` manifest.
310 - If the snapshot has one file: resolve `<root>/muse-work/<file>`.
311 - If the snapshot has multiple files: prompt the user to select one interactively.
312 - Exit 1 if the prefix is ambiguous (> 1 commit) or the file no longer exists
313 in the working tree.
314
315 ### Why files must still exist in `muse-work/`
316
317 Muse stores **metadata** (file paths → sha256 hashes) in Postgres, not the raw
318 bytes. The actual content lives only on the local filesystem in `muse-work/`.
319 If a user deletes or overwrites a file after committing, the snapshot manifest
320 knows what _was_ there but the bytes are gone. `muse open` / `muse play` will
321 exit 1 with a clear error in that case.
322
323 ---
324
325 ## `muse status` Output Formats
326
327 `muse status` operates in several modes depending on repository state and active flags.
328
329 **Usage:**
330 ```bash
331 muse status [OPTIONS]
332 ```
333
334 **Flags:**
335
336 | Flag | Short | Description |
337 |------|-------|-------------|
338 | `--short` | `-s` | Condensed one-line-per-file output (`M`=modified, `A`=added, `D`=deleted, `?`=untracked) |
339 | `--branch` | `-b` | Emit only the branch and tracking info line |
340 | `--porcelain` | — | Machine-readable `XY path` format, stable for scripting (like `git status --porcelain`) |
341 | `--sections` | — | Group output by first path component under `muse-work/` (musical sections) |
342 | `--tracks` | — | Group output by first path component under `muse-work/` (instrument tracks) |
343
344 Flags are combinable where it makes sense: `--short --sections` emits short-format codes grouped under section headers; `--porcelain --tracks` emits porcelain codes grouped under track headers.
345
346 ### Mode 1 — Clean working tree
347
348 No changes since the last commit:
349
350 ```
351 On branch main
352 nothing to commit, working tree clean
353 ```
354
355 With `--porcelain` (clean): emits only the branch header `## main`.
356
357 ### Mode 2 — Uncommitted changes
358
359 Files have been modified, added, or deleted relative to the last snapshot:
360
361 **Default (verbose):**
362 ```
363 On branch main
364
365 Changes since last commit:
366 (use "muse commit -m <msg>" to record changes)
367
368 modified: beat.mid
369 new file: lead.mp3
370 deleted: scratch.mid
371 ```
372
373 - `modified:` — file exists in both the last snapshot and `muse-work/` but its sha256 hash differs.
374 - `new file:` — file is present in `muse-work/` but absent from the last committed snapshot.
375 - `deleted:` — file was in the last committed snapshot but is no longer present in `muse-work/`.
376
377 **`--short`:**
378 ```
379 On branch main
380 M beat.mid
381 A lead.mp3
382 D scratch.mid
383 ```
384
385 **`--porcelain`:**
386 ```
387 ## main
388 M beat.mid
389 A lead.mp3
390 D scratch.mid
391 ```
392
393 The two-character code column follows the git porcelain convention: first char = index, second = working tree. Since Muse tracks working-tree changes only, the first char is always a space.
394
395 **`--sections` (group by musical section directory):**
396 ```
397 On branch main
398
399 ## chorus
400 modified: chorus/bass.mid
401
402 ## verse
403 modified: verse/bass.mid
404 new file: verse/drums.mid
405 ```
406
407 **`--tracks` (group by instrument track directory):**
408 ```
409 On branch main
410
411 ## bass
412 modified: bass/verse.mid
413
414 ## drums
415 new file: drums/chorus.mid
416 ```
417
418 Files not under a subdirectory appear under `## (root)` when grouping is active.
419
420 **Combined `--short --sections`:**
421 ```
422 On branch main
423 ## chorus
424 M chorus/bass.mid
425
426 ## verse
427 M verse/bass.mid
428 ```
429
430 ### Mode 3 — In-progress merge
431
432 When `.muse/MERGE_STATE.json` exists (written by `muse merge` when conflicts are detected):
433
434 ```
435 On branch main
436
437 You have unmerged paths.
438 (fix conflicts and run "muse commit")
439
440 Unmerged paths:
441 both modified: beat.mid
442 both modified: lead.mp3
443 ```
444
445 Resolve conflicts manually, then `muse commit` to record the merge.
446
447 ### No commits yet
448
449 On a branch that has never been committed to:
450
451 ```
452 On branch main, no commits yet
453
454 Untracked files:
455 (use "muse commit -m <msg>" to record changes)
456
457 beat.mid
458 ```
459
460 If `muse-work/` is empty or missing: `On branch main, no commits yet` (single line).
461
462 ### `--branch` only
463
464 Emits only the branch line regardless of working-tree state:
465
466 ```
467 On branch main
468 ```
469
470 This is useful when a script needs the branch name without triggering a full DB round-trip for the diff.
471
472 ### Agent use case
473
474 An AI music agent uses `muse status` to:
475
476 - **Detect drift:** `muse status --porcelain` gives a stable, parseable list of all changed files before deciding whether to commit.
477 - **Section-aware generation:** `muse status --sections` reveals which musical sections have uncommitted changes, letting the agent focus generation on modified sections only.
478 - **Track inspection:** `muse status --tracks` shows which instrument tracks differ from HEAD, useful when coordinating multi-track edits across agent turns.
479 - **Pre-commit guard:** `muse status --short` gives a compact human-readable summary to include in agent reasoning traces before committing.
480
481 ### Implementation
482
483 | Layer | File | Responsibility |
484 |-------|------|----------------|
485 | Command | `maestro/muse_cli/commands/status.py` | Typer callback + `_status_async` |
486 | Diff engine | `maestro/muse_cli/snapshot.py` | `diff_workdir_vs_snapshot()` |
487 | Merge reader | `maestro/muse_cli/merge_engine.py` | `read_merge_state()` / `MergeState` |
488 | DB helper | `maestro/muse_cli/db.py` | `get_head_snapshot_manifest()` |
489
490 `_status_async` is the injectable async core (tested directly without a running server).
491 Exit codes: 0 success, 2 outside a Muse repo, 3 internal error.
492
493 ---
494
495 ## `muse log` Output Formats
496
497 ### Default (`git log` style)
498
499 ```
500 commit a1b2c3d4e5f6... (HEAD -> main)
501 Parent: f9e8d7c6
502 Date: 2026-02-27 17:30:00
503
504 boom bap demo take 1
505
506 commit f9e8d7c6...
507 Date: 2026-02-27 17:00:00
508
509 initial take
510 ```
511
512 Commits are printed newest-first. The first commit (root) has no `Parent:` line.
513
514 ### `--graph` mode
515
516 Reuses `maestro.services.muse_log_render.render_ascii_graph` by adapting `MuseCliCommit` rows to the `MuseLogGraph`/`MuseLogNode` dataclasses the renderer expects.
517
518 ```
519 * a1b2c3d4 boom bap demo take 1 (HEAD)
520 * f9e8d7c6 initial take
521 ```
522
523 Merge commits (two parents) require `muse merge` (issue #35) — `parent2_commit_id` is reserved for that iteration.
524
525 ### `--oneline` mode
526
527 One line per commit: `<short_id> [HEAD marker] <message>`.
528
529 ```
530 a1b2c3d4 (HEAD -> main) boom bap demo take 1
531 f9e8d7c6 initial take
532 ```
533
534 ### `--stat` mode
535
536 Standard header per commit followed by per-file change lines and a totals summary.
537
538 ```
539 commit a1b2c3d4 (HEAD -> main)
540 Date: 2026-02-27 17:30:00
541
542 boom bap demo take 1
543
544 muse-work/drums/jazz.mid | added
545 muse-work/bass/old.mid | removed
546 2 files changed, 1 added, 1 removed
547 ```
548
549 ### `--patch` / `-p` mode
550
551 Standard header per commit followed by path-level diff blocks showing which files
552 were added, removed, or modified. This is a structural (path-level) diff since
553 Muse tracks MIDI/audio blobs, not line-diffable text.
554
555 ```
556 commit a1b2c3d4 (HEAD -> main)
557 Date: 2026-02-27 17:30:00
558
559 boom bap demo take 1
560
561 --- /dev/null
562 +++ muse-work/drums/jazz.mid
563 --- muse-work/bass/old.mid
564 +++ /dev/null
565 ```
566
567 ### Flags
568
569 | Flag | Default | Description |
570 |------|---------|-------------|
571 | `--limit N` / `-n N` | 1000 | Cap the walk at N commits |
572 | `--graph` | off | ASCII DAG mode |
573 | `--oneline` | off | One line per commit: `<short_id> [HEAD] <message>` |
574 | `--stat` | off | Show file-change statistics per commit |
575 | `--patch` / `-p` | off | Show path-level diff per commit |
576 | `--since DATE` | — | Only commits after DATE (ISO or "2 weeks ago") |
577 | `--until DATE` | — | Only commits before DATE (ISO or "2 weeks ago") |
578 | `--author TEXT` | — | Case-insensitive substring match on author field |
579 | `--emotion TEXT` | — | Filter by `emotion:<TEXT>` tag (e.g. `melancholic`) |
580 | `--section TEXT` | — | Filter by `section:<TEXT>` tag (e.g. `chorus`) |
581 | `--track TEXT` | — | Filter by `track:<TEXT>` tag (e.g. `drums`) |
582
583 All flags are combinable. Filters narrow the commit set; output mode flags control formatting.
584 Priority when multiple output modes specified: `--graph` > `--oneline` > `--stat` > `--patch` > default.
585
586 ### Date parsing
587
588 `--since` and `--until` accept:
589 - ISO dates: `2026-01-15`, `2026-01-15T12:00:00`, `2026-01-15 12:00:00`
590 - Relative: `N days ago`, `N weeks ago`, `N months ago`, `N years ago`, `yesterday`, `today`
591
592 ### Music-native tag filters
593
594 `--emotion`, `--section`, and `--track` filter by tags stored in `muse_cli_tags`.
595 Tags follow the `emotion:<value>`, `section:<value>`, `track:<value>` naming
596 convention. Multiple tag filters are AND-combined — a commit must carry all
597 specified tags to appear in the output.
598
599 **Agent use case:** An agent debugging a melancholic chorus can run
600 `muse log --emotion melancholic --section chorus` to find exactly when that
601 emotional character was committed, then `muse show <commit>` to inspect the
602 snapshot or `muse revert <commit>` to undo it.
603
604 ### Result type
605
606 `parse_date_filter(text: str) -> datetime` — converts a human date string to
607 UTC-aware `datetime`. Raises `ValueError` on unrecognised formats.
608
609 `CommitDiff` — fields: `added: list[str]`, `removed: list[str]`,
610 `changed: list[str]`, `total_files: int` (computed property).
611
612 ---
613
614 ---
615
616 ## `muse arrange [<commit>]` — Arrangement Map (issue #115)
617
618 `muse arrange` displays the **arrangement matrix**: which instruments are active in which musical sections for a given commit. This is the single most useful command for an AI orchestration agent — before generating a new string part, the agent can run `muse arrange --format json HEAD` to see exactly which sections already have strings, preventing doubling mistakes and enabling coherent orchestration decisions.
619
620 ### Path Convention
621
622 Files committed to Muse must follow the three-level path convention to participate in the arrangement map:
623
624 ```
625 muse-work/<section>/<instrument>/<filename>
626 ```
627
628 | Level | Example | Description |
629 |-------|---------|-------------|
630 | `<section>` | `intro`, `verse`, `chorus`, `bridge`, `outro` | Musical section name (normalised to lowercase) |
631 | `<instrument>` | `drums`, `bass`, `strings`, `piano`, `vocals` | Instrument / track name |
632 | `<filename>` | `beat.mid`, `pad.mid` | The actual file |
633
634 Files with fewer than three path components are excluded from the arrangement map (they carry no section metadata).
635
636 Section aliases are normalised: `pre-chorus`, `pre_chorus`, and `prechoruse` all map to `prechorus`.
637
638 ### Output Formats
639
640 **Text (default)**:
641
642 ```
643 Arrangement Map — commit abc1234
644
645 Intro Verse Chorus Bridge Outro
646 drums ████ ████ ████ ████ ████
647 bass ░░░░ ████ ████ ████ ████
648 piano ████ ░░░░ ████ ░░░░ ████
649 strings ░░░░ ░░░░ ████ ████ ░░░░
650 ```
651
652 `████` = active (at least one file for that section/instrument pair).
653 `░░░░` = inactive (no files).
654
655 **JSON (`--format json`)** — structured, AI-agent-consumable:
656
657 ```json
658 {
659 "commit_id": "abc1234...",
660 "sections": ["intro", "verse", "chorus", "bridge", "outro"],
661 "instruments": ["bass", "drums", "piano", "strings"],
662 "arrangement": {
663 "drums": { "intro": true, "verse": true, "chorus": true },
664 "strings": { "intro": false, "verse": false, "chorus": true }
665 }
666 }
667 ```
668
669 **CSV (`--format csv`)** — spreadsheet-ready rows with `0`/`1` cells.
670
671 ## `muse describe` — Structured Musical Change Description
672
673 `muse describe [<commit>] [OPTIONS]` compares a commit against its parent (or two commits via `--compare`) and outputs a structured description of what changed at the snapshot level.
674
675 ### Output example (standard depth)
676
677 ```
678 Commit abc1234: "Add piano melody to verse"
679 Changed files: 2 (beat.mid, keys.mid)
680 Dimensions analyzed: structural (2 files modified)
681 Note: Full harmonic/melodic analysis requires muse harmony and muse motif (planned)
682 ```
683
684 ### Flags
685
686 | Flag | Default | Description |
687 |------|---------|-------------|
688 | `[COMMIT]` | `HEAD` | Target commit: HEAD, branch name, or commit-ID prefix |
689 | `--section TEXT` | none | Show only a specific section's instrumentation |
690 | `--track TEXT` | none | Show only a specific instrument's section participation |
691 | `--compare A --compare B` | — | Diff two arrangements (show added/removed cells) |
692 | `--density` | off | Show byte-size total per cell instead of binary active/inactive |
693 | `--format text\|json\|csv` | `text` | Output format |
694
695 ### Compare Mode (`--compare`)
696
697 ```
698 Arrangement Diff — abc1234 → def5678
699
700 Intro Verse Chorus
701 drums ████ ████ ████
702 strings ░░░░ ░░░░ +████
703 piano ████ ░░░░ -████
704 ```
705
706 `+████` = cell added in commit-b.
707 `-████` = cell removed in commit-b.
708
709 ### Density Mode (`--density`)
710
711 Each cell shows the total byte size of all files for that (section, instrument) pair. Byte size correlates with note density for MIDI files and serves as a useful heuristic for AI orchestration agents:
712
713 ```
714 Intro Verse Chorus
715 drums 4,096 3,200 5,120
716 bass - 1,024 2,048
717 ```
718 | `<commit>` (positional) | HEAD | Commit to describe |
719 | `--compare A B` | — | Compare commit A against commit B explicitly |
720 | `--depth brief\|standard\|verbose` | `standard` | Output verbosity |
721 | `--dimensions TEXT` | — | Comma-separated dimension labels (informational, passed through to output) |
722 | `--json` | off | Output as JSON |
723 | `--auto-tag` | off | Add a heuristic tag based on change scope |
724
725 ### Depth modes
726
727 | Depth | Output |
728 |-------|--------|
729 | `brief` | One-line: `Commit <id>: N file changes` |
730 | `standard` | Message, changed files list, inferred dimensions, LLM note |
731 | `verbose` | Full commit ID, parent ID, per-file M/A/D markers, dimensions |
732
733 ### Implementation
734
735 | Layer | File | Responsibility |
736 |-------|------|----------------|
737 | Service | `maestro/services/muse_arrange.py` | `build_arrangement_matrix()`, diff, renderers |
738 | Command | `maestro/muse_cli/commands/arrange.py` | Typer callback + `_arrange_async` |
739 | App | `maestro/muse_cli/app.py` | Registration under `arrange` subcommand |
740
741 `_arrange_async` is fully injectable for unit tests (accepts a `root: pathlib.Path` and `session: AsyncSession`).
742
743 Exit codes: `0` success, `1` user error (unknown format, missing reference, ambiguous prefix), `2` outside a Muse repo, `3` internal error.
744
745 ### Named Result Types
746
747 See `docs/reference/type_contracts.md`:
748 - `ArrangementCell` — per (section, instrument) data
749 - `ArrangementMatrix` — full matrix for one commit
750 - `ArrangementDiffCell` — change status for one cell
751 - `ArrangementDiff` — full diff between two matrices
752 | Command | `maestro/muse_cli/commands/describe.py` | Typer callback + `_describe_async` |
753 | Diff engine | `maestro/muse_cli/commands/describe.py` | `_diff_manifests()` |
754 | Renderers | `maestro/muse_cli/commands/describe.py` | `_render_brief/standard/verbose/result` |
755 | DB helpers | `maestro/muse_cli/db.py` | `get_commit_snapshot_manifest()` |
756
757 `_describe_async` is the injectable async core (tested directly without a running server). Exit codes: 0 success, 1 user error (bad commit ID or wrong `--compare` count), 2 outside a Muse repo, 3 internal error.
758
759 **Result type:** `DescribeResult` (class) — fields: `commit_id` (str), `message` (str), `depth` (DescribeDepth), `parent_id` (str | None), `compare_commit_id` (str | None), `changed_files` (list[str]), `added_files` (list[str]), `removed_files` (list[str]), `dimensions` (list[str]), `auto_tag` (str | None). Methods: `.file_count()` → int, `.to_dict()` → dict[str, object]. See `docs/reference/type_contracts.md § DescribeResult`.
760
761 **Agent use case:** Before generating new material, an agent calls `muse describe --json` to understand what changed in the most recent commit. If a bass and melody file were both modified, the agent knows a harmonic rewrite occurred and adjusts generation accordingly. `--auto-tag` provides a quick `minor-revision` / `major-revision` signal without full MIDI analysis.
762
763 > **Planned enhancement:** Full harmonic, melodic, and rhythmic analysis (chord progression diffs, motif tracking, groove scoring) is tracked as a follow-up. Current output is purely structural — file-level snapshot diffs with no MIDI parsing.
764
765 ---
766
767 ## `muse export` — Export a Snapshot to External Formats
768
769 `muse export [<commit>] --format <format>` exports a Muse snapshot to a
770 file format usable outside the DAW. This is a **read-only** operation —
771 no commit is created and no DB writes occur. Given the same commit ID and
772 format, the output is always identical (deterministic).
773
774 ### Usage
775
776 ```
777 muse export [<commit>] --format <format> [OPTIONS]
778
779 Arguments:
780 <commit> Short commit ID prefix (default: HEAD).
781
782 Options:
783 --format, -f Target format (required): midi | json | musicxml | abc | wav
784 --output, -o Destination path (default: ./exports/<commit8>.<format>)
785 --track TEXT Export only files whose path contains TEXT (substring match).
786 --section TEXT Export only files whose path contains TEXT (substring match).
787 --split-tracks Write one file per MIDI track (MIDI only).
788 ```
789
790 ### Supported Formats
791
792 | Format | Extension | Description |
793 |------------|-----------|-------------|
794 | `midi` | `.mid` | Copy raw MIDI files from the snapshot (lossless, native). |
795 | `json` | `.json` | Structured JSON index of snapshot files (AI/tooling consumption). |
796 | `musicxml` | `.xml` | MusicXML for notation software (MuseScore, Sibelius, etc.). |
797 | `abc` | `.abc` | ABC notation for folk/traditional music tools. |
798 | `wav` | `.wav` | Audio render via Storpheus (requires Storpheus running). |
799
800 ### Examples
801
802 ```bash
803 # Export HEAD snapshot as MIDI
804 muse export --format midi --output /tmp/my-song.mid
805
806 # Export only the piano track from a specific commit
807 muse export a1b2c3d4 --format midi --track piano
808
809 # Export the chorus section as MusicXML
810 muse export --format musicxml --section chorus
811
812 # Export all tracks as separate MIDI files
813 muse export --format midi --split-tracks
814
815 # Export JSON note structure
816 muse export --format json --output /tmp/snapshot.json
817
818 # WAV render (Storpheus must be running)
819 muse export --format wav
820 ```
821
822 ### Implementation
823
824 | Component | Location |
825 |-----------|----------|
826 | CLI command | `maestro/muse_cli/commands/export.py` |
827 | Format engine | `maestro/muse_cli/export_engine.py` |
828 | Tests | `tests/muse_cli/test_export.py` |
829
830 `export_engine.py` provides:
831
832 - `ExportFormat` — enum of supported formats.
833 - `MuseExportOptions` — frozen dataclass with export settings.
834 - `MuseExportResult` — result dataclass listing written paths.
835 - `StorpheusUnavailableError` — raised when WAV export is attempted
836 but Storpheus is unreachable (callers surface a clean error message).
837 - `filter_manifest()` — applies `--track` / `--section` filters.
838 - `export_snapshot()` — top-level dispatcher.
839 - Format handlers: `export_midi`, `export_json`, `export_musicxml`, `export_abc`, `export_wav`.
840 - MIDI conversion helpers: `_midi_to_musicxml`, `_midi_to_abc` (minimal, best-effort).
841
842 ### WAV Export and Storpheus Dependency
843
844 `--format wav` delegates audio rendering to the Storpheus service
845 (port 10002). Before attempting any conversion, `export_wav` performs
846 a synchronous health check against `GET /health`. If Storpheus is not
847 reachable or returns a non-200 response, `StorpheusUnavailableError` is
848 raised and the CLI exits with a clear human-readable error:
849
850 ```
851 ❌ WAV export requires Storpheus.
852 Storpheus is not reachable at http://localhost:10002: Connection refused
853 Start Storpheus (docker compose up storpheus) and retry.
854 ```
855
856 ### Filter Semantics
857
858 `--track` and `--section` are **case-insensitive substring matches** against
859 the full relative path of each file in the snapshot manifest. Both filters
860 are applied with AND semantics: a file must match all provided filters to be
861 included.
862
863 ```
864 manifest:
865 chorus/piano/take1.mid
866 verse/piano/take1.mid
867 chorus/bass/take1.mid
868
869 --track piano → chorus/piano/take1.mid, verse/piano/take1.mid
870 --section chorus → chorus/piano/take1.mid, chorus/bass/take1.mid
871 --track piano --section chorus → chorus/piano/take1.mid
872 ```
873
874 ### Postgres State
875
876 Export is read-only. It reads `muse_cli_commits` and `muse_cli_snapshots`
877 but writes nothing to the database.
878
879 ---
880
881
882 ## Commit Data Model
883
884 `muse commit` persists three content-addressed table types to Postgres:
885
886 ### `muse_cli_objects` — File blobs (sha256-keyed)
887
888 | Column | Type | Description |
889 |--------|------|-------------|
890 | `object_id` | `String(64)` PK | `sha256(file_bytes)` hex digest |
891 | `size_bytes` | `Integer` | Raw file size |
892 | `created_at` | `DateTime(tz=True)` | Wall-clock insert time |
893
894 Objects are deduplicated across commits: the same file committed on two branches is stored exactly once.
895
896 ### `muse_cli_snapshots` — Snapshot manifests
897
898 | Column | Type | Description |
899 |--------|------|-------------|
900 | `snapshot_id` | `String(64)` PK | `sha256(sorted("path:object_id" pairs))` |
901 | `manifest` | `JSON` | `{rel_path: object_id}` mapping |
902 | `created_at` | `DateTime(tz=True)` | Wall-clock insert time |
903
904 Two identical working trees always produce the same `snapshot_id`.
905
906 ### `muse_cli_commits` — Commit history
907
908 | Column | Type | Description |
909 |--------|------|-------------|
910 | `commit_id` | `String(64)` PK | Deterministic sha256 (see below) |
911 | `repo_id` | `String(36)` | UUID from `.muse/repo.json` |
912 | `branch` | `String(255)` | Branch name at commit time |
913 | `parent_commit_id` | `String(64)` nullable | Previous HEAD commit on branch |
914 | `snapshot_id` | `String(64)` FK | Points to the snapshot row |
915 | `message` | `Text` | User-supplied commit message (may include Co-authored-by trailers) |
916 | `author` | `String(255)` | Reserved (empty for MVP) |
917 | `committed_at` | `DateTime(tz=True)` | Timestamp used in hash derivation |
918 | `created_at` | `DateTime(tz=True)` | Wall-clock DB insert time |
919 | `metadata` | `JSON` nullable | Extensible music-domain annotations (see below) |
920
921 **`metadata` JSON blob — current keys:**
922
923 | Key | Type | Set by |
924 |-----|------|--------|
925 | `section` | `string` | `muse commit --section` |
926 | `track` | `string` | `muse commit --track` |
927 | `emotion` | `string` | `muse commit --emotion` |
928 | `tempo_bpm` | `float` | `muse tempo --set` |
929
930 All keys are optional and co-exist in the same blob. Absent keys are simply not present (not `null`). Future music-domain annotations extend this blob without schema migrations.
931
932 ### ID Derivation (deterministic)
933
934 ```
935 object_id = sha256(file_bytes)
936 snapshot_id = sha256("|".join(sorted(f"{path}:{oid}" for path, oid in manifest.items())))
937 commit_id = sha256(
938 "|".join(sorted(parent_ids))
939 + "|" + snapshot_id
940 + "|" + message
941 + "|" + committed_at.isoformat()
942 )
943 ```
944
945 Given the same working tree state, message, and timestamp two machines produce identical IDs. `sorted()` ensures insertion-order independence for both snapshot manifests and parent lists.
946
947 ---
948
949 ## Local Repository Structure (`.muse/`)
950
951 `muse init` creates the following layout in the current working directory:
952
953 ```
954 .muse/
955 repo.json Repo identity: repo_id (UUID), schema_version, created_at[, bare]
956 HEAD Current branch pointer, e.g. "refs/heads/main"
957 config.toml [core] (bare repos only), [user], [auth], [remotes] configuration
958 objects/ Local content-addressed object store (written by muse commit)
959 <object_id> One file per unique object (sha256 of file bytes)
960 refs/
961 heads/
962 main Commit ID of branch HEAD (empty = no commits yet)
963 <branch> One file per branch
964 muse-work/ Working-tree root (absent for --bare repos)
965 ```
966
967 ### `muse init` flags
968
969 | Flag | Type | Default | Description |
970 |------|------|---------|-------------|
971 | `--bare` | flag | off | Initialise as a bare repository — no `muse-work/` checkout. Writes `bare = true` into `repo.json` and `[core] bare = true` into `config.toml`. Used for Muse Hub remote/server-side repos. |
972 | `--template PATH` | path | — | Copy the contents of *PATH* into `muse-work/` after initialisation. Lets studios pre-populate a standard folder structure (e.g. `drums/`, `bass/`, `keys/`, `vocals/`) for every new project. Ignored when `--bare` is set. |
973 | `--default-branch BRANCH` | text | `main` | Name of the initial branch. Sets `HEAD → refs/heads/<BRANCH>` and creates the matching ref file. |
974 | `--force` | flag | off | Re-initialise even if `.muse/` already exists. Preserves the existing `repo_id` so remote-tracking metadata stays coherent. Does not overwrite `config.toml`. |
975
976 **Bare repository layout** (`--bare`):
977
978 ```
979 .muse/
980 repo.json … bare = true …
981 HEAD refs/heads/<branch>
982 refs/heads/<branch>
983 config.toml [core] bare = true + [user] [auth] [remotes] stubs
984 ```
985
986 Bare repos are used as Muse Hub remotes — objects and refs only, no live working copy.
987
988 **Usage examples:**
989
990 ```bash
991 muse init # standard repo, branch = main
992 muse init --default-branch develop # standard repo, branch = develop
993 muse init --bare # bare repo (Hub remote)
994 muse init --bare --default-branch trunk # bare repo, branch = trunk
995 muse init --template /path/to/studio-tmpl # copy template into muse-work/
996 muse init --template /studio --default-branch release # template + custom branch
997 muse init --force # reinitialise, preserve repo_id
998 ```
999
1000 ### File semantics
1001
1002 | File | Source of truth for | Notes |
1003 |------|-------------------|-------|
1004 | `repo.json` | Repo identity | `repo_id` persists across `--force` reinitialise; `bare = true` written for bare repos |
1005 | `HEAD` | Current branch name | Always `refs/heads/<branch>`; branch name set by `--default-branch` |
1006 | `refs/heads/<branch>` | Branch → commit pointer | Empty string = branch has no commits yet |
1007 | `config.toml` | User identity, auth token, remotes | Not overwritten on `--force`; bare repos include `[core] bare = true` |
1008 | `muse-work/` | Working-tree root | Created by non-bare init; populated from `--template` if provided |
1009
1010 ### Repo-root detection
1011
1012 Every CLI command locates the active repo by walking up the directory tree until `.muse/` is found:
1013
1014 ```python
1015 # Public API — maestro/muse_cli/repo.py (issue #46)
1016 from maestro.muse_cli.repo import find_repo_root, require_repo_root
1017
1018 root: Path | None = find_repo_root() # returns None if not found, never raises
1019 root: Path = require_repo_root() # exits 2 with git-style error if not found
1020 ```
1021
1022 Detection rules (in priority order):
1023
1024 1. If `MUSE_REPO_ROOT` env var is set, use it (useful in tests and scripts — no traversal).
1025 2. Walk from `start` (default `Path.cwd()`) upward until a directory containing `.muse/` is found.
1026 3. If the filesystem root is reached with no `.muse/`, return `None`.
1027
1028 `require_repo_root()` exits 2 with:
1029 ```
1030 fatal: not a muse repository (or any parent up to mount point /)
1031 Run "muse init" to initialize a new repository.
1032 ```
1033
1034 **Import path:** prefer the public `maestro.muse_cli.repo` module for new code; existing commands use `maestro.muse_cli._repo` which is kept for compatibility. Both expose the same functions. `MuseNotARepoError` in `errors.py` is the canonical alias for `RepoNotFoundError`.
1035
1036 ### `config.toml` example
1037
1038 ```toml
1039 [user]
1040 name = "Gabriel"
1041 email = "g@example.com"
1042
1043 [auth]
1044 token = "eyJ..." # Muse Hub Bearer token — keep out of version control
1045
1046 [remotes]
1047 [remotes.origin]
1048 url = "https://story.audio/musehub/repos/abcd1234"
1049 ```
1050
1051 > **Security note:** `.muse/config.toml` contains the Hub auth token. Add `.muse/config.toml` to `.gitignore` (or `.museignore`) to prevent accidental exposure.
1052
1053 ### VCS Services
1054
1055 ```
1056 app/services/
1057 ├── muse_repository.py — Persistence adapter (DB reads/writes)
1058 ├── muse_replay.py — History reconstruction (lineage walking)
1059 ├── muse_drift.py — Drift detection engine (HEAD vs working)
1060 ├── muse_checkout.py — Checkout plan builder (pure data → tool calls)
1061 ├── muse_checkout_executor.py — Checkout execution (applies plan to StateStore)
1062 ├── muse_merge_base.py — Merge base finder (LCA in the DAG)
1063 ├── muse_merge.py — Three-way merge engine
1064 ├── muse_history_controller.py— Orchestrates checkout + merge flows
1065 ├── muse_log_graph.py — DAG serializer (topological sort → JSON)
1066 ├── muse_log_render.py — ASCII graph + JSON + summary renderer
1067 └── variation/
1068 └── note_matching.py — Note + controller event matching/diffing
1069
1070 app/api/routes/
1071 ├── muse.py — Production HTTP routes (5 endpoints)
1072 └── variation/ — Existing variation proposal routes
1073
1074 app/db/
1075 └── muse_models.py — ORM: Variation, Phrase, NoteChange tables
1076
1077 tests/
1078 ├── test_muse_persistence.py — Repository + lineage tests
1079 ├── test_muse_drift.py — Drift detection tests
1080 ├── test_muse_drift_controllers.py — Controller drift tests
1081 ├── test_commit_drift_safety.py — 409 conflict enforcement
1082 ├── test_muse_checkout.py — Checkout plan tests
1083 ├── test_muse_checkout_execution.py — Checkout execution tests
1084 ├── test_muse_merge.py — Merge engine tests
1085 ├── test_muse_log_graph.py — Log graph serialization tests
1086 └── e2e/
1087 ├── muse_fixtures.py — Deterministic IDs + snapshot builders
1088 └── test_muse_e2e_harness.py — Full VCS lifecycle E2E test
1089 ```
1090
1091 ---
1092
1093 ## Data Model
1094
1095 ### Variation (ORM: `app/db/muse_models.py`)
1096
1097 | Column | Type | Purpose |
1098 |--------|------|---------|
1099 | `variation_id` | PK | Unique ID |
1100 | `project_id` | FK | Project this belongs to |
1101 | `parent_variation_id` | FK (self) | Primary parent (lineage) |
1102 | `parent2_variation_id` | FK (self) | Second parent (merge commits only) |
1103 | `is_head` | bool | Whether this is the current HEAD |
1104 | `commit_state_id` | str | State version at commit time |
1105 | `intent` | text | User intent / description |
1106 | `status` | str | `ready` / `committed` / `discarded` |
1107
1108 ### HeadSnapshot (`app/services/muse_replay.py`)
1109
1110 Reconstructed from walking the variation lineage. Contains the cumulative state at any point in history:
1111
1112 | Field | Type | Contents |
1113 |-------|------|----------|
1114 | `notes` | `dict[region_id, list[note_dict]]` | All notes per region |
1115 | `cc` | `dict[region_id, list[cc_event]]` | CC events per region |
1116 | `pitch_bends` | `dict[region_id, list[pb_event]]` | Pitch bends per region |
1117 | `aftertouch` | `dict[region_id, list[at_event]]` | Aftertouch per region |
1118 | `track_regions` | `dict[region_id, track_id]` | Region-to-track mapping |
1119
1120 ---
1121
1122 ## HTTP API
1123
1124 All routes require JWT auth (`Authorization: Bearer <token>`).
1125 Prefix: `/api/v1/muse/`
1126
1127 | Method | Path | Purpose |
1128 |--------|------|---------|
1129 | `POST` | `/muse/variations` | Save a variation directly into history |
1130 | `POST` | `/muse/head` | Set HEAD pointer to a variation |
1131 | `GET` | `/muse/log?project_id=X` | Get the full commit DAG as `MuseLogGraph` JSON |
1132 | `POST` | `/muse/checkout` | Checkout to a variation (time travel) |
1133 | `POST` | `/muse/merge` | Three-way merge of two variations |
1134
1135 ### Response codes
1136
1137 | Code | Meaning |
1138 |------|---------|
1139 | 200 | Success |
1140 | 404 | Variation not found (checkout) |
1141 | 409 | Checkout blocked by drift / merge has conflicts |
1142
1143 ---
1144
1145 ## VCS Primitives
1146
1147 ### Commit (save + set HEAD)
1148
1149 ```
1150 save_variation(session, variation, project_id, parent_variation_id, ...)
1151 set_head(session, variation_id)
1152 ```
1153
1154 ### Lineage
1155
1156 ```
1157 get_lineage(session, variation_id) → [root, ..., target]
1158 get_head(session, project_id) → HistoryNode | None
1159 get_children(session, variation_id) → [HistoryNode, ...]
1160 ```
1161
1162 ### Drift Detection
1163
1164 ```
1165 compute_drift_report(head_snapshot, working_snapshot, ...) → DriftReport
1166 ```
1167
1168 Compares HEAD (from DB) against working state (from StateStore). Severity levels: `CLEAN`, `DIRTY`, `DIVERGED`.
1169
1170 ### Replay / Reconstruction
1171
1172 ```
1173 reconstruct_head_snapshot(session, project_id) → HeadSnapshot
1174 reconstruct_variation_snapshot(session, variation_id) → HeadSnapshot
1175 build_replay_plan(session, project_id, target_id) → ReplayPlan
1176 ```
1177
1178 ### Checkout
1179
1180 ```
1181 build_checkout_plan(target_notes, working_notes, ...) → CheckoutPlan
1182 execute_checkout_plan(plan, store, trace) → CheckoutExecutionResult
1183 checkout_to_variation(session, project_id, target_id, store, ...) → CheckoutSummary
1184 ```
1185
1186 ### Merge
1187
1188 ```
1189 find_merge_base(session, a, b) → str | None
1190 build_merge_result(base, left, right) → MergeResult
1191 merge_variations(session, project_id, left, right, store, ...) → MergeSummary
1192 ```
1193
1194 ### Log Graph
1195
1196 ```
1197 build_muse_log_graph(session, project_id) → MuseLogGraph
1198 ```
1199
1200 Topologically sorted (Kahn's algorithm), deterministic tie-breaking by `(timestamp, variation_id)`. Output is camelCase JSON for the Swift frontend.
1201
1202 ---
1203
1204 ## Architectural Boundaries
1205
1206 17 AST-enforced rules in `scripts/check_boundaries.py`. Key constraints:
1207
1208 | Module | Must NOT import |
1209 |--------|----------------|
1210 | `muse_repository` | StateStore, executor, VariationService |
1211 | `muse_replay` | StateStore, executor, LLM handlers |
1212 | `muse_drift` | StateStore, executor, LLM handlers |
1213 | `muse_checkout` | StateStore, executor, handlers |
1214 | `muse_checkout_executor` | LLM handlers, VariationService |
1215 | `muse_merge`, `muse_merge_base` | StateStore, executor, MCP, handlers |
1216 | `muse_log_graph` | StateStore, executor, handlers, engines |
1217 | `note_matching` | handlers, StateStore |
1218
1219 The boundary philosophy: Muse VCS modules are **pure data** — they consume snapshots and produce plans/reports. StateStore mutation only happens in `muse_checkout_executor` (via duck-typed store parameter) and the history controller.
1220
1221 ---
1222
1223 ## `muse find` — Search Commit History by Musical Properties
1224
1225 `muse find` is the musical grep: it queries the full commit history for the
1226 current repository and returns commits whose messages match musical criteria.
1227 All filter flags combine with **AND logic** — a commit must satisfy every
1228 supplied criterion to appear in results.
1229
1230 ### Command Flags
1231
1232 | Flag | Example | Description |
1233 |------|---------|-------------|
1234 | `--harmony <query>` | `"key=Eb"`, `"mode=minor"` | Harmonic filter |
1235 | `--rhythm <query>` | `"tempo=120-130"`, `"meter=7/8"` | Rhythmic filter |
1236 | `--melody <query>` | `"shape=arch"`, `"motif=main-theme"` | Melodic filter |
1237 | `--structure <query>` | `"has=bridge"`, `"form=AABA"` | Structural filter |
1238 | `--dynamic <query>` | `"avg_vel>80"`, `"arc=crescendo"` | Dynamic filter |
1239 | `--emotion <tag>` | `melancholic` | Emotion tag |
1240 | `--section <text>` | `"chorus"` | Named section filter |
1241 | `--track <text>` | `"bass"` | Track presence filter |
1242 | `--since <date>` | `"2026-01-01"` | Commits after this date (UTC) |
1243 | `--until <date>` | `"2026-03-01"` | Commits before this date (UTC) |
1244 | `--limit N` / `-n N` | `20` (default) | Cap results |
1245 | `--json` | — | Machine-readable JSON output |
1246
1247 ### Query DSL
1248
1249 #### Equality match (default)
1250
1251 All property filters do a **case-insensitive substring match** against the
1252 commit message:
1253
1254 ```
1255 muse find --harmony "key=F minor"
1256 ```
1257
1258 Finds every commit whose message contains the string `key=F minor` (any case).
1259
1260 #### Numeric range match
1261
1262 When the value portion of a `key=value` expression contains two numbers
1263 separated by a hyphen (`low-high`), the filter extracts the numeric value of
1264 the key from the message and checks whether it falls within the range
1265 (inclusive):
1266
1267 ```
1268 muse find --rhythm "tempo=120-130"
1269 ```
1270
1271 Matches commits whose message contains `tempo=<N>` where 120 ≤ N ≤ 130.
1272
1273 ### Output Formats
1274
1275 #### Default (text)
1276
1277 One commit block per match, newest-first:
1278
1279 ```
1280 commit a1b2c3d4...
1281 Branch: main
1282 Parent: f9e8d7c6
1283 Date: 2026-02-27 17:30:00
1284
1285 ambient sketch, key=F minor, tempo=90 bpm
1286
1287 ```
1288
1289 #### `--json` output
1290
1291 A JSON array of commit objects:
1292
1293 ```json
1294 [
1295 {
1296 "commit_id": "a1b2c3d4...",
1297 "branch": "main",
1298 "message": "ambient sketch, key=F minor, tempo=90 bpm",
1299 "author": "",
1300 "committed_at": "2026-02-27T17:30:00+00:00",
1301 "parent_commit_id": "f9e8d7c6...",
1302 "snapshot_id": "bac947cf..."
1303 }
1304 ]
1305 ```
1306
1307 ### Examples
1308
1309 ```bash
1310 # All commits in F minor
1311 muse find --harmony "key=F minor"
1312
1313 # Up-tempo commits in a date window
1314 muse find --rhythm "tempo=120-130" --since "2026-01-01"
1315
1316 # Melancholic commits that include a bridge, as JSON
1317 muse find --emotion melancholic --structure "has=bridge" --json
1318
1319 # Bass track presence, capped at 10 results
1320 muse find --track bass --limit 10
1321 ```
1322
1323 ### Architecture
1324
1325 - **Service:** `maestro/services/muse_find.py`
1326 - `MuseFindQuery` — frozen dataclass of all search criteria
1327 - `MuseFindCommitResult` — a single matching commit
1328 - `MuseFindResults` — container with matches, total scanned, and the query
1329 - `search_commits(session, repo_id, query)` — async search function
1330 - **CLI command:** `maestro/muse_cli/commands/find.py`
1331 - `_find_async(root, session, query, output_json)` — injectable core (tested directly)
1332 - Registered in `maestro/muse_cli/app.py` as `find`
1333
1334 ### Postgres Behaviour
1335
1336 Read-only operation — no writes. Plain-text filters are pushed to SQL via
1337 `ILIKE` for efficiency; numeric range filters are applied in Python after
1338 the SQL result set is fetched. `committed_at` date range filters use SQL
1339 `>=` / `<=` comparisons.
1340
1341 ---
1342
1343 ## `muse session` — Recording Session Metadata
1344
1345 **Purpose:** Track who was in the room, where you recorded, and why — purely as local JSON files. Sessions are decoupled from VCS commits: they capture the human context around a recording block and can later reference commit IDs that were created during that time.
1346
1347 Sessions live in `.muse/sessions/` as plain JSON files — no database tables, no Alembic migrations. This mirrors git's philosophy of storing metadata as plain files rather than in a relational store.
1348
1349 ### Subcommands
1350
1351 | Subcommand | Flags | Purpose |
1352 |------------|-------|---------|
1353 | `muse session start` | `--participants`, `--location`, `--intent` | Open a new session; writes `current.json`. Only one active session at a time. |
1354 | `muse session end` | `--notes` | Finalise active session; moves `current.json` → `<uuid>.json`. |
1355 | `muse session log` | _(none)_ | List all completed sessions, newest first. |
1356 | `muse session show <id>` | _(prefix match supported)_ | Print full JSON for a specific completed session. |
1357 | `muse session credits` | _(none)_ | Aggregate participants across all completed sessions, sorted by count descending. |
1358
1359 ### Storage Layout
1360
1361 ```
1362 .muse/
1363 sessions/
1364 current.json ← active session (exists only while recording)
1365 <session-uuid>.json ← one file per completed session
1366 ```
1367
1368 ### Session JSON Schema (`MuseSessionRecord`)
1369
1370 ```json
1371 {
1372 "session_id": "<uuid4>",
1373 "schema_version": "1",
1374 "started_at": "2026-02-27T15:49:19+00:00",
1375 "ended_at": "2026-02-27T17:30:00+00:00",
1376 "participants": ["Alice", "Bob"],
1377 "location": "Studio A",
1378 "intent": "Record the bridge",
1379 "commits": ["abc123", "def456"],
1380 "notes": "Nailed the third take."
1381 }
1382 ```
1383
1384 The `commits` list is populated externally (e.g., by `muse commit` in a future integration); it starts empty.
1385
1386 ### Output Examples
1387
1388 **`muse session log`**
1389
1390 ```
1391 3f2a1b0c 2026-02-27T15:49:19 → 2026-02-27T17:30:00 [Alice, Bob]
1392 a1b2c3d4 2026-02-26T10:00:00 → 2026-02-26T12:00:00 []
1393 ```
1394
1395 **`muse session credits`**
1396
1397 ```
1398 Session credits:
1399 Alice 2 sessions
1400 Bob 1 session
1401 Carol 1 session
1402 ```
1403
1404 ### Result Type
1405
1406 `MuseSessionRecord` — TypedDict defined in `maestro/muse_cli/commands/session.py`. See `docs/reference/type_contracts.md` for the full field table.
1407
1408 ### Atomicity
1409
1410 `muse session end` writes a temp file (`.tmp-<uuid>.json`) in the same directory, then renames it to `<uuid>.json` before unlinking `current.json`. This guarantees that a crash between write and cleanup never leaves both `current.json` and `<uuid>.json` present simultaneously, which would block future `muse session start` calls.
1411
1412 ### Agent Use Case
1413
1414 An AI composition agent can:
1415 - Call `muse session start --participants "Claude,Gabriel" --intent "Groove track"` before a generation run.
1416 - Call `muse session end --notes "Generated 4 variations"` after the run completes.
1417 - Query `muse session credits` to see which participants have contributed most across the project's history.
1418
1419 ---
1420
1421 ## E2E Demo
1422
1423 Run the full VCS lifecycle test:
1424
1425 ```bash
1426 docker compose exec maestro pytest tests/e2e/test_muse_e2e_harness.py -v -s
1427 ```
1428
1429 Exercises: commit → branch → merge → conflict detection → checkout traversal.
1430 Produces: ASCII graph, JSON dump, summary table. See `muse_e2e_demo.md` for details.
1431
1432 ---
1433
1434 ## Muse Hub — Remote Backend
1435
1436 The Muse Hub is a lightweight GitHub-equivalent that lives inside the Maestro FastAPI app. It provides remote repo hosting for CLI clients using `muse push` and `muse pull`.
1437
1438 ### DB Tables
1439
1440 | Table | Purpose |
1441 |-------|---------|
1442 | `musehub_repos` | Remote repos (name, visibility, owner, music-semantic metadata) |
1443 | `musehub_branches` | Branch pointers inside a repo |
1444 | `musehub_commits` | Commits pushed from CLI clients |
1445 | `musehub_objects` | Binary artifact metadata (MIDI, MP3, WebP piano rolls) |
1446 | `musehub_issues` | Issue tracker entries per repo |
1447 | `musehub_pull_requests` | Pull requests proposing branch merges |
1448 | `musehub_sessions` | Recording session records pushed from CLI clients |
1449 | `musehub_releases` | Published version releases with download package URLs |
1450 | `musehub_stars` | Per-user repo starring (one row per user×repo pair) |
1451
1452 ### Module Map
1453
1454 ```
1455 maestro/
1456 ├── db/musehub_models.py — SQLAlchemy ORM models
1457 ├── models/musehub.py — Pydantic v2 request/response models (incl. SearchCommitMatch, SearchResponse)
1458 ├── services/musehub_repository.py — Async DB queries for repos/branches/commits
1459 ├── services/musehub_credits.py — Credits aggregation from commit history
1460 ├── services/musehub_issues.py — Async DB queries for issues (single point of DB access)
1461 ├── services/musehub_pull_requests.py — Async DB queries for PRs (single point of DB access)
1462 ├── services/musehub_releases.py — Async DB queries for releases (single point of DB access)
1463 ├── services/musehub_release_packager.py — Download package URL builder (pure, no DB access)
1464 ├── services/musehub_search.py — In-repo search service (property / ask / keyword / pattern)
1465 ├── services/musehub_sync.py — Push/pull sync protocol (ingest_push, compute_pull_delta)
1466 └── api/routes/musehub/
1467 ├── __init__.py — Composes sub-routers under /musehub prefix
1468 ├── repos.py — Repo/branch/commit/credits route handlers
1469 ├── issues.py — Issue tracking route handlers
1470 ├── pull_requests.py — Pull request route handlers
1471 ├── releases.py — Release management route handlers
1472 ├── search.py — In-repo search route handler
1473 ├── sync.py — Push/pull sync route handlers
1474 ├── objects.py — Artifact list + content-by-object-id endpoints (auth required)
1475 ├── raw.py — Raw file download by path (public repos: no auth)
1476 └── ui.py — HTML UI pages (incl. releases, credits and /search pages)
1477 ├── db/musehub_models.py — SQLAlchemy ORM models (includes MusehubStar)
1478 ├── models/musehub.py — Pydantic v2 request/response models (includes ExploreRepoResult, ExploreResponse, StarResponse, SearchCommitMatch, SearchResponse)
1479 ├── services/musehub_repository.py — Async DB queries for repos/branches/commits
1480 ├── services/musehub_discover.py — Public repo discovery with filters, sorting, star/unstar
1481 ├── services/musehub_credits.py — Credits aggregation from commit history
1482 ├── services/musehub_issues.py — Async DB queries for issues (single point of DB access)
1483 ├── services/musehub_pull_requests.py — Async DB queries for PRs (single point of DB access)
1484 ├── services/musehub_sessions.py — Async DB queries for sessions (upsert, list, get)
1485 ├── services/musehub_search.py — In-repo search service (property / ask / keyword / pattern)
1486 ├── services/musehub_sync.py — Push/pull sync protocol (ingest_push, compute_pull_delta)
1487 ├── services/musehub_divergence.py — Five-dimension divergence between two remote branches
1488 └── api/routes/musehub/
1489 ├── __init__.py — Composes sub-routers under /musehub prefix (authed)
1490 ├── repos.py — Repo/branch/commit route handlers
1491 ├── __init__.py — Composes sub-routers under /musehub prefix
1492 ├── repos.py — Repo/branch/commit/session route handlers
1493 ├── repos.py — Repo/branch/commit route handlers + divergence endpoint
1494 ├── repos.py — Repo/branch/commit/credits route handlers
1495 ├── issues.py — Issue tracking route handlers
1496 ├── pull_requests.py — Pull request route handlers
1497 ├── search.py — In-repo search route handler
1498 ├── sync.py — Push/pull sync route handlers
1499 ├── discover.py — Public discover API + authed star/unstar (registered in main.py separately)
1500 ├── objects.py — Artifact list + content-by-object-id endpoints (auth required)
1501 ├── raw.py — Raw file download by path (public repos: no auth)
1502 └── ui.py — Browser UI HTML shell pages (repo, commits, PRs, issues, sessions, search)
1503 └── ui.py — HTML UI pages (divergence radar chart, search mode tabs)
1504 └── ui.py — HTML shells for browser: explore, trending, repo, commit, PR, issue pages (incl. /search page with mode tabs)
1505 ├── negotiate.py — Content negotiation helper (HTML vs JSON from one URL)
1506 └── ui.py — HTML UI pages (incl. credits and /search pages)
1507 ```
1508
1509 ### Content Negotiation — Dual-Format Endpoints
1510
1511 Key MuseHub UI routes implement **content negotiation**: the same URL serves HTML
1512 to browsers and JSON to agents, decided by the `Accept` header (or `?format=json`).
1513
1514 **Why this exists:** The Stori DAW philosophy is agent-first. An AI agent composing
1515 music should call `GET /{owner}/{repo_slug}` and receive structured JSON — not
1516 navigate a parallel `/api/v1/...` endpoint tree that requires separate maintenance.
1517
1518 **Mechanism (`negotiate.py`):**
1519
1520 ```python
1521 # Decision order (first match wins):
1522 # 1. ?format=json → JSON (explicit override, works in browser <a> links)
1523 # 2. Accept: application/json → JSON (standard HTTP content negotiation)
1524 # 3. default → text/html
1525 ```
1526
1527 JSON uses `CamelModel.model_dump(by_alias=True)` — camelCase keys matching the
1528 `/api/v1/musehub/...` convention. No schema divergence.
1529
1530 **Current dual-format endpoints:**
1531
1532 | URL | JSON response model |
1533 |-----|---------------------|
1534 | `GET /musehub/ui/{owner}/{repo_slug}` | `RepoResponse` |
1535 | `GET /musehub/ui/{owner}/{repo_slug}/commits` | `CommitListResponse` |
1536 | `GET /musehub/ui/{owner}/{repo_slug}/commits/{commit_id}` | `CommitResponse` |
1537
1538 All other UI endpoints still return HTML only. As new pages are added, adopt
1539 `negotiate_response()` immediately so agents automatically get JSON support.
1540
1541 ### Endpoints
1542
1543 #### Repos, Branches, Commits, Credits
1544
1545 | Method | Path | Description |
1546 |--------|------|-------------|
1547 | POST | `/api/v1/musehub/repos` | Create remote repo |
1548 | GET | `/api/v1/musehub/repos/{id}` | Get repo metadata |
1549 | GET | `/api/v1/musehub/repos/{id}/branches` | List branches |
1550 | GET | `/api/v1/musehub/repos/{id}/commits` | List commits (newest first) |
1551 | GET | `/api/v1/musehub/repos/{id}/timeline` | Chronological timeline with emotion/section/track layers |
1552 | GET | `/api/v1/musehub/repos/{id}/divergence` | Five-dimension musical divergence between two branches (`?branch_a=...&branch_b=...`) |
1553 | GET | `/api/v1/musehub/repos/{id}/credits` | Aggregated contributor credits (`?sort=count\|recency\|alpha`) |
1554
1555 #### Credits Page
1556
1557 `GET /api/v1/musehub/repos/{repo_id}/credits` returns a `CreditsResponse` — the full contributor roll aggregated from commit history, analogous to dynamic album liner notes.
1558
1559 **Sort options:**
1560
1561 | `sort` value | Ordering |
1562 |---|---|
1563 | `count` (default) | Most prolific contributor first |
1564 | `recency` | Most recently active contributor first |
1565 | `alpha` | Alphabetical by author name |
1566
1567 **Result type:** `CreditsResponse` — fields: `repo_id`, `contributors` (list of `ContributorCredits`), `sort`, `total_contributors`.
1568
1569 **`ContributorCredits` fields:**
1570
1571 | Field | Type | Description |
1572 |---|---|---|
1573 | `author` | `str` | Contributor name (from commit `author` field) |
1574 | `session_count` | `int` | Number of commits attributed to this author |
1575 | `contribution_types` | `list[str]` | Inferred roles: composer, arranger, producer, performer, mixer, editor, lyricist, sound designer |
1576 | `first_active` | `datetime` | Timestamp of earliest commit |
1577 | `last_active` | `datetime` | Timestamp of most recent commit |
1578
1579 **Contribution type inference:** Roles are inferred from commit message keywords using `_ROLE_KEYWORDS` in `musehub_credits.py`. No role matched → falls back to `["contributor"]`. The list evolves as musicians describe their work more richly in commit messages.
1580
1581 **Machine-readable credits:** The UI page (`GET /musehub/ui/{owner}/{repo_slug}/credits`) injects a `<script type="application/ld+json">` block using schema.org `MusicComposition` vocabulary for embeddable, machine-readable attribution.
1582
1583 **Agent use case:** An AI agent generating release notes or liner notes calls `GET /api/v1/musehub/repos/{id}/credits?sort=count` to enumerate all contributors and their roles, then formats the result as attribution text. The JSON-LD block is ready for schema.org consumers (streaming platforms, metadata aggregators).
1584 | GET | `/api/v1/musehub/repos/{id}/dag` | Full commit DAG (topologically sorted nodes + edges) |
1585 | GET | `/api/v1/musehub/repos/{id}/context/{ref}` | Musical context document for a commit (JSON) |
1586
1587 #### DAG Graph Page (UI)
1588
1589 | Method | Path | Description |
1590 |--------|------|-------------|
1591 | GET | `/musehub/ui/{id}/graph` | Interactive SVG commit graph (no auth required — HTML shell) |
1592
1593 #### Context Viewer
1594
1595 The context viewer exposes what the AI agent sees when generating music for a given commit.
1596
1597 **API endpoint:** `GET /api/v1/musehub/repos/{repo_id}/context/{ref}` — requires JWT auth.
1598 Returns a `MuseHubContextResponse` document with:
1599 - `musical_state` — active tracks derived from stored artifact paths; musical dimensions (key, tempo, etc.) are `null` until Storpheus MIDI analysis is integrated.
1600 - `history` — up to 5 ancestor commits (newest-first), built by walking `parent_ids`.
1601 - `missing_elements` — list of dimensions the agent cannot determine from stored data.
1602 - `suggestions` — composer-facing hints about what to work on next.
1603
1604 **UI page:** `GET /musehub/ui/{owner}/{repo_slug}/context/{ref}` — no auth required (JS shell handles auth).
1605 Renders the context document in structured HTML with:
1606 - "What the Agent Sees" explainer at the top
1607 - Collapsible sections for Musical State, History, Missing Elements, and Suggestions
1608 - Raw JSON panel with a Copy-to-Clipboard button for pasting context into agent prompts
1609 - Breadcrumb navigation back to the repo page
1610
1611 **Service:** `maestro/services/musehub_repository.py::get_context_for_commit()` — read-only, deterministic.
1612
1613 **Agent use case:** A musician debugging why the AI generated something unexpected can load the context page for that commit and see exactly what musical knowledge the agent had. The copy button lets them paste the raw JSON into a new agent conversation for direct inspection or override.
1614
1615 #### Listen Page — Full-Mix and Per-Track Audio Playback
1616
1617 The listen page gives musicians a dedicated listening experience without requiring them to export files to a DAW.
1618
1619 | Method | Path | Description |
1620 |--------|------|-------------|
1621 | GET | `/musehub/ui/{owner}/{repo_slug}/listen/{ref}` | Full-mix player + per-track listing (HTML; no auth required) |
1622 | GET | `/musehub/ui/{owner}/{repo_slug}/listen/{ref}/{path}` | Single-stem player focused on one artifact (HTML; no auth required) |
1623 | GET | `/api/v1/musehub/repos/{repo_id}/listen/{ref}/tracks` | `TrackListingResponse` JSON (optional JWT) |
1624 | GET | `/musehub/ui/{owner}/{repo_slug}/listen/{ref}?format=json` | Same `TrackListingResponse` via content negotiation |
1625
1626 **Page sections:**
1627
1628 1. **Full-mix player** — a top-of-page audio player pointing to the first file whose basename contains a mix/master keyword (`mix`, `full`, `master`, `bounce`); falls back to the first audio artifact when no such file exists.
1629 2. **Track listing** — every `.mp3`, `.ogg`, `.wav`, `.m4a`, or `.flac` artifact for the repo, sorted by path. Each row shows: instrument name, path, mini waveform visualisation, file size, play button, optional piano-roll link (if a matching `.webp` image exists), and download button.
1630 3. **No-renders fallback** — when no audio artifacts exist the page renders a friendly call-to-action with a link to the file-tree browser.
1631
1632 **Content negotiation:** `?format=json` or `Accept: application/json` returns a `TrackListingResponse` with `repoId`, `ref`, `fullMixUrl`, `tracks`, and `hasRenders`.
1633
1634 **Piano-roll matching:** if an `.webp` (or `.png`/`.jpg`) file shares the same basename as an audio file (e.g. `tracks/bass.webp` matches `tracks/bass.mp3`), the listen page links to it as a piano-roll image.
1635
1636 **Audio state management:** the client-side JS keeps a single `<Audio>` instance per track; playing one track pauses all others and the full-mix player, so musicians can solo stems without browser audio conflicts.
1637
1638 **Agent use case:** `GET .../listen/{ref}?format=json` returns a machine-readable track listing with audio URLs, letting AI agents enumerate stems and report on the arrangement without rendering HTML.
1639
1640 **Backend model:** `TrackListingResponse` in `maestro/models/musehub.py`; registered in `docs/reference/type_contracts.md`.
1641
1642 #### Analysis Dashboard
1643
1644 The analysis dashboard provides a single-page overview of all musical dimensions for a given ref.
1645
1646 | Method | Path | Description |
1647 |--------|------|-------------|
1648 | GET | `/musehub/ui/{owner}/{repo_slug}/analysis/{ref}` | HTML dashboard with 10 dimension summary cards (no auth required) |
1649 | GET | `/api/v1/musehub/repos/{repo_id}/analysis/{ref}` | Aggregate analysis JSON with all 13 dimensions (JWT required) |
1650
1651 **Dashboard cards (10 dimensions):** Key, Tempo, Meter, Chord Map, Dynamics, Groove, Emotion, Form, Motifs, Contour.
1652
1653 Each card shows:
1654 - A headline metric derived from the dimension's stub data (e.g. "C Major", "120 BPM")
1655 - A sub-text with confidence or range context
1656 - A mini sparkline bar chart for time-series dimensions (dynamics velocity curve, contour pitch curve)
1657 - A clickable link to the per-dimension analysis page at `/{owner}/{repo_slug}/analysis/{ref}/{dim_id}`
1658
1659 **Branch/tag selector:** A `<select>` populated by `GET /api/v1/musehub/repos/{repo_id}/branches`. Changing the selected branch navigates to `/{owner}/{repo_slug}/analysis/{branch}`.
1660
1661 **Missing data:** When a dimension has no analysis data, the card displays "Not yet analyzed" gracefully — no errors or empty states break the layout.
1662
1663 **Content negotiation (API):** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}` returns `AggregateAnalysisResponse` JSON with a `dimensions` array. All 13 dimensions are present (including similarity and divergence, which are not shown as cards but are available to agents).
1664
1665 **Auth model:** The HTML page at `/musehub/ui/{owner}/{repo_slug}/analysis/{ref}` is a Jinja2 template shell — no server-side auth required. The embedded JavaScript fetches the API with a JWT from `localStorage`.
1666
1667 **Agent use case:** An AI agent assessing the current musical state of a repo calls `GET /api/v1/musehub/repos/{id}/analysis/{ref}` and reads the `dimensions` array to understand key, tempo, emotion, and harmonic complexity before proposing a new composition direction.
1668
1669 **Files:**
1670 | Layer | File |
1671 |-------|------|
1672 | UI handler | `maestro/api/routes/musehub/ui.py::analysis_dashboard_page()` |
1673 | UI template | `maestro/templates/musehub/pages/analysis.html` |
1674 | API handler | `maestro/api/routes/musehub/analysis.py::get_aggregate_analysis()` |
1675 | Service | `maestro/services/musehub_analysis.py::compute_aggregate_analysis()` |
1676 | Models | `maestro/models/musehub_analysis.py::AggregateAnalysisResponse` |
1677 | UI tests | `tests/test_musehub_ui.py` — `test_analysis_dashboard_*`, `test_analysis_aggregate_endpoint` |
1678
1679 #### Issues
1680
1681 | Method | Path | Description |
1682 |--------|------|-------------|
1683 | POST | `/api/v1/musehub/repos/{id}/issues` | Open a new issue (`state: open`) |
1684 | GET | `/api/v1/musehub/repos/{id}/issues` | List issues (`?state=open\|closed\|all`, `?label=<string>`) |
1685 | GET | `/api/v1/musehub/repos/{id}/issues/{number}` | Get a single issue by per-repo number |
1686 | POST | `/api/v1/musehub/repos/{id}/issues/{number}/close` | Close an issue |
1687
1688 #### Pull Requests
1689
1690 | Method | Path | Description |
1691 |--------|------|-------------|
1692 | POST | `/api/v1/musehub/repos/{id}/pull-requests` | Open a PR proposing to merge `from_branch` into `to_branch` |
1693 | GET | `/api/v1/musehub/repos/{id}/pull-requests` | List PRs (`?state=open\|merged\|closed\|all`) |
1694 | GET | `/api/v1/musehub/repos/{id}/pull-requests/{pr_id}` | Get a single PR by ID |
1695 | POST | `/api/v1/musehub/repos/{id}/pull-requests/{pr_id}/merge` | Merge an open PR |
1696
1697 #### Sessions
1698
1699 | Method | Path | Description |
1700 |--------|------|-------------|
1701 | POST | `/api/v1/musehub/repos/{id}/sessions` | Push a session record (upsert — idempotent) |
1702 | GET | `/api/v1/musehub/repos/{id}/sessions` | List sessions, newest first (`?limit=N`, default 50, max 200) |
1703 | GET | `/api/v1/musehub/repos/{id}/sessions/{session_id}` | Get a single session by UUID |
1704
1705 #### In-Repo Search
1706
1707 | Method | Path | Description |
1708 |--------|------|-------------|
1709 | GET | `/api/v1/musehub/repos/{id}/search` | Search commits by mode (property / ask / keyword / pattern) |
1710 | GET | `/musehub/ui/{id}/search` | HTML search page with four mode tabs (no auth required) |
1711
1712 **Search query parameters:**
1713
1714 | Param | Type | Description |
1715 |-------|------|-------------|
1716 | `mode` | string | `property` \| `ask` \| `keyword` \| `pattern` (default: `keyword`) |
1717 | `q` | string | Query string — interpreted differently per mode |
1718 | `harmony` | string | [property mode] Harmony filter (e.g. `key=Eb` or `key=C-Eb` range) |
1719 | `rhythm` | string | [property mode] Rhythm filter (e.g. `tempo=120-130`) |
1720 | `melody` | string | [property mode] Melody filter |
1721 | `structure` | string | [property mode] Structure filter |
1722 | `dynamic` | string | [property mode] Dynamics filter |
1723 | `emotion` | string | [property mode] Emotion filter |
1724 | `since` | ISO datetime | Only include commits on or after this datetime |
1725 | `until` | ISO datetime | Only include commits on or before this datetime |
1726 | `limit` | int | Max results (1–200, default 20) |
1727
1728 **Result type:** `SearchResponse` — fields: `mode`, `query`, `matches`, `totalScanned`, `limit`.
1729 Each match is a `SearchCommitMatch` with: `commitId`, `branch`, `message`, `author`, `timestamp`, `score`, `matchSource`.
1730
1731 **Search modes explained:**
1732
1733 - **`property`** — delegates to `muse_find.search_commits()`. All non-empty property filters are ANDed. Accepts `key=low-high` range syntax for numeric properties. Score is always 1.0 (exact filter match). `matchSource = "property"`.
1734
1735 - **`ask`** — natural-language query. Stop-words are stripped from `q`; remaining tokens are scored against each commit message using the overlap coefficient (`|Q ∩ M| / |Q|`). Commits with zero overlap are excluded. This is a keyword-matching stub; LLM-powered answer generation is a planned enhancement.
1736
1737 - **`keyword`** — raw keyword overlap scoring. Tokenises `q` and each commit message; scores by `|Q ∩ M| / |Q|`. Commits with zero overlap excluded. Useful for precise term search.
1738
1739 - **`pattern`** — case-insensitive substring match of `q` against commit messages (primary) and branch names (secondary). Score is always 1.0. `matchSource` is `"message"` or `"branch"`.
1740
1741 **Agent use case:** AI music composition agents can call the search endpoint to locate commits by musical property (e.g. find all commits with `harmony=Fm`) before applying `muse checkout`, `muse diff`, or `muse replay` to reconstruct or compare those versions.
1742
1743 #### Releases
1744
1745 | Method | Path | Description |
1746 |--------|------|-------------|
1747 | POST | `/api/v1/musehub/repos/{id}/releases` | Create a release tied to a tag and optional commit |
1748 | GET | `/api/v1/musehub/repos/{id}/releases` | List all releases (newest first) |
1749 | GET | `/api/v1/musehub/repos/{id}/releases/{tag}` | Get a single release by tag (e.g. `v1.0`) |
1750
1751 #### Sync Protocol
1752
1753 | Method | Path | Description |
1754 |--------|------|-------------|
1755 | POST | `/api/v1/musehub/repos/{id}/push` | Upload commits and objects (fast-forward enforced) |
1756 | POST | `/api/v1/musehub/repos/{id}/pull` | Fetch missing commits and objects |
1757
1758 #### Explore / Discover (public — no auth required for browse)
1759
1760 | Method | Path | Description |
1761 |--------|------|-------------|
1762 | GET | `/api/v1/musehub/discover/repos` | List public repos with optional filters and sort |
1763 | POST | `/api/v1/musehub/repos/{id}/star` | Star a public repo (idempotent add, auth required) |
1764 | DELETE | `/api/v1/musehub/repos/{id}/star` | Unstar a repo (idempotent remove, auth required) |
1765 | GET | `/api/v1/musehub/repos/{id}/stargazers` | List users who starred the repo (public repos unauthenticated) |
1766 | POST | `/api/v1/musehub/repos/{id}/fork` | Fork a public repo under the caller's account (auth required) |
1767 | GET | `/api/v1/musehub/repos/{id}/forks` | List all forks of this repo |
1768
1769 **Filter parameters for `GET /discover/repos`:**
1770
1771 | Parameter | Type | Description |
1772 |-----------|------|-------------|
1773 | `genre` | string | Substring match on tags (e.g. `jazz`, `lo-fi`) |
1774 | `key` | string | Exact match on `key_signature` (e.g. `F# minor`) |
1775 | `tempo_min` | int | Minimum BPM (inclusive) |
1776 | `tempo_max` | int | Maximum BPM (inclusive) |
1777 | `instrumentation` | string | Substring match on tags for instrument presence |
1778 | `sort` | string | `stars` \| `activity` \| `commits` \| `created` (default) |
1779 | `page` | int | 1-based page number |
1780 | `page_size` | int | Results per page (default 24, max 100) |
1781
1782 **Result type:** `ExploreResponse` — fields: `repos: list[ExploreRepoResult]`, `total: int`, `page: int`, `page_size: int`
1783
1784 **ExploreRepoResult fields:** `repo_id`, `name`, `owner_user_id`, `description`, `tags`, `key_signature`, `tempo_bpm`, `star_count`, `commit_count`, `created_at`
1785
1786 **UI pages (no auth required):**
1787
1788 | Path | Description |
1789 |------|-------------|
1790 | `GET /musehub/ui/explore` | Filterable grid of all public repos (newest first) |
1791 | `GET /musehub/ui/trending` | Public repos sorted by star count |
1792
1793 #### Raw File Download
1794
1795 | Method | Path | Description |
1796 |--------|------|-------------|
1797 | GET | `/api/v1/musehub/repos/{id}/raw/{ref}/{path}` | Download file by path with correct MIME type |
1798
1799 The raw endpoint is designed for `curl`, `wget`, and scripted pipelines. It
1800 serves files with `Accept-Ranges: bytes` so audio clients can perform range
1801 requests for partial playback.
1802
1803 **Auth:** No token required for **public** repos. Private repos return 401
1804 without a valid Bearer token.
1805
1806 ```bash
1807 # Public repo — no auth needed
1808 curl https://musehub.stori.com/api/v1/musehub/repos/<repo_id>/raw/main/tracks/bass.mid \
1809 -o bass.mid
1810
1811 # Private repo — Bearer token required
1812 curl -H "Authorization: Bearer <token>" \
1813 https://musehub.stori.com/api/v1/musehub/repos/<repo_id>/raw/main/mix/final.mp3 \
1814 -o final.mp3
1815 ```
1816
1817 See [api.md](../reference/api.md#get-apiv1musehub-reposrepo_idrawrefpath) for the
1818 full MIME type table and error reference.
1819
1820 All authed endpoints require `Authorization: Bearer <token>`. See [api.md](../reference/api.md#muse-hub-api) for full field docs.
1821
1822 #### Web UI (no auth required)
1823
1824 | Method | Path | Description |
1825 |--------|------|-------------|
1826 | GET | `/musehub/ui/{owner}/{repo_slug}` | Repo home page (arrangement matrix, audio player, stats bar, recent commits) |
1827 | GET | `/musehub/ui/{owner}/{repo_slug}/commits/{commit_id}` | Commit detail (metadata + artifact browser) |
1828 | GET | `/musehub/ui/{owner}/{repo_slug}/pulls` | Pull request list |
1829 | GET | `/musehub/ui/{owner}/{repo_slug}/pulls/{pr_id}` | PR detail (with merge button) |
1830 | GET | `/musehub/ui/{owner}/{repo_slug}/issues` | Issue list |
1831 | GET | `/musehub/ui/{owner}/{repo_slug}/issues/{number}` | Issue detail (with close button) |
1832 | GET | `/musehub/ui/{owner}/{repo_slug}/branches` | Branch list with ahead/behind counts and divergence scores |
1833 | GET | `/musehub/ui/{owner}/{repo_slug}/tags` | Tag browser — releases grouped by namespace prefix |
1834 | GET | `/musehub/ui/{owner}/{repo_slug}/sessions` | Session list (newest first) |
1835 | GET | `/musehub/ui/{owner}/{repo_slug}/sessions/{session_id}` | Session detail page |
1836 | GET | `/musehub/ui/{owner}/{repo_slug}/arrange/{ref}` | Arrangement matrix — interactive instrument × section density grid |
1837
1838 UI pages are Jinja2-rendered HTML shells — auth is handled client-side via `localStorage` JWT (loaded from `/musehub/static/musehub.js`). The page JavaScript fetches from the authed JSON API above.
1839
1840 ### Branch List Page
1841
1842 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/branches`
1843
1844 Lists all branches with enriched context to help musicians decide which branches to merge or discard:
1845
1846 - **Default branch badge** — the repo default branch (name "main", or first alphabetically) is marked with a `default` badge.
1847 - **Ahead/behind counts** — number of commits unique to this branch (ahead) vs commits on the default not yet here (behind). Computed as a commit-ID set difference over the `musehub_commits` table.
1848 - **Musical divergence scores** — five dimensions (melodic, harmonic, rhythmic, structural, dynamic) each in [0, 1]. Shown as mini bar charts. All `null` (placeholder) until audio snapshots are attached to commits and server-side computation is implemented.
1849 - **Compare link** — navigates to `/{owner}/{repo_slug}/compare/{default}...{branch}`.
1850 - **New Pull Request button** — navigates to `/{owner}/{repo_slug}/pulls/new?head={branch}`.
1851
1852 Content negotiation: `?format=json` or `Accept: application/json` returns `BranchDetailListResponse` (camelCase).
1853
1854 ### Tag Browser Page
1855
1856 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/tags`
1857
1858 Displays all releases as browseable tags, grouped by namespace prefix:
1859
1860 - Tags are sourced from `musehub_releases`. The `tag` field of a release is used as the tag name.
1861 - **Namespace extraction** — `emotion:happy` → namespace `emotion`; `v1.0` → namespace `version` (no colon).
1862 - **Namespace filter dropdown** — client-side filter that re-renders without a round trip.
1863 - **Per-tag info** — tag name, commit SHA (links to commit detail), release title, creation date.
1864 - **View commit** link per tag.
1865
1866 Content negotiation: `?format=json` or `Accept: application/json` returns `TagListResponse` (camelCase) including a `namespaces` list. Optional `?namespace=<ns>` query parameter filters by namespace server-side.
1867
1868 ### Repo Home Page
1869
1870 **Purpose:** Provide an "album cover" view of a Muse Hub repo — hearing the latest mix, seeing the arrangement structure, and understanding project activity at a glance. Replaces the plain commit-list landing page with a rich dashboard suited for musicians, collaborators, and AI agents.
1871
1872 **Routes:**
1873
1874 | Route | Auth | Description |
1875 |-------|------|-------------|
1876 | `GET /musehub/ui/{owner}/{repo_slug}` | None (HTML shell) | Repo home page — arrangement matrix, audio player, stats bar, recent commits |
1877 | `GET /musehub/ui/{owner}/{repo_slug}` (`Accept: application/json`) | Optional JWT | Returns `{ stats, recent_commits }` as JSON |
1878 | `GET /api/v1/musehub/repos/{repo_id}/stats` | Optional JWT | `RepoStatsResponse` — commit/branch/release counts |
1879 | `GET /api/v1/musehub/repos/{repo_id}/arrange/{ref}` | Optional JWT | `ArrangementMatrixResponse` — instrument × section density grid |
1880
1881 **Sections rendered by the home page template (`repo_home.html`):**
1882
1883 1. **Hero** — repo name, owner link, visibility badge (Public/Private), description, key/BPM pills, and genre tags.
1884 2. **Stats bar** — three clickable pills showing: commit count (links to commit history), branch count (links to DAG graph), release count (links to releases).
1885 3. **Quick-link tabs** — Code, Commits, Graph, PRs, Issues, Analysis, Sessions — one-click navigation to all repo sub-pages.
1886 4. **Audio player** — embeds an `<audio controls>` element pointing to the latest MP3/OGG/WAV object from the most recent commit. Shows a placeholder when no audio render is found.
1887 5. **Arrangement matrix** — a colour-coded grid showing tracks (columns) × sections (rows) from the latest commit snapshot. Falls back to a placeholder when the repo has no commits.
1888 6. **Recent commits** — last 5 commits with SHA link, conventional-commit type/scope badges, author avatar, and relative timestamp.
1889 7. **README** — raw text of `README.md` from HEAD rendered in a `<pre>` block; hidden when no README exists.
1890
1891 **Content negotiation:**
1892
1893 Sending `Accept: application/json` returns:
1894
1895 ```json
1896 {
1897 "stats": {
1898 "commit_count": 42,
1899 "branch_count": 3,
1900 "release_count": 2
1901 },
1902 "recent_commits": [
1903 {
1904 "commit_id": "abc123...",
1905 "branch": "main",
1906 "message": "feat(drums): add hi-hat pattern",
1907 "author": "gabriel",
1908 "timestamp": "2025-01-15T12:00:00+00:00"
1909 }
1910 ]
1911 }
1912 ```
1913
1914 This allows AI agents and CLI tools to query repo activity without rendering HTML.
1915
1916 **Stats endpoint:** `GET /api/v1/musehub/repos/{repo_id}/stats`
1917
1918 Returns a `RepoStatsResponse` with:
1919 - `commit_count` — total commits across all branches (from `list_commits` total)
1920 - `branch_count` — number of branch pointers (from `list_branches`)
1921 - `release_count` — number of published releases/tags (from `list_releases`)
1922
1923 All counts are 0 when the repo is empty. Respects visibility: private repos return 401 for unauthenticated callers.
1924
1925 ### DAG Graph — Interactive Commit Graph
1926
1927 **Purpose:** Visualise the full commit history of a Muse Hub repo as an interactive directed acyclic graph, equivalent to `muse inspect --format mermaid` but explorable in the browser.
1928
1929 **Routes:**
1930
1931 | Route | Auth | Description |
1932 |-------|------|-------------|
1933 | `GET /api/v1/musehub/repos/{id}/dag` | JWT required | Returns `DagGraphResponse` JSON |
1934 | `GET /musehub/ui/{id}/graph` | None (HTML shell) | Interactive SVG graph page |
1935
1936 **DAG data endpoint:** `GET /api/v1/musehub/repos/{id}/dag`
1937
1938 Returns a `DagGraphResponse` with:
1939 - `nodes` — `DagNode[]` in topological order (oldest ancestor first, Kahn's algorithm)
1940 - `edges` — `DagEdge[]` where `source` = child commit, `target` = parent commit
1941 - `headCommitId` — SHA of the current HEAD (highest-timestamp branch head)
1942
1943 Each `DagNode` carries: `commitId`, `message`, `author`, `timestamp`, `branch`, `parentIds`, `isHead`, `branchLabels`, `tagLabels`.
1944
1945 **Client-side renderer features:**
1946 - Branch colour-coding: each unique branch name maps to a stable colour via a deterministic hash → palette index. Supports up to 10 distinct colours before wrapping.
1947 - Merge commits: nodes with `parentIds.length > 1` are rendered as rotated diamonds rather than circles.
1948 - HEAD node: an outer ring (orange `#f0883e`) marks the current HEAD commit.
1949 - Zoom: mouse-wheel scales the SVG transform around the cursor position (range 0.2× – 4×).
1950 - Pan: click-drag translates the SVG viewport.
1951 - Hover popover: shows full SHA, commit message, author, timestamp, and branch for any node.
1952 - Branch labels: `branchLabels` for each node are drawn as coloured badge overlays on the graph.
1953 - Click to navigate: clicking any node or its label navigates to the commit detail page.
1954 - Virtualised rendering: the SVG is positioned absolutely inside a fixed-height viewport; only the visible portion is painted by the browser.
1955
1956 **Legend:** The top bar of the graph page shows each distinct branch name with its colour swatch, plus shape-key reminders for merge commits (♦) and HEAD (○).
1957
1958 **Performance:** The `/dag` endpoint fetches all commits without a limit to build a complete graph. For repos with 100+ commits the response is typically < 50 KB (well within browser tolerance). The SVG renderer does not re-layout on scroll — panning is a pure CSS transform.
1959
1960 **Result type:** `DagGraphResponse` — fields: `nodes: DagNode[]`, `edges: DagEdge[]`, `headCommitId: str | None`.
1961
1962 **Agent use case:** An AI music generation agent can call `GET /dag` to reason about branching topology, find common ancestors between branches, determine which commits are reachable from HEAD, and identify merge points without scanning the linear commit list.
1963
1964 ### Issue Workflow
1965
1966 Issues let musicians track production problems and creative tasks within a repo, keeping feedback close to the music data rather than in out-of-band chat.
1967
1968 - **Issue numbers** are sequential per repo (1, 2, 3…) and independent across repos.
1969 - **Labels** are free-form strings — e.g. `bug`, `musical`, `timing`, `mix`. No validation at MVP.
1970 - **States:** `open` (default on creation) → `closed` (via the close endpoint). No re-open at MVP.
1971 - **Filtering:** `GET /issues?state=all` includes both open and closed; `?label=bug` narrows by label.
1972
1973 ### Timeline — Chronological Evolution View
1974
1975 The timeline view lets musicians (and AI agents) see how a project evolved over time, with four independently toggleable layers:
1976 ### Release System
1977
1978 Releases publish a specific version of a composition as a named snapshot that listeners and collaborators can download in multiple formats.
1979
1980 #### Concept
1981
1982 A release binds a human-readable **tag** (e.g. `v1.0`) to:
1983 - A **title** — the name of this version (e.g. "First Release")
1984 - **Release notes** — Markdown body describing what changed
1985 - An optional **commit ID** — pins the release to a specific commit snapshot
1986 - **Download URLs** — structured map of package download links
1987
1988 #### Download Packages
1989
1990 Each release exposes download URLs for these package types:
1991
1992 | Package | Field | Format | Description |
1993 |---------|-------|--------|-------------|
1994 | Full MIDI | `midiBundle` | `.mid` | All tracks merged into a single MIDI file |
1995 | Stems | `stems` | `.zip` of `.mid` files | Individual per-track MIDI stems |
1996 | MP3 | `mp3` | `.mp3` | Full mix audio render |
1997 | MusicXML | `musicxml` | `.xml` | Notation export for sheet music editors |
1998 | Metadata | `metadata` | `.json` | Tempo, key, time signature, arrangement info |
1999
2000 Package URLs are `null` when the corresponding artifact is unavailable (no pinned commit,
2001 or no stored objects for that commit). The frontend renders "Not available" cards instead
2002 of broken links.
2003
2004 #### Uniqueness Constraint
2005
2006 Tags are unique per repo — attempting to create a second `v1.0` release in the same repo
2007 returns `409 Conflict`. The same tag can be reused across different repos without conflict.
2008
2009 #### Latest Release Badge
2010
2011 The repo home page (`GET /musehub/ui/{owner}/{repo_slug}`) fetches the release list on load and
2012 displays a green "Latest: v1.0" badge in the navigation bar when at least one release exists.
2013 Clicking the badge navigates to the release detail page.
2014
2015 #### Package Generation (MVP Stub)
2016
2017 At MVP, download URLs are deterministic paths based on `repo_id` and `release_id`. The actual
2018 package generation (MIDI export, MP3 rendering, MusicXML conversion) is not implemented — the
2019 URL shape is established so the contract is stable for future implementation without an API change.
2020
2021 The packager module (`maestro/services/musehub_release_packager.py`) controls URL construction.
2022 Callers pass boolean flags (`has_midi`, `has_stems`, `has_mp3`, `has_musicxml`) based on what
2023 stored objects are available for the pinned commit.
2024 ### Divergence Visualization
2025
2026 The divergence endpoint and UI let producers compare two branches across five musical dimensions before deciding what to merge.
2027
2028 #### API Endpoint
2029
2030 ```
2031 GET /api/v1/musehub/repos/{repo_id}/timeline?limit=200
2032 Authorization: Bearer <token>
2033 ```
2034
2035 **Response shape (`TimelineResponse`):**
2036
2037 ```json
2038 {
2039 "commits": [
2040 {
2041 "eventType": "commit",
2042 "commitId": "deadbeef...",
2043 "branch": "main",
2044 "message": "added chorus",
2045 "author": "musician",
2046 "timestamp": "2026-02-01T12:00:00Z",
2047 "parentIds": ["..."]
2048 }
2049 ],
2050 "emotion": [
2051 {
2052 "eventType": "emotion",
2053 "commitId": "deadbeef...",
2054 "timestamp": "2026-02-01T12:00:00Z",
2055 "valence": 0.8711,
2056 "energy": 0.3455,
2057 "tension": 0.2190
2058 }
2059 ],
2060 "sections": [
2061 {
2062 "eventType": "section",
2063 "commitId": "deadbeef...",
2064 "timestamp": "2026-02-01T12:00:00Z",
2065 "sectionName": "chorus",
2066 "action": "added"
2067 }
2068 ],
2069 "tracks": [
2070 {
2071 "eventType": "track",
2072 "commitId": "deadbeef...",
2073 "timestamp": "2026-02-01T12:00:00Z",
2074 "trackName": "bass",
2075 "action": "added"
2076 }
2077 ],
2078 "totalCommits": 42
2079 }
2080 ```
2081
2082 **Layer descriptions:**
2083
2084 | Layer | Source | Description |
2085 |-------|--------|-------------|
2086 | `commits` | DB: `musehub_commits` | Every pushed commit — always present. Oldest-first for temporal rendering. |
2087 | `emotion` | Derived from commit SHA | Deterministic valence/energy/tension in [0,1] — reproducible without ML inference. |
2088 | `sections` | Commit message heuristics | Keywords: intro, verse, chorus, bridge, outro, hook, etc. Action inferred from verb (added/removed). |
2089 | `tracks` | Commit message heuristics | Keywords: bass, drums, keys, guitar, synth, etc. Action inferred from verb (added/removed). |
2090
2091 **Emotion derivation:** Three non-overlapping 4-hex-character windows of the commit SHA are converted to floats in [0,1]. This is deterministic, fast, and requires no model — sufficient for visualisation. Future versions may substitute ML-derived vectors without changing the API shape.
2092
2093 **Section/track heuristics:** Verb patterns (`add`, `remove`, `delete`, `create`, etc.) in the commit message determine `action`. No NLP is required — keyword scanning is fast and sufficient for commit message conventions used by `muse commit`.
2094
2095 #### Web UI Page
2096
2097 ```
2098 GET /musehub/ui/{owner}/{repo_slug}/timeline
2099 ```
2100
2101 No auth required — HTML shell whose JS fetches the JSON API using the JWT from `localStorage`.
2102
2103 **Features:**
2104 - Horizontal SVG canvas with commit markers on a time spine
2105 - Emotion line chart (valence = blue, energy = green, tension = red) overlaid above the spine
2106 - Section-change markers (green = added, red = removed) below the spine
2107 - Track add/remove markers (purple = added, yellow = removed) at the bottom
2108 - Toggleable layers via checkboxes in the toolbar
2109 - Zoom controls: Day / Week / Month / All-time
2110 - Time scrubber to navigate through history
2111 - Click any commit marker to open an audio preview modal
2112
2113 **Agent use case:** An AI agent calls `GET /api/v1/musehub/repos/{id}/timeline --json` to understand the creative arc of a project before generating new material — identifying when the emotional character shifted, when sections were introduced, and which instruments were layered in. The deterministic emotion vectors give agents a structured signal without requiring audio analysis.
2114
2115 **Result types:** `TimelineResponse`, `TimelineCommitEvent`, `TimelineEmotionEvent`, `TimelineSectionEvent`, `TimelineTrackEvent` — see `docs/reference/type_contracts.md § Muse Hub Timeline Types`.
2116
2117 ---
2118 GET /api/v1/musehub/repos/{repo_id}/divergence?branch_a=<name>&branch_b=<name>
2119 ```
2120
2121 Returns `DivergenceResponse` (JSON):
2122
2123 | Field | Type | Description |
2124 |-------|------|-------------|
2125 | `repoId` | `str` | Repository ID |
2126 | `branchA` | `str` | First branch name |
2127 | `branchB` | `str` | Second branch name |
2128 | `commonAncestor` | `str \| null` | Merge-base commit ID |
2129 | `dimensions` | `list[DivergenceDimensionResponse]` | Five dimension scores |
2130 | `overallScore` | `float` | Mean of dimension scores in [0.0, 1.0] |
2131
2132 Each `DivergenceDimensionResponse`:
2133
2134 | Field | Type | Description |
2135 |-------|------|-------------|
2136 | `dimension` | `str` | `melodic` / `harmonic` / `rhythmic` / `structural` / `dynamic` |
2137 | `level` | `str` | `NONE` / `LOW` / `MED` / `HIGH` |
2138 | `score` | `float` | Jaccard divergence in [0.0, 1.0] |
2139 | `description` | `str` | Human-readable summary |
2140 | `branchACommits` | `int` | Commits touching this dimension on branch A |
2141 | `branchBCommits` | `int` | Commits touching this dimension on branch B |
2142
2143 **Level thresholds:**
2144
2145 | Level | Score range |
2146 |-------|-------------|
2147 | NONE | < 0.15 |
2148 | LOW | 0.15–0.40 |
2149 | MED | 0.40–0.70 |
2150 | HIGH | ≥ 0.70 |
2151
2152 #### Score Formula
2153
2154 Divergence per dimension = `|symmetric_diff| / |union|` over commit IDs classified into that dimension via keyword matching on commit messages:
2155
2156 - **melodic:** melody, lead, solo, vocal, tune, note, pitch, riff, arpeggio
2157 - **harmonic:** chord, harmony, key, scale, progression, voicing
2158 - **rhythmic:** beat, drum, rhythm, groove, perc, swing, tempo, bpm, quantize
2159 - **structural:** struct, form, section, bridge, chorus, verse, intro, outro, arrangement
2160 - **dynamic:** mix, master, volume, level, dynamic, eq, compress, reverb, fx
2161
2162 Overall score = arithmetic mean of all five dimension scores.
2163
2164 #### Browser UI
2165
2166 ```
2167 GET /musehub/ui/{owner}/{repo_slug}/divergence?branch_a=<name>&branch_b=<name>
2168 ```
2169
2170 Renders an interactive page featuring:
2171 - Five-axis SVG radar chart with colour-coded dimension labels (NONE=blue, LOW=teal, MED=amber, HIGH=red)
2172 - Overall divergence percentage display with merge-base commit reference
2173 - Per-dimension progress bars + level badges
2174 - Click-to-expand detail panels showing commit counts per branch
2175 - Branch selector dropdowns with URL state sync
2176
2177 **AI agent use case:** Call `GET /divergence` before opening a PR to determine if two branches are safe to merge automatically (overall score < 0.15) or need producer review (HIGH on any dimension).
2178
2179 ### Pull Request Workflow
2180
2181 Pull requests let musicians propose merging one branch variation into another, enabling async review before incorporating changes into the canonical arrangement.
2182
2183 - **States:** `open` (on creation) → `merged` (via merge endpoint) | `closed` (future: manual close).
2184 - **Merge strategies:** `merge_commit` (default), `squash`, and `rebase` are accepted by the API. All three currently use merge-commit semantics — distinct strategy behavior is tracked as a follow-up.
2185 - **Validation:** `from_branch == to_branch` → 422. Missing `from_branch` → 404. Already merged/closed → 409 on merge attempt.
2186 - **Filtering:** `GET /pull-requests?state=open` returns only open PRs. Default (`state=all`) returns all states.
2187
2188 #### PR Detail Page — Musical Diff (issue #215)
2189
2190 The PR detail page (`GET /musehub/ui/{owner}/{repo_slug}/pulls/{pr_id}`) now shows a full musical diff UI on top of the existing metadata and merge button:
2191
2192 | Component | Description |
2193 |-----------|-------------|
2194 | **Five-axis radar chart** | Visual divergence across harmonic, rhythmic, melodic, structural, dynamic dimensions — each axis shows delta magnitude in [0.0, 1.0] |
2195 | **Diff summary badges** | Per-dimension delta labels (e.g. `+23.5%`, `unchanged`) computed from the Jaccard divergence score |
2196 | **Before/after piano roll** | Deterministic note-grid comparison seeded from branch names — green = added, red = removed, grey = unchanged |
2197 | **Audio A/B toggle** | Switch between base (`to_branch`) and head (`from_branch`) audio renders; gracefully degrades if renders not available |
2198 | **Merge strategy selector** | Choose `merge_commit`, `squash`, or `rebase` before clicking merge |
2199 | **PR timeline** | Open → (review) → merge/close state progression with timestamps |
2200 | **Affected sections** | Commit-message-derived list of structural sections touched by the PR (bridge, chorus, verse, etc.) |
2201
2202 **Musical diff endpoint:** `GET /api/v1/musehub/repos/{repo_id}/pull-requests/{pr_id}/diff`
2203
2204 Returns `PRDiffResponse` — a `PRDiffDimensionScore` for each of the five musical dimensions plus `overall_score`, `common_ancestor`, and `affected_sections`.
2205
2206 `affected_sections` is derived by scanning commit messages from both branches since the merge base for structural section keywords (bridge, chorus, verse, intro, outro, section) using a word-boundary regex. Only sections actually mentioned in commit text are returned; an empty list is correct when no commit references a section name. The field is **never** derived from divergence scores alone.
2207
2208 **Content negotiation:** `GET /musehub/ui/{owner}/{repo_slug}/pulls/{pr_id}?format=json` returns the full `PRDiffResponse` for AI agent consumption. Agents use this to reason about musical impact before approving a merge — e.g. a large harmonic delta with unchanged rhythm signals a chord progression update that preserves the groove.
2209
2210 **Graceful degradation:** When one or both branches have no commits (divergence engine raises `ValueError`), the endpoint returns five zero-score placeholder dimensions so the page always renders cleanly.
2211
2212 **Divergence builder service:** Both the pull-requests route and the UI route delegate PRDiffResponse assembly to `maestro.services.musehub_divergence.build_pr_diff_response` (success path) and `build_zero_diff_response` (no-commit fallback). Route handlers remain thin callers with no duplicated mapping logic.
2213
2214 ### Sync Protocol Design
2215
2216 The push/pull protocol is intentionally simple for MVP:
2217
2218 #### Push — fast-forward enforcement
2219
2220 A push is accepted when one of the following is true:
2221 1. The branch has no head yet (first push).
2222 2. `headCommitId` equals the current remote head (no-op).
2223 3. The current remote head appears in the ancestry graph of the pushed commits — i.e. the client built on top of the remote head.
2224
2225 When none of these conditions hold the push is **rejected with HTTP 409** and body `{"error": "non_fast_forward"}`. Set `force: true` in the request to overwrite the remote head regardless (equivalent to `git push --force`).
2226
2227 Commits and objects are **upserted by ID** — re-pushing the same content is safe and idempotent.
2228
2229 #### Pull — exclusion-list delta
2230
2231 The client sends `haveCommits` and `haveObjects` as exclusion lists. The Hub returns all commits for the requested branch and all objects for the repo that are NOT in those lists. No ancestry traversal is performed — the client receives the full delta in one response.
2232
2233 **MVP limitation:** Large objects (> 1 MB) are base64-encoded inline. Pre-signed URL upload is planned as a follow-up.
2234
2235 #### Object storage
2236
2237 Binary artifact bytes are written to disk at:
2238
2239 ```
2240 <settings.musehub_objects_dir>/<repo_id>/<object_id_with_colon_replaced_by_dash>
2241 ```
2242
2243 Default: `/data/musehub/objects`. Mount this path on a persistent volume in production.
2244
2245 Only metadata (`object_id`, `path`, `size_bytes`, `disk_path`) is stored in Postgres; the bytes live on disk.
2246
2247 ### Session Workflow
2248
2249 Recording sessions let musicians capture the creative context of a generation or performance session — who was present, where they recorded, what they intended, which commits were produced, and any closing notes.
2250
2251 - **Session IDs** are the local UUIIDv4 from `.muse/sessions/<uuid>.json`. The same ID is used in the Hub.
2252 - **Push semantics:** `POST /sessions` is **idempotent** — re-pushing the same `session_id` updates the existing record. This allows updating notes after the initial push.
2253 - **Participants with session count badges:** The UI loads all sessions to compute per-participant session counts, displaying them next to each name on the detail page.
2254 - **Commits cross-reference:** `commits` is a JSON list of Muse commit IDs. The detail page links each commit to the commit detail page. No FK enforcement at DB level — commits may arrive before or after sessions.
2255 - **Previous/next navigation:** The session detail page fetches all sessions for the repo and renders prev/next links based on `started_at` order, mirroring the `muse session log` traversal pattern.
2256
2257 ### Architecture Boundary
2258
2259 Service modules are the only place that touches `musehub_*` tables:
2260 - `musehub_repository.py` → `musehub_repos`, `musehub_branches`, `musehub_commits`
2261 - `musehub_issues.py` → `musehub_issues`
2262 - `musehub_pull_requests.py` → `musehub_pull_requests`
2263 - `musehub_sessions.py` → `musehub_sessions`
2264 - `musehub_sync.py` → `musehub_commits`, `musehub_objects`, `musehub_branches` (sync path only)
2265
2266 Route handlers delegate all persistence to the service layer. No business logic in route handlers.
2267
2268 ---
2269
2270 ## Maestro → Muse Integration: Generate → Commit Pipeline
2271
2272 The stress test (`scripts/e2e/stress_test.py`) produces music artifacts in a
2273 deterministic `muse-work/` layout consumable directly by `muse commit`.
2274
2275 ### Output Contract (`--output-dir ./muse-work`)
2276
2277 ```
2278 muse-work/
2279 tracks/<instrument_combo>/<genre>_<bars>b_<composition_id>.mid
2280 renders/<genre>_<bars>b_<composition_id>.mp3
2281 previews/<genre>_<bars>b_<composition_id>.webp
2282 meta/<genre>_<bars>b_<composition_id>.json
2283 muse-batch.json (written next to muse-work/, i.e. in the repo root)
2284 ```
2285
2286 ### `muse-batch.json` Schema
2287
2288 ```json
2289 {
2290 "run_id": "stress-20260227_172919",
2291 "generated_at": "2026-02-27T17:29:19Z",
2292 "commit_message_suggestion": "feat: 2-genre stress test (jazz, house)",
2293 "files": [
2294 {
2295 "path": "muse-work/tracks/drums_bass/jazz_4b_stress-20260227_172919-0000.mid",
2296 "role": "midi",
2297 "genre": "jazz",
2298 "bars": 4,
2299 "cached": false
2300 }
2301 ],
2302 "provenance": {
2303 "prompt": "stress_test.py --quick --genre jazz,house",
2304 "model": "storpheus",
2305 "seed": "stress-20260227_172919",
2306 "storpheus_version": "1.0.0"
2307 }
2308 }
2309 ```
2310
2311 **Field rules:**
2312 - `files[].path` — relative to repo root, always starts with `muse-work/`
2313 - `files[].role` — one of `"midi"`, `"mp3"`, `"webp"`, `"meta"`
2314 - `files[].cached` — `true` when the result was served from the Storpheus cache
2315 - Failed generations are **omitted** from `files[]`; only successful results appear
2316 - Cache hits **are included** in `files[]` with `"cached": true`
2317
2318 ### `muse commit` — Full Flag Reference
2319
2320 **Usage:**
2321 ```bash
2322 muse commit -m <message> [OPTIONS]
2323 muse commit --from-batch muse-batch.json [OPTIONS]
2324 ```
2325
2326 **Core flags:**
2327
2328 | Flag | Type | Default | Description |
2329 |------|------|---------|-------------|
2330 | `-m / --message TEXT` | string | — | Commit message. Required unless `--from-batch` is used |
2331 | `--from-batch PATH` | path | — | Use `commit_message_suggestion` from `muse-batch.json`; snapshot is restricted to listed files |
2332 | `--amend` | flag | off | Fold working-tree changes into the most recent commit (equivalent to `muse amend`) |
2333 | `--no-verify` | flag | off | Bypass pre-commit hooks (no-op until hook system is implemented) |
2334 | `--allow-empty` | flag | off | Allow committing even when the working tree has not changed since HEAD |
2335
2336 **Music-domain flags (Muse-native metadata):**
2337
2338 | Flag | Type | Default | Description |
2339 |------|------|---------|-------------|
2340 | `--section TEXT` | string | — | Tag commit as belonging to a musical section (e.g. `verse`, `chorus`, `bridge`) |
2341 | `--track TEXT` | string | — | Tag commit as affecting a specific instrument track (e.g. `drums`, `bass`, `keys`) |
2342 | `--emotion TEXT` | string | — | Attach an emotion vector label (e.g. `joyful`, `melancholic`, `tense`) |
2343 | `--co-author TEXT` | string | — | Append `Co-authored-by: Name <email>` trailer to the commit message |
2344
2345 Music-domain flags are stored in the `commit_metadata` JSON column on `muse_cli_commits`. They are surfaced at the top level in `muse show <commit> --json` output and form the foundation for future queries like `muse log --emotion melancholic` or `muse diff --section chorus`.
2346
2347 **Examples:**
2348
2349 ```bash
2350 # Standard commit with message
2351 muse commit -m "feat: add Rhodes piano to chorus"
2352
2353 # Tag with music-domain metadata
2354 muse commit -m "groove take 3" --section verse --track drums --emotion joyful
2355
2356 # Collaborative session — attribute a co-author
2357 muse commit -m "keys arrangement" --co-author "Alice <alice@stori.app>"
2358
2359 # Amend the last commit with new emotion tag
2360 muse commit --amend --emotion melancholic
2361
2362 # Milestone commit with no file changes
2363 muse commit --allow-empty -m "session handoff" --section bridge
2364
2365 # Fast path from stress test
2366 muse commit --from-batch muse-batch.json --emotion tense
2367 ```
2368
2369 ### Fast-Path Commit: `muse commit --from-batch`
2370
2371 ```bash
2372 # Run stress test → write muse-work/ layout + muse-batch.json
2373 docker compose exec storpheus python scripts/e2e/stress_test.py \
2374 --quick --genre jazz,house --flush --output-dir ./muse-work
2375
2376 # Commit only the files produced by this run, using the suggested message
2377 muse commit --from-batch muse-batch.json
2378 ```
2379
2380 `muse commit --from-batch <path>`:
2381 1. Reads `muse-batch.json` from `<path>`
2382 2. Uses `commit_message_suggestion` as the commit message (overrides `-m`)
2383 3. Builds the snapshot manifest **restricted to files listed in `files[]`** — the rest of `muse-work/` is excluded
2384 4. Proceeds with the standard commit pipeline (snapshot → DB → HEAD pointer update)
2385
2386 The `-m` flag is optional when `--from-batch` is present. If both are supplied,
2387 `--from-batch`'s suggestion wins. All music-domain flags (`--section`, `--track`,
2388 `--emotion`, `--co-author`) can be combined with `--from-batch`.
2389
2390 ### Workflow Summary
2391
2392 ```
2393 stress_test.py --output-dir ./muse-work
2394
2395 ├── saves artifacts → muse-work/{tracks,renders,previews,meta}/
2396 └── emits muse-batch.json (manifest + commit_message_suggestion)
2397
2398
2399 muse commit --from-batch muse-batch.json
2400
2401 ├── reads batch → restrict snapshot to listed files
2402 ├── uses commit_message_suggestion
2403 └── creates versioned commit in Postgres
2404 ```
2405
2406 ---
2407
2408 ## Muse CLI — Plumbing Command Reference
2409
2410 Plumbing commands expose the raw object model and allow scripted or programmatic
2411 construction of history without the side-effects of porcelain commands. They
2412 mirror the design of `git commit-tree`, `git update-ref`, and `git hash-object`.
2413
2414 AI agents use plumbing commands when they need to build commit graphs
2415 programmatically — for example when replaying a merge, synthesising history from
2416 an external source, or constructing commits without changing the working branch.
2417
2418 ---
2419
2420 ### `muse commit-tree`
2421
2422 **Purpose:** Create a raw commit object directly from an existing `snapshot_id`
2423 and explicit metadata. Does not walk the filesystem, does not update any branch
2424 ref, does not touch `.muse/HEAD`. Use `muse update-ref` (planned) to associate
2425 the resulting commit with a branch.
2426
2427 **Usage:**
2428 ```bash
2429 muse commit-tree <snapshot_id> -m <message> [OPTIONS]
2430 ```
2431
2432 **Flags:**
2433
2434 | Flag | Type | Default | Description |
2435 |------|------|---------|-------------|
2436 | `snapshot_id` | positional | — | ID of an existing snapshot row in the database |
2437 | `-m / --message TEXT` | string | required | Commit message |
2438 | `-p / --parent TEXT` | string | — | Parent commit ID. Repeat for merge commits (max 2) |
2439 | `--author TEXT` | string | `[user] name` from `.muse/config.toml` or `""` | Author name |
2440
2441 **Output example:**
2442 ```
2443 a3f8c21d4e9b0712c5d6f7a8e3b2c1d0a4f5e6b7c8d9e0f1a2b3c4d5e6f7a8b9
2444 ```
2445
2446 The commit ID (64-char SHA-256 hex) is printed to stdout. Pipe it to
2447 `muse update-ref` to advance a branch ref.
2448
2449 **Result type:** `CommitTreeResult` — fields: `commit_id` (str, 64-char hex).
2450
2451 **Idempotency contract:** The commit ID is derived deterministically from
2452 `(parent_ids, snapshot_id, message, author)` with **no timestamp** component.
2453 Repeating the same call returns the same `commit_id` without inserting a
2454 duplicate row. This makes `muse commit-tree` safe to call in retry loops and
2455 idempotent scripts.
2456
2457 **Agent use case:** An AI music generation agent that needs to construct a merge
2458 commit (e.g. combining the groove from branch A with the lead from branch B)
2459 without moving either branch pointer:
2460
2461 ```bash
2462 SNAP=$(muse write-tree) # planned plumbing command
2463 COMMIT=$(muse commit-tree "$SNAP" -m "Merge groove+lead" -p "$A_HEAD" -p "$B_HEAD")
2464 muse update-ref refs/heads/merge-candidate "$COMMIT" # planned
2465 ```
2466
2467 **Error cases:**
2468 - `snapshot_id` not found → exits 1 with a clear message
2469 - More than 2 `-p` parents → exits 1 (DB model stores at most 2)
2470 - Not inside a Muse repo → exits 2
2471
2472 **Implementation:** `maestro/muse_cli/commands/commit_tree.py`
2473
2474 ---
2475
2476 ### `muse hash-object`
2477
2478 **Purpose:** Compute the SHA-256 content-address of a file (or stdin) and
2479 optionally write it into the Muse object store. The hash produced is
2480 identical to what `muse commit` would assign to the same file, ensuring
2481 cross-command content-addressability. Use this for scripting, pre-upload
2482 deduplication checks, and debugging the object store.
2483
2484 **Usage:**
2485 ```bash
2486 muse hash-object <file> [OPTIONS]
2487 muse hash-object --stdin [OPTIONS]
2488 ```
2489
2490 **Flags:**
2491
2492 | Flag | Type | Default | Description |
2493 |------|------|---------|-------------|
2494 | `<file>` | positional | — | Path to the file to hash. Omit when using `--stdin`. |
2495 | `-w / --write` | flag | off | Write the object to `.muse/objects/` and the `muse_cli_objects` table in addition to printing the hash. |
2496 | `--stdin` | flag | off | Read content from stdin instead of a file. |
2497
2498 **Output example:**
2499
2500 ```
2501 a3f2e1b0d4c5... (64-character SHA-256 hex digest)
2502 ```
2503
2504 **Result type:** `HashObjectResult` — fields: `object_id` (str, 64-char hex), `stored` (bool), `already_existed` (bool).
2505
2506 **Agent use case:** An AI agent can call `muse hash-object <file>` to derive the
2507 object ID before committing, enabling optimistic checks ("is this drum loop
2508 already in the store?") without running a full `muse commit`. Piping output
2509 to `muse cat-object` verifies whether the stored content matches expectations.
2510
2511 **Implementation:** `maestro/muse_cli/commands/hash_object.py` — registered as
2512 `muse hash-object`. `HashObjectResult` (class), `hash_bytes()` (pure helper),
2513 `_hash_object_async()` (fully injectable for tests).
2514
2515 ---
2516
2517 ## Muse CLI — Remote Sync Command Reference
2518
2519 These commands connect the local Muse repo to the remote Muse Hub, enabling
2520 collaboration between musicians (push from one machine, pull on another) and
2521 serving as the CLI-side counterpart to the Hub's sync API.
2522
2523 ---
2524
2525 ### `muse remote`
2526
2527 **Purpose:** Manage named remote Hub URLs in `.muse/config.toml`. Every push
2528 and pull needs a remote configured — `muse remote add` is the prerequisite.
2529 Use `remove`, `rename`, and `set-url` to maintain remotes over the repository
2530 lifecycle (switching Hub instances, renaming origin to upstream, etc.).
2531
2532 **Usage:**
2533 ```bash
2534 muse remote add <name> <url> # register a new remote
2535 muse remote remove <name> # remove a remote and its tracking refs
2536 muse remote rename <old> <new> # rename a remote (config + tracking refs)
2537 muse remote set-url <name> <url> # update URL of an existing remote
2538 muse remote -v # list all remotes with their URLs
2539 ```
2540
2541 **Flags:**
2542 | Flag | Type | Default | Description |
2543 |------|------|---------|-------------|
2544 | `-v` / `--verbose` | flag | off | Print all configured remotes with their URLs |
2545
2546 **Subcommands:**
2547
2548 | Subcommand | Description |
2549 |-----------|-------------|
2550 | `add <name> <url>` | Write `[remotes.<name>] url = "<url>"` to `.muse/config.toml`; creates or overwrites |
2551 | `remove <name>` | Delete `[remotes.<name>]` from config and remove `.muse/remotes/<name>/` tracking refs |
2552 | `rename <old> <new>` | Rename config entry and move `.muse/remotes/<old>/` → `.muse/remotes/<new>/` |
2553 | `set-url <name> <url>` | Update `[remotes.<name>] url` without touching tracking refs; errors if remote absent |
2554
2555 **Output example:**
2556 ```
2557 # muse remote add origin https://story.audio/musehub/repos/my-repo-id
2558 ✅ Remote 'origin' set to https://story.audio/musehub/repos/my-repo-id
2559
2560 # muse remote -v
2561 origin https://story.audio/musehub/repos/my-repo-id
2562 staging https://staging.example.com/musehub/repos/my-repo-id
2563
2564 # muse remote rename origin upstream
2565 ✅ Remote 'origin' renamed to 'upstream'.
2566
2567 # muse remote set-url upstream https://new-hub.example.com/musehub/repos/my-repo-id
2568 ✅ Remote 'upstream' URL changed to https://new-hub.example.com/musehub/repos/my-repo-id
2569
2570 # muse remote remove staging
2571 ✅ Remote 'staging' removed.
2572 ```
2573
2574 **Security:** Token values in `[auth]` are never shown by `muse remote -v`.
2575
2576 **Exit codes:** 0 — success; 1 — bad URL, empty name, remote not found, or name conflict; 2 — not a repo.
2577
2578 **Agent use case:** An orchestration agent registers the Hub URL once at repo
2579 setup time with `add`, then uses `set-url` to point at a different Hub instance
2580 when the workspace migrates, `rename` to canonicalize `origin` → `upstream`,
2581 and `remove` to clean up stale collaborator remotes.
2582
2583 ---
2584
2585 ### `muse push`
2586
2587 **Purpose:** Upload local commits that the remote Hub does not yet have.
2588 Enables collaborative workflows where one musician pushes and others pull.
2589 Supports force-push, lease-guarded override, tag syncing, and upstream tracking.
2590
2591 **Usage:**
2592 ```bash
2593 muse push
2594 muse push --branch feature/groove-v2
2595 muse push --remote staging
2596 muse push --force-with-lease
2597 muse push --force -f
2598 muse push --tags
2599 muse push --set-upstream -u
2600 ```
2601
2602 **Flags:**
2603 | Flag | Type | Default | Description |
2604 |------|------|---------|-------------|
2605 | `--branch` / `-b` | str | current branch | Branch to push |
2606 | `--remote` | str | `origin` | Named remote to push to |
2607 | `--force` / `-f` | flag | off | Overwrite remote branch even on non-fast-forward. Use with caution — this discards remote history. |
2608 | `--force-with-lease` | flag | off | Overwrite remote only if its current HEAD matches our last-known tracking pointer. Safer than `--force`; the Hub must return HTTP 409 if the remote has advanced. |
2609 | `--tags` | flag | off | Push all VCS-style tag refs from `.muse/refs/tags/` alongside the branch commits. |
2610 | `--set-upstream` / `-u` | flag | off | After a successful push, record the remote as the upstream for the current branch in `.muse/config.toml`. |
2611
2612 **Push algorithm:**
2613 1. Read `repo_id` from `.muse/repo.json` and branch from `.muse/HEAD`.
2614 2. Read local HEAD commit from `.muse/refs/heads/<branch>`.
2615 3. Resolve remote URL from `[remotes.<name>] url` in `.muse/config.toml`.
2616 4. Read last-known remote HEAD from `.muse/remotes/<name>/<branch>` (absent on first push).
2617 5. Compute delta: commits from local HEAD down to (but not including) remote HEAD.
2618 6. If `--tags`, enumerate `.muse/refs/tags/` and include as `PushTagPayload` list.
2619 7. POST `{ branch, head_commit_id, commits[], objects[], [force], [force_with_lease], [expected_remote_head], [tags] }` to `<remote>/push`.
2620 8. On HTTP 200, update `.muse/remotes/<name>/<branch>` to the new HEAD; if `--set-upstream`, write `branch = <branch>` under `[remotes.<name>]` in `.muse/config.toml`.
2621 9. On HTTP 409 with `--force-with-lease`, exit 1 with instructive message.
2622
2623 **Force-with-lease contract:** `expected_remote_head` is the commit ID in our local
2624 tracking pointer before the push. The Hub must compare it against its current HEAD and
2625 reject (HTTP 409) if they differ — this prevents clobbering commits pushed by others
2626 since our last fetch.
2627
2628 **Output example:**
2629 ```
2630 ⬆️ Pushing 3 commit(s) to origin/main [--force-with-lease] …
2631 ✅ Branch 'main' set to track 'origin/main'
2632 ✅ Pushed 3 commit(s) → origin/main [aabbccdd]
2633
2634 # When force-with-lease rejected:
2635 ❌ Push rejected: remote origin/main has advanced since last fetch.
2636 Run `muse pull` then retry, or use `--force` to override.
2637 ```
2638
2639 **Exit codes:** 0 — success; 1 — no remote, no commits, or force-with-lease mismatch; 3 — network/server error.
2640
2641 **Result type:** `PushRequest` / `PushResponse` — see `maestro/muse_cli/hub_client.py`.
2642 New TypedDicts: `PushTagPayload` (tag_name, commit_id).
2643
2644 **Agent use case:** After `muse commit`, an agent runs `muse push` to publish
2645 the committed variation to the shared Hub. For CI workflows, `--force-with-lease`
2646 prevents clobbering concurrent pushes from other agents.
2647
2648 ---
2649
2650 ### `muse pull`
2651
2652 **Purpose:** Download commits from the remote Hub that are missing locally,
2653 then integrate them into the local branch via fast-forward, merge, or rebase.
2654 After pull, the AI agent has the full commit history of remote collaborators
2655 available for `muse context`, `muse diff`, `muse ask`, etc.
2656
2657 **Usage:**
2658 ```bash
2659 muse pull
2660 muse pull --rebase
2661 muse pull --ff-only
2662 muse pull --branch feature/groove-v2
2663 muse pull --remote staging
2664 ```
2665
2666 **Flags:**
2667 | Flag | Type | Default | Description |
2668 |------|------|---------|-------------|
2669 | `--branch` / `-b` | str | current branch | Branch to pull |
2670 | `--remote` | str | `origin` | Named remote to pull from |
2671 | `--rebase` | flag | off | After fetching, rebase local commits onto remote HEAD rather than merge. Fast-forwards when remote is simply ahead; replays local commits (linear rebase) when diverged. |
2672 | `--ff-only` | flag | off | Only integrate if the result would be a fast-forward. Fails with exit 1 and leaves local branch unchanged if branches have diverged. |
2673
2674 **Pull algorithm:**
2675 1. Resolve remote URL from `[remotes.<name>] url` in `.muse/config.toml`.
2676 2. Collect `have_commits` (all local commit IDs) and `have_objects` (all local object IDs).
2677 3. POST `{ branch, have_commits[], have_objects[], [rebase], [ff_only] }` to `<remote>/pull`.
2678 4. Store returned commits and object descriptors in local Postgres.
2679 5. Update `.muse/remotes/<name>/<branch>` tracking pointer.
2680 6. Apply post-fetch integration strategy:
2681 - **Default:** If diverged, print warning and suggest `muse merge`.
2682 - **`--ff-only`:** If `local_head` is ancestor of `remote_head`, advance branch ref (fast-forward). Otherwise exit 1.
2683 - **`--rebase`:** If `local_head` is ancestor of `remote_head`, fast-forward. If diverged, find merge base and replay local commits above base onto `remote_head` using `compute_commit_tree_id` (deterministic IDs).
2684
2685 **Rebase contract:** Linear rebase only — no path-level conflict detection.
2686 For complex divergence with conflicting file changes, use `muse merge`.
2687 The rebased commit IDs are deterministic (via `compute_commit_tree_id`), so
2688 re-running the same rebase is idempotent.
2689
2690 **Divergence detection:** Pull succeeds (exit 0) even when diverged in default
2691 mode. The divergence warning is informational.
2692
2693 **Output example:**
2694 ```
2695 ⬇️ Pulling origin/main (--rebase) …
2696 ✅ Fast-forwarded main → aabbccdd
2697 ✅ Pulled 2 new commit(s), 5 new object(s) from origin/main
2698
2699 # Diverged + --rebase:
2700 ⟳ Rebasing 2 local commit(s) onto aabbccdd …
2701 ✅ Rebase complete — main → eeff1122
2702 ✅ Pulled 3 new commit(s), 0 new object(s) from origin/main
2703
2704 # Diverged + --ff-only:
2705 ❌ Cannot fast-forward: main has diverged from origin/main.
2706 Run `muse merge origin/main` or use `muse pull --rebase` to integrate.
2707 ```
2708
2709 **Exit codes:** 0 — success; 1 — no remote, or `--ff-only` on diverged branch; 3 — network/server error.
2710
2711 **Result type:** `PullRequest` / `PullResponse` — see `maestro/muse_cli/hub_client.py`.
2712
2713 **Agent use case:** Before generating a new arrangement, an agent runs
2714 `muse pull --rebase` to ensure it works from the latest shared composition
2715 state with a clean linear history. `--ff-only` is useful in strict CI pipelines
2716 where merges are not permitted.
2717
2718 ---
2719
2720 ### `muse clone`
2721
2722 **Purpose:** Clone a remote Muse Hub repository into a new local directory — the
2723 entry point for collaboration. A session musician or AI agent calls `muse clone`
2724 once to obtain a local copy of a producer's project. Subsequent `muse pull` and
2725 `muse push` operations use the "origin" remote written by `muse clone`.
2726
2727 **Usage:**
2728 ```bash
2729 muse clone <url> [directory] [OPTIONS]
2730 ```
2731
2732 **Flags:**
2733 | Flag | Type | Default | Description |
2734 |------|------|---------|-------------|
2735 | `<url>` | positional | — | Muse Hub repository URL (e.g. `https://hub.stori.app/repos/<repo_id>`) |
2736 | `[directory]` | positional | repo name from URL | Local directory to clone into |
2737 | `--depth N` | int | None | Shallow clone: fetch only the last N commits |
2738 | `--branch TEXT` | str | Hub default | Clone and check out a specific branch |
2739 | `--single-track TEXT` | str | None | Restrict downloaded files to one instrument track |
2740 | `--no-checkout` | flag | off | Set up `.muse/` and fetch objects but leave `muse-work/` empty |
2741
2742 **Output example:**
2743 ```
2744 Cloning into 'producer-beats' …
2745 ✅ Cloned: 12 commit(s), 48 object(s) → 'producer-beats'
2746
2747 # Shallow clone (last commit only)
2748 Cloning into 'producer-beats' …
2749 ✅ Cloned (depth 1): 1 commit(s), 4 object(s) → 'producer-beats'
2750
2751 # Keys-only track clone
2752 Cloning into 'producer-beats' …
2753 ✅ Cloned, track='keys': 12 commit(s), 8 object(s) → 'producer-beats'
2754 ```
2755
2756 **Result type:** `CloneRequest` / `CloneResponse` — see `maestro/muse_cli/hub_client.py`.
2757
2758 Fields of `CloneResponse`:
2759 - `repo_id: str` — canonical Hub identifier, written to `.muse/repo.json`
2760 - `default_branch: str` — branch HEAD was cloned from
2761 - `remote_head: str | None` — HEAD commit ID on the remote branch
2762 - `commits: list[CloneCommitPayload]` — commit DAG to seed local DB
2763 - `objects: list[CloneObjectPayload]` — content-addressed object descriptors
2764
2765 **Exit codes:** 0 — success; 1 — target directory already exists or bad args; 3 — network/server error.
2766
2767 **Agent use case:** An AI agent clones the producer's project to inspect the
2768 commit history (`muse log`), understand the musical state (`muse context`), and
2769 add new variations before pushing back. `--single-track keys` lets the keys
2770 agent download only keyboard files, avoiding multi-gigabyte drum/bass downloads.
2771 `--no-checkout` is useful when an agent only needs the commit graph metadata, not
2772 the working-tree snapshot.
2773
2774 ---
2775
2776 ### `muse fetch`
2777
2778 **Purpose:** Update remote-tracking refs to reflect the current state of the remote
2779 without modifying the local branch or muse-work/. Use `muse fetch` when you want
2780 to inspect what collaborators have pushed before deciding whether to merge. This
2781 is the non-destructive alternative to `muse pull` (fetch + merge).
2782
2783 **Usage:**
2784 ```bash
2785 muse fetch
2786 muse fetch --all
2787 muse fetch --prune
2788 muse fetch --remote staging --branch main --branch feature/bass-v2
2789 ```
2790
2791 **Flags:**
2792 | Flag | Type | Default | Description |
2793 |------|------|---------|-------------|
2794 | `--remote` | str | `origin` | Named remote to fetch from |
2795 | `--all` | flag | off | Fetch from every configured remote |
2796 | `--prune` / `-p` | flag | off | Remove local remote-tracking refs for branches deleted on the remote |
2797 | `--branch` / `-b` | str (repeatable) | all branches | Specific branch(es) to fetch |
2798
2799 **Fetch algorithm:**
2800 1. Resolve remote URL(s) from `[remotes.<name>] url` in `.muse/config.toml`.
2801 2. POST `{ branches: [] }` (empty = all) to `<remote>/fetch`.
2802 3. For each branch in the Hub response, update `.muse/remotes/<remote>/<branch>` with the remote HEAD commit ID.
2803 4. If `--prune`, remove any `.muse/remotes/<remote>/<branch>` files whose branch was NOT in the Hub response.
2804 5. Local branches (`refs/heads/`) and `muse-work/` are NEVER modified.
2805
2806 **Fetch vs Pull:**
2807 | Operation | Modifies local branch | Modifies muse-work/ | Merges remote commits |
2808 |-----------|----------------------|---------------------|----------------------|
2809 | `muse fetch` | No | No | No |
2810 | `muse pull` | Yes (via merge) | Yes | Yes |
2811
2812 **Output example:**
2813 ```
2814 From origin: + abc1234 feature/guitar -> origin/feature/guitar (new branch)
2815 From origin: + def5678 main -> origin/main
2816 ✅ origin is already up to date.
2817
2818 # With --all:
2819 From origin: + abc1234 main -> origin/main
2820 From staging: + xyz9999 main -> staging/main
2821 ✅ Fetched 2 branch update(s) across all remotes.
2822
2823 # With --prune:
2824 ✂️ Pruned origin/deleted-branch (no longer exists on remote)
2825 ```
2826
2827 **Exit codes:** 0 — success; 1 — no remote configured or `--all` with no remotes; 3 — network/server error.
2828
2829 **Result type:** `FetchRequest` / `FetchResponse` / `FetchBranchInfo` — see `maestro/muse_cli/hub_client.py`.
2830
2831 **Agent use case:** An agent runs `muse fetch` before deciding whether to compose a new
2832 variation, to check if remote collaborators have pushed conflicting changes. Since fetch
2833 does not modify the working tree, it is safe to run mid-composition without interrupting
2834 the current generation pipeline. Follow with `muse log origin/main` to inspect what
2835 arrived, then `muse merge origin/main` if the agent decides to incorporate remote changes.
2836
2837 ---
2838
2839 ## Muse CLI — Bisect Command Reference
2840
2841 `muse bisect` implements a binary search over the commit graph to identify the
2842 exact commit that introduced a musical regression — a rhythmic drift, mix
2843 artefact, or tonal shift. It is the music-domain analogue of `git bisect`.
2844
2845 Session state is persisted in `.muse/BISECT_STATE.json` across shell invocations.
2846
2847 ---
2848
2849 ### `muse bisect start`
2850
2851 **Purpose:** Open a bisect session and record the pre-bisect HEAD so that
2852 `muse bisect reset` can cleanly restore the workspace.
2853
2854 **Usage:**
2855 ```bash
2856 muse bisect start
2857 ```
2858
2859 **Blocked by:** `.muse/MERGE_STATE.json` (merge in progress) or an already-active
2860 `BISECT_STATE.json`.
2861
2862 **Output example:**
2863 ```
2864 ✅ Bisect session started.
2865 Now mark a good commit: muse bisect good <commit>
2866 And a bad commit: muse bisect bad <commit>
2867 ```
2868
2869 **Agent use case:** An AI agent detects rhythmic drift in the latest mix and
2870 opens a bisect session to automatically locate the offending commit.
2871
2872 **Implementation:** `maestro/muse_cli/commands/bisect.py`
2873
2874 ---
2875
2876 ### `muse bisect good <commit>`
2877
2878 **Purpose:** Mark *commit* as a known-good revision. Once both a good and bad
2879 bound are set, Muse selects the midpoint commit and checks it out into muse-work/
2880 for inspection.
2881
2882 **Usage:**
2883 ```bash
2884 muse bisect good [<commit>] # default: HEAD
2885 ```
2886
2887 **Flags:**
2888 | Flag | Type | Default | Description |
2889 |------|------|---------|-------------|
2890 | `<commit>` | positional | `HEAD` | Commit to mark as good: SHA, branch name, or `HEAD` |
2891
2892 **Output example:**
2893 ```
2894 ✅ Marked a1b2c3d4 as good. Checking out f9e8d7c6 (~2 step(s) remaining, 3 commits in range)
2895 ```
2896
2897 **Agent use case:** After listening to the muse-work/ snapshot and confirming the
2898 groove is intact, the agent marks the current commit as good and awaits the next
2899 midpoint checkout.
2900
2901 ---
2902
2903 ### `muse bisect bad <commit>`
2904
2905 **Purpose:** Mark *commit* as a known-bad revision. Mirrors `muse bisect good`.
2906
2907 **Usage:**
2908 ```bash
2909 muse bisect bad [<commit>] # default: HEAD
2910 ```
2911
2912 ---
2913
2914 ### `muse bisect run <cmd>`
2915
2916 **Purpose:** Automate the bisect loop. Runs *cmd* after each checkout; exit 0
2917 means good, any non-zero exit code means bad. Stops when the culprit is found.
2918
2919 **Usage:**
2920 ```bash
2921 muse bisect run <shell-command> [--max-steps N]
2922 ```
2923
2924 **Flags:**
2925 | Flag | Type | Default | Description |
2926 |------|------|---------|-------------|
2927 | `<cmd>` | positional | — | Shell command to test each midpoint |
2928 | `--max-steps` | int | 50 | Safety limit on bisect iterations |
2929
2930 **Output example:**
2931 ```
2932 ⟳ Testing f9e8d7c6…
2933 exit=1 → bad
2934 ✅ Marked f9e8d7c6 as bad. Checking out d3c2b1a0 (~1 step(s) remaining, 1 in range)
2935 ⟳ Testing d3c2b1a0…
2936 exit=0 → good
2937 🎯 Bisect complete! First bad commit: f9e8d7c6
2938 Run 'muse bisect reset' to restore your workspace.
2939 ```
2940
2941 **Result type:** `BisectStepResult` — fields: `culprit` (str | None),
2942 `next_commit` (str | None), `remaining` (int), `message` (str).
2943
2944 **Agent use case:** An AI orchestrator runs `muse bisect run python check_groove.py`
2945 to automate the full regression hunt without human input.
2946
2947 ---
2948
2949 ### `muse bisect reset`
2950
2951 **Purpose:** End the bisect session: restore `.muse/HEAD` and muse-work/ to the
2952 pre-bisect state, then remove `BISECT_STATE.json`.
2953
2954 **Usage:**
2955 ```bash
2956 muse bisect reset
2957 ```
2958
2959 **Output example:**
2960 ```
2961 ✅ muse-work/ restored (3 files) from pre-bisect snapshot.
2962 ✅ Bisect session ended.
2963 ```
2964
2965 ---
2966
2967 ### `muse bisect log`
2968
2969 **Purpose:** Display the verdicts recorded so far and the current good/bad bounds.
2970
2971 **Usage:**
2972 ```bash
2973 muse bisect log [--json]
2974 ```
2975
2976 **Flags:**
2977 | Flag | Type | Default | Description |
2978 |------|------|---------|-------------|
2979 | `--json` | flag | off | Emit structured JSON for agent consumption |
2980
2981 **Output example (default):**
2982 ```
2983 Bisect session state:
2984 good: a1b2c3d4...
2985 bad: f9e8d7c6...
2986 current: d3c2b1a0...
2987 tested (2 commit(s)):
2988 a1b2c3d4 good
2989 f9e8d7c6 bad
2990 ```
2991
2992 **Result type:** `BisectState` — fields: `good`, `bad`, `current`, `tested`,
2993 `pre_bisect_ref`, `pre_bisect_commit`.
2994
2995 **Agent use case:** An agent queries `muse bisect log --json` to resume a
2996 suspended bisect session and determine the next commit to test.
2997
2998 ---
2999
3000 ## Muse CLI — Music Analysis Command Reference
3001
3002 These commands expose musical dimensions across the commit graph — the layer that
3003 makes Muse fundamentally different from Git. Each command is consumed by AI agents
3004 to make musically coherent generation decisions. Every flag is part of a stable
3005 CLI contract; stub implementations are clearly marked.
3006
3007 **Agent pattern:** Run with `--json` to get machine-readable output. Pipe into
3008 `muse context` for a unified musical state document.
3009
3010 ---
3011
3012 ### `muse cat-object`
3013
3014 **Purpose:** Read and display a raw Muse object by its SHA-256 hash. The
3015 plumbing equivalent of `git cat-file` — lets an AI agent inspect any stored
3016 blob, snapshot manifest, or commit record without running the full `muse log`
3017 pipeline.
3018
3019 **Usage:**
3020 ```bash
3021 muse cat-object [OPTIONS] <object-id>
3022 ```
3023
3024 **Flags:**
3025
3026 | Flag | Type | Default | Description |
3027 |------|------|---------|-------------|
3028 | `<object-id>` | positional | required | Full 64-char SHA-256 hash to look up |
3029 | `-t / --type` | flag | off | Print only the object type (`object`, `snapshot`, or `commit`) |
3030 | `-p / --pretty` | flag | off | Pretty-print the object content as indented JSON |
3031
3032 `-t` and `-p` are mutually exclusive.
3033
3034 **Output example (default):**
3035 ```
3036 type: commit
3037 commit_id: a1b2c3d4...
3038 branch: main
3039 snapshot: f9e8d7c6...
3040 message: boom bap demo take 1
3041 parent: 00112233...
3042 committed_at: 2026-02-27T17:30:00+00:00
3043 ```
3044
3045 **Output example (`-t`):**
3046 ```
3047 commit
3048 ```
3049
3050 **Output example (`-p <snapshot_id>`):**
3051 ```json
3052 {
3053 "type": "snapshot",
3054 "snapshot_id": "f9e8d7c6...",
3055 "manifest": {
3056 "beat.mid": "a1b2c3d4...",
3057 "keys.mid": "11223344..."
3058 },
3059 "created_at": "2026-02-27T17:20:00+00:00"
3060 }
3061 ```
3062
3063 **Result type:** `CatObjectResult` — fields: `object_type` (str), `row`
3064 (MuseCliObject | MuseCliSnapshot | MuseCliCommit). Call `.to_dict()` for a
3065 JSON-serialisable representation.
3066
3067 **Agent use case:** Use `muse cat-object -t <hash>` to determine the type of
3068 an unknown ID before deciding how to process it. Use `-p` to extract the
3069 snapshot manifest (file → object_id map) or commit metadata for downstream
3070 generation context. Combine with `muse log` short IDs: copy the full commit_id
3071 from `muse log`, then `muse cat-object -p <id>` to inspect its snapshot.
3072
3073 **Error behaviour:** Exits with code 1 (`USER_ERROR`) when the ID is not found
3074 in any object table; prints `❌ Object not found: <id>`.
3075
3076 ---
3077
3078 ### `muse harmony`
3079
3080 **Purpose:** Analyze the harmonic content (key center, mode, chord progression, harmonic
3081 rhythm, and tension profile) of a commit. The primary tool for understanding what a
3082 composition is doing harmonically — information that is completely invisible to Git.
3083 An AI agent calling `muse harmony --json` knows whether the current arrangement is in
3084 Eb major with a II-V-I progression and moderate tension, and can use this to make
3085 musically coherent generation decisions.
3086
3087 **Usage:**
3088 ```bash
3089 muse harmony [<commit>] [OPTIONS]
3090 ```
3091
3092 **Flags:**
3093
3094 | Flag | Type | Default | Description |
3095 |------|------|---------|-------------|
3096 | `--track TEXT` | string | all tracks | Restrict to a named MIDI track (e.g. `--track keys`) |
3097 | `--section TEXT` | string | — | Restrict to a named musical section/region (planned) |
3098 | `--compare COMMIT` | string | — | Compare harmonic content against another commit |
3099 | `--range FROM..TO` | string | — | Analyze across a commit range (planned) |
3100 | `--progression` | flag | off | Show only the chord progression sequence |
3101 | `--key` | flag | off | Show only the detected key center |
3102 | `--mode` | flag | off | Show only the detected mode |
3103 | `--tension` | flag | off | Show only the harmonic tension profile |
3104 | `--json` | flag | off | Emit structured JSON for agent consumption |
3105
3106 **Output example (text):**
3107 ```
3108 Commit abc1234 — Harmonic Analysis
3109 (stub — full MIDI analysis pending)
3110
3111 Key: Eb (confidence: 0.92)
3112 Mode: major
3113 Chord progression: Ebmaj7 | Fm7 | Bb7sus4 | Bb7 | Ebmaj7 | Abmaj7 | Gm7 | Cm7
3114 Harmonic rhythm: 2.1 chords/bar avg
3115 Tension profile: Low → Medium → High → Resolution (textbook tension-release arc) [0.2 → 0.4 → 0.8 → 0.3]
3116 ```
3117
3118 **Output example (`--json`):**
3119 ```json
3120 {
3121 "commit_id": "abc1234",
3122 "branch": "main",
3123 "key": "Eb",
3124 "mode": "major",
3125 "confidence": 0.92,
3126 "chord_progression": ["Ebmaj7", "Fm7", "Bb7sus4", "Bb7", "Ebmaj7", "Abmaj7", "Gm7", "Cm7"],
3127 "harmonic_rhythm_avg": 2.1,
3128 "tension_profile": [0.2, 0.4, 0.8, 0.3],
3129 "track": "all",
3130 "source": "stub"
3131 }
3132 ```
3133
3134 **Output example (`--compare <commit> --json`):**
3135 ```json
3136 {
3137 "head": { "commit_id": "abc1234", "key": "Eb", "mode": "major", ... },
3138 "compare": { "commit_id": "def5678", "key": "Eb", "mode": "major", ... },
3139 "key_changed": false,
3140 "mode_changed": false,
3141 "chord_progression_delta": []
3142 }
3143 ```
3144
3145 **Result type:** `HarmonyResult` — fields: `commit_id`, `branch`, `key`, `mode`,
3146 `confidence`, `chord_progression`, `harmonic_rhythm_avg`, `tension_profile`, `track`, `source`.
3147 Compare path returns `HarmonyCompareResult` — fields: `head`, `compare`, `key_changed`,
3148 `mode_changed`, `chord_progression_delta`.
3149
3150 **Agent use case:** Before generating a new instrument layer, an agent calls
3151 `muse harmony --json` to discover the harmonic context. If the arrangement is in
3152 Eb major with a II-V-I progression, the agent ensures its generated voicings stay
3153 diatonic to Eb. If the tension profile shows a build toward the chorus, the agent
3154 adds chromatic tension at the right moment rather than resolving early.
3155 `muse harmony --compare HEAD~5 --json` reveals whether the composition has
3156 modulated, shifted mode, or changed its harmonic rhythm — all decisions an AI
3157 needs to make coherent musical choices across versions.
3158
3159 **HTTP API endpoint (issue #414):**
3160
3161 ```
3162 GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/harmony
3163 ```
3164
3165 Returns `HarmonyAnalysisResponse` — a Roman-numeral-centric view of the harmonic content.
3166 Unlike the generic `/analysis/{ref}/{dimension}` endpoint (which returns the dimension
3167 envelope with `HarmonyData`), this dedicated endpoint returns:
3168
3169 | Field | Type | Description |
3170 |-------|------|-------------|
3171 | `key` | string | Full key label, e.g. `"C major"`, `"F# minor"` |
3172 | `mode` | string | Detected mode: `major`, `minor`, `dorian`, etc. |
3173 | `romanNumerals` | `RomanNumeralEvent[]` | Chord events with beat, Roman symbol, root, quality, function |
3174 | `cadences` | `CadenceEvent[]` | Phrase-ending cadences with beat, type, from/to chord |
3175 | `modulations` | `HarmonyModulationEvent[]` | Key-area changes with from/to key and pivot chord |
3176 | `harmonicRhythmBpm` | float | Rate of chord changes in chords per minute |
3177
3178 Optional query params: `?track=<instrument>`, `?section=<label>`.
3179 Auth: public repos accessible without token; private repos require Bearer JWT.
3180 Cache: `ETag` and `Cache-Control: private, max-age=60` headers included.
3181
3182 **Agent use case:** An agent that needs to compose a harmonically coherent continuation
3183 calls this endpoint to get Roman numerals (tonal function), cadence positions (phrase
3184 boundaries), and any modulations (tonal narrative), without having to parse raw chord
3185 symbols from the generic `HarmonyData` model.
3186
3187 **Implementation:** `maestro/api/routes/musehub/analysis.py` (`harmony_router` / `get_harmony_analysis`),
3188 `maestro/services/musehub_analysis.py` (`compute_harmony_analysis`),
3189 `maestro/models/musehub_analysis.py` (`RomanNumeralEvent`, `CadenceEvent`, `HarmonyModulationEvent`, `HarmonyAnalysisResponse`).
3190
3191 **Implementation:** `maestro/muse_cli/commands/harmony.py` — `_harmony_analyze_async`
3192 (injectable async core), `HarmonyResult` / `HarmonyCompareResult` (TypedDict result
3193 entities), `_stub_harmony` (placeholder data), `_tension_label` (arc classifier),
3194 `_render_result_human` / `_render_result_json` / `_render_compare_human` /
3195 `_render_compare_json` (renderers). Exit codes: 0 success, 2 outside repo, 3 internal.
3196
3197 > **Stub note:** Chord detection, key inference, and tension computation are placeholder
3198 > values derived from a static Eb major II-V-I template. Full implementation requires
3199 > MIDI note extraction from committed snapshot objects (future: Storpheus chord detection
3200 > route). The CLI contract, result types, and flag set are stable.
3201
3202 ---
3203
3204 ### `muse dynamics`
3205
3206 **Purpose:** Analyze the velocity (loudness) profile of a commit across all instrument
3207 tracks. The primary tool for understanding the dynamic arc of an arrangement and
3208 detecting flat, robotic, or over-compressed MIDI.
3209
3210 **Usage:**
3211 ```bash
3212 muse dynamics [<commit>] [OPTIONS]
3213 ```
3214
3215 **Flags:**
3216
3217 | Flag | Type | Default | Description |
3218 |------|------|---------|-------------|
3219 | `COMMIT` | positional | HEAD | Commit ref to analyze |
3220 | `--track TEXT` | string | all tracks | Case-insensitive prefix filter (e.g. `--track bass`) |
3221 | `--section TEXT` | string | — | Restrict to a named section/region (planned) |
3222 | `--compare COMMIT` | string | — | Side-by-side comparison with another commit (planned) |
3223 | `--history` | flag | off | Show dynamics for every commit in branch history (planned) |
3224 | `--peak` | flag | off | Show only tracks whose peak velocity exceeds the branch average |
3225 | `--range` | flag | off | Sort output by velocity range descending |
3226 | `--arc` | flag | off | When combined with `--track`, treat its value as an arc label filter |
3227 | `--json` | flag | off | Emit structured JSON for agent consumption |
3228
3229 **Arc labels:**
3230
3231 | Label | Meaning |
3232 |-------|---------|
3233 | `flat` | Velocity variance < 10; steady throughout |
3234 | `crescendo` | Monotonically rising from start to end |
3235 | `decrescendo` | Monotonically falling from start to end |
3236 | `terraced` | Step-wise plateaus; sudden jumps between stable levels |
3237 | `swell` | Rises then falls (arch shape) |
3238
3239 **Output example (text):**
3240 ```
3241 Dynamic profile — commit a1b2c3d4 (HEAD -> main)
3242
3243 Track Avg Vel Peak Range Arc
3244 --------- ------- ---- ----- -----------
3245 drums 88 110 42 terraced
3246 bass 72 85 28 flat
3247 keys 64 95 56 crescendo
3248 lead 79 105 38 swell
3249 ```
3250
3251 **Output example (`--json`):**
3252 ```json
3253 {
3254 "commit": "a1b2c3d4",
3255 "branch": "main",
3256 "tracks": [
3257 {"track": "drums", "avg_velocity": 88, "peak_velocity": 110, "velocity_range": 42, "arc": "terraced"}
3258 ]
3259 }
3260 ```
3261
3262 **Result type:** `TrackDynamics` — fields: `name`, `avg_velocity`, `peak_velocity`, `velocity_range`, `arc`
3263
3264 **Agent use case:** Before generating a new layer, an agent calls `muse dynamics --json` to understand the current velocity landscape. If the arrangement is `flat` across all tracks, the agent adds velocity variation to the new part. If the arc is `crescendo`, the agent ensures the new layer contributes to rather than fights the build.
3265
3266 **Implementation:** `maestro/muse_cli/commands/dynamics.py` — `_dynamics_async` (injectable async core), `TrackDynamics` (result entity), `_render_table` / `_render_json` (renderers). Exit codes: 0 success, 2 outside repo, 3 internal.
3267
3268 > **Stub note:** Arc classification and velocity statistics are placeholder values. Full implementation requires MIDI note velocity extraction from committed snapshot objects (future: Storpheus MIDI parse route).
3269
3270 ---
3271
3272 ### `muse swing`
3273 ## `muse swing` — Swing Factor Analysis and Annotation
3274
3275 **Purpose:** Measure or annotate the swing factor of a commit — the ratio that
3276 distinguishes a straight 8th-note grid from a shuffled jazz feel. Swing is one
3277 of the most musically critical dimensions and is completely invisible to Git.
3278
3279 **Usage:**
3280 ```bash
3281 muse swing [<commit>] [OPTIONS]
3282 ```
3283
3284 **Flags:**
3285
3286 | Flag | Type | Default | Description |
3287 |------|------|---------|-------------|
3288 | `COMMIT` | positional | working tree | Commit SHA to analyze |
3289 | `--set FLOAT` | float | — | Annotate with an explicit swing factor (0.5–0.67) |
3290 | `--detect` | flag | on | Detect and display the swing factor (default) |
3291 | `--track TEXT` | string | all | Restrict to a named MIDI track (e.g. `--track bass`) |
3292 | `--compare COMMIT` | string | — | Compare HEAD swing against another commit |
3293 | `--history` | flag | off | Show swing history for the current branch |
3294 | `--json` | flag | off | Emit machine-readable JSON |
3295
3296 **Swing factor scale:**
3297
3298 | Range | Label | Feel |
3299 |-------|-------|------|
3300 | < 0.53 | Straight | Equal 8th notes — pop, EDM, quantized |
3301 | 0.53–0.58 | Light | Subtle shuffle — R&B, Neo-soul |
3302 | 0.58–0.63 | Medium | Noticeable swing — jazz, hip-hop |
3303 | ≥ 0.63 | Hard | Triplet feel — bebop, heavy jazz |
3304
3305 **Output example (text):**
3306 ```
3307 Swing factor: 0.55 (Light)
3308 Commit: a1b2c3d4 Branch: main
3309 Track: all
3310 (stub — full MIDI analysis pending)
3311 ```
3312
3313 **Output example (`--json`):**
3314 ```json
3315 {"factor": 0.55, "label": "Light", "commit": "a1b2c3d4", "branch": "main", "track": "all"}
3316 ```
3317
3318 **Result type:** `dict` with keys `factor` (float), `label` (str), `commit` (str), `branch` (str), `track` (str). Future: typed `SwingResult` dataclass.
3319
3320 **Agent use case:** An AI generating a bass line runs `muse swing --json` to know whether to quantize straight or add shuffle. A Medium swing result means the bass should land slightly behind the grid to stay in pocket with the existing drum performance.
3321
3322 **Implementation:** `maestro/muse_cli/commands/swing.py` — `swing_label()`, `_swing_detect_async()`, `_swing_history_async()`, `_swing_compare_async()`, formatters. Exit codes: 0 success, 1 invalid `--set` value, 2 outside repo.
3323
3324 > **Stub note:** Returns a placeholder factor of 0.55. Full implementation requires onset-to-onset ratio measurement from committed MIDI note events (future: Storpheus MIDI parse route).
3325
3326 ---
3327
3328 ### `muse transpose`
3329
3330 **Purpose:** Apply MIDI pitch transposition to all files in `muse-work/` and record the result as a new Muse commit. Transposition is the most fundamental musical transformation — this makes it a first-class versioned operation rather than a silent destructive edit. Drum channels (MIDI channel 9) are always excluded because drums are unpitched.
3331
3332 **Usage:**
3333 ```bash
3334 muse transpose <interval> [<commit>] [OPTIONS]
3335 ```
3336
3337 **Flags:**
3338
3339 | Flag | Type | Default | Description |
3340 |------|------|---------|-------------|
3341 | `<interval>` | positional | required | Signed integer (`+3`, `-5`) or named interval (`up-minor3rd`, `down-perfect5th`) |
3342 | `[<commit>]` | positional | HEAD | Source commit to transpose from |
3343 | `--track TEXT` | string | all tracks | Transpose only the MIDI track whose name contains TEXT (case-insensitive substring) |
3344 | `--section TEXT` | string | — | Transpose only a named section (stub — full implementation pending) |
3345 | `--message TEXT` | string | `"Transpose +N semitones"` | Custom commit message |
3346 | `--dry-run` | flag | off | Show what would change without writing files or creating a commit |
3347 | `--json` | flag | off | Emit machine-readable JSON output |
3348
3349 **Interval syntax:**
3350
3351 | Form | Example | Semitones |
3352 |------|---------|-----------|
3353 | Signed integer | `+3` | +3 |
3354 | Signed integer | `-5` | -5 |
3355 | Named up | `up-minor3rd` | +3 |
3356 | Named down | `down-perfect5th` | -7 |
3357 | Named down | `down-octave` | -12 |
3358
3359 **Named interval identifiers:**
3360 `unison`, `minor2nd`, `major2nd`, `minor3rd`, `major3rd`, `perfect4th`,
3361 `perfect5th`, `minor6th`, `major6th`, `minor7th`, `major7th`, `octave`
3362 (prefix with `up-` or `down-`)
3363
3364 **Output example (text):**
3365 ```
3366 ✅ [a1b2c3d4] Transpose +3 semitones
3367 Key: Eb major → F# major
3368 Modified: 2 file(s)
3369 ✅ tracks/melody.mid
3370 ✅ tracks/bass.mid
3371 Skipped: 1 file(s) (non-MIDI or no pitched notes)
3372 ```
3373
3374 **Output example (`--json`):**
3375 ```json
3376 {
3377 "source_commit_id": "a1b2c3d4...",
3378 "semitones": 3,
3379 "files_modified": ["tracks/melody.mid", "tracks/bass.mid"],
3380 "files_skipped": ["notes.json"],
3381 "new_commit_id": "b2c3d4e5...",
3382 "original_key": "Eb major",
3383 "new_key": "F# major",
3384 "dry_run": false
3385 }
3386 ```
3387
3388 **Result type:** `TransposeResult` — fields: `source_commit_id`, `semitones`, `files_modified`, `files_skipped`, `new_commit_id` (None in dry-run), `original_key`, `new_key`, `dry_run`.
3389
3390 **Key metadata update:** If the source commit has a `key` field in its `metadata` JSON blob (e.g. `"Eb major"`), the new commit's `metadata.key` is automatically updated to reflect the transposition (e.g. `"F# major"` after `+3`). The service uses flat note names for accidentals (Db, Eb, Ab, Bb) — G# is stored as Ab, etc.
3391
3392 **MIDI transposition rules:**
3393 - Scans `muse-work/` recursively for `.mid` and `.midi` files.
3394 - Parses MTrk chunks and modifies Note-On (0x9n) and Note-Off (0x8n) events.
3395 - **Channel 9 (drums) is never transposed** — drums are unpitched and shifting their note numbers would change the GM drum map mapping.
3396 - Notes are clamped to [0, 127] to stay within MIDI range.
3397 - All other events (meta, sysex, CC, program change, pitch bend) are preserved byte-for-byte.
3398 - Track length headers remain unchanged — only note byte values differ.
3399
3400 **Agent use case:** A producer experimenting with key runs `muse transpose +3` and immediately has a versioned, reversible pitch shift on the full arrangement. The agent can then run `muse context --json` to confirm the new key before generating new parts that fit the updated harmonic center. The `--dry-run` flag lets agents preview impact before committing, and the `--track` flag lets them scope transposition to a single instrument (e.g. `--track melody`) without shifting the bass or chords.
3401
3402 **Implementation:** `maestro/services/muse_transpose.py` — `parse_interval`, `update_key_metadata`, `transpose_midi_bytes`, `apply_transpose_to_workdir`, `TransposeResult`. CLI: `maestro/muse_cli/commands/transpose.py` — `_transpose_async` (injectable async core), `_print_result` (renderer). Exit codes: 0 success, 1 user error (bad interval, empty workdir), 2 outside repo, 3 internal error.
3403
3404 > **Section filter note:** `--section TEXT` is accepted by the CLI and logged as a warning but not yet applied. Full section-scoped transposition requires section boundary markers embedded in committed MIDI metadata — tracked as a follow-up enhancement.
3405
3406 ---
3407
3408 ### `muse recall`
3409
3410 **Purpose:** Search the full commit history using natural language. Returns ranked
3411 commits whose messages best match the query. The musical memory retrieval command —
3412 "find me that arrangement I made three months ago."
3413
3414 **Usage:**
3415 ```bash
3416 muse recall "<description>" [OPTIONS]
3417 ```
3418
3419 **Flags:**
3420
3421 | Flag | Type | Default | Description |
3422 |------|------|---------|-------------|
3423 | `QUERY` | positional | required | Natural-language description of what to find |
3424 | `--limit N` | int | 5 | Maximum results to return |
3425 | `--threshold FLOAT` | float | 0.6 | Minimum similarity score (0.0–1.0) |
3426 | `--branch TEXT` | string | all branches | Restrict search to a specific branch |
3427 | `--since DATE` | `YYYY-MM-DD` | — | Only search commits after this date |
3428 | `--until DATE` | `YYYY-MM-DD` | — | Only search commits before this date |
3429 | `--json` | flag | off | Emit structured JSON array |
3430
3431 **Scoring (current stub):** Normalized keyword overlap coefficient — `|Q ∩ M| / |Q|` — where Q is the set of query tokens and M is the set of message tokens. Score 1.0 means every query word appeared in the commit message.
3432
3433 **Output example (text):**
3434 ```
3435 Recall: "dark jazz bassline"
3436 keyword match · threshold 0.60 · limit 5
3437
3438 1. [a1b2c3d4] 2026-02-15 22:00 boom bap demo take 3 score 0.67
3439 2. [f9e8d7c6] 2026-02-10 18:30 jazz bass overdub session score 0.50
3440 ```
3441
3442 **Result type:** `RecallResult` (TypedDict) — fields: `rank` (int), `score` (float), `commit_id` (str), `date` (str), `branch` (str), `message` (str)
3443
3444 **Agent use case:** An agent asked to "generate something like that funky bass riff from last month" calls `muse recall "funky bass" --json --limit 3` to retrieve the closest historical commits, then uses those as style references for generation.
3445
3446 **Implementation:** `maestro/muse_cli/commands/recall.py` — `RecallResult` (TypedDict), `_tokenize()`, `_score()`, `_recall_async()`. Exit codes: 0 success, 1 bad date format, 2 outside repo.
3447
3448 > **Stub note:** Uses keyword overlap. Full implementation: vector embeddings stored in Qdrant, cosine similarity retrieval. The CLI interface will not change when vector search is added.
3449
3450 ---
3451
3452 ### `muse rebase`
3453
3454 **Purpose:** Rebase commits onto a new base, producing a linear history. Given a current branch that has diverged from `<upstream>`, `muse rebase <upstream>` collects all commits since the divergence point and replays them one-by-one on top of the upstream tip — each producing a new commit ID with the same snapshot delta. An AI agent uses this to linearise a sequence of late-night fixup commits before merging to main, making the musical narrative readable and bisectable.
3455
3456 **Usage:**
3457 ```bash
3458 muse rebase <upstream> [OPTIONS]
3459 muse rebase --continue
3460 muse rebase --abort
3461 ```
3462
3463 **Flags:**
3464
3465 | Flag | Type | Default | Description |
3466 |------|------|---------|-------------|
3467 | `UPSTREAM` | positional | — | Branch name or commit ID to rebase onto. Omit with `--continue` / `--abort`. |
3468 | `--interactive` / `-i` | flag | off | Open `$EDITOR` with a rebase plan (pick/squash/drop per commit) before executing. |
3469 | `--autosquash` | flag | off | Automatically move `fixup! <msg>` commits immediately after their matching target commit. |
3470 | `--rebase-merges` | flag | off | Preserve merge commits during replay (experimental). |
3471 | `--continue` | flag | off | Resume a rebase that was paused by a conflict. |
3472 | `--abort` | flag | off | Cancel the in-progress rebase and restore the branch to its original HEAD. |
3473
3474 **Output example (linear rebase):**
3475 ```
3476 ✅ Rebased 3 commit(s) onto 'dev' [main a1b2c3d4]
3477 ```
3478
3479 **Output example (conflict):**
3480 ```
3481 ❌ Conflict while replaying c2d3e4f5 ('Add strings'):
3482 both modified: tracks/strings.mid
3483 Resolve conflicts, then run 'muse rebase --continue'.
3484 ```
3485
3486 **Output example (abort):**
3487 ```
3488 ✅ Rebase aborted. Branch 'main' restored to deadbeef.
3489 ```
3490
3491 **Interactive plan format:**
3492 ```
3493 # Interactive rebase plan.
3494 # Actions: pick, squash (fold into previous), drop (skip), fixup (squash no msg), reword
3495 # Lines starting with '#' are ignored.
3496
3497 pick a1b2c3d4 Add piano
3498 squash b2c3d4e5 Tweak piano velocity
3499 drop c3d4e5f6 Stale WIP commit
3500 pick d4e5f6a7 Add strings
3501 ```
3502
3503 **Result type:** `RebaseResult` (dataclass, frozen) — fields:
3504 - `branch` (str): The branch that was rebased.
3505 - `upstream` (str): The upstream branch or commit ref.
3506 - `upstream_commit_id` (str): Resolved commit ID of the upstream tip.
3507 - `base_commit_id` (str): LCA commit where the histories diverged.
3508 - `replayed` (tuple[RebaseCommitPair, ...]): Ordered list of (original, new) commit ID pairs.
3509 - `conflict_paths` (tuple[str, ...]): Conflicting paths (empty on clean completion).
3510 - `aborted` (bool): True when `--abort` cleared the in-progress rebase.
3511 - `noop` (bool): True when there were no commits to replay.
3512 - `autosquash_applied` (bool): True when `--autosquash` reordered commits.
3513
3514 **State file:** `.muse/REBASE_STATE.json` — written on conflict; cleared on `--continue` completion or `--abort`. Contains: `upstream_commit`, `base_commit`, `original_branch`, `original_head`, `commits_to_replay`, `current_onto`, `completed_pairs`, `current_commit`, `conflict_paths`.
3515
3516 **Agent use case:** An agent that maintains a feature branch can call `muse rebase dev` before opening a merge request. If conflicts are detected, the agent receives the conflict paths in `REBASE_STATE.json`, resolves them by picking the correct version of each affected file, then calls `muse rebase --continue`. The `--autosquash` flag is useful after a generation loop that emits intermediate `fixup!` commits — the agent can clean up history automatically before finalising.
3517
3518 **Algorithm:**
3519 1. LCA of HEAD and upstream (via BFS over the commit graph).
3520 2. Collect commits on the current branch since the LCA (oldest first).
3521 3. For each commit, compute its snapshot delta relative to its own parent.
3522 4. Apply the delta onto the current onto-tip manifest; detect conflicts.
3523 5. On conflict: write `REBASE_STATE.json` and exit 1 (await `--continue`).
3524 6. On success: insert a new commit record; advance the onto pointer.
3525 7. After all commits: write the final commit ID to the branch ref.
3526
3527 **Implementation:** `maestro/muse_cli/commands/rebase.py` (Typer CLI), `maestro/services/muse_rebase.py` (`_rebase_async`, `_rebase_continue_async`, `_rebase_abort_async`, `RebaseResult`, `RebaseState`, `InteractivePlan`, `compute_delta`, `apply_delta`, `apply_autosquash`).
3528
3529 ---
3530
3531 ### `muse stash`
3532
3533 **Purpose:** Temporarily shelve uncommitted muse-work/ changes so the producer can switch context without losing work-in-progress. Push saves the current working state into a filesystem stack (`.muse/stash/`) and restores HEAD; pop brings it back. An AI agent uses this when it needs to checkpoint partial generation state, switch to a different branch task, then resume exactly where it left off.
3534
3535 **Usage:**
3536 ```bash
3537 muse stash [push] [OPTIONS] # save + restore HEAD (default subcommand)
3538 muse stash push [OPTIONS] # explicit push
3539 muse stash pop [stash@{N}] # apply + drop most recent entry
3540 muse stash apply [stash@{N}] # apply without dropping
3541 muse stash list # list all entries
3542 muse stash drop [stash@{N}] # remove a specific entry
3543 muse stash clear [--yes] # remove all entries
3544 ```
3545
3546 **Flags (push):**
3547
3548 | Flag | Type | Default | Description |
3549 |------|------|---------|-------------|
3550 | `--message / -m TEXT` | string | `"On <branch>: stash"` | Label for this stash entry |
3551 | `--track TEXT` | string | — | Scope to `tracks/<track>/` paths only |
3552 | `--section TEXT` | string | — | Scope to `sections/<section>/` paths only |
3553
3554 **Flags (clear):**
3555
3556 | Flag | Type | Default | Description |
3557 |------|------|---------|-------------|
3558 | `--yes / -y` | flag | off | Skip confirmation prompt |
3559
3560 **Output example (push):**
3561 ```
3562 Saved working directory and index state stash@{0}
3563 On main: half-finished chorus rearrangement
3564 ```
3565
3566 **Output example (pop):**
3567 ```
3568 ✅ Applied stash@{0}: On main: half-finished chorus rearrangement
3569 3 file(s) restored.
3570 Dropped stash@{0}
3571 ```
3572
3573 **Output example (list):**
3574 ```
3575 stash@{0}: On main: WIP chorus changes
3576 stash@{1}: On main: drums experiment
3577 ```
3578
3579 **Result types:**
3580
3581 `StashPushResult` (dataclass, frozen) — fields:
3582 - `stash_ref` (str): Human label (e.g. `"stash@{0}"`); empty string when nothing was stashed.
3583 - `message` (str): Label stored in the entry.
3584 - `branch` (str): Branch name at the time of push.
3585 - `files_stashed` (int): Number of files saved into the stash.
3586 - `head_restored` (bool): Whether HEAD snapshot was restored to muse-work/.
3587 - `missing_head` (tuple[str, ...]): Paths that could not be restored from the object store after push.
3588
3589 `StashApplyResult` (dataclass, frozen) — fields:
3590 - `stash_ref` (str): Human label of the entry that was applied.
3591 - `message` (str): The entry's label.
3592 - `files_applied` (int): Number of files written to muse-work/.
3593 - `missing` (tuple[str, ...]): Paths whose object bytes were absent from the store.
3594 - `dropped` (bool): True when the entry was removed (pop); False for apply.
3595
3596 `StashEntry` (dataclass, frozen) — fields:
3597 - `stash_id` (str): Unique filesystem stem.
3598 - `index` (int): Position in the stack (0 = most recent).
3599 - `branch` (str): Branch at the time of stash.
3600 - `message` (str): Human label.
3601 - `created_at` (str): ISO-8601 timestamp.
3602 - `manifest` (dict[str, str]): `{rel_path: sha256_object_id}` of stashed files.
3603 - `track` (str | None): Track scope used during push (or None).
3604 - `section` (str | None): Section scope used during push (or None).
3605
3606 **Storage:** Filesystem-only. Each entry is a JSON file in `.muse/stash/stash-<timestamp>-<uuid8>.json`. File content is preserved in the existing `.muse/objects/<oid[:2]>/<oid[2:]>` content-addressed blob store (same layout as `muse commit` and `muse reset --hard`). No Postgres rows are written.
3607
3608 **Agent use case:** An AI composition agent mid-generation on the chorus wants to quickly address a client request on the intro. It calls `muse stash` to save the in-progress chorus state (files + object blobs), then `muse checkout intro-branch` to switch context, makes the intro fix, then returns and calls `muse stash pop` to restore the chorus work exactly as it was. For scoped saves, `--track drums` limits the stash to drum files only, leaving other tracks untouched in muse-work/.
3609
3610 **Conflict strategy on apply:** Last-write-wins. Files in muse-work/ not in the stash manifest are left untouched. Files whose objects are missing from the store are reported in `missing` but do not abort the operation.
3611
3612 **Stack ordering:** `stash@{0}` is always the most recently pushed entry. `stash@{N}` refers to the Nth entry in reverse chronological order. Multiple `push` calls build a stack; `pop` always takes from the top.
3613
3614 **Implementation:** `maestro/muse_cli/commands/stash.py` (Typer CLI with subcommands), `maestro/services/muse_stash.py` (`push_stash`, `apply_stash`, `list_stash`, `drop_stash`, `clear_stash`, result types).
3615
3616 ---
3617
3618 ### `muse revert`
3619
3620 **Purpose:** Create a new commit that undoes a prior commit without rewriting history. The safe undo: given commit C with parent P, `muse revert <commit>` creates a forward commit whose snapshot is P's state (the world before C was applied). An AI agent uses this after discovering a committed arrangement degraded the score — rather than resetting (which loses history), the revert preserves the full audit trail.
3621
3622 **Usage:**
3623 ```bash
3624 muse revert <commit> [OPTIONS]
3625 ```
3626
3627 **Flags:**
3628
3629 | Flag | Type | Default | Description |
3630 |------|------|---------|-------------|
3631 | `COMMIT` | positional | required | Commit ID to revert (full or abbreviated SHA) |
3632 | `--no-commit` | flag | off | Apply the inverse changes to muse-work/ without creating a new commit |
3633 | `--track TEXT` | string | — | Scope the revert to paths under `tracks/<track>/` only |
3634 | `--section TEXT` | string | — | Scope the revert to paths under `sections/<section>/` only |
3635
3636 **Output example (full revert):**
3637 ```
3638 ✅ [main a1b2c3d4] Revert 'bad drum arrangement'
3639 ```
3640
3641 **Output example (scoped revert):**
3642 ```
3643 ✅ [main b2c3d4e5] Revert 'bad drum arrangement' (scoped to 2 path(s))
3644 ```
3645
3646 **Output example (--no-commit):**
3647 ```
3648 ✅ Staged revert (--no-commit). Files removed:
3649 deleted: tracks/drums/fill.mid
3650 ```
3651
3652 **Result type:** `RevertResult` (dataclass, frozen) — fields:
3653 - `commit_id` (str): New commit ID (empty string when `--no-commit` or noop).
3654 - `target_commit_id` (str): Commit that was reverted.
3655 - `parent_commit_id` (str): Parent of the reverted commit (whose snapshot was restored).
3656 - `revert_snapshot_id` (str): Snapshot ID of the reverted state.
3657 - `message` (str): Auto-generated commit message (`"Revert '<original message>'"`)
3658 - `no_commit` (bool): Whether the revert was staged only.
3659 - `noop` (bool): True when reverting would produce no change.
3660 - `scoped_paths` (tuple[str, ...]): Paths selectively reverted (empty = full revert).
3661 - `paths_deleted` (tuple[str, ...]): Files removed from muse-work/ during `--no-commit`.
3662 - `paths_missing` (tuple[str, ...]): Files that could not be auto-restored (no object bytes).
3663 - `branch` (str): Branch on which the revert commit was created.
3664
3665 **Agent use case:** An agent that evaluates generated arrangements after each commit can run `muse log --json` to detect quality regressions, then call `muse revert <bad_commit>` to undo the offending commit and resume generation from the prior good state. For instrument-specific corrections, `--track drums` limits the revert to drum tracks only, preserving bass and melodic changes.
3666
3667 **Blocking behaviour:** Blocked during an in-progress merge with unresolved conflicts — exits 1 with a clear message directing the user to resolve conflicts first.
3668
3669 **Object store limitation:** The Muse CLI stores file manifests (path→sha256) in Postgres but does not retain raw file bytes. For `--no-commit`, files that should be restored but whose bytes are no longer in `muse-work/` are listed as warnings in `paths_missing`. The commit-only path (default) is unaffected — it references an existing snapshot ID directly with no file restoration needed.
3670
3671 **Implementation:** `maestro/muse_cli/commands/revert.py` (Typer CLI), `maestro/services/muse_revert.py` (`_revert_async`, `compute_revert_manifest`, `apply_revert_to_workdir`, `RevertResult`).
3672
3673 ---
3674
3675 ### `muse cherry-pick`
3676
3677 **Purpose:** Apply the changes introduced by a single commit from any branch onto the current branch, without merging the entire source branch. An AI agent uses this to transplant a winning take (the perfect guitar solo, the ideal bass groove) from an experimental branch into main without importing 20 unrelated commits.
3678
3679 **Usage:**
3680 ```bash
3681 muse cherry-pick <commit> [OPTIONS]
3682 ```
3683
3684 **Flags:**
3685
3686 | Flag | Type | Default | Description |
3687 |------|------|---------|-------------|
3688 | `COMMIT` | positional | required | Commit ID to cherry-pick (full or abbreviated SHA) |
3689 | `--no-commit` | flag | off | Apply changes to muse-work/ without creating a new commit |
3690 | `--continue` | flag | off | Resume after resolving conflicts from a paused cherry-pick |
3691 | `--abort` | flag | off | Abort an in-progress cherry-pick and restore the pre-cherry-pick HEAD |
3692
3693 **Output example (clean apply):**
3694 ```
3695 ✅ [main a1b2c3d4] add guitar solo
3696 (cherry picked from commit f3e2d1c0)
3697 ```
3698
3699 **Output example (conflict):**
3700 ```
3701 ❌ Cherry-pick conflict in 1 file(s):
3702 both modified: tracks/guitar/solo.mid
3703 Fix conflicts and run 'muse cherry-pick --continue' to create the commit.
3704 ```
3705
3706 **Output example (--abort):**
3707 ```
3708 ✅ Cherry-pick aborted. HEAD restored to a1b2c3d4.
3709 ```
3710
3711 **Algorithm:** 3-way merge model — base=P (cherry commit's parent), ours=HEAD, theirs=C (cherry commit). For each path C changed vs P: if HEAD also changed that path differently → conflict; otherwise apply C's version on top of HEAD. Commit message is prefixed with `(cherry picked from commit <short-id>)` for auditability.
3712
3713 **State file:** `.muse/CHERRY_PICK_STATE.json` — written when conflicts are detected, consumed by `--continue` and `--abort`.
3714
3715 ```json
3716 {
3717 "cherry_commit": "f3e2d1c0...",
3718 "head_commit": "a1b2c3d4...",
3719 "conflict_paths": ["tracks/guitar/solo.mid"]
3720 }
3721 ```
3722
3723 **Result type:** `CherryPickResult` (dataclass, frozen) — fields:
3724 - `commit_id` (str): New commit ID (empty when `--no-commit` or conflict).
3725 - `cherry_commit_id` (str): Source commit that was cherry-picked.
3726 - `head_commit_id` (str): HEAD commit at cherry-pick time.
3727 - `new_snapshot_id` (str): Snapshot ID of the resulting state.
3728 - `message` (str): Commit message with cherry-pick attribution suffix.
3729 - `no_commit` (bool): Whether changes were staged but not committed.
3730 - `conflict` (bool): True when conflicts were detected and state file was written.
3731 - `conflict_paths` (tuple[str, ...]): Conflicting paths (non-empty iff `conflict=True`).
3732 - `branch` (str): Branch on which the new commit was created.
3733
3734 **Agent use case:** An AI music agent runs `muse log --json` across branches to score each commit, identifies the highest-scoring take on `experiment/guitar-solo`, then calls `muse cherry-pick <commit>` to transplant just that take into main. After cherry-pick, the agent can immediately continue composing on the enriched HEAD without merging the entire experimental branch.
3735
3736 **Blocking behaviour:**
3737 - Blocked when a merge is in progress with unresolved conflicts (exits 1).
3738 - Blocked when a previous cherry-pick is in progress (exits 1 — use `--continue` or `--abort`).
3739 - Cherry-picking HEAD itself exits 0 (noop).
3740
3741 **Implementation:** `maestro/muse_cli/commands/cherry_pick.py` (Typer CLI), `maestro/services/muse_cherry_pick.py` (`_cherry_pick_async`, `_cherry_pick_continue_async`, `_cherry_pick_abort_async`, `compute_cherry_manifest`, `CherryPickResult`, `CherryPickState`).
3742
3743 ---
3744
3745 ### `muse grep`
3746
3747 **Purpose:** Search all commits for a musical pattern — a note sequence, interval
3748 pattern, or chord symbol. Currently searches commit messages; full MIDI content
3749 search is the planned implementation.
3750 {"factor": 0.55, "label": "Light", "commit": "a1b2c3d4", "branch": "main", "track": "all", "source": "stub"}
3751 ```
3752
3753 **Result type:** `SwingDetectResult` (TypedDict) — fields: `factor` (float),
3754 `label` (str), `commit` (str), `branch` (str), `track` (str), `source` (str).
3755 `--compare` returns `SwingCompareResult` — fields: `head` (SwingDetectResult),
3756 `compare` (SwingDetectResult), `delta` (float). See
3757 `docs/reference/type_contracts.md § Muse CLI Types`.
3758
3759 **Agent use case:** An AI generating a bass line runs `muse swing --json` to
3760 know whether to quantize straight or add shuffle. A Medium swing result means
3761 the bass should land slightly behind the grid to stay in pocket with the
3762 existing drum performance.
3763
3764 **Implementation:** `maestro/muse_cli/commands/swing.py` — `swing_label()`,
3765 `_swing_detect_async()`, `_swing_history_async()`, `_swing_compare_async()`,
3766 `_format_detect()`, `_format_history()`, `_format_compare()`. Exit codes:
3767 0 success, 1 invalid `--set` value, 2 outside repo, 3 internal error.
3768
3769 > **Stub note:** Returns a placeholder factor of 0.55. Full implementation
3770 > requires onset-to-onset ratio measurement from committed MIDI note events
3771 > (future: Storpheus MIDI parse route).
3772
3773 ---
3774
3775 ## `muse chord-map` — Chord Progression Timeline
3776
3777 `muse chord-map [<commit>]` extracts and displays the chord timeline of a
3778 specific commit — showing *when* each chord occurs in the arrangement, not
3779 just which chords are present. This is the foundation for AI-generated
3780 harmonic analysis and chord-substitution suggestions.
3781
3782 **Purpose:** Give AI agents a precise picture of the harmonic structure at any
3783 commit so they can reason about the progression in time, propose substitutions,
3784 or detect tension/resolution cycles.
3785
3786 ### Flags
3787
3788 | Flag | Type | Default | Description |
3789 |------|------|---------|-------------|
3790 | `COMMIT` | positional | HEAD | Commit ref to analyse. |
3791 | `--section TEXT` | string | — | Scope to a named section/region. |
3792 | `--track TEXT` | string | — | Scope to a specific track (e.g. piano for chord voicings). |
3793 | `--bar-grid / --no-bar-grid` | flag | on | Align chord events to musical bar numbers. |
3794 | `--format FORMAT` | string | `text` | Output format: `text`, `json`, or `mermaid`. |
3795 | `--voice-leading` | flag | off | Show how individual notes move between consecutive chords. |
3796
3797 ### Output example
3798
3799 **Text (default, `--bar-grid`):**
3800
3801 ```
3802 Chord map -- commit a1b2c3d4 (HEAD -> main)
3803
3804 Bar 1: Cmaj9 ########
3805 Bar 2: Am11 ########
3806 Bar 3: Dm7 #### Gsus4 ####
3807 Bar 4: G7 ########
3808 Bar 5: Cmaj9 ########
3809
3810 (stub -- full MIDI chord detection pending)
3811 ```
3812
3813 **With `--voice-leading`:**
3814
3815 ```
3816 Chord map -- commit a1b2c3d4 (HEAD -> main)
3817
3818 Bar 2: Cmaj9 -> Am11 (E->E, G->G, B->A, D->C)
3819 Bar 3: Am11 -> Dm7 (A->D, C->C, E->F, G->A)
3820 ...
3821 ```
3822
3823 **JSON (`--format json`):**
3824
3825 ```json
3826 {
3827 "commit": "a1b2c3d4",
3828 "branch": "main",
3829 "track": "all",
3830 "section": "",
3831 "chords": [
3832 { "bar": 1, "beat": 1, "chord": "Cmaj9", "duration": 1.0, "track": "keys" },
3833 { "bar": 2, "beat": 1, "chord": "Am11", "duration": 1.0, "track": "keys" }
3834 ],
3835 "voice_leading": []
3836 }
3837 ```
3838
3839 **Mermaid (`--format mermaid`):**
3840
3841 ```
3842 timeline
3843 title Chord map -- a1b2c3d4
3844 section Bar 1
3845 Cmaj9
3846 section Bar 2
3847 Am11
3848 ```
3849
3850 ### Result type
3851
3852 `muse chord-map` returns a `ChordMapResult` TypedDict (see
3853 `docs/reference/type_contracts.md § ChordMapResult`). Each chord event is a
3854 `ChordEvent`; each voice-leading step is a `VoiceLeadingStep`.
3855
3856 ### Agent use case
3857
3858 An AI agent writing a counter-melody calls `muse chord-map HEAD --format json`
3859 to retrieve the exact bar-by-bar harmonic grid. It then selects chord tones
3860 that land on the strong beats. With `--voice-leading`, the agent can also
3861 detect smooth inner-voice motion and mirror it in the new part.
3862
3863 **Implementation:** `maestro/muse_cli/commands/chord_map.py` —
3864 `_chord_map_async()`, `_render_text()`, `_render_json()`, `_render_mermaid()`.
3865 Exit codes: 0 success, 1 invalid `--format`, 2 outside repo, 3 internal error.
3866
3867 > **Stub note:** Returns a placeholder I-vi-ii-V-I progression. Full
3868 > implementation requires chord-detection from committed MIDI note events
3869 > (future: Storpheus MIDI parse route).
3870
3871 ---
3872
3873 ## `muse key` — Read or Annotate the Musical Key of a Commit
3874
3875 `muse key` reads or annotates the tonal center (key) of a Muse commit.
3876 Key is the most fundamental property of a piece of music — knowing the key is a
3877 prerequisite for harmonic generation, chord-scale selection, and tonal arc
3878 analysis. An AI agent calls `muse key --json` before generating new material to
3879 stay in the correct tonal center.
3880
3881 **Usage:**
3882 ```bash
3883 muse key [<commit>] [OPTIONS]
3884 ```
3885
3886 **Flags:**
3887
3888 | Flag | Type | Default | Description |
3889 |------|------|---------|-------------|
3890 | `<commit>` | arg | HEAD | Commit SHA to analyse |
3891 | `--set KEY` | str | — | Annotate with an explicit key (e.g. `"F# minor"`) |
3892 | `--detect` | flag | on | Detect and display the key (default behaviour) |
3893 | `--track TEXT` | str | — | Restrict key detection to a specific instrument track |
3894 | `--relative` | flag | off | Show the relative key (e.g. `"Eb major / C minor"`) |
3895 | `--history` | flag | off | Show how the key changed across all commits |
3896 | `--json` | flag | off | Emit machine-readable JSON for agent consumption |
3897
3898 **Key format:** `<tonic> <mode>` — e.g. `"F# minor"`, `"Eb major"`. Valid tonics
3899 include all 12 chromatic pitches with `#` and `b` enharmonics. Valid modes are
3900 `major` and `minor`.
3901
3902 **Output example (text):**
3903 ```
3904 Key: C major
3905 Commit: a1b2c3d4 Branch: main
3906 Track: all
3907 (stub — full MIDI key detection pending)
3908 ```
3909
3910 **Output example (`--relative`):**
3911 ```
3912 Key: A minor
3913 Commit: a1b2c3d4 Branch: main
3914 Track: all
3915 Relative: C major
3916 (stub — full MIDI key detection pending)
3917 ```
3918
3919 **Output example (`--json`):**
3920 ```json
3921 {
3922 "key": "C major",
3923 "tonic": "C",
3924 "mode": "major",
3925 "relative": "",
3926 "commit": "a1b2c3d4",
3927 "branch": "main",
3928 "track": "all",
3929 "source": "stub"
3930 }
3931 ```
3932
3933 **Output example (`--history --json`):**
3934 ```json
3935 [
3936 {"commit": "a1b2c3d4", "key": "C major", "tonic": "C", "mode": "major", "source": "stub"}
3937 ]
3938 ```
3939
3940 **Result types:** `KeyDetectResult` (TypedDict) — fields: `key` (str), `tonic` (str),
3941 `mode` (str), `relative` (str), `commit` (str), `branch` (str), `track` (str),
3942 `source` (str). History mode returns `list[KeyHistoryEntry]`. See
3943 `docs/reference/type_contracts.md § Muse CLI Types`.
3944
3945 **Agent use case:** Before generating a chord progression or melody, an agent runs
3946 `muse key --json` to discover the tonal center of the most recent commit.
3947 `muse key --history --json` reveals modulations across an album — if the key
3948 changed from D major to F major at commit `abc123`, the agent knows a modulation
3949 occurred and can generate transitional material accordingly.
3950
3951 **Implementation:** `maestro/muse_cli/commands/key.py` — `parse_key()`,
3952 `relative_key()`, `_key_detect_async()`, `_key_history_async()`,
3953 `_format_detect()`, `_format_history()`. Exit codes: 0 success, 1 invalid
3954 `--set` value, 2 outside repo, 3 internal error.
3955
3956 > **Stub note:** Returns a placeholder key of `C major`. Full implementation
3957 > requires chromatic pitch-class distribution analysis from committed MIDI note
3958 > events (Krumhansl-Schmuckler or similar key-finding algorithm, future:
3959 > Storpheus MIDI parse route).
3960
3961 ---
3962
3963 ## `muse ask` — Natural Language Query over Musical History
3964
3965 `muse ask "<question>"` searches Muse commit messages for keywords derived
3966 from the user's question and returns matching commits in a structured answer.
3967
3968 **Purpose:** Give musicians and AI agents a conversational interface to
3969 retrieve relevant moments from the composition history without remembering
3970 exact commit messages or timestamps.
3971
3972 ### Flags
3973
3974 | Flag | Default | Description |
3975 |------|---------|-------------|
3976 | `<question>` | *(required)* | Natural language question about your musical history. |
3977 | `--branch <name>` | current HEAD branch | Restrict search to commits on this branch. |
3978 | `--since YYYY-MM-DD` | *(none)* | Include only commits on or after this date. |
3979 | `--until YYYY-MM-DD` | *(none)* | Include only commits on or before this date (inclusive, end-of-day). |
3980 | `--json` | `false` | Emit machine-readable JSON instead of plain text. |
3981 | `--cite` | `false` | Show full 64-character commit IDs instead of 8-character prefixes. |
3982
3983 ### Output example
3984
3985 **Plain text:**
3986
3987 ```
3988 Based on Muse history (14 commits searched):
3989 Commits matching your query: 2 found
3990
3991 [a3f2c1b0] 2026-02-10 14:32 boom bap take 1
3992 [d9e8f7a6] 2026-02-11 09:15 boom bap take 2
3993
3994 Note: Full LLM-powered answer generation is a planned enhancement.
3995 ```
3996
3997 **JSON (`--json`):**
3998
3999 ```json
4000 {
4001 "question": "boom bap sessions",
4002 "total_searched": 14,
4003 "matches": [
4004 {
4005 "commit_id": "a3f2c1b0",
4006 "branch": "main",
4007 "message": "boom bap take 1",
4008 "committed_at": "2026-02-10T14:32:00+00:00"
4009 }
4010 ],
4011 "note": "Full LLM-powered answer generation is a planned enhancement."
4012 }
4013 ```
4014
4015 ### Result type
4016
4017 `muse ask` returns an `AnswerResult` object (see
4018 `docs/reference/type_contracts.md § AnswerResult`). The `to_plain()` and
4019 `to_json()` methods on `AnswerResult` render the two output formats.
4020
4021 ### Agent use case
4022
4023 An AI agent reviewing a composition session calls `muse ask "piano intro" --json`
4024 to retrieve all commits where piano intro work was recorded. The JSON output
4025 feeds directly into the agent's context without screen-scraping, allowing it to
4026 reference specific commit IDs when proposing the next variation.
4027
4028 The `--branch` filter lets an agent scope queries to a feature branch
4029 (e.g., `feat/verse-2`) rather than searching across all experimental branches.
4030 The `--cite` flag gives the agent full commit IDs for downstream `muse checkout`
4031 or `muse log` calls.
4032
4033 **Implementation:** `maestro/muse_cli/commands/ask.py` — `_keywords()`,
4034 `_ask_async()`, `AnswerResult`. Exit codes: 0 success, 2 outside repo,
4035 3 internal error.
4036
4037 > **Stub note:** Keyword matching over commit messages only. Full LLM-powered
4038 > semantic search (embedding similarity over commit content) is a planned
4039 > enhancement (future: integrate with Qdrant vector store).
4040
4041 ---
4042
4043 ## `muse grep` — Search for a Musical Pattern Across All Commits
4044
4045 **Purpose:** Walk the full commit chain on the current branch and return every
4046 commit whose message or branch name contains the given pattern. Designed as
4047 the textual precursor to full MIDI content search — the CLI contract (flags,
4048 output modes, result type) is frozen now so agents can rely on it before the
4049 deeper analysis is wired in.
4050
4051 **Usage:**
4052 ```bash
4053 muse grep <pattern> [OPTIONS]
4054 ```
4055
4056 **Flags:**
4057
4058 | Flag | Type | Default | Description |
4059 |------|------|---------|-------------|
4060 | `PATTERN` | positional | required | Pattern to find (note seq, interval, chord, or text) |
4061 | `--track TEXT` | string | — | Restrict to a named track (MIDI content search — planned) |
4062 | `--section TEXT` | string | — | Restrict to a named section (planned) |
4063 | `--transposition-invariant` | flag | on | Match in any key (planned for MIDI search) |
4064 | `--rhythm-invariant` | flag | off | Match regardless of rhythm (planned) |
4065 | `--commits` | flag | off | Output one commit ID per line instead of full table |
4066 | `--json` | flag | off | Emit structured JSON array |
4067
4068 **Pattern formats (planned for MIDI content search):**
4069
4070 | Format | Example | Matches |
4071 |--------|---------|---------|
4072 | Note sequence | `"C4 E4 G4"` | Those exact pitches in sequence |
4073 | Interval run | `"+4 +3"` | Major 3rd + minor 3rd (Cm arpeggio) |
4074 | Chord symbol | `"Cm7"` | That chord anywhere in the arrangement |
4075 | Text | `"verse piano"` | Commit message substring (current implementation) |
4076
4077 **Output example (text):**
4078 ```
4079 Pattern: "dark jazz" (2 matches)
4080
4081 Commit Branch Committed Message Source
4082 ----------- ------- ------------------- ------------------- -------
4083 a1b2c3d4 main 2026-02-15 22:00 boom bap dark jazz message
4084 f9e8d7c6 main 2026-02-10 18:30 dark jazz bass message
4085 ```
4086
4087 **Result type:** `GrepMatch` (dataclass) — fields: `commit_id` (str), `branch` (str), `message` (str), `committed_at` (str ISO-8601), `match_source` (str: `"message"` | `"branch"` | `"midi_content"`)
4088
4089 **Agent use case:** An agent searching for prior uses of a Cm7 chord calls `muse grep "Cm7" --commits --json` to get a list of commits containing that chord. It can then pull those commits as harmonic reference material.
4090
4091 **Implementation:** `maestro/muse_cli/commands/grep_cmd.py` — registered as `muse grep`. `GrepMatch` (dataclass), `_load_all_commits()`, `_grep_async()`, `_render_matches()`. Exit codes: 0 success, 2 outside repo.
4092
4093 > **Stub note:** Pattern matched against commit messages only. MIDI content scanning (parsing note events from snapshot objects) is tracked as a follow-up issue.
4094
4095 ---
4096
4097 ### `muse ask`
4098
4099 **Purpose:** Natural language question answering over commit history. Ask questions
4100 in plain English; Muse searches history and returns a grounded answer citing specific
4101 commits. The conversational interface to musical memory.
4102
4103 **Usage:**
4104 ```bash
4105 muse ask "<question>" [OPTIONS]
4106 | `PATTERN` | positional | — | Pattern to search (note sequence, interval, chord, or free text) |
4107 | `--track TEXT` | string | — | [Future] Restrict to a named MIDI track |
4108 | `--section TEXT` | string | — | [Future] Restrict to a labelled section |
4109 | `--transposition-invariant / --no-transposition-invariant` | flag | on | [Future] Match regardless of key |
4110 | `--rhythm-invariant` | flag | off | [Future] Match regardless of rhythm/timing |
4111 | `--commits` | flag | off | Output one commit ID per line (like `git grep --name-only`) |
4112 | `--json` | flag | off | Emit machine-readable JSON array |
4113
4114 **Output example (text):**
4115 ```
4116 Pattern: 'pentatonic' (1 match(es))
4117
4118 commit c1d2e3f4...
4119 Branch: feature/pentatonic-solo
4120 Date: 2026-02-27T15:00:00+00:00
4121 Match: [message]
4122 Message: add pentatonic riff to chorus
4123 ```
4124
4125 **Output example (`--commits`):**
4126 ```
4127 c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2
4128 ```
4129
4130 **Output example (`--json`):**
4131 ```json
4132 [
4133 {
4134 "commit_id": "c1d2e3f4...",
4135 "branch": "feature/pentatonic-solo",
4136 "message": "add pentatonic riff to chorus",
4137 "committed_at": "2026-02-27T15:00:00+00:00",
4138 "match_source": "message"
4139 }
4140 ]
4141 ```
4142
4143 **Result type:** `GrepMatch` (dataclass) — fields: `commit_id` (str),
4144 `branch` (str), `message` (str), `committed_at` (str, ISO-8601),
4145 `match_source` (str: `"message"` | `"branch"` | `"midi_content"`).
4146 See `docs/reference/type_contracts.md § Muse CLI Types`.
4147
4148 **Agent use case:** An AI composing a variation searches previous commits for
4149 all times "pentatonic" appeared in the history before deciding whether to
4150 reuse or invert the motif. The `--json` flag makes the result directly
4151 parseable; `--commits` feeds a shell loop that checks out each matching
4152 commit for deeper inspection.
4153
4154 **Implementation:** `maestro/muse_cli/commands/grep_cmd.py` —
4155 `GrepMatch` (dataclass), `_load_all_commits()`, `_match_commit()`,
4156 `_grep_async()`, `_render_matches()`. Exit codes: 0 success,
4157 2 outside repo, 3 internal error.
4158
4159 > **Stub note:** The current implementation matches commit *messages* and
4160 > *branch names* only. Full MIDI content search (note sequences, intervals,
4161 > chord symbols, `--track`, `--section`, `--transposition-invariant`,
4162 > `--rhythm-invariant`) is reserved for a future iteration. Flags are accepted
4163 > now to keep the CLI contract stable; supplying them emits a clear warning.
4164
4165 ---
4166
4167 ### `muse blame`
4168
4169 **Purpose:** Annotate each tracked file with the commit that last changed it.
4170 Answers the producer's question "whose idea was this bass line?" or "which take
4171 introduced this change?" Output is per-file (not per-line) because MIDI and
4172 audio files are binary — the meaningful unit of change is the whole file.
4173
4174 **Usage:**
4175 ```bash
4176 muse blame [PATH] [OPTIONS]
4177 ```
4178
4179 **Flags:**
4180
4181 | Flag | Type | Default | Description |
4182 |------|------|---------|-------------|
4183 | `PATH` | positional string | — | Relative path within `muse-work/` to annotate. Omit to blame all tracked files |
4184 | `--track TEXT` | string | — | Filter to files whose basename matches this fnmatch glob (e.g. `bass*` or `*.mid`) |
4185 | `--section TEXT` | string | — | Filter to files inside this section directory (first directory component) |
4186 | `--line-range N,M` | string | — | Annotate sub-range (informational only — MIDI/audio are binary, not line-based) |
4187 | `--json` | flag | off | Emit structured JSON for agent consumption |
4188
4189 **Output example (text):**
4190 ```
4191 a1b2c3d4 producer 2026-02-27 14:30:00 ( modified) muse-work/bass/bassline.mid
4192 update bass groove
4193 f9e8d7c6 producer 2026-02-26 10:00:00 ( added) muse-work/keys/melody.mid
4194 initial take
4195 ```
4196
4197 **Output example (`--json`):**
4198 ```json
4199 {
4200 "path_filter": null,
4201 "track_filter": null,
4202 "section_filter": null,
4203 "line_range": null,
4204 "entries": [
4205 {
4206 "path": "muse-work/bass/bassline.mid",
4207 "commit_id": "a1b2c3d4e5f6...",
4208 "commit_short": "a1b2c3d4",
4209 "author": "producer",
4210 "committed_at": "2026-02-27 14:30:00",
4211 "message": "update bass groove",
4212 "change_type": "modified"
4213 }
4214 ]
4215 }
4216 ```
4217
4218 **Result type:** `BlameEntry` (TypedDict) — fields: `path` (str), `commit_id` (str),
4219 `commit_short` (str, 8-char), `author` (str), `committed_at` (str, `YYYY-MM-DD HH:MM:SS`),
4220 `message` (str), `change_type` (str: `"added"` | `"modified"` | `"unchanged"`).
4221 Wrapped in `BlameResult` (TypedDict) — fields: `path_filter`, `track_filter`,
4222 `section_filter`, `line_range` (all `str | None`), `entries` (list of `BlameEntry`).
4223 See `docs/reference/type_contracts.md § Muse CLI Types`.
4224
4225 **Agent use case:** An AI composing a new bass arrangement asks `muse blame --track 'bass*' --json`
4226 to find the commit that last changed every bass file. It then calls `muse show <commit_id>` on
4227 those commits to understand what musical choices were made, before deciding whether to build on
4228 or diverge from the existing groove.
4229
4230 **Implementation:** `maestro/muse_cli/commands/blame.py` —
4231 `BlameEntry` (TypedDict), `BlameResult` (TypedDict), `_load_commit_chain()`,
4232 `_load_snapshot_manifest()`, `_matches_filters()`, `_blame_async()`, `_render_blame()`.
4233 Exit codes: 0 success, 2 outside repo, 3 internal error.
4234
4235 ---
4236
4237 ## `muse tempo` — Read or Set the Tempo of a Commit
4238
4239 `muse tempo [<commit>] [--set <bpm>] [--history] [--json]` reads or annotates
4240 the BPM of a specific commit. Tempo (BPM) is the most fundamental rhythmic property
4241 of a Muse project — this command makes it a first-class commit attribute.
4242
4243 ### Flags
4244
4245 | Flag | Description |
4246 |------|-------------|
4247 | `[<commit>]` | Target commit SHA (full or abbreviated) or `HEAD` (default) |
4248 | `--set <bpm>` | Annotate the commit with an explicit BPM (20–400 range) |
4249 | `--history` | Show BPM timeline across all commits in the parent chain |
4250 | `--json` | Emit machine-readable JSON instead of human-readable text |
4251
4252 ### Tempo Resolution Order (read path)
4253
4254 1. **Annotated BPM** — explicitly set via `muse tempo --set` and stored in `commit_metadata.tempo_bpm`.
4255 2. **Detected BPM** — auto-extracted from MIDI Set Tempo meta-events (FF 51 03) in the commit's snapshot files.
4256 3. **None** — displayed as `--` when neither source is available.
4257
4258 ### Tempo Storage (write path)
4259
4260 `--set` writes `{tempo_bpm: <float>}` into the `metadata` JSON column of the
4261 `muse_cli_commits` table. Other metadata keys in that column are preserved
4262 (merge-patch semantics). No new rows are created — only the existing commit row
4263 is annotated.
4264
4265 ### Schema
4266
4267 The `muse_cli_commits` table has a nullable `metadata` JSON column (added in
4268 migration `0002_muse_cli_commit_metadata`). Current keys:
4269
4270 | Key | Type | Set by |
4271 |-----|------|--------|
4272 | `tempo_bpm` | `float` | `muse tempo --set` |
4273
4274 ### History Traversal
4275
4276 `--history` walks the full parent chain from the target commit (or HEAD),
4277 collecting annotated BPM values and computing signed deltas between consecutive
4278 commits.
4279
4280 Auto-detected BPM is shown on the single-commit read path but is not persisted,
4281 so it does not appear in history (history only reflects explicitly set annotations).
4282
4283 ### MIDI Tempo Parsing
4284
4285 `maestro/services/muse_tempo.extract_bpm_from_midi(data: bytes)` is a pure
4286 function that scans a raw MIDI byte string for the Set Tempo meta-event
4287 (FF 51 03). The three bytes encode microseconds-per-beat as a 24-bit big-endian
4288 integer. BPM = 60_000_000 / microseconds_per_beat. Only the first event is
4289 returned; `detect_all_tempos_from_midi` returns all events (used for rubato
4290 detection).
4291
4292 ### Result Types
4293
4294 | Type | Module | Purpose |
4295 |------|--------|---------|
4296 | `MuseTempoResult` | `maestro.services.muse_tempo` | Single-commit tempo query result |
4297 | `MuseTempoHistoryEntry` | `maestro.services.muse_tempo` | One row in a `--history` traversal |
4298
4299 ### DB Helpers
4300
4301 | Helper | Module | Purpose |
4302 |--------|--------|---------|
4303 | `resolve_commit_ref` | `maestro.muse_cli.db` | Resolve HEAD / full SHA / abbreviated SHA to a `MuseCliCommit` |
4304 | `set_commit_tempo_bpm` | `maestro.muse_cli.db` | Write `tempo_bpm` into `commit_metadata` (merge-patch) |
4305
4306 ### Exit Codes
4307
4308 | Code | Meaning |
4309 |------|---------|
4310 | 0 | Success |
4311 | 1 | User error: unknown ref, BPM out of range |
4312 | 2 | Outside a Muse repository |
4313 | 3 | Internal error |
4314
4315 ---
4316
4317 ### Command Registration Summary
4318
4319 | Command | File | Status | Issue |
4320 |---------|------|--------|-------|
4321 | `muse dynamics` | `commands/dynamics.py` | ✅ stub (PR #130) | #120 |
4322 | `muse swing` | `commands/swing.py` | ✅ stub (PR #131) | #121 |
4323 | `muse recall` | `commands/recall.py` | ✅ stub (PR #135) | #122 |
4324 | `muse tag` | `commands/tag.py` | ✅ implemented (PR #133) | #123 |
4325 | `muse grep` | `commands/grep_cmd.py` | ✅ stub (PR #128) | #124 |
4326 | `muse humanize` | `commands/humanize.py` | ✅ stub (PR #151) | #107 |
4327 | `muse describe` | `commands/describe.py` | ✅ stub (PR #134) | #125 |
4328 | `muse ask` | `commands/ask.py` | ✅ stub (PR #132) | #126 |
4329 | `muse session` | `commands/session.py` | ✅ implemented (PR #129) | #127 |
4330 | `muse tempo` | `commands/tempo.py` | ✅ fully implemented (PR TBD) | #116 |
4331
4332 All stub commands have stable CLI contracts. Full musical analysis (MIDI content
4333 parsing, vector embeddings, LLM synthesis) is tracked as follow-up issues.
4334
4335 ## `muse recall` — Keyword Search over Musical Commit History
4336
4337 **Purpose:** Walk the commit history on the current (or specified) branch and
4338 return the top-N commits ranked by keyword overlap against their commit
4339 messages. Designed as the textual precursor to full vector embedding search —
4340 the CLI interface (flags, output modes, result type) is frozen so agents can
4341 rely on it before Qdrant-backed semantic search is wired in.
4342
4343 **Usage:**
4344 ```bash
4345 muse recall <query> [OPTIONS]
4346 ```
4347
4348 **Flags:**
4349
4350 | Flag | Type | Default | Description |
4351 |------|------|---------|-------------|
4352 | `QUESTION` | positional | required | Natural-language question about musical history |
4353 | `--branch TEXT` | string | all | Restrict history to a branch |
4354 | `--since DATE` | `YYYY-MM-DD` | — | Only consider commits after this date |
4355 | `--until DATE` | `YYYY-MM-DD` | — | Only consider commits before this date |
4356 | `--cite` | flag | off | Show full commit IDs in the answer (default: short IDs) |
4357 | `--json` | flag | off | Emit structured JSON response |
4358
4359 **Output example (text):**
4360 ```
4361 Based on Muse history (47 commits searched):
4362 Commits matching your query: 3 found
4363
4364 [a1b2c3d] 2026-02-15 22:00 boom bap dark jazz session
4365 [f9e8d7c] 2026-02-10 18:30 Add bass overdub — minor key
4366 [3b2a1f0] 2026-01-28 14:00 Initial tempo work at 118 BPM
4367
4368 Note: Full LLM-powered answer generation is a planned enhancement.
4369 | `QUERY` | positional | — | Natural-language description to search for |
4370 | `--limit / -n INT` | integer | 5 | Maximum number of results to return |
4371 | `--threshold FLOAT` | float | 0.6 | Minimum keyword-overlap score (0–1) to include a commit |
4372 | `--branch TEXT` | string | current branch | Filter to a specific branch name |
4373 | `--since YYYY-MM-DD` | date string | — | Only include commits on or after this date |
4374 | `--until YYYY-MM-DD` | date string | — | Only include commits on or before this date |
4375 | `--json` | flag | off | Emit machine-readable JSON array |
4376
4377 **Scoring algorithm:** Overlap coefficient — `|Q ∩ M| / |Q|` — where Q is the
4378 set of lowercase word tokens in the query and M is the set of tokens in the
4379 commit message. A score of 1.0 means every query word appears in the message;
4380 0.0 means none do. Commits with score below `--threshold` are excluded.
4381
4382 **Output example (text):**
4383 ```
4384 Recall: "dark jazz bassline"
4385 (keyword match · threshold 0.60 · vector search is a planned enhancement)
4386
4387 #1 score=1.0000 commit a1b2c3d4... [2026-02-20 14:30:00]
4388 add dark jazz bassline to verse
4389
4390 #2 score=0.6667 commit e5f6a7b8... [2026-02-18 09:15:00]
4391 jazz bassline variation with reverb
4392 ```
4393
4394 **Output example (`--json`):**
4395 ```json
4396 {
4397 "question": "when did we work on jazz?",
4398 "total_searched": 47,
4399 "matches_found": 3,
4400 "commits": [{"id": "a1b2c3d4...", "short_id": "a1b2c3d", "date": "2026-02-15", "message": "..."}],
4401 "stub_note": "Full LLM answer generation is a planned enhancement."
4402 }
4403 ```
4404
4405 **Result type:** `AnswerResult` (class) — fields: `question` (str), `total_searched` (int), `matches` (list[MuseCliCommit]), `cite` (bool). Methods: `.to_plain()`, `.to_json_dict()`.
4406
4407 **Agent use case:** An AI agent composing a bridge asks `muse ask "what was the emotional arc of the chorus?" --json`. The answer grounds the agent in the actual commit history of the project before it generates, preventing stylistic drift.
4408
4409 **Implementation:** `maestro/muse_cli/commands/ask.py` — `AnswerResult`, `_keywords()`, `_ask_async()`. Exit codes: 0 success, 1 bad date, 2 outside repo.
4410
4411 > **Stub note:** Keyword matching over commit messages. Full implementation: RAG over Qdrant musical context embeddings + LLM answer synthesis via OpenRouter (Claude Sonnet/Opus). CLI interface is stable and will not change when LLM is wired in.
4412
4413 ---
4414
4415 ### `muse session`
4416
4417 **Purpose:** Record and query recording session metadata — who played, when, where,
4418 and what they intended to create. Sessions are stored as local JSON files (not in
4419 Postgres), mirroring how Git stores config as plain files.
4420
4421 **Usage:**
4422 ```bash
4423 muse session <subcommand> [OPTIONS]
4424 ```
4425
4426 **Subcommands:**
4427
4428 | Subcommand | Description |
4429 |------------|-------------|
4430 | `muse session start` | Open a new recording session |
4431 | `muse session end` | Finalize the active session |
4432 | `muse session log` | List all completed sessions, newest first |
4433 | `muse session show <id>` | Print a specific session by ID (prefix match) |
4434 | `muse session credits` | Aggregate participants across all sessions |
4435
4436 **`muse session start` flags:**
4437
4438 | Flag | Type | Default | Description |
4439 |------|------|---------|-------------|
4440 | `--participants TEXT` | string | — | Comma-separated participant names |
4441 | `--location TEXT` | string | — | Studio or location name |
4442 | `--intent TEXT` | string | — | Creative intent for this session |
4443
4444 **`muse session end` flags:**
4445
4446 | Flag | Type | Default | Description |
4447 |------|------|---------|-------------|
4448 | `--notes TEXT` | string | — | Session notes / retrospective |
4449
4450 **Session JSON schema** (stored in `.muse/sessions/<uuid>.json`):
4451 ```json
4452 {
4453 "session_id": "<uuid4>",
4454 "started_at": "2026-02-27T22:00:00+00:00",
4455 "ended_at": "2026-02-27T02:30:00+00:00",
4456 "participants": ["Gabriel (producer)", "Sarah (keys)"],
4457 "location": "Studio A",
4458 "intent": "Piano overdubs for verse sections",
4459 "commits": [],
4460 "notes": "Great tone on the Steinway today."
4461 }
4462 ```
4463
4464 **`muse session log` output:**
4465 ```
4466 SESSION 2026-02-27T22:00 → 2026-02-27T02:30 2h30m
4467 Participants: Gabriel (producer), Sarah (keys)
4468 Location: Studio A
4469 Intent: Piano overdubs for verse sections
4470 ```
4471
4472 **`muse session credits` output:**
4473 ```
4474 Gabriel (producer) 7 sessions
4475 Sarah (keys) 3 sessions
4476 Marcus (bass) 2 sessions
4477 ```
4478
4479 **Agent use case:** An AI agent summarizing a project's creative history calls `muse session credits --json` to attribute musical contributions. An AI generating liner notes reads `muse session log --json` to reconstruct the session timeline.
4480
4481 **Implementation:** `maestro/muse_cli/commands/session.py` — all synchronous (no DB, no async). Storage: `.muse/sessions/current.json` (active) → `.muse/sessions/<uuid>.json` (completed). Exit codes: 0 success, 1 user error (duplicate session, no active session, ambiguous ID), 2 outside repo, 3 internal.
4482
4483 ---
4484
4485 ## `muse meter` — Time Signature Read/Set/Detect
4486
4487 ### `muse meter`
4488
4489 **Purpose:** Read or set the time signature (meter) annotation for any commit. The
4490 time signature defines the rhythmic framework of a piece — a shift from 4/4 to 7/8 is
4491 a fundamental compositional decision. `muse meter` makes that history first-class.
4492
4493 **Status:** ✅ Fully implemented (issue #117)
4494
4495 **Storage:** The time signature is stored as the `meter` key inside the nullable
4496 `extra_metadata` JSON column on `muse_cli_commits`. No MIDI file is modified. The
4497 annotation is layered on top of the immutable content-addressed snapshot.
4498
4499 **Time signature format:** `<numerator>/<denominator>` where the denominator must be a
4500 power of 2. Examples: `4/4`, `3/4`, `7/8`, `5/4`, `12/8`, `6/8`.
4501
4502 #### Flags
4503
4504 | Flag | Argument | Description |
4505 |------|----------|-------------|
4506 | *(none)* | `[COMMIT]` | Read the stored time signature. Default: HEAD. |
4507 | `--set` | `TIME_SIG` | Store a time signature annotation on the commit. |
4508 | `--detect` | — | Auto-detect from MIDI time-signature meta events in `muse-work/`. |
4509 | `--history` | — | Walk the branch and show when the time signature changed. |
4510 | `--polyrhythm` | — | Detect tracks with conflicting time signatures in `muse-work/`. |
4511
4512 #### Examples
4513
4514 ```bash
4515 # Read the stored time signature for HEAD
4516 muse meter
4517
4518 # Read the time signature for a specific commit (abbreviated SHA)
4519 muse meter a1b2c3d4
4520
4521 # Set the time signature on HEAD
4522 muse meter --set 7/8
4523
4524 # Set the time signature on a specific commit
4525 muse meter a1b2c3d4 --set 5/4
4526
4527 # Auto-detect from MIDI files and store the result
4528 muse meter --detect
4529
4530 # Show time signature history (newest-first, with change markers)
4531 muse meter --history
4532
4533 # Check for polyrhythmic tracks
4534 muse meter --polyrhythm
4535 ```
4536
4537 #### Sample output
4538
4539 **Read (no flag):**
4540 ```
4541 commit a1b2c3d4
4542 meter 7/8
4543 ```
4544
4545 **History (`--history`):**
4546 ```
4547 a1b2c3d4 7/8 switched to odd meter ← changed
4548 f9e8d7c6 4/4 boom bap demo take 1
4549 e7d6c5b4 4/4 initial take
4550 ```
4551
4552 **Polyrhythm (`--polyrhythm`, conflict detected):**
4553 ```
4554 ⚠️ Polyrhythm detected — multiple time signatures in this commit:
4555
4556 4/4 tracks/drums.mid
4557 7/8 tracks/melody.mid
4558 ```
4559
4560 #### MIDI Detection
4561
4562 `--detect` scans `.mid` and `.midi` files in `muse-work/` for MIDI time-signature
4563 meta events (type `0xFF 0x58`). The event layout is:
4564
4565 ```
4566 FF 58 04 nn dd cc bb
4567 │ │ │ └── 32nd notes per 24 MIDI clocks
4568 │ │ └───── MIDI clocks per metronome tick
4569 │ └──────── denominator exponent (denominator = 2^dd)
4570 └─────────── numerator
4571 ```
4572
4573 The most common signature across all files is selected and written to the commit.
4574 Files with no time-signature event report `?` and are excluded from polyrhythm
4575 detection (only known signatures are compared).
4576
4577 #### Result types
4578
4579 | Type | Module | Description |
4580 |------|--------|-------------|
4581 | `MuseMeterReadResult` | `maestro/muse_cli/commands/meter.py` | Commit ID + stored time signature (or `None`) |
4582 | `MuseMeterHistoryEntry` | `maestro/muse_cli/commands/meter.py` | Single entry in the parent-chain meter walk |
4583 | `MusePolyrhythmResult` | `maestro/muse_cli/commands/meter.py` | Per-file time signatures + polyrhythm flag |
4584
4585 **Agent use case:** An AI generating a new section calls `muse meter` to discover whether
4586 the project is in 4/4 or an odd meter before producing MIDI. An agent reviewing a composition
4587 calls `muse meter --history` to identify when meter changes occurred and correlate them with
4588 creative decisions. `muse meter --polyrhythm` surfaces conflicts that would cause tracks to
4589 drift out of sync.
4590
4591 **Implementation:** `maestro/muse_cli/commands/meter.py`. All DB-touching paths are async
4592 (`open_session()` pattern). Exit codes: 0 success, 1 user error, 3 internal error.
4593
4594 ---
4595
4596 ### `muse emotion-diff`
4597
4598 **Purpose:** Compare emotion vectors between two commits to track how the emotional character of a composition changed over time. An AI agent uses this to detect whether a recent edit reinforced or subverted the intended emotional arc, and to decide whether to continue or correct the creative direction.
4599
4600 **Status:** ✅ Implemented (issue #100)
4601
4602 **Usage:**
4603 ```bash
4604 muse emotion-diff [COMMIT_A] [COMMIT_B] [OPTIONS]
4605 ```
4606
4607 **Flags:**
4608
4609 | Flag | Type | Default | Description |
4610 |------|------|---------|-------------|
4611 | `COMMIT_A` | positional | `HEAD~1` | Baseline commit ref (full hash, abbreviated hash, `HEAD`, or `HEAD~N`). |
4612 | `COMMIT_B` | positional | `HEAD` | Target commit ref. |
4613 | `--track TEXT` | option | — | Scope analysis to a specific track (noted in output; full per-track MIDI scoping is a follow-up). |
4614 | `--section TEXT` | option | — | Scope to a named section (same stub note as `--track`). |
4615 | `--json` | flag | off | Emit structured JSON for agent or tool consumption. |
4616
4617 **Sourcing strategy:**
4618
4619 1. **`explicit_tags`** — Both commits have `emotion:*` tags (set via `muse tag add emotion:<label>`). Vectors are looked up from the canonical emotion table.
4620 2. **`mixed`** — One commit has a tag, the other is inferred from metadata.
4621 3. **`inferred`** — Neither commit has an emotion tag. Vectors are inferred from available commit metadata (tempo, annotations). Full MIDI-feature inference (mode, note density, velocity) is tracked as a follow-up.
4622
4623 **Canonical emotion labels** (for `muse tag add emotion:<label>`):
4624 `joyful`, `melancholic`, `anxious`, `cinematic`, `peaceful`, `dramatic`, `hopeful`, `tense`, `dark`, `euphoric`, `serene`, `epic`, `mysterious`, `aggressive`, `nostalgic`.
4625
4626 **Output example (text):**
4627 ```
4628 Emotion diff — a1b2c3d4 → f9e8d7c6
4629 Source: explicit_tags
4630
4631 Commit A (a1b2c3d4): melancholic
4632 Commit B (f9e8d7c6): joyful
4633
4634 Dimension Commit A Commit B Delta
4635 ----------- -------- -------- -----
4636 energy 0.3000 0.8000 +0.5000
4637 valence 0.3000 0.9000 +0.6000
4638 tension 0.4000 0.2000 -0.2000
4639 darkness 0.6000 0.1000 -0.5000
4640
4641 Drift: 0.9747
4642 Dramatic emotional departure — a fundamentally different mood. melancholic → joyful (drift=0.975, major, dominant: +valence)
4643 ```
4644
4645 **Output example (JSON):**
4646 ```json
4647 {
4648 "commit_a": "a1b2c3d4",
4649 "commit_b": "f9e8d7c6",
4650 "source": "explicit_tags",
4651 "label_a": "melancholic",
4652 "label_b": "joyful",
4653 "vector_a": {"energy": 0.3, "valence": 0.3, "tension": 0.4, "darkness": 0.6},
4654 "vector_b": {"energy": 0.8, "valence": 0.9, "tension": 0.2, "darkness": 0.1},
4655 "dimensions": [
4656 {"dimension": "energy", "value_a": 0.3, "value_b": 0.8, "delta": 0.5},
4657 {"dimension": "valence", "value_a": 0.3, "value_b": 0.9, "delta": 0.6},
4658 {"dimension": "tension", "value_a": 0.4, "value_b": 0.2, "delta": -0.2},
4659 {"dimension": "darkness", "value_a": 0.6, "value_b": 0.1, "delta": -0.5}
4660 ],
4661 "drift": 0.9747,
4662 "narrative": "...",
4663 "track": null,
4664 "section": null
4665 }
4666 ```
4667
4668 **Result types:** `EmotionDiffResult`, `EmotionVector`, `EmotionDimDelta` — all defined in `maestro/services/muse_emotion_diff.py` and registered in `docs/reference/type_contracts.md`.
4669
4670 **Drift distance:** Euclidean distance in the 4-D emotion space. Range [0.0, 2.0].
4671 - < 0.05 — unchanged
4672 - 0.05–0.25 — subtle shift
4673 - 0.25–0.50 — moderate shift
4674 - 0.50–0.80 — significant shift
4675 - > 0.80 — major / dramatic departure
4676
4677 **Agent use case:** An AI composing a new verse calls `muse emotion-diff HEAD~1 HEAD --json` after each commit to verify the composition is tracking toward the intended emotional destination (e.g., building from `melancholic` to `hopeful` across an album arc). A drift > 0.5 on the wrong dimension triggers a course-correction prompt.
4678
4679 **Implementation:** `maestro/muse_cli/commands/emotion_diff.py` (CLI entry point) and `maestro/services/muse_emotion_diff.py` (core engine). All DB-touching paths are async (`open_session()` pattern). Commit refs support `HEAD`, `HEAD~N`, full 64-char hashes, and 8-char abbreviated hashes.
4680
4681 ---
4682
4683 ### Command Registration Summary
4684
4685 | Command | File | Status | Issue |
4686 |---------|------|--------|-------|
4687 | `muse dynamics` | `commands/dynamics.py` | ✅ stub (PR #130) | #120 |
4688 | `muse emotion-diff` | `commands/emotion_diff.py` | ✅ implemented (PR #100) | #100 |
4689 | `muse swing` | `commands/swing.py` | ✅ stub (PR #131) | #121 |
4690 | `muse recall` | `commands/recall.py` | ✅ stub (PR #135) | #122 |
4691 | `muse tag` | `commands/tag.py` | ✅ implemented (PR #133) | #123 |
4692 | `muse grep` | `commands/grep_cmd.py` | ✅ stub (PR #128) | #124 |
4693 | `muse describe` | `commands/describe.py` | ✅ stub (PR #134) | #125 |
4694 | `muse ask` | `commands/ask.py` | ✅ stub (PR #132) | #126 |
4695 | `muse session` | `commands/session.py` | ✅ implemented (PR #129) | #127 |
4696 | `muse meter` | `commands/meter.py` | ✅ implemented (PR #141) | #117 |
4697
4698 All stub commands have stable CLI contracts. Full musical analysis (MIDI content
4699 parsing, vector embeddings, LLM synthesis) is tracked as follow-up issues.
4700 [
4701 {
4702 "rank": 1,
4703 "score": 1.0,
4704 "commit_id": "a1b2c3d4...",
4705 "date": "2026-02-20 14:30:00",
4706 "branch": "main",
4707 "message": "add dark jazz bassline to verse"
4708 }
4709 ]
4710 ```
4711
4712 **Result type:** `RecallResult` (`TypedDict`) — fields: `rank` (int),
4713 `score` (float, rounded to 4 decimal places), `commit_id` (str),
4714 `date` (str, `"YYYY-MM-DD HH:MM:SS"`), `branch` (str), `message` (str).
4715 See `docs/reference/type_contracts.md § RecallResult`.
4716
4717 **Agent use case:** An AI composing a new variation queries `muse recall
4718 "dark jazz bassline"` to surface all commits that previously explored that
4719 texture — letting the agent reuse, invert, or contrast those ideas. The
4720 `--json` flag makes the result directly parseable in an agentic pipeline;
4721 `--threshold 0.0` with a broad query retrieves the full ranked history.
4722
4723 **Implementation:** `maestro/muse_cli/commands/recall.py` —
4724 `RecallResult` (TypedDict), `_tokenize()`, `_score()`, `_fetch_commits()`,
4725 `_recall_async()`, `_render_results()`. Exit codes: 0 success,
4726 1 bad date format (`USER_ERROR`), 2 outside repo (`REPO_NOT_FOUND`),
4727 3 internal error (`INTERNAL_ERROR`).
4728
4729 > **Planned enhancement:** Full semantic vector search via Qdrant with
4730 > cosine similarity over pre-computed embeddings. When implemented, the
4731 > scoring function will be replaced with no change to the CLI interface.
4732
4733 ---
4734 ## `muse context` — Structured Musical Context for AI Agents
4735
4736 **Purpose:** Output a structured, self-contained musical context document for AI agent consumption. This is the **primary interface between Muse VCS and AI music generation agents** — agents run `muse context` before any generation task to understand the current key, tempo, active tracks, form, harmonic profile, and evolutionary history of the composition.
4737
4738 **Usage:**
4739 ```bash
4740 muse context [<commit>] [OPTIONS]
4741 ```
4742
4743 **Flags:**
4744
4745 | Flag | Type | Default | Description |
4746 |------|------|---------|-------------|
4747 | `<commit>` | positional | HEAD | Target commit ID to inspect |
4748 | `--depth N` | int | 5 | Number of ancestor commits to include in `history` |
4749 | `--sections` | flag | off | Expand section-level detail in `musical_state.sections` |
4750 | `--tracks` | flag | off | Add per-track harmonic and dynamic breakdowns |
4751 | `--include-history` | flag | off | Annotate history entries with dimensional deltas (future Storpheus integration) |
4752 | `--format json\|yaml` | string | json | Output format |
4753
4754 **Output example (`--format json`):**
4755 ```json
4756 {
4757 "repo_id": "a1b2c3d4-...",
4758 "current_branch": "main",
4759 "head_commit": {
4760 "commit_id": "abc1234...",
4761 "message": "Add piano melody to verse",
4762 "author": "Gabriel",
4763 "committed_at": "2026-02-27T22:00:00+00:00"
4764 },
4765 "musical_state": {
4766 "active_tracks": ["bass", "drums", "piano"],
4767 "key": null,
4768 "tempo_bpm": null,
4769 "sections": null,
4770 "tracks": null
4771 },
4772 "history": [
4773 {
4774 "commit_id": "...",
4775 "message": "Add bass line",
4776 "active_tracks": ["bass", "drums"],
4777 "key": null,
4778 "tempo_bpm": null
4779 }
4780 ],
4781 "missing_elements": [],
4782 "suggestions": {}
4783 }
4784 ```
4785
4786 **Result type:** `MuseContextResult` — fields: `repo_id`, `current_branch`, `head_commit` (`MuseHeadCommitInfo`), `musical_state` (`MuseMusicalState`), `history` (`list[MuseHistoryEntry]`), `missing_elements`, `suggestions`. See `docs/reference/type_contracts.md`.
4787
4788 **Agent use case:** When Maestro receives a "generate a new section" request, it runs `muse context --format json` to obtain the current musical state, passes the result to the LLM, and the LLM generates music that is harmonically, rhythmically, and structurally coherent with the existing composition. Without this command, generation decisions are musically incoherent.
4789
4790 **Implementation notes:**
4791 - `active_tracks` is populated from MIDI/audio file names in the snapshot manifest (real data).
4792 - Musical dimensions (`key`, `tempo_bpm`, `form`, `emotion`, harmonic/dynamic/melodic profiles) are `null` until Storpheus MIDI analysis is integrated. The full schema is defined and stable.
4793 - `sections` and `tracks` are populated when the respective flags are passed; sections currently use a single "main" stub section containing all active tracks until MIDI region metadata is available.
4794 - Output is **deterministic**: for the same `commit_id` and flags, the output is always identical.
4795
4796 **Implementation:** `maestro/services/muse_context.py` (service layer), `maestro/muse_cli/commands/context.py` (CLI command). Exit codes: 0 success, 1 user error (bad commit, no commits), 2 outside repo, 3 internal.
4797
4798 ---
4799
4800 ## `muse dynamics` — Dynamic (Velocity) Profile Analysis
4801
4802 **Purpose:** Analyze the velocity (loudness) profile of a commit across all instrument
4803 tracks. The primary tool for understanding the dynamic arc of an arrangement and
4804 detecting flat, robotic, or over-compressed MIDI.
4805
4806 **Usage:**
4807 ```bash
4808 muse dynamics [<commit>] [OPTIONS]
4809 ```
4810
4811 **Flags:**
4812
4813 | Flag | Type | Default | Description |
4814 |------|------|---------|-------------|
4815 | `COMMIT` | positional | HEAD | Commit ref to analyze |
4816 | `--track TEXT` | string | all tracks | Case-insensitive prefix filter (e.g. `--track bass`) |
4817 | `--section TEXT` | string | — | Restrict to a named section/region (planned) |
4818 | `--compare COMMIT` | string | — | Side-by-side comparison with another commit (planned) |
4819 | `--history` | flag | off | Show dynamics for every commit in branch history (planned) |
4820 | `--peak` | flag | off | Show only tracks whose peak velocity exceeds the branch average |
4821 | `--range` | flag | off | Sort output by velocity range descending |
4822 | `--arc` | flag | off | When combined with `--track`, treat its value as an arc label filter |
4823 | `--json` | flag | off | Emit structured JSON for agent consumption |
4824
4825 **Arc labels:**
4826
4827 | Label | Meaning |
4828 |-------|---------|
4829 | `flat` | Velocity variance < 10; steady throughout |
4830 | `crescendo` | Monotonically rising from start to end |
4831 | `decrescendo` | Monotonically falling from start to end |
4832 | `terraced` | Step-wise plateaus; sudden jumps between stable levels |
4833 | `swell` | Rises then falls (arch shape) |
4834
4835 **Output example (text):**
4836 ```
4837 Dynamic profile — commit a1b2c3d4 (HEAD -> main)
4838
4839 Track Avg Vel Peak Range Arc
4840 --------- ------- ---- ----- -----------
4841 drums 88 110 42 terraced
4842 bass 72 85 28 flat
4843 keys 64 95 56 crescendo
4844 lead 79 105 38 swell
4845 ```
4846
4847 **Output example (`--json`):**
4848 ```json
4849 {
4850 "commit": "a1b2c3d4",
4851 "branch": "main",
4852 "tracks": [
4853 {"track": "drums", "avg_velocity": 88, "peak_velocity": 110, "velocity_range": 42, "arc": "terraced"}
4854 ]
4855 }
4856 ```
4857
4858 **Result type:** `TrackDynamics` — fields: `name`, `avg_velocity`, `peak_velocity`, `velocity_range`, `arc`
4859
4860 **Agent use case:** Before generating a new layer, an agent calls `muse dynamics --json` to understand the current velocity landscape. If the arrangement is `flat` across all tracks, the agent adds velocity variation to the new part. If the arc is `crescendo`, the agent ensures the new layer contributes to rather than fights the build.
4861
4862 **Implementation:** `maestro/muse_cli/commands/dynamics.py` — `_dynamics_async` (injectable async core), `TrackDynamics` (result entity), `_render_table` / `_render_json` (renderers). Exit codes: 0 success, 2 outside repo, 3 internal.
4863
4864 > **Stub note:** Arc classification and velocity statistics are placeholder values. Full implementation requires MIDI note velocity extraction from committed snapshot objects (future: Storpheus MIDI parse route).
4865
4866 ---
4867
4868 ## `muse humanize` — Apply Micro-Timing and Velocity Humanization to Quantized MIDI
4869
4870 **Purpose:** Apply realistic human-performance variation to machine-quantized MIDI, producing a new Muse commit that sounds natural. AI agents use this after generating quantized output to make compositions feel human before presenting them to DAW users.
4871
4872 **Usage:**
4873 ```bash
4874 muse humanize [COMMIT] [OPTIONS]
4875 ```
4876
4877 **Flags:**
4878
4879 | Flag | Type | Default | Description |
4880 |------|------|---------|-------------|
4881 | `COMMIT` | argument | HEAD | Source commit ref to humanize |
4882 | `--tight` | flag | off | Subtle: timing +/-5 ms, velocity +/-5 |
4883 | `--natural` | flag | on | Moderate: timing +/-12 ms, velocity +/-10 (default) |
4884 | `--loose` | flag | off | Heavy: timing +/-20 ms, velocity +/-15 |
4885 | `--factor FLOAT` | float | - | Custom factor 0.0-1.0 (overrides preset) |
4886 | `--timing-only` | flag | off | Apply timing variation only; preserve velocities |
4887 | `--velocity-only` | flag | off | Apply velocity variation only; preserve timing |
4888 | `--track TEXT` | string | all | Restrict to one track (prefix match) |
4889 | `--section TEXT` | string | all | Restrict to a named section |
4890 | `--seed N` | int | - | Fix random seed for reproducible output |
4891 | `--message TEXT` | string | auto | Commit message |
4892 | `--json` | flag | off | Emit structured JSON for agent consumption |
4893
4894 **Result types:** `HumanizeResult` and `TrackHumanizeResult` (both TypedDict). See `docs/reference/type_contracts.md`.
4895
4896 **Agent use case:** After `muse commit` records a machine-generated MIDI variation, an AI agent runs `muse humanize --natural --seed 42` to add realistic performance feel. Drum groove is preserved automatically (GM channel 10 excluded from timing variation).
4897
4898 **Implementation:** `maestro/muse_cli/commands/humanize.py`. Exit codes: 0 success, 1 flag conflict, 2 outside repo, 3 internal.
4899
4900 > **Stub note:** Full MIDI note rewrite pending Storpheus note-level access. CLI interface is stable.
4901
4902 ---
4903
4904 ## `muse import` — Import a MIDI or MusicXML File as a New Muse Commit
4905
4906 ### Overview
4907
4908 `muse import <file>` ingests an external music file into a Muse-tracked project
4909 by copying it into `muse-work/imports/` and creating a Muse commit. It is the
4910 primary on-ramp for bringing existing DAW sessions, MIDI exports, or orchestral
4911 scores under Muse version control.
4912
4913 ### Supported Formats
4914
4915 | Extension | Format | Parser |
4916 |-----------|--------|--------|
4917 | `.mid`, `.midi` | Standard MIDI File | `mido` library |
4918 | `.xml`, `.musicxml` | MusicXML (score-partwise) | `xml.etree.ElementTree` |
4919
4920 ### Command Signature
4921
4922 ```
4923 muse import <file> [OPTIONS]
4924
4925 Arguments:
4926 file Path to the MIDI or MusicXML file to import.
4927
4928 Options:
4929 --message, -m TEXT Commit message (default: "Import <filename>").
4930 --track-map TEXT Map MIDI channels to track names.
4931 Format: "ch0=bass,ch1=piano,ch9=drums"
4932 --section TEXT Tag the imported content as a specific section.
4933 --analyze Run multi-dimensional analysis and display results.
4934 --dry-run Validate only — do not write files or commit.
4935 ```
4936
4937 ### What It Does
4938
4939 1. **Validate** — Checks that the file extension is supported. Clear error on unsupported types.
4940 2. **Parse** — Extracts `NoteEvent` objects (pitch, velocity, timing, channel) using format-specific parsers.
4941 3. **Apply track map** — Renames `channel_name` fields for any channels listed in `--track-map`.
4942 4. **Copy** — Copies the source file to `muse-work/imports/<filename>`.
4943 5. **Write metadata** — Creates `muse-work/imports/<filename>.meta.json` with note count, tracks, tempo, and track-map.
4944 6. **Commit** — Calls `_commit_async` to create a Muse commit with the imported content.
4945 7. **Analyse (optional)** — Prints a three-dimensional analysis: harmonic (pitch range, top pitches), rhythmic (note count, density, beats), dynamic (velocity distribution).
4946
4947 ### Track Map Syntax
4948
4949 The `--track-map` option accepts a comma-separated list of `KEY=VALUE` pairs where
4950 KEY is either `ch<N>` (e.g. `ch0`) or a bare channel number (e.g. `0`):
4951
4952 ```
4953 muse import song.mid --track-map "ch0=bass,ch1=piano,ch9=drums"
4954 ```
4955
4956 Unmapped channels retain their default label `ch<N>`. The mapping is persisted
4957 in `muse-work/imports/<filename>.meta.json` so downstream tooling can reconstruct
4958 track assignments from a commit.
4959
4960 ### Metadata JSON Format
4961
4962 Every import writes a sidecar JSON file alongside the imported file:
4963
4964 ```json
4965 {
4966 "source": "/absolute/path/to/source.mid",
4967 "format": "midi",
4968 "ticks_per_beat": 480,
4969 "tempo_bpm": 120.0,
4970 "note_count": 64,
4971 "tracks": ["bass", "piano", "drums"],
4972 "track_map": {"ch0": "bass", "ch1": "piano", "ch9": "drums"},
4973 "section": "verse",
4974 "raw_meta": {"num_tracks": 3}
4975 }
4976 ```
4977
4978 ### Dry Run
4979
4980 `--dry-run` validates the file and shows what would be committed without creating
4981 any files or DB rows:
4982
4983 ```
4984 $ muse import song.mid --dry-run
4985 ✅ Dry run: 'song.mid' is valid (midi)
4986 Notes: 128, Tracks: 3, Tempo: 120.0 BPM
4987 Would commit: "Import song.mid"
4988 ```
4989
4990 ### Analysis Output
4991
4992 `--analyze` appends a three-section report after the import:
4993
4994 ```
4995 Analysis:
4996 Format: midi
4997 Tempo: 120.0 BPM
4998 Tracks: bass, piano, drums
4999
5000 ── Harmonic ──────────────────────────────────
5001 Pitch range: C2–G5
5002 Top pitches: E4(12x), C4(10x), G4(8x), D4(6x), A4(5x)
5003
5004 ── Rhythmic ──────────────────────────────────
5005 Notes: 128
5006 Span: 32.0 beats
5007 Density: 4.0 notes/beat
5008
5009 ── Dynamic ───────────────────────────────────
5010 Velocity: avg=82, min=64, max=110
5011 Character: f (loud)
5012 ```
5013
5014 ### Implementation
5015
5016 | File | Role |
5017 |------|------|
5018 | `maestro/muse_cli/midi_parser.py` | Parsing, track-map, analysis — all pure functions, no DB or I/O |
5019 | `maestro/muse_cli/commands/import_cmd.py` | Typer command and `_import_async` core |
5020 | `tests/muse_cli/test_import.py` | 23 unit + integration tests |
5021
5022 ### Muse VCS Considerations
5023
5024 - **Affected operation:** `commit` — creates a new commit row.
5025 - **Postgres state:** One new `muse_cli_commits` row, one `muse_cli_snapshots` row, and two `muse_cli_objects` rows (the MIDI/XML file + the `.meta.json`).
5026 - **No schema migration required** — uses existing tables.
5027 - **Reproducibility:** Deterministic — same file + same flags → identical commit content (same `snapshot_id`).
5028 - **`muse-work/imports/`** — the canonical import landing zone, parallel to `muse-work/tracks/`, `muse-work/renders/`, etc.
5029
5030 ### Error Handling
5031
5032 | Scenario | Exit code | Message |
5033 |----------|-----------|---------|
5034 | File not found | 1 (USER_ERROR) | `❌ File not found: <path>` |
5035 | Unsupported extension | 1 (USER_ERROR) | `❌ Unsupported file extension '.<ext>'. Supported: …` |
5036 | Malformed MIDI | 1 (USER_ERROR) | `❌ Cannot parse MIDI file '<path>': <reason>` |
5037 | Malformed MusicXML | 1 (USER_ERROR) | `❌ Cannot parse MusicXML file '<path>': <reason>` |
5038 | Invalid `--track-map` | 1 (USER_ERROR) | `❌ --track-map: Invalid track-map entry …` |
5039 | Not in a repo | 2 (REPO_NOT_FOUND) | Standard `require_repo()` message |
5040 | Unexpected failure | 3 (INTERNAL_ERROR) | `❌ muse import failed: <exc>` |
5041
5042 ---
5043
5044 ## `muse divergence` — Musical Divergence Between Two Branches
5045
5046 **Purpose:** Show how two branches have diverged *musically* — useful when two
5047 producers are working on different arrangements of the same project and you need
5048 to understand the creative distance before deciding which to merge.
5049
5050 **Implementation:** `maestro/muse_cli/commands/divergence.py`\
5051 **Service:** `maestro/services/muse_divergence.py`\
5052 **Status:** ✅ implemented (issue #119)
5053
5054 ### Flags
5055
5056 | Flag | Type | Default | Description |
5057 |------|------|---------|-------------|
5058 | `BRANCH_A` | positional | required | First branch name |
5059 | `BRANCH_B` | positional | required | Second branch name |
5060 | `--since COMMIT` | string | auto | Common ancestor commit ID (auto-detected via merge-base BFS if omitted) |
5061 | `--dimensions TEXT` | string (repeatable) | all five | Musical dimension(s) to analyse |
5062 | `--json` | flag | off | Machine-readable JSON output |
5063
5064 ### What It Computes
5065
5066 1. **Finds the merge base** — BFS over `MuseCliCommit.parent_commit_id` / `parent2_commit_id`, equivalent to `git merge-base`.
5067 2. **Collects changed paths** — diff from merge-base snapshot to branch-tip (added + deleted + modified paths).
5068 3. **Classifies paths by dimension** — keyword matching on lowercase filename.
5069 4. **Scores each dimension** — `score = |sym_diff(A, B)| / |union(A, B)|`. 0.0 = identical; 1.0 = completely diverged.
5070 5. **Classifies level** — `NONE` (<0.15), `LOW` (0.15–0.40), `MED` (0.40–0.70), `HIGH` (≥0.70).
5071 6. **Computes overall score** — mean of per-dimension scores.
5072
5073 ### Result types
5074
5075 `DivergenceLevel` (Enum), `DimensionDivergence` (frozen dataclass), `MuseDivergenceResult` (frozen dataclass).
5076 See `docs/reference/type_contracts.md § Muse Divergence Types`.
5077
5078 ### Agent use case
5079
5080 An AI deciding which branch to merge calls `muse divergence feature/guitar feature/piano --json`
5081 before generation. HIGH harmonic divergence + LOW rhythmic divergence means lean on the piano
5082 branch for chord voicings while preserving the guitar branch's groove patterns.
5083
5084 ### `muse timeline`
5085
5086 **Purpose:** Render a commit-by-commit chronological view of a composition's
5087 creative arc — emotion transitions, section progress, and per-track activity.
5088 This is the "album liner notes" view that no Git command provides. Agents
5089 use it to understand how a project's emotional and structural character
5090 evolved before making generation decisions.
5091
5092 **Usage:**
5093 ```bash
5094 muse timeline [RANGE] [OPTIONS]
5095 ```
5096
5097 **Flags:**
5098 | Flag | Type | Default | Description |
5099 |------|------|---------|-------------|
5100 | `RANGE` | positional string | full history | Commit range (reserved — full history shown for now) |
5101 | `--emotion` | flag | off | Add emotion column (from `emotion:*` tags) |
5102 | `--sections` | flag | off | Group commits under section headers (from `section:*` tags) |
5103 | `--tracks` | flag | off | Show per-track activity column (from `track:*` tags) |
5104 | `--json` | flag | off | Emit structured JSON for UI rendering or agent consumption |
5105 | `--limit N` | int | 1000 | Maximum commits to walk |
5106
5107 **Output example (text):**
5108 ```
5109 Timeline — branch: main (3 commit(s))
5110
5111 ── verse ──
5112 2026-02-01 abc1234 Initial drum arrangement [drums] [melancholic] ████
5113 2026-02-02 def5678 Add bass line [bass] [melancholic] ██████
5114 ── chorus ──
5115 2026-02-03 ghi9012 Chorus melody [keys,vocals] [joyful] █████████
5116
5117 Emotion arc: melancholic → joyful
5118 Sections: verse → chorus
5119 ```
5120
5121 **Output example (JSON):**
5122 ```json
5123 {
5124 "branch": "main",
5125 "total_commits": 3,
5126 "emotion_arc": ["melancholic", "joyful"],
5127 "section_order": ["verse", "chorus"],
5128 "entries": [
5129 {
5130 "commit_id": "abc1234...",
5131 "short_id": "abc1234",
5132 "committed_at": "2026-02-01T00:00:00+00:00",
5133 "message": "Initial drum arrangement",
5134 "emotion": "melancholic",
5135 "sections": ["verse"],
5136 "tracks": ["drums"],
5137 "activity": 1
5138 }
5139 ]
5140 }
5141 ```
5142
5143 **Result types:** `MuseTimelineEntry`, `MuseTimelineResult` — see `docs/reference/type_contracts.md § Muse Timeline Types`.
5144
5145 **Agent use case:** An AI agent calls `muse timeline --json` before composing a new
5146 section to understand the emotional arc to date (e.g. `melancholic → joyful → tense`).
5147 It uses `section_order` to determine what structural elements have been established
5148 and `emotion_arc` to decide whether to maintain or contrast the current emotional
5149 character. `activity` per commit helps identify which sections were most actively
5150 developed.
5151
5152 **Implementation note:** Emotion, section, and track data are derived entirely from
5153 tags attached via `muse tag add`. Commits with no tags show `—` in filtered columns.
5154 The commit range argument (`RANGE`) is accepted but reserved for a future iteration
5155 that supports `HEAD~10..HEAD` syntax.
5156
5157 ---
5158
5159 ### `muse validate`
5160
5161 **Purpose:** Run integrity checks against the working tree before `muse commit`.
5162 Detects corrupted MIDI files, manifest mismatches, duplicate instrument roles,
5163 non-conformant section names, and unknown emotion tags — giving agents and
5164 producers an actionable quality gate before bad state enters history.
5165
5166 **Status:** ✅ Fully implemented (issue #99)
5167
5168 **Usage:**
5169 ```bash
5170 muse validate [OPTIONS]
5171 ```
5172
5173 **Flags:**
5174
5175 | Flag | Type | Default | Description |
5176 |------|------|---------|-------------|
5177 | `--strict` | flag | off | Exit 2 on warnings as well as errors. |
5178 | `--track TEXT` | string | — | Restrict checks to files/paths containing TEXT (case-insensitive). |
5179 | `--section TEXT` | string | — | Restrict section-naming check to directories containing TEXT. |
5180 | `--fix` | flag | off | Auto-fix correctable issues (conservative; no data-loss risk). |
5181 | `--json` | flag | off | Emit full structured JSON for agent consumption. |
5182
5183 **Exit codes:**
5184
5185 | Code | Meaning |
5186 |------|---------|
5187 | 0 | All checks passed — working tree is clean. |
5188 | 1 | One or more ERROR issues found (corrupted MIDI, orphaned files). |
5189 | 2 | WARN issues found AND `--strict` was passed. |
5190 | 3 | Internal error (unexpected exception). |
5191
5192 **Checks performed:**
5193
5194 | Check | Severity | Description |
5195 |-------|----------|-------------|
5196 | `midi_integrity` | ERROR | Verifies each `.mid`/`.midi` has a valid SMF `MThd` header. |
5197 | `manifest_consistency` | ERROR/WARN | Compares committed snapshot manifest vs actual working tree. |
5198 | `no_duplicate_tracks` | WARN | Detects multiple MIDI files sharing the same instrument role. |
5199 | `section_naming` | WARN | Verifies section dirs match `[a-z][a-z0-9_-]*`. |
5200 | `emotion_tags` | WARN | Checks emotion tags (`.muse/tags.json`) against the allowed vocabulary. |
5201
5202 **Output example (human-readable):**
5203 ```
5204 Validating working tree …
5205
5206 ✅ midi_integrity PASS
5207 ❌ manifest_consistency FAIL
5208 ❌ ERROR beat.mid File in committed manifest is missing from working tree.
5209 ✅ no_duplicate_tracks PASS
5210 ⚠️ section_naming WARN
5211 ⚠️ WARN Verse Section directory 'Verse' does not follow naming convention.
5212 ✅ emotion_tags PASS
5213
5214 ⚠️ 1 error, 1 warning — working tree has integrity issues.
5215 ```
5216
5217 **Output example (`--json`):**
5218 ```json
5219 {
5220 "clean": false,
5221 "has_errors": true,
5222 "has_warnings": true,
5223 "checks": [
5224 { "name": "midi_integrity", "passed": true, "issues": [] },
5225 {
5226 "name": "manifest_consistency",
5227 "passed": false,
5228 "issues": [
5229 {
5230 "severity": "error",
5231 "check": "manifest_consistency",
5232 "path": "beat.mid",
5233 "message": "File in committed manifest is missing from working tree (orphaned)."
5234 }
5235 ]
5236 }
5237 ],
5238 "fixes_applied": []
5239 }
5240 ```
5241
5242 **Result types:** `MuseValidateResult`, `ValidationCheckResult`, `ValidationIssue`, `ValidationSeverity`
5243 — all defined in `maestro/services/muse_validate.py` and registered in `docs/reference/type_contracts.md`.
5244
5245 **Agent use case:** An AI composition agent calls `muse validate --json` before every
5246 `muse commit` to confirm the working tree is consistent. If `has_errors` is true the agent
5247 must investigate the failing check before committing — a corrupted MIDI would silently
5248 corrupt the composition history. With `--strict`, agents can enforce zero-warning quality gates.
5249
5250 ---
5251 ## `muse diff` — Music-Dimension Diff Between Commits
5252
5253 **Purpose:** Compare two commits across five orthogonal musical dimensions —
5254 harmonic, rhythmic, melodic, structural, and dynamic. Where `git diff` tells
5255 you "file changed," `muse diff --harmonic` tells you "the song modulated from
5256 Eb major to F minor and the tension profile doubled." This is the killer
5257 feature that proves Muse's value over Git: musically meaningful version control.
5258
5259 **Usage:**
5260 ```bash
5261 muse diff [<COMMIT_A>] [<COMMIT_B>] [OPTIONS]
5262 ```
5263
5264 Defaults: `COMMIT_A` = HEAD~1, `COMMIT_B` = HEAD.
5265
5266 **Flags:**
5267
5268 | Flag | Type | Default | Description |
5269 |------|------|---------|-------------|
5270 | `COMMIT_A` | positional | HEAD~1 | Earlier commit ref |
5271 | `COMMIT_B` | positional | HEAD | Later commit ref |
5272 | `--harmonic` | flag | off | Compare key, mode, chord progression, tension |
5273 | `--rhythmic` | flag | off | Compare tempo, meter, swing, groove drift |
5274 | `--melodic` | flag | off | Compare motifs, contour, pitch range |
5275 | `--structural` | flag | off | Compare sections, instrumentation, form |
5276 | `--dynamic` | flag | off | Compare velocity arc, per-track loudness |
5277 | `--all` | flag | off | Run all five dimensions simultaneously |
5278 | `--json` | flag | off | Emit structured JSON for agent consumption |
5279
5280 **Output example (`muse diff HEAD~1 HEAD --harmonic`):**
5281 ```
5282 Harmonic diff: abc1234 -> def5678
5283
5284 Key: Eb major -> F minor
5285 Mode: Major -> Minor
5286 Chord prog: I-IV-V-I -> i-VI-III-VII
5287 Tension: Low (0.2) -> Medium-High (0.65)
5288 Summary: Major harmonic restructuring — key modulation down a minor 3rd, shift to Andalusian cadence
5289 ```
5290
5291 **Output example (`muse diff HEAD~1 HEAD --rhythmic`):**
5292 ```
5293 Rhythmic diff: abc1234 -> def5678
5294
5295 Tempo: 120.0 BPM -> 128.0 BPM (+8.0 BPM)
5296 Meter: 4/4 -> 4/4
5297 Swing: Straight (0.5) -> Light swing (0.57)
5298 Groove drift: 12.0ms -> 6.0ms
5299 Summary: Slightly faster, more swung, tighter quantization
5300 ```
5301
5302 **Output example (`muse diff HEAD~1 HEAD --all`):**
5303 ```
5304 Music diff: abc1234 -> def5678
5305 Changed: harmonic, rhythmic, melodic, structural, dynamic
5306 Unchanged: (none)
5307
5308 -- Harmonic --
5309 ...
5310
5311 -- Rhythmic --
5312 ...
5313 ```
5314
5315 **Unchanged dimensions:** When a dimension shows no change, the renderer appends
5316 `Unchanged` to the block rather than omitting it. This guarantees agents always
5317 receive a complete report — silence is never ambiguous.
5318
5319 **Result types:**
5320
5321 | Type | Fields |
5322 |------|--------|
5323 | `HarmonicDiffResult` | `commit_a/b`, `key_a/b`, `mode_a/b`, `chord_prog_a/b`, `tension_a/b`, `tension_label_a/b`, `summary`, `changed` |
5324 | `RhythmicDiffResult` | `commit_a/b`, `tempo_a/b`, `meter_a/b`, `swing_a/b`, `swing_label_a/b`, `groove_drift_ms_a/b`, `summary`, `changed` |
5325 | `MelodicDiffResult` | `commit_a/b`, `motifs_introduced`, `motifs_removed`, `contour_a/b`, `range_low_a/b`, `range_high_a/b`, `summary`, `changed` |
5326 | `StructuralDiffResult` | `commit_a/b`, `sections_added`, `sections_removed`, `instruments_added`, `instruments_removed`, `form_a/b`, `summary`, `changed` |
5327 | `DynamicDiffResult` | `commit_a/b`, `avg_velocity_a/b`, `arc_a/b`, `tracks_louder`, `tracks_softer`, `tracks_silent`, `summary`, `changed` |
5328 | `MusicDiffReport` | All five dimension results + `changed_dimensions`, `unchanged_dimensions`, `summary` |
5329
5330 See `docs/reference/type_contracts.md § Muse Diff Types`.
5331
5332 **Agent use case:** An AI composing a new variation runs
5333 `muse diff HEAD~3 HEAD --harmonic --json` before generating to understand
5334 whether the last three sessions have been converging on a key or exploring
5335 multiple tonalities. The `changed_dimensions` field in `MusicDiffReport` lets
5336 the agent prioritize which musical parameters to vary next.
5337
5338 **Implementation:** `maestro/muse_cli/commands/diff.py` —
5339 `HarmonicDiffResult`, `RhythmicDiffResult`, `MelodicDiffResult`,
5340 `StructuralDiffResult`, `DynamicDiffResult`, `MusicDiffReport` (TypedDicts);
5341 `_harmonic_diff_async()`, `_rhythmic_diff_async()`, `_melodic_diff_async()`,
5342 `_structural_diff_async()`, `_dynamic_diff_async()`, `_diff_all_async()`;
5343 `_render_harmonic()`, `_render_rhythmic()`, `_render_melodic()`,
5344 `_render_structural()`, `_render_dynamic()`, `_render_report()`;
5345 `_resolve_refs()`, `_tension_label()`.
5346 Exit codes: 0 success, 2 outside repo (`REPO_NOT_FOUND`), 3 internal error.
5347
5348 > **Stub note:** All dimension analyses return realistic placeholder data.
5349 > Full implementation requires Storpheus MIDI parsing for chord/tempo/motif
5350 > extraction. The CLI contract (flags, output schema, result types) is frozen
5351 > so agents can rely on it before the analysis pipeline is wired in.
5352
5353 ---
5354
5355 ## `muse inspect` — Print Structured JSON of the Muse Commit Graph
5356
5357 **Purpose:** Serialize the full commit graph reachable from a starting reference
5358 into machine-readable output. This is the primary introspection tool for AI
5359 agents and tooling that need to programmatically traverse or audit commit history,
5360 branch state, and compositional metadata without parsing human-readable output.
5361
5362 **Implementation:** `maestro/muse_cli/commands/inspect.py`\
5363 **Service:** `maestro/services/muse_inspect.py`\
5364 **Status:** ✅ implemented (issue #98)
5365
5366 ### Usage
5367
5368 ```bash
5369 muse inspect # JSON of HEAD branch history
5370 muse inspect abc1234 # start from a specific commit
5371 muse inspect --depth 5 # limit to 5 commits
5372 muse inspect --branches # include all branch heads
5373 muse inspect --format dot # Graphviz DOT graph
5374 muse inspect --format mermaid # Mermaid.js graph definition
5375 ```
5376
5377 ### Flags
5378
5379 | Flag | Type | Default | Description |
5380 |------|------|---------|-------------|
5381 | `[<ref>]` | positional | HEAD | Starting commit ID or branch name |
5382 | `--depth N` | int | unlimited | Limit graph traversal to N commits per branch |
5383 | `--branches` | flag | off | Include all branch heads and their reachable commits |
5384 | `--tags` | flag | off | Include tag refs in the output |
5385 | `--format` | enum | `json` | Output format: `json`, `dot`, `mermaid` |
5386
5387 ### Output example (JSON)
5388
5389 ```json
5390 {
5391 "repo_id": "550e8400-e29b-41d4-a716-446655440000",
5392 "current_branch": "main",
5393 "branches": {
5394 "main": "a1b2c3d4e5f6...",
5395 "feature/guitar": "f9e8d7c6b5a4..."
5396 },
5397 "commits": [
5398 {
5399 "commit_id": "a1b2c3d4e5f6...",
5400 "short_id": "a1b2c3d4",
5401 "branch": "main",
5402 "parent_commit_id": "f9e8d7c6b5a4...",
5403 "parent2_commit_id": null,
5404 "message": "boom bap demo take 2",
5405 "author": "",
5406 "committed_at": "2026-02-27T17:30:00+00:00",
5407 "snapshot_id": "deadbeef...",
5408 "metadata": {"tempo_bpm": 95.0},
5409 "tags": ["emotion:melancholic", "stage:rough-mix"]
5410 }
5411 ]
5412 }
5413 ```
5414
5415 ### Result types
5416
5417 `MuseInspectCommit` (frozen dataclass) — one commit node in the graph.\
5418 `MuseInspectResult` (frozen dataclass) — full serialized graph with branch pointers.\
5419 `InspectFormat` (str Enum) — `json`, `dot`, `mermaid`.\
5420 See `docs/reference/type_contracts.md § Muse Inspect Types`.
5421
5422 ### Format: DOT
5423
5424 Graphviz DOT directed graph. Pipe to `dot -Tsvg` to render a visual DAG:
5425
5426 ```bash
5427 muse inspect --format dot | dot -Tsvg -o graph.svg
5428 ```
5429
5430 Each commit becomes an ellipse node labelled `<short_id>\n<message[:40]>`.
5431 Parent edges point child → parent (matching git convention). Branch refs
5432 appear as bold rectangle nodes pointing to their HEAD commit.
5433
5434 ### Format: Mermaid
5435
5436 Mermaid.js `graph LR` definition. Embed in GitHub markdown:
5437
5438 ```
5439 muse inspect --format mermaid
5440 ```
5441
5442 ```mermaid
5443 graph LR
5444 a1b2c3d4["a1b2c3d4: boom bap demo take 2"]
5445 f9e8d7c6["f9e8d7c6: boom bap demo take 1"]
5446 a1b2c3d4 --> f9e8d7c6
5447 main["main"]
5448 main --> a1b2c3d4
5449 ```
5450
5451 ### Agent use case
5452
5453 An AI composition agent calls `muse inspect --format json` before generating
5454 new music to understand the full lineage of the project:
5455
5456 1. **Branch discovery** — which creative threads exist (`branches` dict).
5457 2. **Graph traversal** — which commits are ancestors, which are on feature branches.
5458 3. **Metadata audit** — which commits have explicit tempo, meter, or emotion tags.
5459 4. **Divergence awareness** — combined with `muse divergence`, informs merge decisions.
5460
5461 The JSON output is deterministic for a fixed graph state, making it safe to cache
5462 between agent invocations and diff to detect graph changes.
5463
5464 ---
5465
5466 ## `muse render-preview [<commit>]` — Audio Preview of a Commit Snapshot
5467
5468 **Purpose:** Render the MIDI snapshot of any commit to an audio file, letting producers and AI agents hear what the project sounded like at any point in history — without opening a DAW session. The musical equivalent of `git show <commit>` with audio playback.
5469
5470 **Implementation:** `maestro/muse_cli/commands/render_preview.py`\
5471 **Service:** `maestro/services/muse_render_preview.py`\
5472 **Status:** ✅ implemented (issue #96)
5473
5474 ### Flags
5475
5476 | Flag | Type | Default | Description |
5477 |------|------|---------|-------------|
5478 | `[<commit>]` | positional string | HEAD | Short commit ID prefix to preview |
5479 | `--format` / `-f` | `wav\|mp3\|flac` | `wav` | Output audio format |
5480 | `--track TEXT` | string | all | Render only MIDI files matching this track name substring |
5481 | `--section TEXT` | string | all | Render only MIDI files matching this section name substring |
5482 | `--output` / `-o` | path | `/tmp/muse-preview-<short_id>.<fmt>` | Write the preview to this path |
5483 | `--open` | flag | off | Open the rendered preview in the system default audio player (macOS only) |
5484 | `--json` | flag | off | Emit structured JSON for agent consumption |
5485
5486 ### Output example (text mode)
5487
5488 ```
5489 ⚠️ Preview generated (stub — Storpheus /render not yet deployed):
5490 /tmp/muse-preview-abc12345.wav
5491 (1 MIDI files used)
5492 ```
5493
5494 ### JSON output example (`--json`)
5495
5496 ```json
5497 {
5498 "commit_id": "abc12345def67890...",
5499 "commit_short": "abc12345",
5500 "output_path": "/tmp/muse-preview-abc12345.wav",
5501 "format": "wav",
5502 "midi_files_used": 1,
5503 "skipped_count": 0,
5504 "stubbed": true
5505 }
5506 ```
5507
5508 ### Result type: `RenderPreviewResult`
5509
5510 Defined in `maestro/services/muse_render_preview.py`.
5511
5512 | Field | Type | Description |
5513 |-------|------|-------------|
5514 | `output_path` | `pathlib.Path` | Absolute path of the rendered audio file |
5515 | `format` | `PreviewFormat` | Audio format enum (`wav` / `mp3` / `flac`) |
5516 | `commit_id` | `str` | Full commit ID (64-char SHA) |
5517 | `midi_files_used` | `int` | Number of MIDI files from the snapshot used |
5518 | `skipped_count` | `int` | Manifest entries skipped (wrong type / filter / missing) |
5519 | `stubbed` | `bool` | `True` when Storpheus `/render` is not yet deployed and the file is a MIDI placeholder |
5520
5521 ### Error handling
5522
5523 | Scenario | Exit code | Message |
5524 |----------|-----------|---------|
5525 | Not in a Muse repo | 2 (REPO_NOT_FOUND) | Standard `require_repo()` message |
5526 | No commits yet | 1 (USER_ERROR) | `❌ No commits yet — nothing to export.` |
5527 | Ambiguous commit prefix | 1 (USER_ERROR) | Lists all matching commits |
5528 | No MIDI files after filter | 1 (USER_ERROR) | `❌ No MIDI files found in snapshot…` |
5529 | Storpheus unreachable | 3 (INTERNAL_ERROR) | `❌ Storpheus not reachable — render aborted.` |
5530
5531 ### Storpheus render status
5532
5533 The Storpheus service currently exposes MIDI *generation* at `POST /generate`. A dedicated `POST /render` endpoint (MIDI-in → audio-out) is planned but not yet deployed. Until it ships:
5534
5535 - A health-check confirms Storpheus is reachable (fast probe, 3 s timeout).
5536 - The first matching MIDI file from the snapshot is **copied** to `output_path` as a placeholder.
5537 - `RenderPreviewResult.stubbed` is set to `True`.
5538 - The CLI prints a clear `⚠️ Preview generated (stub…)` warning.
5539
5540 When `POST /render` is available, replace `_render_via_storpheus` in the service with a multipart POST call and set `stubbed=False`.
5541
5542 ### Agent use case
5543
5544 An AI music generation agent uses `muse render-preview HEAD~10 --json` to obtain a path to the audio preview of a historical snapshot before deciding whether to branch from it or continue the current line. The `stubbed` field tells the agent whether the file is a true audio render or a MIDI placeholder, so it can adjust its reasoning accordingly.
5545
5546 ---
5547
5548 ## `muse rev-parse` — Resolve a Revision Expression to a Commit ID
5549
5550 **Purpose:** Translate a symbolic revision expression into a concrete 64-character
5551 commit ID. Mirrors `git rev-parse` semantics and is the plumbing primitive used
5552 internally by other Muse commands that accept revision arguments.
5553
5554 ```
5555 muse rev-parse <revision> [OPTIONS]
5556
5557 ```
5558
5559 ### Flags
5560
5561 | Flag | Type | Default | Description |
5562 |------|------|---------|-------------|
5563 | `REVISION` | positional | required | Revision expression to resolve |
5564 | `--short` | flag | off | Print only the first 8 characters of the commit ID |
5565 | `--verify` | flag | off | Exit 1 if the expression does not resolve (default: print nothing) |
5566 | `--abbrev-ref` | flag | off | Print the branch name instead of the commit ID |
5567
5568 ### Supported Revision Expressions
5569
5570 | Expression | Resolves to |
5571 |------------|-------------|
5572 | `HEAD` | Tip of the current branch |
5573 | `<branch>` | Tip of the named branch |
5574 | `<commit_id>` | Exact or prefix-matched commit |
5575 | `HEAD~N` | N parents back from HEAD |
5576 | `<branch>~N` | N parents back from the branch tip |
5577
5578 ### Output Example
5579
5580 ```
5581 $ muse rev-parse HEAD
5582 a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
5583
5584 $ muse rev-parse --short HEAD
5585 a1b2c3d4
5586
5587 $ muse rev-parse --abbrev-ref HEAD
5588 main
5589
5590 $ muse rev-parse HEAD~2
5591 f9e8d7c6b5a4f9e8d7c6b5a4f9e8d7c6b5a4f9e8d7c6b5a4f9e8d7c6b5a4f9e8
5592
5593 $ muse rev-parse --verify nonexistent
5594 fatal: Not a valid revision: 'nonexistent'
5595 # exit code 1
5596 ```
5597
5598 ### Result Type
5599
5600 `RevParseResult` — see `docs/reference/type_contracts.md § Muse rev-parse Types`.
5601
5602 ### Agent Use Case
5603
5604 An AI agent resolves `HEAD~1` before generating a new variation to obtain the
5605 parent commit ID, which it passes as a `base_commit` argument to downstream
5606 commands. Use `--verify` in automation scripts to fail fast rather than
5607 silently producing empty output.
5608
5609 ---
5610
5611 ## `muse symbolic-ref` — Read or Write a Symbolic Ref
5612
5613 **Purpose:** Read or write a symbolic ref (e.g. `HEAD`), answering "which branch
5614 is currently checked out?" — the primitive that all checkout, branch, and HEAD
5615 management operations depend on.
5616
5617 **Implementation:** `maestro/muse_cli/commands/symbolic_ref.py`\
5618 **Status:** ✅ implemented (issue #93)
5619
5620 ### Usage
5621
5622 ```bash
5623 muse symbolic-ref HEAD # read: prints refs/heads/main
5624 muse symbolic-ref --short HEAD # read short form: prints main
5625 muse symbolic-ref HEAD refs/heads/feature/x # write: update .muse/HEAD
5626 muse symbolic-ref --delete HEAD # delete the symbolic ref file
5627
5628 ```
5629
5630 ### Flags
5631
5632 | Flag | Type | Default | Description |
5633 |------|------|---------|-------------|
5634 | `<name>` | positional | required | Ref name, e.g. `HEAD` or `refs/heads/main` |
5635 | `<ref>` | positional | none | When supplied, write this target into the ref (must start with `refs/`) |
5636 | `--short` | flag | off | Print just the branch name (`main`) instead of the full ref path |
5637 | `--delete / -d` | flag | off | Delete the symbolic ref file entirely |
5638 | `--quiet / -q` | flag | off | Suppress error output when the ref is not symbolic |
5639
5640 ### Output example
5641
5642 ```
5643 # Read
5644 refs/heads/main
5645
5646 # Read --short
5647 main
5648
5649 # Write
5650 ✅ HEAD → refs/heads/feature/guitar
5651
5652 # Delete
5653 ✅ Deleted symbolic ref 'HEAD'
5654 ```
5655
5656 ### Result type
5657
5658 `SymbolicRefResult` — fields: `name` (str), `ref` (str), `short` (str).
5659
5660 ### Agent use case
5661
5662 An AI agent inspecting the current branch before generating new variations calls
5663 `muse symbolic-ref --short HEAD` to confirm it is operating on the expected branch.
5664 Before creating a new branch it calls `muse symbolic-ref HEAD refs/heads/feature/guitar`
5665 to update the HEAD pointer atomically. These are pure filesystem operations — no DB
5666 round-trip, sub-millisecond latency.
5667
5668 ### Error handling
5669
5670 | Scenario | Exit code | Message |
5671 |----------|-----------|---------|
5672 | Ref file does not exist | 1 (USER_ERROR) | `❌ HEAD is not a symbolic ref or does not exist` |
5673 | Ref content is a bare SHA (detached HEAD) | 1 (USER_ERROR) | same |
5674 | `<ref>` does not start with `refs/` | 1 (USER_ERROR) | `❌ Invalid symbolic-ref target '…': must start with 'refs/'` |
5675 | `--delete` on absent file | 1 (USER_ERROR) | `❌ HEAD: not found — nothing to delete` |
5676 | Not in a repo | 2 (REPO_NOT_FOUND) | Standard `require_repo()` message |
5677
5678 ---
5679
5680 ## `muse tempo-scale` — Stretch or Compress the Timing of a Commit
5681
5682 **Purpose:** Apply a deterministic time-scaling transformation to a commit,
5683 stretching or compressing all MIDI note onset/offset times by a factor while
5684 preserving pitch. Records the result as a new commit, leaving the source
5685 commit intact. Agents use this to explore half-time grooves, double-time
5686 feels, or to normalise a session to a target BPM before merge.
5687
5688 **Usage:**
5689 ```bash
5690 muse tempo-scale [<factor>] [<commit>] [OPTIONS]
5691 ```
5692
5693 **Flags:**
5694
5695 | Flag | Type | Default | Description |
5696 |------|------|---------|-------------|
5697 | `<factor>` | float | — | Scaling factor: `0.5` = half-time, `2.0` = double-time |
5698 | `<commit>` | string | HEAD | Source commit SHA to scale |
5699 | `--bpm N` | float | — | Scale to reach exactly N BPM (`factor = N / source_bpm`). Mutually exclusive with `<factor>` |
5700 | `--track TEXT` | string | all | Scale only a specific MIDI track |
5701 | `--preserve-expressions` | flag | off | Scale CC/expression event timing proportionally |
5702 | `--message TEXT` | string | auto | Commit message for the new scaled commit |
5703 | `--json` | flag | off | Emit structured JSON for agent consumption |
5704
5705 > **Note on argument order:** Because `muse tempo-scale` is a Typer group
5706 > command, place all `--options` before the positional `<factor>` argument to
5707 > ensure correct parsing (e.g. `muse tempo-scale --json 2.0`, not
5708 > `muse tempo-scale 2.0 --json`).
5709
5710 **Output example (text):**
5711 ```
5712 Tempo scaled: abc12345 -> d9f3a1b2
5713 Factor: 0.5000 (/2.0000)
5714 Tempo: 120.0 BPM -> 60.0 BPM
5715 Track: all
5716 Message: tempo-scale 0.5000x (stub)
5717 (stub -- full MIDI note manipulation pending)
5718 ```
5719
5720 **Output example (JSON, `--json`):**
5721 ```json
5722 {
5723 "source_commit": "abc12345",
5724 "new_commit": "d9f3a1b2",
5725 "factor": 0.5,
5726 "source_bpm": 120.0,
5727 "new_bpm": 60.0,
5728 "track": "all",
5729 "preserve_expressions": false,
5730 "message": "tempo-scale 0.5000x (stub)"
5731 }
5732 ```
5733
5734 **Result type:** `TempoScaleResult` (TypedDict) — fields: `source_commit`,
5735 `new_commit`, `factor`, `source_bpm`, `new_bpm`, `track`,
5736 `preserve_expressions`, `message`.
5737
5738 **Factor computation from BPM:** `factor = target_bpm / source_bpm`.
5739 Example: to go from 120 BPM to 128 BPM, `factor = 128 / 120 ≈ 1.0667`.
5740 Both operations are exposed as pure functions (`compute_factor_from_bpm`,
5741 `apply_factor`) that agents may call directly without spawning the CLI.
5742
5743 **Determinism:** Same `source_commit` + `factor` + `track` + `preserve_expressions`
5744 always produces the same `new_commit` SHA. This makes tempo-scale operations
5745 safe to cache and replay in agentic pipelines.
5746
5747 **Agent use case:** An AI generating a groove variation queries `muse tempo-scale
5748 --bpm 128 --json` to normalise a 120 BPM sketch to the session BPM before
5749 committing. A post-generation agent can scan `muse timeline` to verify the
5750 tempo evolution, then use `muse tempo-scale 0.5` to create a half-time B-section
5751 for contrast.
5752
5753 **Implementation:** `maestro/muse_cli/commands/tempo_scale.py` —
5754 `TempoScaleResult` (TypedDict), `compute_factor_from_bpm()`, `apply_factor()`,
5755 `_tempo_scale_async()`, `_format_result()`. Exit codes: 0 success, 1 bad
5756 arguments (`USER_ERROR`), 2 outside repo (`REPO_NOT_FOUND`), 3 internal error
5757 (`INTERNAL_ERROR`).
5758
5759 > **Stub note:** The current implementation computes the correct schema and
5760 > factor but uses a placeholder 120 BPM as the source tempo and generates a
5761 > deterministic stub commit SHA. Full MIDI note-event manipulation will be
5762 > wired in when the Storpheus note-event query route is available.
5763
5764 ---
5765
5766 ## `muse motif` — Recurring Melodic Motif Analysis
5767
5768 ### `muse motif find`
5769
5770 **Purpose:** Detect recurring melodic and rhythmic patterns in a single commit.
5771 An AI agent uses this before generating a new variation to identify the established
5772 motific language of the composition, ensuring new material is thematically coherent.
5773
5774 **Usage:**
5775 ```bash
5776 muse motif find [<commit>] [OPTIONS]
5777 ```
5778
5779 **Flags:**
5780 | Flag | Type | Default | Description |
5781 |------|------|---------|-------------|
5782 | `--min-length N` | int | 3 | Minimum motif length in notes |
5783 | `--track TEXT` | str | — | Restrict to a named MIDI track |
5784 | `--section TEXT` | str | — | Restrict to a named section/region |
5785 | `--json` | flag | off | Emit structured JSON for agent consumption |
5786
5787 **Output example:**
5788 ```
5789 Recurring motifs — commit a1b2c3d4 (HEAD -> main)
5790 ── stub mode: full MIDI analysis pending ──
5791
5792 # Fingerprint Contour Count
5793 - ---------------------- ------------------ -----
5794 1 [+2, +2, -1, +2] ascending-step 3
5795 2 [-2, -2, +1, -2] descending-step 2
5796 3 [+4, -2, +3] arch 2
5797
5798 3 motif(s) found (min-length 3)
5799 ```
5800
5801 **Result type:** `MotifFindResult` — fields: `commit_id`, `branch`, `min_length`, `motifs` (list of `MotifGroup`), `total_found`, `source`.
5802
5803 **Agent use case:** Call `muse motif find --json HEAD` before composing a new section.
5804 Parse `motifs[0].fingerprint` to retrieve the primary interval sequence, then instruct
5805 the generation model to build on that pattern rather than introducing unrelated material.
5806
5807 **Implementation note:** Stub — returns realistic placeholder motifs. Full
5808 implementation requires MIDI note data queryable from the commit snapshot store.
5809
5810 ---
5811
5812 ### `muse motif track`
5813
5814 **Purpose:** Search all commits in branch history for appearances of a specific motif.
5815 Detects not only exact transpositions but also melodic inversion, retrograde, and
5816 retrograde-inversion — the four canonical classical transformations.
5817
5818 **Usage:**
5819 ```bash
5820 muse motif track "<pattern>" [OPTIONS]
5821 ```
5822
5823 **Arguments:**
5824 | Argument | Description |
5825 |----------|-------------|
5826 | `pattern` | Space-separated note names (`"C D E G"`) or MIDI numbers (`"60 62 64 67"`) |
5827
5828 **Flags:**
5829 | Flag | Type | Default | Description |
5830 |------|------|---------|-------------|
5831 | `--json` | flag | off | Emit structured JSON |
5832
5833 **Output example:**
5834 ```
5835 Tracking motif: 'C D E G'
5836 Fingerprint: [+2, +2, +3]
5837 Commits scanned: 12
5838
5839 Commit Track Transform Position
5840 ---------- ------------ -------------- --------
5841 a1b2c3d4 melody exact 0
5842 f4e3d2c1 melody inversion 4
5843 b2c3d4e5 bass retrograde 2
5844
5845 3 occurrence(s) found.
5846 ```
5847
5848 **Result type:** `MotifTrackResult` — fields: `pattern`, `fingerprint`, `occurrences` (list of `MotifOccurrence`), `total_commits_scanned`, `source`.
5849
5850 **Agent use case:** When the agent needs to understand how a theme has evolved,
5851 call `muse motif track "C D E G" --json` and inspect the `transformation` field
5852 on each occurrence to chart the motif's journey through the composition's history.
5853
5854 ---
5855
5856 ### `muse motif diff`
5857
5858 **Purpose:** Show how the dominant motif transformed between two commits.
5859 Classifies the change as one of: exact (transposition), inversion, retrograde,
5860 retrograde-inversion, augmentation, diminution, or approximate.
5861
5862 **Usage:**
5863 ```bash
5864 muse motif diff <commit-a> <commit-b> [OPTIONS]
5865 ```
5866
5867 **Flags:**
5868 | Flag | Type | Default | Description |
5869 |------|------|---------|-------------|
5870 | `--json` | flag | off | Emit structured JSON |
5871
5872 **Output example:**
5873 ```
5874 Motif diff: a1b2c3d4 → f4e3d2c1
5875
5876 A (a1b2c3d4): [+2, +2, -1, +2] [ascending-step]
5877 B (f4e3d2c1): [-2, -2, +1, -2] [descending-step]
5878
5879 Transformation: INVERSION
5880 The motif was inverted — ascending intervals became descending.
5881 ```
5882
5883 **Result type:** `MotifDiffResult` — fields: `commit_a` (`MotifDiffEntry`), `commit_b` (`MotifDiffEntry`), `transformation` (`MotifTransformation`), `description`, `source`.
5884
5885 **Agent use case:** Use after detecting a structural change to understand whether
5886 the composer inverted or retrogressed the theme — crucial context for deciding
5887 how to develop material in the next variation.
5888
5889 ---
5890
5891 ### `muse motif list`
5892
5893 **Purpose:** List all named motifs saved in `.muse/motifs/`. Named motifs are
5894 user-annotated melodic ideas that the composer has labelled for future recall.
5895
5896 **Usage:**
5897 ```bash
5898 muse motif list [OPTIONS]
5899 ```
5900
5901 **Flags:**
5902 | Flag | Type | Default | Description |
5903 |------|------|---------|-------------|
5904 | `--json` | flag | off | Emit structured JSON |
5905
5906 **Output example:**
5907 ```
5908 Named motifs:
5909
5910 Name Fingerprint Created Description
5911 -------------------- ---------------------- ------------------------ ------------------------------
5912 main-theme [+2, +2, -1, +2] 2026-01-15T10:30:00Z The central ascending motif…
5913 bass-riff [-2, -3, +2] 2026-01-20T14:15:00Z Chromatic bass figure…
5914 ```
5915
5916 **Result type:** `MotifListResult` — fields: `motifs` (list of `SavedMotif`), `source`.
5917
5918 **Agent use case:** Load the named motif library at session start. Cross-reference
5919 `fingerprint` values against `muse motif find` output to check whether detected
5920 patterns match known named motifs before introducing new thematic material.
5921 ---
5922
5923 ---
5924
5925 ## `muse read-tree` — Read a Snapshot into muse-work/
5926
5927 **Purpose:** Hydrate `muse-work/` from any historical snapshot without modifying
5928 HEAD or branch refs. The plumbing analog of `git read-tree`. AI agents use this to
5929 inspect or restore a specific composition state before making decisions.
5930
5931 **Usage:**
5932 ```bash
5933 muse read-tree <snapshot_id> [OPTIONS]
5934 ```
5935
5936 **Flags:**
5937
5938 | Flag | Type | Default | Description |
5939 |------|------|---------|-------------|
5940 | `<snapshot_id>` | positional | — | Full 64-char snapshot SHA or abbreviated prefix (≥ 4 chars) |
5941 | `--dry-run` | flag | off | Print the file list without writing anything |
5942 | `--reset` | flag | off | Clear all files from muse-work/ before populating |
5943
5944 **Output example (default):**
5945 ```
5946 ✅ muse-work/ populated from snapshot a3f7b891 (3 file(s)).
5947 ```
5948
5949 **Output example (`--dry-run`):**
5950 ```
5951 Snapshot a3f7b891 — 3 file(s):
5952 tracks/bass/groove.mid (1a2b3c4d)
5953 tracks/keys/voicing.mid (9e8f7a6b)
5954 mix/final.json (c4d5e6f7)
5955 ```
5956
5957 **Output example (`--reset`):**
5958 ```
5959 ✅ muse-work/ reset and populated from snapshot a3f7b891 (3 file(s)).
5960 ```
5961
5962 **Result type:** `ReadTreeResult` — fields: `snapshot_id` (str), `files_written` (list[str]),
5963 `dry_run` (bool), `reset` (bool).
5964
5965 **How objects are stored:** `muse commit` writes each committed file via
5966 `maestro.muse_cli.object_store.write_object_from_path()` into a sharded layout
5967 that mirrors Git's loose-object store:
5968
5969 ```
5970 .muse/objects/
5971 ab/ ← first two hex chars of sha256 (256 possible shard dirs)
5972 cdef1234… ← remaining 62 chars — the raw file bytes
5973 ```
5974
5975 `muse read-tree` reads from that same store via `read_object()` to reconstruct
5976 `muse-work/`. If an object is missing (e.g. the snapshot was pulled from a
5977 remote without a local commit), the command exits with a clear error listing the
5978 missing paths. All Muse commands share this single canonical module —
5979 `maestro/muse_cli/object_store.py` — for all object I/O.
5980
5981 **Does NOT modify:**
5982 - `.muse/HEAD`
5983 - `.muse/refs/heads/<branch>` (any branch ref)
5984 - The database (read-only command)
5985
5986 **Agent use case:** After `muse pull`, an agent calls `muse read-tree <snapshot_id>`
5987 to materialize a specific checkpoint into `muse-work/` for further analysis (e.g.
5988 running `muse dynamics` or `muse swing`) without advancing the branch pointer. This
5989 is safer than `muse checkout` because it leaves all branch metadata intact.
5990
5991 ---
5992 ## `muse update-ref` — Write or Delete a Ref (Branch or Tag Pointer)
5993
5994 **Purpose:** Directly update a branch or tag pointer (`refs/heads/*` or `refs/tags/*`)
5995 in the `.muse/` object store. This is the plumbing primitive scripting agents use when
5996 they need to advance a branch tip, retarget a tag, or remove a stale ref — without going
5997 through a higher-level command like `checkout` or `merge`.
5998
5999 **Implementation:** `maestro/muse_cli/commands/update_ref.py`\
6000 **Status:** ✅ implemented (PR #143) — issue #91
6001
6002 ### Usage
6003
6004 ```bash
6005 muse update-ref <ref> <new-value> [OPTIONS]
6006 muse update-ref <ref> -d
6007 ```
6008
6009 ### Flags
6010
6011 | Flag | Type | Default | Description |
6012 |------|------|---------|-------------|
6013 | `<ref>` | positional | required | Fully-qualified ref (e.g. `refs/heads/main`, `refs/tags/v1.0`) |
6014 | `<new-value>` | positional | required (unless `-d`) | Commit ID to write to the ref |
6015 | `--old-value <commit_id>` | string | off | CAS guard — only update if the current ref value matches this commit ID |
6016 | `-d / --delete` | flag | off | Delete the ref file instead of writing it |
6017
6018 ### Output example
6019
6020 ```
6021 # Standard write
6022 ✅ refs/heads/main → 3f9ab2c1
6023
6024 # CAS failure
6025 ❌ CAS failure: expected '3f9ab2c1' but found 'a1b2c3d4'. Ref not updated.
6026
6027 # Delete
6028 ✅ Deleted ref 'refs/heads/feature'.
6029
6030 # Commit not in DB
6031 ❌ Commit 3f9ab2c1 not found in database.
6032 ```
6033
6034 ### Validation
6035
6036 - **Ref format:** Must start with `refs/heads/` or `refs/tags/`. Any other prefix exits with `USER_ERROR`.
6037 - **Commit existence:** Before writing, the commit_id is looked up in `muse_cli_commits`. If absent, exits `USER_ERROR`.
6038 - **CAS (`--old-value`):** Reads the current file contents and compares to the provided value. Mismatch → `USER_ERROR`, ref unchanged. Absent ref + any `--old-value` → `USER_ERROR`.
6039 - **Delete (`-d`):** Exits `USER_ERROR` when the ref file does not exist.
6040
6041 ### Result type
6042
6043 `None` — this is a write command; output is emitted via `typer.echo`.
6044
6045 ### Agent use case
6046
6047 An AI orchestration agent that manages multiple arrangement branches can call
6048 `muse update-ref refs/heads/feature/guitar <commit_id> --old-value <prev_id>`
6049 to atomically advance the branch tip after generating a new variation. The CAS
6050 guard prevents a race condition when two generation passes complete concurrently —
6051 only the first one wins; the second will receive `USER_ERROR` and retry or backoff.
6052
6053 Use `muse update-ref refs/tags/v1.0 <commit_id>` to mark a production-ready
6054 snapshot with a stable tag pointer that other agents can reference by name.
6055 ---
6056
6057 ## `muse write-tree` — Write Current Working-Tree as a Snapshot Object
6058
6059 ### `muse write-tree`
6060
6061 **Purpose:** Hash all files in `muse-work/`, persist the object and snapshot
6062 rows in the database, and print the `snapshot_id`. This is the plumbing
6063 primitive that underlies `muse commit` — it writes the tree object without
6064 recording any history (no commit row, no branch-pointer update). AI agents
6065 use it to obtain a stable, content-addressed handle to the current working-tree
6066 state before deciding whether to commit.
6067
6068 **Status:** ✅ Fully implemented (issue #89)
6069
6070 **Usage:**
6071 ```bash
6072 muse write-tree [OPTIONS]
6073 ```
6074
6075 **Flags:**
6076
6077 | Flag | Type | Default | Description |
6078 |------|------|---------|-------------|
6079 | `--prefix PATH` | string | *(none)* | Only include files whose path (relative to `muse-work/`) starts with *PATH*. Example: `--prefix drums/` snapshots only the drums sub-directory. |
6080 | `--missing-ok` | flag | off | Do not fail when `muse-work/` is absent or empty, or when `--prefix` matches no files. Still prints a valid (empty) `snapshot_id`. |
6081
6082 **Output example:**
6083 ```
6084 a3f92c1e8b4d5f67890abcdef1234567890abcdef1234567890abcdef12345678
6085 ```
6086 (64-character sha256 hex digest of the sorted `path:object_id` pairs.)
6087
6088 **Idempotency:** Same file content → same `snapshot_id`. Running `muse write-tree`
6089 twice without changing any file makes exactly zero new DB writes (all upserts are
6090 no-ops).
6091
6092 **Result type:** `snapshot_id` is a raw 64-char hex string printed to stdout.
6093 No named result type — the caller decides what to do with the ID (compare,
6094 commit, discard).
6095
6096 **Agent use case:** An AI agent that just generated a batch of MIDI files calls
6097 `muse write-tree` to get the `snapshot_id` for the current working tree. It
6098 then compares that ID against the last committed `snapshot_id` (via `muse log
6099 --json | head -1`) to decide whether the new generation is novel enough to
6100 commit. If the IDs match, the files are identical to the last commit and no
6101 commit is needed.
6102
6103 **Implementation:** `maestro/muse_cli/commands/write_tree.py`. Reuses
6104 `build_snapshot_manifest`, `compute_snapshot_id`, `upsert_object`, and
6105 `upsert_snapshot` from the commit pipeline. No new DB schema — the same
6106 `muse_cli_objects` and `muse_cli_snapshots` tables.
6107
6108 ---
6109
6110 ## Command Registration Summary
6111
6112 | Command | File | Status | Issue |
6113 |---------|------|--------|-------|
6114 | `muse ask` | `commands/ask.py` | ✅ stub (PR #132) | #126 |
6115 | `muse context` | `commands/context.py` | ✅ implemented (PR #138) | #113 |
6116 | `muse describe` | `commands/describe.py` | ✅ stub (PR #134) | #125 |
6117 | `muse divergence` | `commands/divergence.py` | ✅ implemented (PR #140) | #119 |
6118 | `muse diff` | `commands/diff.py` | ✅ stub (this PR) | #104 |
6119 | `muse dynamics` | `commands/dynamics.py` | ✅ stub (PR #130) | #120 |
6120 | `muse export` | `commands/export.py` | ✅ implemented (PR #137) | #112 |
6121 | `muse grep` | `commands/grep_cmd.py` | ✅ stub (PR #128) | #124 |
6122 | `muse groove-check` | `commands/groove_check.py` | ✅ stub (PR #143) | #95 |
6123 | `muse import` | `commands/import_cmd.py` | ✅ implemented (PR #142) | #118 |
6124 | `muse inspect` | `commands/inspect.py` | ✅ implemented (PR #TBD) | #98 |
6125 | `muse meter` | `commands/meter.py` | ✅ implemented (PR #141) | #117 |
6126 | `muse read-tree` | `commands/read_tree.py` | ✅ implemented (PR #157) | #90 |
6127 | `muse recall` | `commands/recall.py` | ✅ stub (PR #135) | #122 |
6128 | `muse render-preview` | `commands/render_preview.py` | ✅ implemented (issue #96) | #96 |
6129 | `muse rev-parse` | `commands/rev_parse.py` | ✅ implemented (PR #143) | #92 |
6130 | `muse session` | `commands/session.py` | ✅ implemented (PR #129) | #127 |
6131 | `muse swing` | `commands/swing.py` | ✅ stub (PR #131) | #121 |
6132 | `muse motif` | `commands/motif.py` | ✅ stub (PR —) | #101 |
6133 | `muse symbolic-ref` | `commands/symbolic_ref.py` | ✅ implemented (issue #93) | #93 |
6134 | `muse tag` | `commands/tag.py` | ✅ implemented (PR #133) | #123 |
6135 | `muse tempo-scale` | `commands/tempo_scale.py` | ✅ stub (PR open) | #111 |
6136 | `muse timeline` | `commands/timeline.py` | ✅ implemented (PR #TBD) | #97 |
6137 | `muse transpose` | `commands/transpose.py` | ✅ implemented | #102 |
6138 | `muse update-ref` | `commands/update_ref.py` | ✅ implemented (PR #143) | #91 |
6139 | `muse validate` | `commands/validate.py` | ✅ implemented (PR #TBD) | #99 |
6140 | `muse write-tree` | `commands/write_tree.py` | ✅ implemented | #89 |
6141
6142 All stub commands have stable CLI contracts. Full musical analysis (MIDI content
6143 parsing, vector embeddings, LLM synthesis) is tracked as follow-up issues.
6144
6145 ## `muse groove-check` — Rhythmic Drift Analysis
6146
6147 **Purpose:** Detect which commit in a range introduced rhythmic inconsistency
6148 by measuring how much the average note-onset deviation from the quantization grid
6149 changed between adjacent commits. The music-native equivalent of a style/lint gate.
6150
6151 **Implementation:** `maestro/muse_cli/commands/groove_check.py` (CLI),
6152 `maestro/services/muse_groove_check.py` (pure service layer).
6153 **Status:** ✅ stub (issue #95)
6154
6155 ### Usage
6156
6157 ```bash
6158 muse groove-check [RANGE] [OPTIONS]
6159 ```
6160
6161 ### Flags
6162
6163 | Flag | Type | Default | Description |
6164 |------|------|---------|-------------|
6165 | `RANGE` | positional | last 10 commits | Commit range to analyze (e.g. `HEAD~5..HEAD`) |
6166 | `--track TEXT` | string | all | Scope analysis to a specific instrument track (e.g. `drums`) |
6167 | `--section TEXT` | string | all | Scope analysis to a specific musical section (e.g. `verse`) |
6168 | `--threshold FLOAT` | float | 0.1 | Drift threshold in beats; commits exceeding it are flagged WARN; >2× = FAIL |
6169 | `--json` | flag | off | Emit structured JSON for agent consumption |
6170
6171 ### Output example
6172
6173 ```
6174 Groove-check — range HEAD~6..HEAD threshold 0.1 beats
6175
6176 Commit Groove Score Drift Δ Status
6177 -------- ------------ ------- ------
6178 a1b2c3d4 0.0400 0.0000 OK
6179 e5f6a7b8 0.0500 0.0100 OK
6180 c9d0e1f2 0.0600 0.0100 OK
6181 a3b4c5d6 0.0900 0.0300 OK
6182 e7f8a9b0 0.1500 0.0600 WARN
6183 c1d2e3f4 0.1300 0.0200 OK
6184
6185 Flagged: 1 / 6 commits (worst: e7f8a9b0)
6186 ```
6187
6188 ### Result types
6189
6190 `GrooveStatus` (Enum: OK/WARN/FAIL), `CommitGrooveMetrics` (frozen dataclass),
6191 `GrooveCheckResult` (frozen dataclass).
6192 See `docs/reference/type_contracts.md § GrooveCheckResult`.
6193
6194 ### Status classification
6195
6196 | Status | Condition |
6197 |--------|-----------|
6198 | OK | `drift_delta ≤ threshold` |
6199 | WARN | `threshold < drift_delta ≤ 2 × threshold` |
6200 | FAIL | `drift_delta > 2 × threshold` |
6201
6202 ### Agent use case
6203
6204 An AI agent runs `muse groove-check HEAD~20..HEAD --json` after a session to
6205 identify which commit degraded rhythmic tightness. The `worst_commit` field
6206 pinpoints the exact SHA to inspect. Feeding that into `muse describe` gives
6207 a natural-language explanation of what changed. If `--threshold 0.05` returns
6208 multiple FAIL commits, the session's quantization workflow needs review before
6209 new layers are added.
6210
6211 ### Implementation stub note
6212
6213 `groove_score` and `drift_delta` are computed from deterministic placeholder data.
6214 Full implementation will walk the `MuseCliCommit` chain, load MIDI snapshots via
6215 `MidiParser`, compute per-note onset deviation from the nearest quantization grid
6216 position (resolved from the commit's time-signature + tempo metadata), and
6217 aggregate by track / section. Storpheus will expose a `/groove` route once
6218 the rhythmic-analysis pipeline is productionized.
6219
6220 ---
6221
6222 ## `muse contour` — Melodic Contour and Phrase Shape Analysis
6223
6224 **Purpose:** Determines whether a melody rises, falls, arches, or waves — the
6225 fundamental expressive character that distinguishes two otherwise similar
6226 melodies. An AI generation agent uses `muse contour --json HEAD` to
6227 understand the melodic shape of the current arrangement before layering a
6228 countermelody, ensuring complementary (not identical) contour.
6229
6230 **Usage:**
6231 ```bash
6232 muse contour [<commit>] [OPTIONS]
6233 ```
6234
6235 **Flags:**
6236 | Flag | Type | Default | Description |
6237 |------|------|---------|-------------|
6238 | `[<commit>]` | string | HEAD | Target commit SHA to analyse |
6239 | `--track TEXT` | string | all tracks | Restrict to a named melodic track (e.g. `keys`, `lead`) |
6240 | `--section TEXT` | string | full piece | Scope analysis to a named section (e.g. `verse`, `chorus`) |
6241 | `--compare COMMIT` | string | — | Compare contour between HEAD (or `[<commit>]`) and this ref |
6242 | `--history` | flag | off | Show contour evolution across all commits |
6243 | `--shape` | flag | off | Print the overall shape label only (one line) |
6244 | `--json` | flag | off | Emit structured JSON for agent consumption |
6245
6246 **Shape vocabulary:**
6247 | Label | Description |
6248 |-------|-------------|
6249 | `ascending` | Net upward movement across the full phrase |
6250 | `descending` | Net downward movement across the full phrase |
6251 | `arch` | Rises then falls (single peak) |
6252 | `inverted-arch` | Falls then rises (valley shape) |
6253 | `wave` | Multiple peaks; alternating rise and fall |
6254 | `static` | Narrow pitch range (< 2 semitones spread) |
6255
6256 **Output example (text):**
6257 ```
6258 Shape: Arch | Range: 2 octaves | Phrases: 4 avg 8 bars
6259 Commit: a1b2c3d4 Branch: main
6260 Track: keys Section: all
6261 Angularity: 2.5 st avg interval
6262 (stub — full MIDI analysis pending)
6263 ```
6264
6265 **Output example (`--shape`):**
6266 ```
6267 Shape: arch
6268 ```
6269
6270 **Output example (`--compare`, text):**
6271 ```
6272 A (a1b2c3d4) Shape: arch | Angularity: 2.5 st
6273 B (HEAD~10) Shape: arch | Angularity: 2.5 st
6274 Delta angularity +0.0 st | tessitura +0 st
6275 ```
6276
6277 **Output example (`--json`):**
6278 ```json
6279 {
6280 "shape": "arch",
6281 "tessitura": 24,
6282 "avg_interval": 2.5,
6283 "phrase_count": 4,
6284 "avg_phrase_bars": 8.0,
6285 "commit": "a1b2c3d4",
6286 "branch": "main",
6287 "track": "keys",
6288 "section": "all",
6289 "source": "stub"
6290 }
6291 ```
6292
6293 **Result types:**
6294 - `ContourResult` — fields: `shape` (str), `tessitura` (int, semitones),
6295 `avg_interval` (float, semitones), `phrase_count` (int), `avg_phrase_bars`
6296 (float), `commit` (str), `branch` (str), `track` (str), `section` (str),
6297 `source` (str).
6298 - `ContourCompareResult` — fields: `commit_a` (ContourResult), `commit_b`
6299 (ContourResult), `shape_changed` (bool), `angularity_delta` (float),
6300 `tessitura_delta` (int).
6301
6302 See `docs/reference/type_contracts.md § ContourResult`.
6303
6304 **Agent use case:** Before generating a countermelody, an agent calls
6305 `muse contour --json HEAD --track keys` to determine whether the existing
6306 melody is arch-shaped with a wide tessitura (high angularity). It then
6307 generates a countermelody that is descending and narrow — complementary, not
6308 imitative. The `--compare` flag lets the agent detect whether recent edits
6309 made a melody more angular (fragmented) or smoother (stepwise), informing
6310 whether the next variation should introduce or reduce leaps.
6311
6312 **Implementation stub note:** `source: "stub"` in the JSON output indicates
6313 that full MIDI pitch-trajectory analysis is pending a Storpheus pitch-detection
6314 route. The CLI contract (flags, output shape, result types) is stable — only
6315 the computed values will change when the full implementation is wired in.
6316
6317 **Implementation:** `maestro/muse_cli/commands/contour.py` —
6318 `ContourResult` (TypedDict), `ContourCompareResult` (TypedDict),
6319 `_contour_detect_async()`, `_contour_compare_async()`,
6320 `_contour_history_async()`, `_format_detect()`, `_format_compare()`,
6321 `_format_history()`. Exit codes: 0 success, 2 outside repo
6322 (`REPO_NOT_FOUND`), 3 internal error (`INTERNAL_ERROR`).
6323
6324 ---
6325
6326 ---
6327
6328 ## `muse reset` — Reset Branch Pointer to a Prior Commit
6329
6330 ### Purpose
6331
6332 Move the current branch's HEAD pointer backward to a prior commit, with three
6333 levels of aggression mirroring git's model. The "panic button" for music
6334 production: when a producer makes ten bad commits and wants to return to the
6335 last known-good take.
6336
6337 ### Usage
6338
6339 ```bash
6340 muse reset [--soft | --mixed | --hard] [--yes] <commit>
6341 ```
6342
6343 ### Flags
6344
6345 | Flag | Type | Default | Description |
6346 |------|------|---------|-------------|
6347 | `<commit>` | positional | required | Target commit (HEAD, HEAD~N, full/abbreviated SHA) |
6348 | `--soft` | flag | off | Move branch pointer only; muse-work/ and object store unchanged |
6349 | `--mixed` | flag | on | Move branch pointer and reset index (default; equivalent to soft in current model) |
6350 | `--hard` | flag | off | Move branch pointer AND overwrite muse-work/ with target snapshot |
6351 | `--yes` / `-y` | flag | off | Skip confirmation prompt for --hard mode |
6352
6353 ### Modes
6354
6355 **Soft** (`--soft`):
6356 - Updates `.muse/refs/heads/<branch>` to point to the target commit.
6357 - `muse-work/` files are completely untouched.
6358 - The next `muse commit` will capture the current working tree on top of the rewound HEAD.
6359 - Use when you want to re-commit with a different message or squash commits.
6360
6361 **Mixed** (`--mixed`, default):
6362 - Same as soft in the current Muse model (no explicit staging area exists yet).
6363 - Included for API symmetry with git and forward-compatibility when a staging
6364 index is added.
6365
6366 **Hard** (`--hard`):
6367 - Moves the branch ref to the target commit.
6368 - Overwrites every file in `muse-work/` with the content from the target
6369 commit's snapshot. Files are read from `.muse/objects/<sha[:2]>/<sha[2:]>`
6370 (the content-addressed blob store populated by `muse commit`).
6371 - Files in `muse-work/` that are NOT present in the target snapshot are deleted.
6372 - **Prompts for confirmation** unless `--yes` is given — this is destructive.
6373 - **Requires `muse commit` to have been run** at least once after repo init so
6374 that object blobs are present in `.muse/objects/`.
6375
6376 ### HEAD~N Syntax
6377
6378 ```bash
6379 muse reset HEAD~1 # one parent back (previous commit)
6380 muse reset HEAD~3 # three parents back
6381 muse reset abc123 # abbreviated SHA prefix
6382 muse reset --hard HEAD~2 # two parents back + restore working tree
6383 ```
6384
6385 `HEAD~N` walks the primary parent chain only. Merge parents
6386 (`parent2_commit_id`) are not traversed.
6387
6388 ### Guards
6389
6390 - **Merge in progress**: blocked when `.muse/MERGE_STATE.json` exists.
6391 Resolve or abort the merge before resetting.
6392 - **No commits on branch**: exits with `USER_ERROR` if the current branch
6393 has never been committed.
6394 - **Missing object blobs** (hard mode): exits with `INTERNAL_ERROR` rather
6395 than silently leaving `muse-work/` in a partial state.
6396
6397 ### Output Examples
6398
6399 ```
6400 # Soft/mixed reset:
6401 ✅ HEAD is now at abc123de
6402
6403 # Hard reset (2 files restored, 1 deleted):
6404 ✅ HEAD is now at abc123de (2 files restored, 1 files deleted)
6405
6406 # Abort:
6407 ⚠️ muse reset --hard will OVERWRITE muse-work/ with the target snapshot.
6408 All uncommitted changes will be LOST.
6409 Proceed? [y/N]: N
6410 Reset aborted.
6411 ```
6412
6413 ### Object Store
6414
6415 `muse commit` now writes every file's blob content to `.muse/objects/` using
6416 a two-level sharding layout identical to git's loose-object store:
6417
6418 ```
6419 .muse/objects/
6420 ab/ ← first two hex chars of sha256
6421 cdef1234... ← remaining 62 chars — the raw file bytes
6422 ```
6423
6424 This store enables `muse reset --hard` to restore any previously committed
6425 snapshot without needing the live `muse-work/` files. Objects are written
6426 idempotently (never overwritten once stored).
6427
6428 ### Result Type
6429
6430 `ResetResult` — see [`docs/reference/type_contracts.md`](../reference/type_contracts.md).
6431
6432 ### Agent Use Case
6433
6434 An AI composition agent uses `muse reset` to recover from a bad generation run:
6435
6436 1. Agent calls `muse log --json` to identify the last known-good commit SHA.
6437 2. Agent calls `muse reset --hard --yes <sha>` to restore the working tree.
6438 3. Agent calls `muse status` to verify the working tree matches expectations.
6439 4. Agent resumes composition from the clean baseline.
6440
6441 ### Implementation
6442
6443 - **Object store (canonical):** `maestro/muse_cli/object_store.py` — single
6444 source of truth for all blob I/O. Public API: `write_object()`,
6445 `write_object_from_path()`, `read_object()`, `restore_object()`,
6446 `has_object()`, `object_path()`. All commands import from here exclusively.
6447 - **Service layer:** `maestro/services/muse_reset.py` — `perform_reset()`,
6448 `resolve_ref()`. Uses `has_object()` and `restore_object()` from
6449 `object_store`. Contains no path-layout logic of its own.
6450 - **CLI command:** `maestro/muse_cli/commands/reset.py` — Typer callback,
6451 confirmation prompt, error display.
6452 - **Commit integration:** `maestro/muse_cli/commands/commit.py` — calls
6453 `write_object_from_path()` for each file during commit to populate
6454 `.muse/objects/` without loading large blobs into memory.
6455 - **Exit codes:** 0 success, 1 user error (`USER_ERROR`), 2 not a repo
6456 (`REPO_NOT_FOUND`), 3 internal error (`INTERNAL_ERROR`).
6457
6458 ---
6459
6460 ### `muse show`
6461
6462 **Purpose:** Inspect any historical commit — its metadata, snapshot manifest,
6463 path-level diff vs parent, MIDI file list, and optionally an audio preview.
6464 The musician's equivalent of `git show`: lets an AI agent or producer examine
6465 exactly what a past creative decision looked like, at any level of detail.
6466
6467 **Usage:**
6468 ```bash
6469 muse show [COMMIT] [OPTIONS]
6470 ```
6471
6472 **Arguments:**
6473
6474 | Argument | Description |
6475 |----------|-------------|
6476 | `COMMIT` | Commit ID (full or 4–64 char hex prefix), branch name, or `HEAD` (default). |
6477
6478 **Flags:**
6479
6480 | Flag | Type | Default | Description |
6481 |------|------|---------|-------------|
6482 | `--json` | flag | off | Output complete commit metadata + snapshot manifest as JSON for agent consumption. |
6483 | `--diff` | flag | off | Show path-level diff vs parent commit with A/M/D status markers. |
6484 | `--midi` | flag | off | List MIDI files (`.mid`, `.midi`, `.smf`) contained in the commit snapshot. |
6485 | `--audio-preview` | flag | off | Open cached audio preview WAV for this snapshot (macOS). Run `muse export <commit> --wav` first. |
6486
6487 Multiple flags can be combined: `muse show abc1234 --diff --midi`.
6488
6489 **Output example (default):**
6490 ```
6491 commit a1b2c3d4e5f6...
6492 Branch: main
6493 Author: producer@stori.app
6494 Date: 2026-02-27 17:30:00
6495 Parent: f9e8d7c6
6496
6497 Add bridge section with Rhodes keys
6498
6499 Snapshot: 3 files
6500 bass.mid
6501 beat.mid
6502 keys.mid
6503 ```
6504
6505 **Output example (`--diff`):**
6506 ```
6507 diff f9e8d7c6..a1b2c3d4
6508
6509 A bass.mid
6510 M beat.mid
6511 D strings.mid
6512
6513 2 path(s) changed
6514 ```
6515
6516 **Output example (`--midi`):**
6517 ```
6518 MIDI files in snapshot a1b2c3d4 (3):
6519 bass.mid (obj_hash)
6520 beat.mid (obj_hash)
6521 keys.mid (obj_hash)
6522 ```
6523
6524 **Output example (`--json`):**
6525 ```json
6526 {
6527 "commit_id": "a1b2c3d4e5f6...",
6528 "branch": "main",
6529 "parent_commit_id": "f9e8d7c6...",
6530 "parent2_commit_id": null,
6531 "message": "Add bridge section with Rhodes keys",
6532 "author": "producer@stori.app",
6533 "committed_at": "2026-02-27 17:30:00",
6534 "snapshot_id": "snap_sha256...",
6535 "snapshot_manifest": {
6536 "bass.mid": "obj_sha256_a",
6537 "beat.mid": "obj_sha256_b",
6538 "keys.mid": "obj_sha256_c"
6539 }
6540 }
6541 ```
6542
6543 **Result types:**
6544 - `ShowCommitResult` (TypedDict) — full commit metadata + snapshot manifest returned by `_show_async()`.
6545 - `ShowDiffResult` (TypedDict) — path-level diff (added/modified/removed lists + total_changed) returned by `_diff_vs_parent_async()`.
6546
6547 **Commit resolution order:**
6548 1. `HEAD` (case-insensitive) → follows the `HEAD` ref file to the current branch tip.
6549 2. 4–64 character hex string → exact commit ID match first, then prefix scan.
6550 3. Anything else → treated as a branch name; reads `.muse/refs/heads/<name>`.
6551
6552 **Agent use case:** An AI music generation agent calls `muse show HEAD` to inspect the
6553 latest committed snapshot before generating the next variation — confirming which
6554 instruments are present, what files changed in the last commit, and whether there are
6555 MIDI files it can use as seeds for generation. Use `--json` for structured consumption
6556 in agent pipelines. Use `--diff` to understand what changed in the last session.
6557 Use `--midi` to enumerate MIDI seeds for the Storpheus generation pipeline.
6558
6559 **`--audio-preview` note:** The full render-preview pipeline (Storpheus → WAV) is
6560 invoked via `muse export <commit> --wav`. The `--audio-preview` flag then plays the
6561 cached WAV via `afplay` (macOS). If no cached file exists, a clear help message is
6562 printed instead.
6563
6564 ---
6565
6566 ## `muse amend` — Amend the Most Recent Commit
6567
6568 **Purpose:** Fold working-tree changes into the most recent commit, replacing
6569 it with a new commit that has the same parent. Equivalent to
6570 `git commit --amend`. The original HEAD commit becomes an orphan (unreachable
6571 from any branch ref) and remains in the database for forensic traceability.
6572
6573 **Usage:**
6574 ```bash
6575 muse amend [OPTIONS]
6576 ```
6577
6578 **Flags:**
6579 | Flag | Type | Default | Description |
6580 |------|------|---------|-------------|
6581 | `-m / --message TEXT` | string | — | Replace the commit message |
6582 | `--no-edit` | flag | off | Keep the original commit message (default when `-m` is omitted; takes precedence over `-m` when both are provided) |
6583 | `--reset-author` | flag | off | Reset the author field to the current user (stub: sets to empty string until a user-identity system is implemented) |
6584
6585 **Output example:**
6586 ```
6587 ✅ [main a1b2c3d4] updated groove pattern (amended)
6588 ```
6589
6590 **Behaviour:**
6591 1. Re-snapshots `muse-work/` using the same content-addressed pipeline as
6592 `muse commit` (sha256 per file, deterministic snapshot_id).
6593 2. Computes a new `commit_id` using the *original commit's parent* (not the
6594 original itself), the new snapshot, the effective message, and the current
6595 timestamp.
6596 3. Writes the new commit row to Postgres and updates
6597 `.muse/refs/heads/<branch>` to the new commit ID.
6598 4. **Blocked** when a merge is in progress (`.muse/MERGE_STATE.json` exists).
6599 5. **Blocked** when there are no commits yet on the current branch.
6600 6. **Blocked** when `muse-work/` does not exist or is empty.
6601
6602 **Result types:**
6603 - Returns the new `commit_id` (64-char sha256 hex string) from `_amend_async`.
6604 - Exit codes: 0 success, 1 user error (`USER_ERROR`), 2 outside repo
6605 (`REPO_NOT_FOUND`), 3 internal error (`INTERNAL_ERROR`).
6606
6607 **Agent use case:** A producer adjusts a MIDI note quantization setting, then
6608 runs `muse amend --no-edit` to fold the change silently into the last commit
6609 without cluttering history with a second "tweak quantization" entry. An
6610 automated agent can call `muse amend -m "fix: tighten quantization on drums"`
6611 to improve the commit message after inspection.
6612
6613 **Implementation:** `maestro/muse_cli/commands/amend.py` —
6614 `_amend_async(message, no_edit, reset_author, root, session)`.
6615 Tests: `tests/muse_cli/test_amend.py`.
6616
6617 ---
6618
6619 ### `muse checkout`
6620
6621 **Purpose:** Switch branches or create a new branch seeded from the current HEAD.
6622 Enables the branching workflows that allow composers and AI agents to explore
6623 divergent musical directions without losing prior work.
6624
6625 **Usage:**
6626 ```bash
6627 muse checkout <branch> # Switch to an existing branch
6628 muse checkout -b <new-branch> # Create branch from HEAD, then switch
6629 ```
6630
6631 **Flags:**
6632
6633 | Flag | Type | Default | Description |
6634 |------|------|---------|-------------|
6635 | `-b` | flag | off | Create the branch from the current HEAD commit and switch to it |
6636
6637 **Output example (create):**
6638 ```
6639 ✅ Switched to a new branch 'experiment'
6640 ```
6641
6642 **Output example (switch):**
6643 ```
6644 ✅ Switched to branch 'main' [a1b2c3d4]
6645 ```
6646
6647 **Agent use case:** Create an experiment branch before exploring a rhythmically
6648 unusual variation. If the experiment fails, checkout main and the original
6649 arrangement is untouched.
6650
6651 **Implementation:** `maestro/muse_cli/commands/checkout.py` — `checkout_branch(root, branch, create)`.
6652 Pure filesystem writes: creates/updates `.muse/refs/heads/<branch>` and `.muse/HEAD`.
6653 No DB interaction at checkout time — the DAG remains intact.
6654
6655 ---
6656
6657 ### `muse restore`
6658
6659 **Purpose:** Restore specific files from a commit or index into `muse-work/` without
6660 touching the branch pointer. Surgical alternative to `muse reset --hard` — bring
6661 back "the bass from take 3" while keeping everything else at HEAD.
6662
6663 **Usage:**
6664 ```bash
6665 muse restore <paths>... # restore from HEAD (default)
6666 muse restore --staged <paths>... # restore index entry from HEAD
6667 muse restore --source <commit> <paths>... # restore from a specific commit
6668 muse restore --worktree --source <commit> <paths>... # explicit worktree restore
6669 ```
6670
6671 **Flags:**
6672
6673 | Flag | Type | Default | Description |
6674 |------|------|---------|-------------|
6675 | `<paths>...` | positional | — | One or more relative paths within `muse-work/` to restore. Accepts paths with or without the `muse-work/` prefix. |
6676 | `--staged` | flag | off | Restore the index (snapshot manifest) entry from the source commit. In the current Muse model (no separate staging area) this is equivalent to `--worktree`. |
6677 | `--worktree` | flag | off | Restore `muse-work/` files from the source snapshot. Default when no mode flag is specified. |
6678 | `--source / -s` | str | HEAD | Commit reference to restore from: `HEAD`, `HEAD~N`, full SHA, or any unambiguous SHA prefix. |
6679
6680 **Output example:**
6681 ```
6682 ✅ Restored 'bass/bassline.mid' from commit ab12cd34
6683 ```
6684
6685 Multiple files:
6686 ```
6687 ✅ Restored 2 files from commit ab12cd34:
6688 • bass/bassline.mid
6689 • drums/kick.mid
6690 ```
6691
6692 **Result type:** `RestoreResult` — fields: `source_commit_id` (str), `paths_restored` (list[str]), `staged` (bool).
6693
6694 **Error cases:**
6695 - `PathNotInSnapshotError` — the requested path does not exist in the source commit's snapshot. Exit code 1.
6696 - `MissingObjectError` — the required blob is absent from `.muse/objects/`. Exit code 3.
6697 - Unknown `--source` ref — exits with code 1 and a clear error message.
6698
6699 **Agent use case:** An AI composition agent can selectively restore individual
6700 instrument tracks from historical commits. For example, after generating several
6701 takes, the agent can restore the best bass line from take 3 while keeping drums
6702 and keys from take 7 — without modifying the branch history. Use `muse log` to
6703 identify commit SHAs, then `muse show <commit>` to inspect the snapshot manifest
6704 before running `muse restore`.
6705
6706 **Implementation:** `maestro/muse_cli/commands/restore.py` (CLI) and
6707 `maestro/services/muse_restore.py` (service). Uses `has_object()` and
6708 `restore_object()` from the canonical `maestro/muse_cli/object_store.py` —
6709 the same module used by `muse commit`, `muse read-tree`, and `muse reset
6710 --hard`. Branch pointer is never modified.
6711
6712 ---
6713
6714 ### `muse resolve`
6715
6716 **Purpose:** Mark a conflicted file as resolved during a paused `muse merge`.
6717 Called after `muse merge` exits with a conflict to accept one side's version
6718 before running `muse merge --continue`. For `--theirs`, the command
6719 automatically fetches the incoming branch's object from the local store and
6720 writes it to `muse-work/<path>` — no manual file editing required.
6721
6722 **Usage:**
6723 ```bash
6724 muse resolve <file-path> --ours # Keep current branch's working-tree version (no file change)
6725 muse resolve <file-path> --theirs # Copy incoming branch's object to muse-work/ automatically
6726 ```
6727
6728 **Flags:**
6729
6730 | Flag | Type | Default | Description |
6731 |------|------|---------|-------------|
6732 | `--ours` | flag | off | Accept the current branch's version (no file change needed) |
6733 | `--theirs` | flag | off | Fetch the incoming branch's object from local store and write to muse-work/ |
6734
6735 **Output example:**
6736 ```
6737 ✅ Resolved 'meta/section-1.json' — keeping theirs
6738 1 conflict(s) remaining. Resolve all, then run 'muse merge --continue'.
6739 ✅ Resolved 'beat.mid' — keeping ours
6740 ✅ All conflicts resolved. Run 'muse merge --continue' to create the merge commit.
6741 ```
6742
6743 **Full conflict resolution workflow:**
6744 ```bash
6745 muse merge experiment # → conflict on beat.mid
6746 muse status # → shows "You have unmerged paths"
6747 muse resolve beat.mid --theirs # → copies theirs version into muse-work/
6748 muse merge --continue # → creates merge commit, clears MERGE_STATE.json
6749 ```
6750
6751 **Note:** After all conflicts are resolved, `.muse/MERGE_STATE.json` persists
6752 with `conflict_paths=[]` so `--continue` can read the stored commit IDs.
6753 `muse merge --continue` is the command that clears MERGE_STATE.json.
6754 If the theirs object is not in the local store (e.g. branch was never
6755 committed locally), run `muse pull` first to fetch remote objects.
6756
6757 **Implementation:** `maestro/muse_cli/commands/resolve.py` — `resolve_conflict_async(file_path, ours, root, session)`.
6758 Reads and rewrites `.muse/MERGE_STATE.json`. For `--theirs`, queries DB for
6759 the theirs commit's snapshot manifest and calls `apply_resolution()` from
6760 `merge_engine.py` to restore the file from the local object store.
6761
6762 ---
6763
6764 ### `muse merge --continue`
6765
6766 **Purpose:** Finalize a merge that was paused due to file conflicts. After all
6767 conflicts are resolved via `muse resolve`, this command creates the merge commit
6768 with two parent IDs and advances the branch pointer.
6769
6770 **Usage:**
6771 ```bash
6772 muse merge --continue
6773 ```
6774
6775 **Flags:**
6776
6777 | Flag | Type | Default | Description |
6778 |------|------|---------|-------------|
6779 | `--continue` | flag | off | Finalize a paused conflicted merge |
6780
6781 **Output example:**
6782 ```
6783 ✅ Merge commit [main a1b2c3d4] — merged 'experiment' into 'main'
6784 ```
6785
6786 **Contract:** Reads `.muse/MERGE_STATE.json` for commit IDs. Fails if any
6787 `conflict_paths` remain (use `muse resolve` first). Snapshots the current
6788 `muse-work/` contents as the merged state. Clears MERGE_STATE.json on success.
6789
6790 **Agent use case:** After resolving a harmonic conflict between two branches,
6791 run `--continue` to record the merged arrangement as an immutable commit.
6792
6793 **Implementation:** `maestro/muse_cli/commands/merge.py` — `_merge_continue_async(root, session)`.
6794
6795 ---
6796
6797 ### `muse merge --abort`
6798
6799 **Purpose:** Cancel an in-progress merge and restore the pre-merge state of all
6800 conflicted files. Use when a conflict is too complex to resolve and you want to
6801 return the working tree to the clean state it was in before `muse merge` ran.
6802
6803 **Usage:**
6804 ```bash
6805 muse merge --abort
6806 ```
6807
6808 **Flags:**
6809
6810 | Flag | Type | Default | Description |
6811 |------|------|---------|-------------|
6812 | `--abort` | flag | off | Cancel the in-progress merge and restore pre-merge files |
6813
6814 **Output example:**
6815 ```
6816 ✅ Merge abort. Restored 2 conflicted file(s).
6817 ```
6818
6819 **Contract:**
6820 - Reads `.muse/MERGE_STATE.json` for `ours_commit` and `conflict_paths`.
6821 - Fetches the ours commit's snapshot manifest from DB.
6822 - For each conflicted path: restores the ours version from the local object
6823 store to `muse-work/`. Paths that existed only on the theirs branch (not
6824 in ours manifest) are deleted from `muse-work/`.
6825 - Clears `.muse/MERGE_STATE.json` on success.
6826 - Exits 1 if no merge is in progress.
6827
6828 **Agent use case:** When an AI agent detects an irresolvable semantic conflict
6829 (e.g. two structural arrangements that cannot be combined), it should call
6830 `muse merge --abort` to restore a clean baseline before proposing an
6831 alternative strategy to the user.
6832
6833 **Implementation:** `maestro/muse_cli/commands/merge.py` — `_merge_abort_async(root, session)`.
6834 Queries DB for the ours commit's manifest, then calls `apply_resolution()` from
6835 `merge_engine.py` for each conflicted path.
6836
6837 ---
6838
6839 ### `muse release`
6840
6841 **Purpose:** Export a tagged commit as distribution-ready release artifacts — the
6842 music-native publish step. Bridges the Muse VCS world and the audio production
6843 world: a producer says "version 1.0 is done" and `muse release v1.0` produces
6844 WAV/MIDI/stem files with SHA-256 checksums for distribution.
6845
6846 **Usage:**
6847 ```bash
6848 muse release <tag> [OPTIONS]
6849 ```
6850
6851 **Flags:**
6852
6853 | Flag | Type | Default | Description |
6854 |------|------|---------|-------------|
6855 | `<tag>` | positional | required | Tag string (created via `muse tag add`) or short commit SHA prefix |
6856 | `--render-audio` | flag | off | Render all MIDI to a single audio file via Storpheus |
6857 | `--render-midi` | flag | off | Bundle all .mid files into a zip archive |
6858 | `--export-stems` | flag | off | Export each instrument track as a separate audio file |
6859 | `--format wav\|mp3\|flac` | option | `wav` | Audio output format |
6860 | `--output-dir PATH` | option | `./releases/<tag>/` | Destination directory for all artifacts |
6861 | `--json` | flag | off | Emit structured JSON for agent consumption |
6862
6863 **Output layout:**
6864 ```
6865 <output-dir>/
6866 release-manifest.json # always written; SHA-256 checksums
6867 audio/<commit8>.<format> # --render-audio
6868 midi/midi-bundle.zip # --render-midi
6869 stems/<stem>.<format> # --export-stems
6870 ```
6871
6872 **Output example:**
6873 ```
6874 ✅ Release artifacts for tag 'v1.0' (commit a1b2c3d4):
6875 [audio] ./releases/v1.0/audio/a1b2c3d4.wav
6876 [midi-bundle] ./releases/v1.0/midi/midi-bundle.zip
6877 [manifest] ./releases/v1.0/release-manifest.json
6878 ⚠️ Audio files are MIDI stubs (Storpheus /render endpoint not yet deployed).
6879 ```
6880
6881 **Result type:** `ReleaseResult` — fields: `tag`, `commit_id`, `output_dir`,
6882 `manifest_path`, `artifacts` (list of `ReleaseArtifact`), `audio_format`, `stubbed`.
6883
6884 **`release-manifest.json` shape:**
6885 ```json
6886 {
6887 "tag": "v1.0",
6888 "commit_id": "<full sha256>",
6889 "commit_short": "<8-char>",
6890 "released_at": "<ISO-8601 UTC>",
6891 "audio_format": "wav",
6892 "stubbed": true,
6893 "files": [
6894 {"path": "audio/a1b2c3d4.wav", "sha256": "...", "size_bytes": 4096, "role": "audio"},
6895 {"path": "midi/midi-bundle.zip", "sha256": "...", "size_bytes": 1024, "role": "midi-bundle"},
6896 {"path": "release-manifest.json", "sha256": "...", "size_bytes": 512, "role": "manifest"}
6897 ]
6898 }
6899 ```
6900
6901 **Agent use case:** An AI music generation agent calls `muse release v1.0 --render-midi --json`
6902 after tagging a completed composition. It reads `stubbed` from the JSON output to
6903 determine whether the audio files are real renders or MIDI placeholders, and inspects
6904 `files[*].sha256` to verify integrity before uploading to a distribution platform.
6905
6906 **Implementation stub note:** The Storpheus `POST /render` endpoint (MIDI-in → audio-out)
6907 is not yet deployed. Until it ships, `--render-audio` and `--export-stems` copy the
6908 source MIDI file as a placeholder and set `stubbed=true` in the manifest. The
6909 `_render_midi_to_audio` function in `maestro/services/muse_release.py` is the only
6910 site to update when the endpoint becomes available.
6911
6912 **Implementation:** `maestro/muse_cli/commands/release.py` — `_release_async(...)`.
6913 Service layer: `maestro/services/muse_release.py` — `build_release(...)`.
6914
6915 ---
6916
6917 ## Muse CLI — Worktree Command Reference
6918
6919 ### `muse worktree`
6920
6921 **Purpose:** Manage local Muse worktrees so a producer can work on two
6922 arrangements simultaneously (e.g. "radio edit" and "extended club version")
6923 without switching branches back and forth.
6924
6925 ---
6926
6927 ### `muse worktree add`
6928
6929 **Purpose:** Create a new linked worktree at a given path, checked out to a
6930 specific branch. Enables parallel arrangement editing without branch switching.
6931
6932 **Usage:**
6933 ```bash
6934 muse worktree add <path> <branch>
6935 ```
6936
6937 **Arguments:**
6938
6939 | Argument | Description |
6940 |----------|-------------|
6941 | `<path>` | Directory to create the linked worktree in (must not exist) |
6942 | `<branch>` | Branch name to check out; created from HEAD if absent |
6943
6944 **Output example:**
6945 ```
6946 ✅ Linked worktree 'feature/extended' created at /path/to/club-mix
6947 ```
6948
6949 **Constraints:**
6950 - `<path>` must not already exist.
6951 - The same branch cannot be checked out in two worktrees simultaneously (mirrors git).
6952 - `<path>` must be outside the main repository root.
6953
6954 **Layout of the new directory:**
6955 ```
6956 <path>/
6957 .muse — plain-text gitdir file: "gitdir: <main-repo>/.muse"
6958 muse-work/ — independent working directory for this worktree
6959 ```
6960
6961 **Registration in main repo:**
6962 ```
6963 .muse/worktrees/<slug>/path → absolute path to <path>
6964 .muse/worktrees/<slug>/branch → branch name
6965 ```
6966
6967 **Result type:** `WorktreeInfo` — fields: `path`, `branch`, `head_commit`, `is_main`, `slug`.
6968
6969 **Agent use case:** An AI music agent can call `muse worktree add` to isolate an
6970 experimental arrangement variant in a separate directory while keeping the main
6971 working tree on the current branch.
6972
6973 **Implementation:** `maestro/muse_cli/commands/worktree.py` — `add_worktree(root, link_path, branch)`.
6974
6975 ---
6976
6977 ### `muse worktree remove`
6978
6979 **Purpose:** Remove a linked worktree directory and de-register it from the main
6980 repo. Branch refs and the shared objects store are preserved.
6981
6982 **Usage:**
6983 ```bash
6984 muse worktree remove <path>
6985 ```
6986
6987 **Arguments:**
6988
6989 | Argument | Description |
6990 |----------|-------------|
6991 | `<path>` | Path of the linked worktree to remove |
6992
6993 **Output example:**
6994 ```
6995 ✅ Worktree at /path/to/club-mix removed.
6996 ```
6997
6998 **Constraints:**
6999 - The main worktree cannot be removed.
7000 - `<path>` must be a registered linked worktree.
7001 - Works even when the linked directory is already absent (stale removal).
7002
7003 **Agent use case:** After finishing a variant arrangement, an agent removes the
7004 linked worktree to clean up disk space while preserving the branch for future use.
7005
7006 **Implementation:** `maestro/muse_cli/commands/worktree.py` — `remove_worktree(root, link_path)`.
7007
7008 ---
7009
7010 ### `muse worktree list`
7011
7012 **Purpose:** Show all worktrees (main + linked) with their paths, branches, and
7013 HEAD commit SHAs. Enables an agent to know which arrangements are active.
7014
7015 **Usage:**
7016 ```bash
7017 muse worktree list
7018 ```
7019
7020 **Output example:**
7021 ```
7022 /path/to/project [main] branch: main head: a1b2c3d4
7023 /path/to/club-mix branch: feature/extended head: a1b2c3d4
7024 ```
7025
7026 **Result type:** `list[WorktreeInfo]` — see `WorktreeInfo` in `type_contracts.md`.
7027
7028 **Agent use case:** Before composing a new section, an agent lists all worktrees
7029 to pick the correct arrangement context to work in.
7030
7031 **Implementation:** `maestro/muse_cli/commands/worktree.py` — `list_worktrees(root)`.
7032
7033 ---
7034
7035 ### `muse worktree prune`
7036
7037 **Purpose:** Remove stale worktree registrations where the target directory no
7038 longer exists. Safe to run any time — no data or branch refs are deleted.
7039
7040 **Usage:**
7041 ```bash
7042 muse worktree prune
7043 ```
7044
7045 **Output example (stale entries found):**
7046 ```
7047 ⚠️ Pruned stale worktree: /path/to/old-mix
7048 ✅ Pruned 1 stale worktree registration(s).
7049 ```
7050
7051 **Output example (nothing to prune):**
7052 ```
7053 ✅ No stale worktrees found.
7054 ```
7055
7056 **Agent use case:** Run periodically or before `muse worktree list` to ensure the
7057 registration index matches the filesystem.
7058
7059 **Implementation:** `maestro/muse_cli/commands/worktree.py` — `prune_worktrees(root)`.
7060
7061 ---
7062
7063 ## Muse Hub — User Profiles
7064
7065 User profiles are the public-facing identity layer of Muse Hub, analogous to
7066 GitHub profile pages. Each authenticated user may create exactly one profile,
7067 identified by a URL-safe username. The profile aggregates data across all
7068 of the user's repos to present a musical portfolio.
7069
7070 ### Data Model
7071
7072 **Table:** `musehub_profiles`
7073
7074 | Column | Type | Description |
7075 |--------|------|-------------|
7076 | `user_id` | String(36) PK | JWT `sub` — same ID used in `musehub_repos.owner_user_id` |
7077 | `username` | String(64) UNIQUE | URL-friendly handle (e.g. `gabriel`) |
7078 | `bio` | Text nullable | Short bio, Markdown supported (max 500 chars) |
7079 | `avatar_url` | String(2048) nullable | Avatar image URL |
7080 | `pinned_repo_ids` | JSON | Up to 6 repo_ids highlighted on the profile page |
7081 | `created_at` | DateTime(tz) | Profile creation timestamp |
7082 | `updated_at` | DateTime(tz) | Last update timestamp |
7083
7084 ### API Endpoints
7085
7086 | Method | Path | Auth | Description |
7087 |--------|------|------|-------------|
7088 | `GET` | `/api/v1/musehub/users/{username}` | Public | Full profile JSON |
7089 | `POST` | `/api/v1/musehub/users` | JWT required | Create a profile |
7090 | `PUT` | `/api/v1/musehub/users/{username}` | JWT, owner only | Update bio/avatar/pins |
7091 | `GET` | `/musehub/ui/users/{username}` | Public HTML shell | Browser profile page |
7092
7093 ### ProfileResponse Fields
7094
7095 | Field | Type | Description |
7096 |-------|------|-------------|
7097 | `username` | str | URL handle |
7098 | `bio` | str \| None | Short bio |
7099 | `avatar_url` | str \| None | Avatar URL |
7100 | `pinned_repo_ids` | list[str] | Pinned repo IDs (order preserved) |
7101 | `repos` | list[ProfileRepoSummary] | Public repos, newest first |
7102 | `contribution_graph` | list[ContributionDay] | 52 weeks of daily commit counts |
7103 | `session_credits` | int | Total commits across all repos (creative sessions) |
7104 | `created_at` / `updated_at` | datetime | Profile timestamps |
7105
7106 ### Contribution Graph
7107
7108 The contribution graph covers the **last 52 weeks** (364 days) ending today.
7109 Each day's count is the number of commits pushed across ALL repos owned by
7110 the user (public and private) on that date. The browser UI renders this as
7111 a GitHub-style heatmap using CSS data attributes (`data-count=0–4`).
7112
7113 ### Session Credits
7114
7115 Session credits are the total number of commits ever pushed to Muse Hub across
7116 all repos owned by the user. Each commit represents one composition session
7117 recorded to the hub. This is the MVP proxy; future releases may tie credits
7118 to token usage from `usage_logs`.
7119
7120 ### Disambiguation
7121
7122 The profile UI page at `/musehub/ui/users/{username}` does NOT conflict with
7123 the repo browser at `/musehub/ui/{owner}/{repo_slug}` — the `users/` path segment
7124 ensures distinct routing. The JSON API is namespaced at
7125 `/api/v1/musehub/users/{username}`.
7126
7127 ### Result Types
7128
7129 - `ProfileResponse` — `maestro/models/musehub.py`
7130 - `ProfileRepoSummary` — compact per-repo entry (repo_id, name, visibility, star_count, last_activity_at, created_at)
7131 - `ContributionDay` — `{ date: "YYYY-MM-DD", count: int }`
7132
7133 Registered in `docs/reference/type_contracts.md`.
7134
7135 ### Implementation Files
7136
7137 | File | Purpose |
7138 |------|---------|
7139 | `maestro/db/musehub_models.py` | `MusehubProfile` ORM model |
7140 | `maestro/services/musehub_profile.py` | CRUD + aggregate queries |
7141 | `maestro/api/routes/musehub/users.py` | JSON API handlers |
7142 | `maestro/api/routes/musehub/ui.py` | `profile_page()` HTML handler |
7143 | `alembic/versions/0001_consolidated_schema.py` | `musehub_profiles` table |
7144 | `tests/test_musehub_ui.py` | Profile acceptance tests |
7145 ## Muse Hub — Cross-Repo Global Search
7146
7147 ### Overview
7148
7149 Global search lets musicians and AI agents search commit messages across **all
7150 public Muse Hub repos** in a single query. It is the cross-repo counterpart of
7151 the per-repo `muse find` command.
7152
7153 Only `visibility='public'` repos are searched — private repos are excluded at
7154 the persistence layer and are never enumerated regardless of caller identity.
7155
7156 ### API
7157
7158 ```
7159 GET /api/v1/musehub/search?q={query}&mode={mode}&page={page}&page_size={page_size}
7160 Authorization: Bearer <jwt>
7161 ```
7162
7163 **Parameters:**
7164
7165 | Parameter | Type | Default | Description |
7166 |-----------|------|---------|-------------|
7167 | `q` | string (required) | — | Search query (1–500 chars) |
7168 | `mode` | `keyword` \| `pattern` | `keyword` | Matching strategy (see below) |
7169 | `page` | int ≥ 1 | 1 | Repo-group page number |
7170 | `page_size` | int 1–50 | 10 | Repo-groups per page |
7171
7172 **Search modes:**
7173
7174 - **keyword** — whitespace-split OR-match of each term against commit messages
7175 and repo names (case-insensitive, uses `lower()` + `LIKE %term%`).
7176 - **pattern** — raw SQL `LIKE` pattern applied to commit messages only. Use
7177 `%` as wildcard (e.g. `q=%minor%`).
7178
7179 ### Response shape
7180
7181 Returns `GlobalSearchResult` (JSON, camelCase):
7182
7183 ```json
7184 {
7185 "query": "jazz groove",
7186 "mode": "keyword",
7187 "totalReposSearched": 42,
7188 "page": 1,
7189 "pageSize": 10,
7190 "groups": [
7191 {
7192 "repoId": "uuid",
7193 "repoName": "jazz-lab",
7194 "repoOwner": "alice",
7195 "repoVisibility": "public",
7196 "totalMatches": 3,
7197 "matches": [
7198 {
7199 "commitId": "abc123",
7200 "message": "jazz groove — walking bass variant",
7201 "author": "alice",
7202 "branch": "main",
7203 "timestamp": "2026-02-27T12:00:00Z",
7204 "repoId": "uuid",
7205 "repoName": "jazz-lab",
7206 "repoOwner": "alice",
7207 "repoVisibility": "public",
7208 "audioObjectId": "sha256:abc..."
7209 }
7210 ]
7211 }
7212 ]
7213 }
7214 ```
7215
7216 Results are **grouped by repo**. Each group contains up to 20 matching commits
7217 (newest-first). `totalMatches` reflects the actual count before the 20-commit
7218 cap. Pagination (`page` / `page_size`) controls how many repo-groups appear
7219 per response.
7220
7221 `audioObjectId` is populated when the repo has at least one `.mp3`, `.ogg`, or
7222 `.wav` artifact — the first one alphabetically by path is chosen. Consumers
7223 can use this to render `<audio>` preview players without a separate API call.
7224
7225 ### Browser UI
7226
7227 ```
7228 GET /musehub/ui/search?q={query}&mode={mode}
7229 ```
7230
7231 Returns a static HTML shell (no JWT required). The page pre-fills the search
7232 form from URL params, submits to the JSON API via localStorage JWT, and renders
7233 grouped results with audio previews and pagination.
7234
7235 ### Implementation
7236
7237 | Layer | File | What it does |
7238 |-------|------|-------------|
7239 | Pydantic models | `maestro/models/musehub.py` | `GlobalSearchCommitMatch`, `GlobalSearchRepoGroup`, `GlobalSearchResult` |
7240 | Service | `maestro/services/musehub_repository.py` | `global_search()` — public-only filter, keyword/pattern predicate, group assembly, audio preview resolution |
7241 | Route | `maestro/api/routes/musehub/search.py` | `GET /musehub/search` — validates params, delegates to service |
7242 | UI | `maestro/api/routes/musehub/ui.py` | `global_search_page()` — static HTML shell at `/musehub/ui/search` |
7243
7244 ### Agent use case
7245
7246 An AI composition agent searching for reference material can call:
7247
7248 ```
7249 GET /api/v1/musehub/search?q=F%23+minor+walking+bass&mode=keyword&page_size=5
7250 ```
7251
7252 The grouped response lets the agent scan commit messages by repo context,
7253 identify matching repos by name and owner, and immediately fetch audio previews
7254 via `audioObjectId` without additional round-trips.
7255
7256 ---
7257
7258 ## Muse Hub — Dynamics Analysis Page (issue #223)
7259
7260 The dynamics analysis page visualises per-track velocity profiles, dynamic arc
7261 classifications, and a cross-track loudness comparison for a given commit ref.
7262
7263 ### JSON endpoint
7264
7265 ```
7266 GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/dynamics/page
7267 ```
7268
7269 Requires JWT. Returns `DynamicsPageData` — a list of `TrackDynamicsProfile`
7270 objects, one per active track.
7271
7272 Query params:
7273 | Param | Type | Default | Description |
7274 |-------|------|---------|-------------|
7275 | `track` | `str` | (all) | Filter to a single named track |
7276 | `section` | `str` | (all) | Filter to a named section |
7277
7278 Response fields: `repo_id`, `ref`, `tracks[]` (each with `track`, `peak_velocity`,
7279 `min_velocity`, `mean_velocity`, `dynamic_range`, `arc`, `curve`).
7280
7281 ### Browser UI
7282
7283 ```
7284 GET /{repo_id}/analysis/{ref}/dynamics
7285 ```
7286
7287 Returns a static HTML shell (no JWT required to load; JWT required to fetch
7288 data). Renders:
7289 - Per-track SVG velocity sparkline (32-point `curve` array)
7290 - Dynamic arc badge (`flat` / `crescendo` / `decrescendo` / `terraced` / `swell` / `hairpin`)
7291 - Peak velocity and velocity range metrics per track
7292 - Cross-track loudness comparison bar chart
7293 - Track and section filter dropdowns
7294
7295 ### Arc vocabulary
7296
7297 | Arc | Meaning |
7298 |-----|---------|
7299 | `flat` | Uniform velocity throughout |
7300 | `terraced` | Step-wise velocity shifts (Baroque-style) |
7301 | `crescendo` | Monotonically increasing velocity |
7302 | `decrescendo` | Monotonically decreasing velocity |
7303 | `swell` | Rise then fall (arch shape) |
7304 | `hairpin` | Fall then rise (valley shape) |
7305
7306 ### Implementation
7307
7308 | Layer | File | What it does |
7309 |-------|------|-------------|
7310 | Pydantic models | `maestro/models/musehub_analysis.py` | `DynamicArc`, `TrackDynamicsProfile`, `DynamicsPageData` |
7311 | Service | `maestro/services/musehub_analysis.py` | `compute_dynamics_page_data()` — builds stub profiles keyed by `(repo_id, ref)` |
7312 | Route | `maestro/api/routes/musehub/analysis.py` | `GET .../dynamics/page` — JWT, ETag, 404 on missing repo |
7313 | UI | `maestro/api/routes/musehub/ui.py` | `dynamics_analysis_page()` — static HTML + JS at `/{repo_id}/analysis/{ref}/dynamics` |
7314 | Tests | `tests/test_musehub_analysis.py` | Endpoint + service unit tests |
7315
7316 ### Agent use case
7317
7318 Before composing a new layer, an agent fetches:
7319
7320 ```
7321 GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/dynamics/page
7322 ```
7323
7324 It examines the `arc` of each track to decide whether to add velocity
7325 variation. If all tracks are `flat`, the agent shapes the new part with a
7326 `crescendo`. If one track is already `crescendo`, the new layer complements it
7327 rather than competing.
7328 ---
7329
7330 ## Muse Hub — Contour and Tempo Analysis Pages
7331
7332 **Purpose:** Browser-readable visualisation of the two most structure-critical
7333 musical dimensions — melodic contour and tempo — derived from a Muse commit
7334 ref. These pages close the gap between the CLI commands `muse contour` and
7335 `muse tempo --history` and the web-first MuseHub experience.
7336
7337 ### Contour Analysis Page
7338
7339 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/contour`
7340
7341 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7342
7343 **What it shows:**
7344 - **Shape label** — coarse melodic shape (`arch`, `ascending`, `descending`,
7345 `flat`, `inverted-arch`, `wave`) rendered as a coloured badge.
7346 - **Pitch-curve SVG** — polyline of MIDI pitch sampled at quarter-note
7347 intervals across the piece, with min/max pitch labels and beat-count axis.
7348 - **Tessitura bar** — horizontal range bar from lowest to highest note,
7349 displaying octave span and note names (e.g. `C3` – `G5 · 2.0 oct`).
7350 - **Direction metadata** — `overallDirection` (up / down / flat),
7351 `directionChanges` count, `peakBeat`, and `valleyBeat`.
7352 - **Track filter** — text input forwarded as `?track=<instrument>` to the
7353 JSON API, restricting analysis to a named instrument (e.g. `lead`, `keys`).
7354
7355 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/contour`
7356
7357 Returns `AnalysisResponse` with `dimension = "contour"` and `data` of type
7358 `ContourData`:
7359
7360 | Field | Type | Description |
7361 |-------|------|-------------|
7362 | `shape` | `str` | Coarse shape label (`arch`, `ascending`, `descending`, `flat`, `wave`) |
7363 | `direction_changes` | `int` | Number of melodic direction reversals |
7364 | `peak_beat` | `float` | Beat position of the melodic peak |
7365 | `valley_beat` | `float` | Beat position of the melodic valley |
7366 | `overall_direction` | `str` | Net direction from first to last note (`up`, `down`, `flat`) |
7367 | `pitch_curve` | `list[float]` | MIDI pitch sampled at quarter-note intervals |
7368
7369 **Result type:** `ContourData` — defined in `maestro/models/musehub_analysis.py`.
7370
7371 **Agent use case:** Before generating a melodic continuation, an AI agent
7372 fetches the contour page JSON to understand the shape of the existing lead
7373 line. If `shape == "arch"`, the agent can elect to continue with a
7374 descending phrase that resolves the arch rather than restarting from the peak.
7375
7376 ### Tempo Analysis Page
7377
7378 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/tempo`
7379
7380 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7381
7382 **What it shows:**
7383 - **BPM display** — large primary tempo in beats per minute.
7384 - **Time feel** — perceptual descriptor (`straight`, `laid-back`, `rushing`).
7385 - **Stability bar** — colour-coded progress bar (green ≥ 80%, orange ≥ 50%,
7386 red < 50%) with percentage and label (`metronomic` / `moderate` / `free tempo`).
7387 - **Tempo change timeline SVG** — polyline of BPM over beat position, with
7388 orange dots marking each tempo change event.
7389 - **Tempo change table** — tabular list of beat → BPM transitions for precise
7390 inspection.
7391 - **Cross-commit note** — guidance linking to the JSON API for BPM history
7392 across multiple refs.
7393
7394 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/tempo`
7395
7396 Returns `AnalysisResponse` with `dimension = "tempo"` and `data` of type
7397 `TempoData`:
7398
7399 | Field | Type | Description |
7400 |-------|------|-------------|
7401 | `bpm` | `float` | Primary (mean) BPM for the ref |
7402 | `stability` | `float` | 0 = free tempo, 1 = perfectly metronomic |
7403 | `time_feel` | `str` | `straight`, `laid-back`, or `rushing` |
7404 | `tempo_changes` | `list[TempoChange]` | Ordered list of beat-position → BPM transitions |
7405
7406 `TempoChange` fields: `beat: float`, `bpm: float`.
7407
7408 **Result type:** `TempoData`, `TempoChange` — defined in
7409 `maestro/models/musehub_analysis.py`.
7410
7411 **Agent use case:** An AI agent generating a new section can query
7412 `/analysis/{ref}/tempo` to detect an accelerando near the end of a piece
7413 (`stability < 0.5`, ascending `tempo_changes`) and avoid locking the generated
7414 part to a rigid grid. For cross-commit BPM evolution, the agent compares
7415 `TempoData.bpm` across multiple refs to track tempo drift across a composition
7416 session.
7417
7418 ### Implementation
7419
7420 | Layer | File | What it does |
7421 |-------|------|-------------|
7422 | Pydantic models | `maestro/models/musehub_analysis.py` | `ContourData`, `TempoData`, `TempoChange` |
7423 | Service | `maestro/services/musehub_analysis.py` | `compute_dimension("contour" \| "tempo", ...)` |
7424 | Analysis route | `maestro/api/routes/musehub/analysis.py` | `GET /repos/{id}/analysis/{ref}/contour` and `.../tempo` |
7425 | UI route | `maestro/api/routes/musehub/ui.py` | `contour_page()`, `tempo_page()` — HTML shells |
7426 | Tests | `tests/test_musehub_ui.py` | `test_contour_page_renders`, `test_contour_json_response`, `test_tempo_page_renders`, `test_tempo_json_response` |
7427 | Tests | `tests/test_musehub_analysis.py` | `test_contour_track_filter`, `test_tempo_section_filter` |
7428
7429 ---
7430
7431 ## Muse Hub — Per-Dimension Analysis Detail Pages (issue #332)
7432
7433 **Purpose:** Each of the 10 analysis dashboard cards links to a dedicated
7434 per-dimension page at `/{owner}/{repo_slug}/analysis/{ref}/{dim_id}`. This
7435 section documents the 6 pages added in issue #332 to complete the set (key,
7436 meter, chord-map, groove, emotion, form).
7437
7438 ### Key Analysis Page
7439
7440 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/key`
7441
7442 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7443
7444 **What it shows:**
7445 - **Tonic + mode display** — large coloured badge showing detected key (e.g. `G dorian`).
7446 - **Relative key** — companion key alongside the primary (e.g. `Bb major`).
7447 - **Confidence bar** — colour-coded progress bar (green >= 80%, orange >= 60%, red < 60%).
7448 - **Alternate key candidates** — ranked list of secondary hypotheses with individual confidence bars.
7449
7450 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/key`
7451
7452 | Field | Type | Description |
7453 |-------|------|-------------|
7454 | `tonic` | `str` | Detected tonic pitch class, e.g. `C`, `F#` |
7455 | `mode` | `str` | Detected mode, e.g. `major`, `dorian`, `mixolydian` |
7456 | `confidence` | `float` | Detection confidence, 0-1 |
7457 | `relative_key` | `str` | Relative major/minor key |
7458 | `alternate_keys` | `list[AlternateKey]` | Secondary candidates ranked by confidence |
7459
7460 **Agent use case:** Before generating harmonic material, an agent fetches the key page
7461 to confirm the tonal centre. When `confidence < 0.7`, the agent inspects `alternate_keys`
7462 to handle tonally ambiguous pieces.
7463
7464 ### Meter Analysis Page
7465
7466 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/meter`
7467
7468 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7469
7470 **What it shows:**
7471 - **Time signature** — large display of the primary time signature (e.g. `6/8`).
7472 - **Compound/simple badge** — classifies the meter type.
7473 - **Beat strength profile SVG** — bar chart of relative beat strengths across one bar.
7474 - **Irregular sections table** — lists any sections where the meter deviates from the primary.
7475
7476 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/meter`
7477
7478 | Field | Type | Description |
7479 |-------|------|-------------|
7480 | `time_signature` | `str` | Primary time signature, e.g. `4/4`, `6/8` |
7481 | `is_compound` | `bool` | True for compound meters like 6/8, 12/8 |
7482 | `beat_strength_profile` | `list[float]` | Relative beat strengths across one bar |
7483 | `irregular_sections` | `list[IrregularSection]` | Sections with non-primary meter |
7484
7485 **Agent use case:** An agent generating rhythmic material checks this page to avoid
7486 placing accents on weak beats and adjusts triplet groupings for compound meters.
7487
7488 ### Chord Map Analysis Page
7489
7490 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/chord-map`
7491
7492 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7493
7494 **What it shows:**
7495 - **Summary counts** — total chords and total beats.
7496 - **Chord progression table** — beat-position, chord, Roman numeral function, and tension
7497 score with per-row tension bars colour-coded green/orange/red.
7498
7499 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/chord-map`
7500
7501 | Field | Type | Description |
7502 |-------|------|-------------|
7503 | `progression` | `list[ChordEvent]` | Time-ordered chord events |
7504 | `total_chords` | `int` | Total number of distinct chord events |
7505 | `total_beats` | `int` | Duration of the ref in beats |
7506
7507 `ChordEvent` fields: `beat: float`, `chord: str`, `function: str`, `tension: float (0-1)`.
7508
7509 **Agent use case:** An agent generating accompaniment inspects the chord map to produce
7510 harmonically idiomatic voicings. High `tension` chords (> 0.7) signal dissonance points
7511 where resolution material is appropriate.
7512
7513 ### Groove Analysis Page
7514
7515 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/groove`
7516
7517 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7518
7519 **What it shows:**
7520 - **Style badge** — detected groove style (`straight`, `swing`, `shuffled`, `latin`, `funk`).
7521 - **BPM + grid resolution** — primary tempo and quantization grid (e.g. `1/16`).
7522 - **Onset deviation** — mean absolute deviation of note onsets from the grid in beats.
7523 - **Groove score gauge** — colour-coded percentage bar (green >= 80%, orange >= 60%, red < 60%).
7524 - **Swing factor bar** — 0.5 = perfectly straight, 0.67 = hard triplet swing.
7525
7526 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/groove`
7527
7528 | Field | Type | Description |
7529 |-------|------|-------------|
7530 | `style` | `str` | Detected groove style |
7531 | `bpm` | `float` | Primary BPM |
7532 | `grid_resolution` | `str` | Quantization grid, e.g. `1/16`, `1/8T` |
7533 | `onset_deviation` | `float` | Mean absolute note onset deviation from grid (beats) |
7534 | `groove_score` | `float` | Aggregate rhythmic tightness (1 = very tight) |
7535 | `swing_factor` | `float` | 0.5 = straight, ~0.67 = triplet swing |
7536
7537 **Agent use case:** When generating continuation material, an agent matches the groove
7538 style and swing factor so generated notes feel rhythmically consistent with the existing
7539 recording rather than mechanically quantized.
7540
7541 ### Emotion Analysis Page
7542
7543 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/emotion`
7544
7545 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7546
7547 **What it shows:**
7548 - **Primary emotion badge** — dominant emotion label (e.g. `joyful`, `melancholic`).
7549 - **Valence-arousal plot** — 2D scatter dot on the valence (x) x arousal (y) plane.
7550 - **Axis bars** — individual bars for valence (re-normalised to 0-1), arousal, and tension.
7551 - **Confidence score** — detection confidence percentage.
7552
7553 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/emotion`
7554
7555 | Field | Type | Description |
7556 |-------|------|-------------|
7557 | `valence` | `float` | -1 (sad/dark) to +1 (happy/bright) |
7558 | `arousal` | `float` | 0 (calm) to 1 (energetic) |
7559 | `tension` | `float` | 0 (relaxed) to 1 (tense/dissonant) |
7560 | `primary_emotion` | `str` | Dominant emotion label |
7561 | `confidence` | `float` | Detection confidence, 0-1 |
7562
7563 **Agent use case:** An agent generating a bridge section checks the emotion page to decide
7564 whether to maintain or contrast the current emotional character. Low confidence (< 0.6)
7565 signals an emotionally ambiguous piece.
7566
7567 ### Form Analysis Page
7568
7569 **Route:** `GET /musehub/ui/{owner}/{repo_slug}/analysis/{ref}/form`
7570
7571 **Auth:** No JWT required — static HTML shell; JS fetches from the authed JSON API.
7572
7573 **What it shows:**
7574 - **Form label** — detected macro form (e.g. `AABA`, `verse-chorus`, `through-composed`).
7575 - **Section counts** — total beats and number of formal sections.
7576 - **Form timeline** — colour-coded horizontal bar with proportional-width segments per section.
7577 - **Sections table** — per-section label, function, and beat range.
7578
7579 **JSON endpoint:** `GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/form`
7580
7581 | Field | Type | Description |
7582 |-------|------|-------------|
7583 | `form_label` | `str` | Detected macro form, e.g. `AABA`, `verse-chorus` |
7584 | `total_beats` | `int` | Total duration in beats |
7585 | `sections` | `list[SectionEntry]` | Formal sections in order |
7586
7587 `SectionEntry` fields: `label`, `function`, `start_beat`, `end_beat`, `length_beats`.
7588
7589 **Agent use case:** Before generating a new section, an agent reads the form page to
7590 understand where it is in the compositional arc (e.g. after two verses, a chorus is
7591 expected). The colour-coded timeline shows how much of the form is already complete.
7592
7593 ### Implementation (all 10 dimension pages)
7594
7595 | Layer | File | What it does |
7596 |-------|------|-------------|
7597 | Pydantic models | `maestro/models/musehub_analysis.py` | `ContourData`, `TempoData`, `KeyData`, `MeterData`, `ChordMapData`, `GrooveData`, `EmotionData`, `FormData` |
7598 | Service | `maestro/services/musehub_analysis.py` | `compute_dimension(dim, ...)` -- handles all 10 dimensions |
7599 | Analysis route | `maestro/api/routes/musehub/analysis.py` | `GET /repos/{id}/analysis/{ref}/{dim}` for each dimension |
7600 | UI route | `maestro/api/routes/musehub/ui.py` | `key_analysis_page()`, `meter_analysis_page()`, `chord_map_analysis_page()`, `groove_analysis_page()`, `emotion_analysis_page()`, `form_analysis_page()` |
7601 | Templates | `maestro/templates/musehub/pages/` | `key.html`, `meter.html`, `chord_map.html`, `groove.html`, `emotion.html`, `form.html` |
7602 | Tests | `tests/test_musehub_ui.py` | `test_*_page_renders`, `test_*_page_no_auth_required`, `test_*_page_contains_*_data_labels` for each dimension |
7603
7604 ---
7605
7606 ## MuseHub Design System
7607
7608 MuseHub pages share a structured CSS framework served as static assets from
7609 `/musehub/static/`. This replaces the former monolithic `_CSS` Python string
7610 that was embedded in every HTML response body.
7611
7612 ### File structure
7613
7614 | File | Served at | Purpose |
7615 |------|-----------|---------|
7616 | `maestro/templates/musehub/static/tokens.css` | `/musehub/static/tokens.css` | CSS custom properties (design tokens) |
7617 | `maestro/templates/musehub/static/components.css` | `/musehub/static/components.css` | Reusable component classes |
7618 | `maestro/templates/musehub/static/layout.css` | `/musehub/static/layout.css` | Grid, header, responsive breakpoints |
7619 | `maestro/templates/musehub/static/icons.css` | `/musehub/static/icons.css` | File-type and musical concept icons |
7620 | `maestro/templates/musehub/static/music.css` | `/musehub/static/music.css` | Piano roll, waveform, radar chart, heatmap |
7621 | `maestro/templates/musehub/static/musehub.js` | `/musehub/static/musehub.js` | Shared JS: JWT helpers, `apiFetch`, token form, date/SHA formatters |
7622 | `maestro/templates/musehub/static/embed.css` | `/musehub/static/embed.css` | Compact dark theme for iframe embed player |
7623
7624 Static files are served by FastAPI's `StaticFiles` mount registered in
7625 `maestro/main.py` at startup. All CSS files and `musehub.js` are linked by
7626 `maestro/templates/musehub/base.html` — the Jinja2 base template that replaced
7627 the old `_page()` Python helper.
7628
7629 ### Jinja2 template layout
7630
7631 All MuseHub web UI pages are rendered via Jinja2 templates
7632 (`jinja2>=3.1.0`, `aiofiles>=23.2.0`). No HTML is generated inside route
7633 handlers; handlers resolve server-side data and pass a minimal context dict to
7634 the template engine.
7635
7636 ```
7637 maestro/templates/musehub/
7638 ├── base.html — main authenticated layout (extends nothing)
7639 ├── explore_base.html — public discover/trending layout (no auth, filter bar)
7640 ├── partials/
7641 │ ├── navbar.html — global navigation bar (logo, search, user menu)
7642 │ ├── breadcrumbs.html — server-side breadcrumb trail from breadcrumb_data
7643 │ ├── repo_nav.html — repo identity card + includes repo_tabs.html
7644 │ └── repo_tabs.html — repo-level tab strip (Commits, Graph, PRs, Issues…)
7645 └── pages/
7646 ├── global_search.html
7647 ├── profile.html
7648 ├── repo.html
7649 ├── commit.html
7650 ├── graph.html
7651 ├── pr_list.html
7652 ├── pr_detail.html
7653 ├── issue_list.html
7654 ├── issue_detail.html — issue body + threaded comment section (renderComments/submitComment/deleteComment/toggleReplyForm)
7655 ├── context.html
7656 ├── credits.html
7657 ├── embed.html — iframe-safe audio player (standalone, no base)
7658 ├── search.html
7659 ├── divergence.html
7660 ├── timeline.html
7661 ├── release_list.html
7662 ├── release_detail.html
7663 ├── sessions.html
7664 ├── session_detail.html
7665 ├── contour.html
7666 ├── tempo.html
7667 └── dynamics.html
7668 ```
7669
7670 **Template inheritance:** page templates extend `base.html` (or
7671 `explore_base.html`) using `{% extends %}` / `{% block %}`. Server-side
7672 dynamic data is injected via Jinja2's `{{ var | tojson }}` filter inside
7673 `<script>` blocks; large JavaScript sections containing template literals are
7674 wrapped in `{% raw %}...{% endraw %}` to prevent Jinja2 from parsing them.
7675
7676 ### Navigation architecture
7677
7678 Every MuseHub page renders a three-layer navigation stack:
7679
7680 ```
7681 ┌───────────────────────────────────────────────────────────┐
7682 │ Global nav bar (.musehub-navbar) │
7683 │ logo · search · explore · notifications · sign-out │
7684 │ included by: base.html, explore_base.html │
7685 ├───────────────────────────────────────────────────────────┤
7686 │ Breadcrumb bar (.breadcrumb-bar) │
7687 │ owner / repo / section / detail │
7688 │ populated via: {% block breadcrumb %} (inline HTML) │
7689 │ or: breadcrumb_data list (rendered by breadcrumbs.html) │
7690 ├───────────────────────────────────────────────────────────┤
7691 │ Repo tab strip (.repo-tabs) — repo-scoped pages only │
7692 │ Commits · Graph · PRs · Issues · Releases · │
7693 │ Sessions · Timeline · Analysis · Credits · Insights │
7694 │ active tab: current_page template variable │
7695 │ tab counts: loaded client-side by loadNavCounts() │
7696 └───────────────────────────────────────────────────────────┘
7697 ```
7698
7699 #### Global nav bar (`partials/navbar.html`)
7700
7701 Included unconditionally from `base.html` and `explore_base.html`. Contains:
7702
7703 - **Logo**: links to `/musehub/ui/explore`
7704 - **Search form**: `<form method="get" action="/musehub/ui/search">` — a plain
7705 HTML GET form, no JavaScript required for basic navigation. The `q` param is
7706 pre-populated by the global search handler for sharable URLs.
7707 - **Explore link**: direct link to the discover grid
7708 - **Notification bell**: `#nav-notif-badge` populated client-side by
7709 `loadNotifBadge()` in `musehub.js`
7710 - **Sign-out button**: `#signout-btn` shown client-side when a JWT is present
7711 - **Hamburger toggle**: CSS-only collapse on xs screens (checkbox trick)
7712
7713 #### Breadcrumbs
7714
7715 Two approaches coexist:
7716
7717 1. **Inline block** (most pages): child templates override
7718 `{% block breadcrumb %}` with hand-crafted anchor tags. Rendered inside
7719 `.breadcrumb-bar > .breadcrumb` by `base.html`.
7720
7721 2. **Data-driven** (commit detail and future pages): route handlers pass a
7722 `breadcrumb_data` list and `breadcrumbs.html` renders it:
7723 ```python
7724 breadcrumb_data = _breadcrumbs(
7725 (owner, f"/musehub/ui/{owner}"),
7726 (repo_slug, base_url),
7727 ("commits", base_url),
7728 (commit_id[:8], ""), # empty url → plain text (current page)
7729 )
7730 ```
7731 `_breadcrumbs()` is a thin helper in `maestro/api/routes/musehub/ui.py`
7732 that converts `(label, url)` tuples into a list of dicts.
7733
7734 #### Repo tab strip (`partials/repo_tabs.html`)
7735
7736 Shown on all repo-scoped pages via `repo_nav.html` (which also renders the
7737 repo identity card above the tabs). The active tab is determined by the
7738 `current_page` string passed from the route handler:
7739
7740 | `current_page` value | Highlighted tab |
7741 |----------------------|-----------------|
7742 | `commits` | Commits |
7743 | `graph` | Graph |
7744 | `pulls` | Pull Requests |
7745 | `issues` | Issues |
7746 | `releases` | Releases |
7747 | `sessions` | Sessions |
7748 | `timeline` | Timeline |
7749 | `analysis` | Analysis |
7750 | `credits` | Credits |
7751 | `insights` | Insights |
7752 | `search` | Search |
7753 | `arrange` | Arrange |
7754
7755 Tab count badges (`id="nav-pr-count"`, `id="nav-issue-count"`) are populated
7756 client-side by `loadNavCounts()` in `musehub.js` so route handlers remain
7757 unauthenticated.
7758
7759 ### Design tokens (`tokens.css`)
7760
7761 All component classes consume CSS custom properties exclusively — no hardcoded
7762 hex values in component or layout files.
7763
7764 | Token group | Examples |
7765 |-------------|---------|
7766 | Background surfaces | `--bg-base`, `--bg-surface`, `--bg-overlay`, `--bg-hover` |
7767 | Borders | `--border-subtle`, `--border-default`, `--border-muted` |
7768 | Text | `--text-primary`, `--text-secondary`, `--text-muted` |
7769 | Accent / brand | `--color-accent`, `--color-success`, `--color-danger`, `--color-warning` |
7770 | Musical dimensions | `--dim-harmonic`, `--dim-rhythmic`, `--dim-melodic`, `--dim-structural`, `--dim-dynamic` |
7771 | Track palette | `--track-0` through `--track-7` (8 distinct colours) |
7772 | Spacing | `--space-1` (4 px) through `--space-12` (48 px) |
7773 | Typography | `--font-sans`, `--font-mono`, `--font-size-*`, `--font-weight-*` |
7774 | Radii | `--radius-sm` (4 px) through `--radius-full` (9999 px) |
7775 | Shadows | `--shadow-sm` through `--shadow-xl` |
7776
7777 ### Musical dimension colours
7778
7779 Each musical dimension has a primary colour and a muted/background variant used
7780 in badges, radar polygons, and diff heatmap bars.
7781
7782 | Dimension | Token | Colour | Use |
7783 |-----------|-------|--------|-----|
7784 | Harmonic | `--dim-harmonic` | Blue `#388bfd` | Chord progressions, intervals |
7785 | Rhythmic | `--dim-rhythmic` | Green `#3fb950` | Time, meter, swing |
7786 | Melodic | `--dim-melodic` | Purple `#bc8cff` | Pitch contour, motifs |
7787 | Structural | `--dim-structural` | Orange `#f0883e` | Form, sections, repeats |
7788 | Dynamic | `--dim-dynamic` | Red `#f85149` | Velocity, loudness arcs |
7789
7790 ### Component reference
7791
7792 | Class | File | Description |
7793 |-------|------|-------------|
7794 | `.badge`, `.badge-open/closed/merged/clean/dirty` | `components.css` | Status badges |
7795 | `.btn`, `.btn-primary/danger/secondary/ghost` | `components.css` | Action buttons |
7796 | `.card` | `components.css` | Surface panel |
7797 | `.table` | `components.css` | Data table |
7798 | `.grid-auto`, `.grid-2`, `.grid-3` | `components.css` | Layout grids |
7799 | `.modal`, `.modal-panel`, `.modal-footer` | `components.css` | Dialog overlay |
7800 | `.tabs`, `.tab-list`, `.tab` | `components.css` | Tab navigation |
7801 | `[data-tooltip]` | `components.css` | CSS-only tooltip |
7802 | `.file-icon .icon-mid/mp3/wav/json/webp/xml/abc` | `icons.css` | File-type icons |
7803 | `.music-icon .icon-key/tempo/dynamics/…` | `icons.css` | Musical concept icons |
7804 | `.piano-roll` | `music.css` | Multi-track note grid |
7805 | `.waveform` | `music.css` | Audio waveform container |
7806 | `.radar-chart`, `.radar-polygon-*` | `music.css` | Dimension radar chart |
7807 | `.contrib-graph`, `.contrib-day` | `music.css` | Contribution heatmap |
7808 | `.diff-heatmap`, `.diff-dim-bar-*` | `music.css` | Commit diff visualisation |
7809
7810 ### Responsive breakpoints
7811
7812 | Name | Width | Override behaviour |
7813 |------|-------|--------------------|
7814 | xs | < 480 px | Single-column layout; breadcrumb hidden |
7815 | sm | 480–767 px | Single-column layout |
7816 | md | 768–1023 px | Sidebar narrows to 200 px |
7817 | lg | ≥ 1024 px | Full layout (base styles) |
7818 | xl | ≥ 1280 px | Container-wide expands to 1440 px |
7819
7820 Minimum supported width: **375 px** (iPhone SE and equivalent Android devices).
7821
7822 ### Future theme support
7823
7824 The design system is dark-theme by default. All colours are defined as CSS
7825 custom properties on `:root`. A future light theme can be implemented by
7826 adding a `[data-theme="light"]` selector block to `tokens.css` that overrides
7827 the `--bg-*`, `--text-*`, and `--color-*` tokens — no changes to component
7828 files required.
7829
7830 ### Implementation
7831
7832 | Layer | File | What it does |
7833 |-------|------|-------------|
7834 | Design tokens | `maestro/templates/musehub/static/tokens.css` | CSS custom properties |
7835 | Components | `maestro/templates/musehub/static/components.css` | Reusable classes |
7836 | Layout | `maestro/templates/musehub/static/layout.css` | Grid, header, breakpoints |
7837 | Icons | `maestro/templates/musehub/static/icons.css` | File-type + music concept icons |
7838 | Music UI | `maestro/templates/musehub/static/music.css` | Piano roll, radar, heatmap |
7839 | Static mount | `maestro/main.py` | `app.mount("/musehub/static", StaticFiles(...))` |
7840 | Page helper | `maestro/api/routes/musehub/ui.py` | `_page()` links all five CSS files |
7841 | Tests | `tests/test_musehub_ui.py` | `test_design_tokens_css_served`, `test_components_css_served`, `test_repo_page_uses_design_system`, `test_responsive_meta_tag_present_*` |
7842
7843 ---
7844
7845 ## Emotion Map Page (issue #227)
7846
7847 The emotion map page visualises four emotional dimensions — **energy**, **valence**, **tension**, and **darkness** — across time within a composition and across its commit history.
7848
7849 ### Motivation
7850
7851 A film scorer needs to verify that the emotional arc of their composition matches the scene's emotional beats across the full commit history. Running `muse emotion-diff` between individual commit pairs is manual and error-prone. The emotion map provides a single-glance visual overview.
7852
7853 ### Route
7854
7855 ```
7856 GET /musehub/ui/{repo_id}/analysis/{ref}/emotion
7857 ```
7858
7859 Returns a static HTML shell (no JWT required). JavaScript fetches the JSON emotion map from the authed API and renders:
7860
7861 - **Evolution chart** — SVG line chart of all four dimensions sampled beat-by-beat within `ref`.
7862 - **Trajectory chart** — Per-commit summary vectors across the 5 most recent ancestor commits plus HEAD.
7863 - **Drift list** — Euclidean distance in emotion space between consecutive commits, with the dominant-change axis identified.
7864 - **Narrative** — Auto-generated text describing the emotional journey.
7865 - **Track / section filters** — Reload the data with instrument or section scope.
7866
7867 ### JSON Endpoint
7868
7869 ```
7870 GET /api/v1/musehub/repos/{repo_id}/analysis/{ref}/emotion-map
7871 ```
7872
7873 Requires JWT Bearer auth. Returns `EmotionMapResponse` (see type contracts). Query params: `?track=` and `?section=`.
7874
7875 ### Emotion Axes (all 0.0–1.0)
7876
7877 | Axis | Description |
7878 |------|-------------|
7879 | `energy` | Compositional drive / activity level |
7880 | `valence` | Brightness / positivity (0=dark, 1=bright) |
7881 | `tension` | Harmonic and rhythmic tension |
7882 | `darkness` | Brooding / ominous quality (inversely correlated with valence) |
7883
7884 Note: `valence` here is re-normalised to [0, 1] relative to the `EmotionData` model (which uses –1…+1) so all four axes share the same visual scale in charts.
7885
7886 ### Implementation
7887
7888 | Layer | File | What it does |
7889 |-------|------|-------------|
7890 | Pydantic models | `maestro/models/musehub_analysis.py` | `EmotionVector`, `EmotionMapPoint`, `CommitEmotionSnapshot`, `EmotionDrift`, `EmotionMapResponse` |
7891 | Service | `maestro/services/musehub_analysis.py` | `compute_emotion_map()` — builds evolution, trajectory, drift, and narrative |
7892 | Route (JSON) | `maestro/api/routes/musehub/analysis.py` | `GET .../emotion-map` — registered before `/{dimension}` to avoid parameter capture |
7893 | Route (UI) | `maestro/api/routes/musehub/ui.py` | `emotion_map_page()` — static HTML shell at `.../analysis/{ref}/emotion` |
7894 | Tests | `tests/test_musehub_ui.py` | Page renders, no-auth, chart JS, filters, JSON fields, trajectory, drift |
7895 | Tests | `tests/test_musehub_analysis.py` | Service unit tests + HTTP endpoint tests |
7896
7897 ### Muse VCS Considerations
7898
7899 - **Affected operations:** Maps conceptually to `muse emotion-diff` — the page shows what `emotion-diff` would show for all pairs in recent history.
7900 - **Reproducibility impact:** Stub data is deterministic for a given `ref` (seeded by MD5 of the ref string). Full MIDI-analysis-based inference will be model-dependent once Storpheus exposes an emotion introspection route.
7901
7902 ---
7903
7904 ## Muse Hub — Tree Browser (issue #204)
7905
7906 **Purpose:** GitHub-style directory tree browser for navigating the muse-work
7907 file structure stored in a Muse Hub repo. Musicians use it to find specific
7908 MIDI tracks, rendered MP3s, metadata files, and preview images without using
7909 the CLI.
7910
7911 ### URL Pattern
7912
7913 | Route | Description |
7914 |-------|-------------|
7915 | `GET /musehub/ui/{owner}/{repo_slug}/tree/{ref}` | Root directory listing |
7916 | `GET /musehub/ui/{owner}/{repo_slug}/tree/{ref}/{path}` | Subdirectory listing |
7917 | `GET /api/v1/musehub/repos/{repo_id}/tree/{ref}?owner=&repo_slug=` | JSON root listing |
7918 | `GET /api/v1/musehub/repos/{repo_id}/tree/{ref}/{path}?owner=&repo_slug=` | JSON subdirectory listing |
7919
7920 **Auth:** No JWT required for HTML shell (public repos). JSON API enforces auth
7921 for private repos.
7922
7923 ### Ref Resolution
7924
7925 `{ref}` can be:
7926 - A **branch name** (e.g. `main`, `feature/groove`) — resolved via `musehub_branches`.
7927 - A **full commit ID** — resolved via `musehub_commits`.
7928
7929 An unknown ref returns HTTP 404. This check is performed by
7930 `musehub_repository.resolve_ref_for_tree()`.
7931
7932 ### Tree Reconstruction
7933
7934 The tree is reconstructed at request time from all `musehub_objects` stored
7935 under the repo. No snapshot manifest table is required — the directory
7936 structure is derived by splitting object `path` fields on `/`.
7937
7938 `musehub_repository.list_tree()` algorithm:
7939 1. Fetch all objects for the repo ordered by path.
7940 2. Strip the `dir_path` prefix from each object's path.
7941 3. If the remainder has no `/` → file entry at this level.
7942 4. If the remainder has a `/` → directory entry (first segment only, deduplicated).
7943 5. Return dirs first (alphabetical), then files (alphabetical).
7944
7945 ### Response Shape (`TreeListResponse`)
7946
7947 ```json
7948 {
7949 "owner": "gabriel",
7950 "repoSlug": "summer-groove",
7951 "ref": "main",
7952 "dirPath": "",
7953 "entries": [
7954 { "type": "dir", "name": "tracks", "path": "tracks", "sizeBytes": null },
7955 { "type": "file", "name": "cover.webp", "path": "cover.webp", "sizeBytes": 4096 },
7956 { "type": "file", "name": "metadata.json","path": "metadata.json", "sizeBytes": 512 }
7957 ]
7958 }
7959 ```
7960
7961 ### File-Type Icons
7962
7963 | Extension | Icon | Symbol code |
7964 |-----------|------|-------------|
7965 | `.mid` / `.midi` | Piano | `&#127929;` |
7966 | `.mp3` / `.wav` / `.ogg` | Waveform | `&#127925;` |
7967 | `.json` | Braces | `&#123;&#125;` |
7968 | `.webp` / `.png` / `.jpg` | Photo | `&#128444;` |
7969 | Other | Generic file | `&#128196;` |
7970
7971 ### Implementation
7972
7973 | Layer | File |
7974 |-------|------|
7975 | Models | `maestro/models/musehub.py` — `TreeEntryResponse`, `TreeListResponse` |
7976 | Repository | `maestro/services/musehub_repository.py` — `resolve_ref_for_tree()`, `list_tree()` |
7977 | JSON API | `maestro/api/routes/musehub/objects.py` — `list_tree_root()`, `list_tree_subdir()` |
7978 | HTML routes | `maestro/api/routes/musehub/ui.py` — `tree_page()`, `tree_subdir_page()` |
7979 | Template | `maestro/templates/musehub/pages/tree.html` |
7980 | Tests | `tests/test_musehub_ui.py` — `test_tree_*` (6 tests) |
7981
7982 ---
7983
7984 ## Audio Player — Listen Page (issue #211)
7985
7986 ### Motivation
7987
7988 The bare `<audio>` element provides no waveform feedback, no A/B looping, and
7989 no speed control. Musicians performing critical listening on MuseHub need:
7990
7991 - Visual waveform for precise seek-by-region.
7992 - A/B loop to isolate a phrase and replay it at will.
7993 - Playback speed control (half-time, double-time, etc.) for transcription and
7994 arrangement work.
7995
7996 ### URL Pattern
7997
7998 ```
7999 GET /musehub/ui/{owner}/{repo_slug}/listen/{ref}
8000 GET /musehub/ui/{owner}/{repo_slug}/listen/{ref}/{path:path}
8001 ```
8002
8003 The first form lists all audio objects at `ref` and auto-loads the first track
8004 with an in-page track switcher. The second form deep-links to a specific audio
8005 file (stem, MIDI export, full mix) by its object path.
8006
8007 ### Waveform Engine
8008
8009 Wavesurfer.js is **vendored locally** at:
8010
8011 ```
8012 maestro/templates/musehub/static/vendor/wavesurfer.min.js
8013 ```
8014
8015 Served at `/musehub/static/vendor/wavesurfer.min.js` via FastAPI `StaticFiles`.
8016 No external CDN is used — the library is a self-contained, lightweight
8017 canvas-based implementation that renders waveform bars using pseudo-random
8018 peaks seeded by audio duration (stable visual fingerprint per track).
8019
8020 A `_generatePeaks()` method using Web Audio API `AudioBuffer` could be dropped
8021 in later to replace the seeded peaks with true amplitude data.
8022
8023 ### Audio Player Component (`audio-player.js`)
8024
8025 A thin wrapper around WaveSurfer that wires up:
8026
8027 | UI Element | ID | Behaviour |
8028 |------------|----|-----------|
8029 | Waveform canvas | `#waveform` | Click to seek; Shift+drag for A/B loop |
8030 | Play/pause button | `#play-btn` | Toggle playback |
8031 | Current time | `#time-cur` | Updated on every `timeupdate` event |
8032 | Duration | `#time-dur` | Populated on `ready` |
8033 | Speed selector | `#speed-sel` | Options: 0.5x 0.75x 1x 1.25x 1.5x 2x |
8034 | Volume slider | `#vol-slider` | Range 0.0–1.0 |
8035 | Loop info | `#loop-info` | Shows `Loop: MM:SS – MM:SS` when active |
8036 | Clear loop button | `#loop-clear-btn` | Hidden until a loop region is set |
8037
8038 **Keyboard shortcuts:**
8039
8040 | Key | Action |
8041 |-----|--------|
8042 | `Space` | Play / pause |
8043 | `←` / `→` | Seek ± 5 seconds |
8044 | `L` | Clear A/B loop region |
8045
8046 ### A/B Loop
8047
8048 Shift+drag on the waveform canvas sets the loop region. While the region is
8049 active the `_audio.currentTime` is reset to `loopStart` whenever it reaches
8050 `loopEnd`. A coloured overlay with boundary markers is drawn on the canvas.
8051 `clearRegion()` removes the loop and hides the loop-info badge.
8052
8053 ### Track List (full-mix view)
8054
8055 On `GET /{owner}/{repo_slug}/listen/{ref}`, the page fetches:
8056
8057 ```
8058 GET /api/v1/musehub/repos/{repo_id}/objects?ref={ref}
8059 ```
8060
8061 and filters for audio extensions (`mp3 wav ogg m4a flac aiff aif`). All
8062 matching objects are rendered as a clickable track list. Clicking a row loads
8063 that track into the WaveSurfer instance and begins playback.
8064
8065 ### Single-Track View
8066
8067 On `GET /{owner}/{repo_slug}/listen/{ref}/{path}`, the page fetches the same
8068 objects endpoint and matches the first object whose `path` matches (or ends
8069 with) the given `path` segment. If no match is found, an inline error message
8070 is shown — no hard 404, allowing the user to try other refs.
8071
8072 ### No CDN Policy
8073
8074 All external script tags are forbidden. The vendor script is served from the
8075 same origin as all other MuseHub assets. This keeps the page functional in
8076 air-gapped DAW network environments and avoids CORS complexity.
8077
8078 ### Implementation
8079
8080 | Layer | File |
8081 |-------|------|
8082 | WaveSurfer | `maestro/templates/musehub/static/vendor/wavesurfer.min.js` |
8083 | AudioPlayer | `maestro/templates/musehub/static/audio-player.js` |
8084 | HTML template | `maestro/templates/musehub/pages/listen.html` |
8085 | HTML routes | `maestro/api/routes/musehub/ui.py` — `listen_page()`, `listen_track_page()` |
8086 | Tests | `tests/test_musehub_ui.py` — `test_listen_*` (12 tests) |
8087
8088 ---
8089
8090 ## Muse Hub — Arrangement Matrix Page (issue #212)
8091
8092 **Purpose:** Provide a bird's-eye orchestration view — which instruments play in which sections — so producers can evaluate arrangement density without downloading or listening to tracks. This is the most useful single page for an AI orchestration agent before generating a new instrument part.
8093
8094 ### Routes
8095
8096 | Route | Auth | Description |
8097 |-------|------|-------------|
8098 | `GET /musehub/ui/{owner}/{repo_slug}/arrange/{ref}` | None (HTML shell) | Interactive instrument × section density grid |
8099 | `GET /api/v1/musehub/repos/{repo_id}/arrange/{ref}` | Optional JWT | `ArrangementMatrixResponse` JSON |
8100
8101 ### Grid Layout
8102
8103 - **Y-axis (rows):** instruments — bass, keys, guitar, drums, lead, pads
8104 - **X-axis (columns):** sections — intro, verse_1, chorus, bridge, outro
8105 - **Cell:** colour-coded by note density; silent cells rendered in dark background
8106 - **Cell click:** navigates to the piano roll (motif browser filtered by instrument + section)
8107 - **Hover tooltip:** note count, beat range, MIDI pitch range
8108 - **Row summaries:** per-instrument total notes, active section count, mean density bar
8109 - **Column summaries:** per-section total notes, active instrument count
8110
8111 ### Data Model
8112
8113 ```python
8114 ArrangementMatrixResponse(
8115 repo_id = "...",
8116 ref = "HEAD",
8117 instruments = ["bass", "keys", "guitar", "drums", "lead", "pads"],
8118 sections = ["intro", "verse_1", "chorus", "bridge", "outro"],
8119 cells = [ArrangementCellData(...)], # 6 × 5 = 30 cells, row-major
8120 row_summaries = [ArrangementRowSummary(...)],
8121 column_summaries = [ArrangementColumnSummary(...)],
8122 total_beats = 128.0,
8123 )
8124 ```
8125
8126 ### Implementation
8127
8128 | Layer | File |
8129 |-------|------|
8130 | Models | `maestro/models/musehub.py` — `ArrangementCellData`, `ArrangementRowSummary`, `ArrangementColumnSummary`, `ArrangementMatrixResponse` |
8131 | Service | `maestro/services/musehub_analysis.py` — `compute_arrangement_matrix()` |
8132 | JSON API | `maestro/api/routes/musehub/repos.py` — `get_arrangement_matrix()` |
8133 | HTML route | `maestro/api/routes/musehub/ui.py` — `arrange_page()` |
8134 | Template | `maestro/templates/musehub/pages/arrange.html` |
8135 | Nav tab | `maestro/templates/musehub/partials/repo_tabs.html` — `current_page = "arrange"` |
8136 | Tests | `tests/test_musehub_ui.py` — `test_arrange_*` (8 tests), `tests/test_musehub_repos.py` — `test_arrange_*` (8 tests) |
8137
8138 ---
8139
8140 ## MuseHub Compare View (issue #217)
8141
8142 ### Motivation
8143
8144 There was no way to compare two branches or commits on MuseHub. GitHub's compare view (`/compare/{base}...{head}`) shows a code diff. MuseHub's compare view shows a multi-dimensional musical diff: a radar chart of divergence scores, a side-by-side piano roll, an emotion diff summary, and the list of commits unique to head.
8145
8146 ### Route
8147
8148 ```
8149 GET /musehub/ui/{owner}/{repo_slug}/compare/{base}...{head}
8150 ```
8151
8152 The `{base}...{head}` segment is a single path parameter (`{refs}`) parsed server-side by splitting on `...`. Both `base` and `head` can be branch names, tags, or commit SHAs. Returns 404 when the `...` separator is absent or either segment is empty.
8153
8154 Content negotiation: `?format=json` or `Accept: application/json` returns the full `CompareResponse` JSON.
8155
8156 ### JSON API Endpoint
8157
8158 ```
8159 GET /api/v1/musehub/repos/{repo_id}/compare?base=X&head=Y
8160 ```
8161
8162 Returns `CompareResponse` with:
8163 - `dimensions` — five per-dimension Jaccard divergence scores (melodic, harmonic, rhythmic, structural, dynamic)
8164 - `overallScore` — mean of the five scores in [0.0, 1.0]
8165 - `commonAncestor` — most recent common ancestor commit ID, or `null` for disjoint histories
8166 - `commits` — commits reachable from head but not from base (newest first)
8167 - `emotionDiff` — energy/valence/tension/darkness deltas (`head − base`)
8168 - `createPrUrl` — URL to open a pull request from this comparison
8169
8170 Raises 422 if either ref has no commits. Raises 404 if the repo is not found.
8171
8172 ### Compare Page Features
8173
8174 | Feature | Description |
8175 |---------|-------------|
8176 | Five-axis radar chart | Per-dimension divergence scores visualised as a pentagon, colour-coded by level (NONE/LOW/MED/HIGH) |
8177 | Dimension detail panels | Clickable cards that expand to show description and per-branch commit counts |
8178 | Piano roll comparison | Deterministic note grid derived from SHA bytes; green = added in head, red = removed |
8179 | Audio A/B toggle | Button pair to queue base or head audio in the player |
8180 | Emotion diff | Side-by-side bar charts for energy, valence, tension, darkness with delta labels |
8181 | Commit list | Commits unique to head with short SHA links, author, and timestamp |
8182 | Create PR button | Links to `/musehub/ui/{owner}/{slug}/pulls/new?base=X&head=Y` |
8183
8184 ### Emotion Vector Algorithm
8185
8186 Each commit's emotion vector is derived deterministically from four non-overlapping 4-hex-char windows of the commit SHA:
8187 - `valence` = `int(sha[0:4], 16) / 0xFFFF`
8188 - `energy` = `int(sha[4:8], 16) / 0xFFFF`
8189 - `tension` = `int(sha[8:12], 16) / 0xFFFF`
8190 - `darkness` = `int(sha[12:16], 16) / 0xFFFF`
8191
8192 The emotion diff is `mean(head commits) − mean(base commits)` per axis, clamped to [−1.0, 1.0].
8193
8194 ### Divergence Reuse
8195
8196 The compare endpoint reuses the existing `musehub_divergence.compute_hub_divergence()` engine. Refs are resolved as branch names; the divergence engine computes Jaccard scores across commit message keyword classification.
8197
8198
8199 ## MuseHub Render Pipeline — Auto-Generated Artifacts on Push
8200
8201 ### Overview
8202
8203 Every successful push to MuseHub triggers a background render pipeline that
8204 automatically generates two artifact types for each MIDI file in the commit:
8205
8206 1. **Piano-roll PNG** — a server-side pixel-art rendering of the MIDI timeline,
8207 colour-coded by MIDI channel (bass = green, keys = blue, lead = orange, …).
8208 2. **MP3 stub** — a copy of the MIDI file until the Storpheus `POST /render`
8209 endpoint ships; labelled `stubbed=True` in the render job record.
8210
8211 Both artifact types are stored as `musehub_objects` rows linked to the repo,
8212 and their object IDs are recorded in the `musehub_render_jobs` row for the
8213 commit.
8214
8215 ### Render Job Lifecycle
8216
8217 ```
8218 push received → ingest_push() → DB commit → BackgroundTasks
8219
8220 trigger_render_background()
8221
8222 status: pending → rendering
8223
8224 discover MIDI objects in push
8225
8226 for each MIDI:
8227 - render_piano_roll() → PNG
8228 - _make_stub_mp3() → MIDI copy
8229 - store as musehub_objects rows
8230
8231 status: complete | failed
8232 ```
8233
8234 **Idempotency:** if a render job already exists for `(repo_id, commit_id)`,
8235 the pipeline exits immediately without creating a duplicate.
8236
8237 **Failure isolation:** render errors are caught, logged, and stored in
8238 `job.error_message`; they never propagate to the push response.
8239
8240 ### Render Status API
8241
8242 ```
8243 GET /api/v1/musehub/repos/{repo_id}/commits/{sha}/render-status
8244 ```
8245
8246 Returns a `RenderStatusResponse`:
8247
8248 ```json
8249 {
8250 "commitId": "abc123...",
8251 "status": "complete",
8252 "midiCount": 2,
8253 "mp3ObjectIds": ["sha256:...", "sha256:..."],
8254 "imageObjectIds": ["sha256:...", "sha256:..."],
8255 "errorMessage": null
8256 }
8257 ```
8258
8259 Status values: `pending` | `rendering` | `complete` | `failed` | `not_found`.
8260
8261 `not_found` is returned (not 404) when no render job exists for the given
8262 commit — this lets callers distinguish "never pushed" from "not yet rendered"
8263 without branching on HTTP status codes.
8264
8265 ### Piano Roll Renderer
8266
8267 **Module:** `maestro/services/musehub_piano_roll_renderer.py`
8268
8269 Pure-Python MIDI-to-PNG renderer — zero external image library dependency.
8270 Uses `mido` (already a project dependency) to parse MIDI and stdlib `zlib`
8271 + `struct` to encode a minimal PNG.
8272
8273 Image layout:
8274 - Width: up to 1920 px (proportional to MIDI duration).
8275 - Height: 256 px (128 MIDI pitches × 2 px per row).
8276 - Background: dark charcoal.
8277 - Octave boundaries: lighter horizontal rules at every C note.
8278 - Notes: coloured rectangles, colour-coded by MIDI channel.
8279
8280 Graceful degradation: invalid or empty MIDI produces a blank canvas PNG
8281 with `stubbed=True` — callers always receive a valid file.
8282
8283 **Note:** WebP output requires Pillow (not yet a project dependency). The
8284 renderer currently emits PNG with a `.png` extension. WebP conversion is
8285 a planned follow-up.
8286
8287 ### MP3 Rendering (Stub)
8288
8289 Storpheus `POST /render` (MIDI-in → audio-out) is not yet deployed. Until
8290 it ships, the MIDI file is copied verbatim to `renders/<commit_short>_<stem>.mp3`.
8291 The render job records this as `mp3_object_ids` entries regardless. When the
8292 endpoint is available, replace `_make_stub_mp3` in
8293 `maestro/services/musehub_render_pipeline.py` with a real HTTP call.
8294
8295 ### Implementation
8296
8297 | Layer | File |
8298 |-------|------|
8299 | Models | `maestro/models/musehub.py` — `EmotionDiffResponse`, `CompareResponse` |
8300 | API endpoint | `maestro/api/routes/musehub/repos.py` — `compare_refs()`, `_compute_emotion_diff()`, `_derive_emotion_vector()` |
8301 | UI route | `maestro/api/routes/musehub/ui.py` — `compare_page()` |
8302 | Template | `maestro/templates/musehub/pages/compare.html` |
8303 | Tests | `tests/test_musehub_ui.py` — `test_compare_*` (10 tests) |
8304 | Tests | `tests/test_musehub_repos.py` — `test_compare_*` (4 tests) |
8305 | Type contracts | `docs/reference/type_contracts.md` — `EmotionDiffResponse`, `CompareResponse` |
8306
8307 ---
8308
8309
8310 | DB model | `maestro/db/musehub_models.py` — `MusehubRenderJob` |
8311 | Piano roll | `maestro/services/musehub_piano_roll_renderer.py` |
8312 | Pipeline | `maestro/services/musehub_render_pipeline.py` |
8313 | Trigger | `maestro/api/routes/musehub/sync.py` — `push()` adds background task |
8314 | Status API | `maestro/api/routes/musehub/repos.py` — `get_commit_render_status()` |
8315 | Response model | `maestro/models/musehub.py` — `RenderStatusResponse` |
8316 | Migration | `alembic/versions/0001_consolidated_schema.py` — `musehub_render_jobs` |
8317 | Tests | `tests/test_musehub_render.py` — 17 tests |
8318
8319 ---
8320
8321 ---
8322
8323 ## Piano Roll Renderer
8324
8325 ### Overview
8326
8327 The MuseHub piano roll provides an interactive Canvas-based MIDI visualisation
8328 accessible from any MIDI artifact stored in a Muse Hub repo. It is split into
8329 a server-side parser and a client-side renderer.
8330
8331 ### Architecture
8332
8333 ```
8334 Browser Maestro API
8335 │ │
8336 │ GET /musehub/ui/{owner}/{slug}/ │
8337 │ piano-roll/{ref} │
8338 │ ──────────────────────────────────► │ piano_roll_page()
8339 │ ◄────────────────────────────────── piano_roll.html shell
8340 │ │
8341 │ [JS] apiFetch /objects?limit=500 │
8342 │ ──────────────────────────────────► │ list_objects()
8343 │ ◄────────────────────────────────── ObjectMetaListResponse
8344 │ │
8345 │ [JS] apiFetch /objects/{id}/ │
8346 │ parse-midi │
8347 │ ──────────────────────────────────► │ parse_midi_object()
8348 │ │ → parse_midi_bytes()
8349 │ ◄────────────────────────────────── MidiParseResult (JSON)
8350 │ │
8351 │ [JS] PianoRoll.render(midi, el) │
8352 │ Canvas draw loop │
8353 ```
8354
8355 ### Server-Side Parser (`musehub_midi_parser.py`)
8356
8357 `parse_midi_bytes(data: bytes) → MidiParseResult`
8358
8359 - Uses the `mido` library to read Standard MIDI Files (types 0, 1, 2).
8360 - Converts all tick offsets to quarter-note beats using `ticks_per_beat`.
8361 - Handles note-on / note-off pairing (including velocity-0 note-off shorthand).
8362 - Closes dangling note-ons at end-of-track with minimum duration.
8363 - Extracts `set_tempo`, `time_signature`, and `track_name` meta messages.
8364 - Returns `MidiParseResult` — a `TypedDict` registered in `type_contracts.md`.
8365
8366 ### Client-Side Renderer (`piano-roll.js`)
8367
8368 `PianoRoll.render(midiParseResult, containerElement, options)`
8369
8370 Renders a `<canvas>` element with:
8371
8372 | Feature | Implementation |
8373 |---------|---------------|
8374 | Pitch axis | Piano keyboard strip (left margin, white/black key shading) |
8375 | Time axis | Beat grid with measure markers, auto-density by zoom level |
8376 | Note rectangles | Per-track colour from design palette; opacity = velocity / 127 |
8377 | Zoom | Horizontal (`px/beat`) and vertical (`px/pitch row`) range sliders |
8378 | Pan | Click-drag on canvas; `panX` in beats, `panY` in pitch rows |
8379 | Tooltip | Hover shows pitch name, MIDI pitch, velocity, beat, duration |
8380 | Track filter | `<select>` — all tracks or single track |
8381 | Device pixel ratio | Renders at native DPR for crisp display on HiDPI screens |
8382
8383 ### Routes
8384
8385 | Method | Path | Handler | Description |
8386 |--------|------|---------|-------------|
8387 | `GET` | `/musehub/ui/{owner}/{slug}/piano-roll/{ref}` | `piano_roll_page` | All MIDI tracks at ref |
8388 | `GET` | `/musehub/ui/{owner}/{slug}/piano-roll/{ref}/{path}` | `piano_roll_track_page` | Single MIDI file |
8389 | `GET` | `/api/v1/musehub/repos/{repo_id}/objects/{id}/parse-midi` | `parse_midi_object` | MIDI-to-JSON endpoint |
8390
8391 ### Static Asset
8392
8393 `/musehub/static/piano-roll.js` — served by the existing `StaticFiles` mount
8394 at `maestro/main.py`. No rebuild required.
8395
8396 ### Navigation Context
8397
8398 Add `current_page: "piano-roll"` to the template context when linking from
8399 other pages (tree browser, commit detail, blob viewer).
8400
8401
8402 ## Muse Hub — Blob Viewer (issue #205)
8403
8404 **Purpose:** Music-aware file blob viewer that renders individual files from a
8405 Muse repo with file-type-specific treatment. Musicians can view MIDI as a
8406 piano roll preview, stream audio files directly in the browser, and inspect
8407 JSON/XML metadata with syntax highlighting — without downloading files first.
8408
8409 ### URL Pattern
8410
8411 | Route | Description |
8412 |-------|-------------|
8413 | `GET /musehub/ui/{owner}/{repo_slug}/blob/{ref}/{path}` | HTML blob viewer page |
8414 | `GET /api/v1/musehub/repos/{repo_id}/blob/{ref}/{path}` | JSON blob metadata + text content |
8415
8416 **Auth:** No JWT required for public repos (HTML shell). Private repos require
8417 a Bearer token passed via `Authorization` header (API) or `localStorage` JWT (UI).
8418
8419 ### File-Type Dispatch
8420
8421 The viewer selects a rendering mode based on the file extension:
8422
8423 | Extension | `file_type` | Rendering |
8424 |-----------|-------------|-----------|
8425 | `.mid`, `.midi` | `midi` | Piano roll placeholder + "View in Piano Roll" quick link |
8426 | `.mp3`, `.wav`, `.flac`, `.ogg` | `audio` | `<audio>` player + "Listen" quick link |
8427 | `.json` | `json` | Syntax-highlighted JSON (keys blue, strings teal, numbers gold, bools red, nulls grey) |
8428 | `.webp`, `.png`, `.jpg`, `.jpeg` | `image` | Inline `<img>` on checkered background |
8429 | `.xml` | `xml` | Syntax-highlighted XML (MusicXML support) |
8430 | all others | `other` | Hex dump preview (first 512 bytes via Range request) + raw download |
8431
8432 ### File Metadata
8433
8434 Every blob response includes:
8435
8436 - `filename` — basename of the file (e.g. `bass.mid`)
8437 - `size_bytes` — file size in bytes
8438 - `sha` — content-addressed ID (e.g. `sha256:abc123...`)
8439 - `created_at` — timestamp of the most-recently-pushed version
8440 - `raw_url` — direct link to `/{owner}/{repo_slug}/raw/{ref}/{path}`
8441 - `file_type` — rendering hint (see table above)
8442 - `content_text` — UTF-8 content for JSON/XML files ≤ 256 KB; `null` for binary/oversized
8443
8444 ### Quick Links
8445
8446 The blob viewer exposes contextual action links:
8447
8448 - **Raw** — always present; links to raw download endpoint (`Content-Disposition: attachment`)
8449 - **View in Piano Roll** — MIDI files only; links to `/{owner}/{repo_slug}/piano-roll/{ref}/{path}`
8450 - **Listen** — audio files only; links to `/{owner}/{repo_slug}/listen/{ref}/{path}`
8451
8452 ### Object Resolution
8453
8454 Object resolution uses `musehub_repository.get_object_by_path()`, which returns
8455 the most-recently-pushed object matching the path in the repo. The `ref`
8456 parameter is validated for URL construction but does not currently filter by
8457 branch HEAD (MVP scope — consistent with raw and tree endpoints).
8458
8459 ### Response Shape (`BlobMetaResponse`)
8460
8461 ```json
8462 {
8463 "objectId": "sha256:abc123...",
8464 "path": "tracks/bass.mid",
8465 "filename": "bass.mid",
8466 "sizeBytes": 2048,
8467 "sha": "sha256:abc123...",
8468 "createdAt": "2025-01-15T12:00:00Z",
8469 "rawUrl": "/musehub/repos/{repo_id}/raw/main/tracks/bass.mid",
8470 "fileType": "midi",
8471 "contentText": null
8472 }
8473 ```
8474
8475 ### Implementation Map
8476
8477 | Component | File |
8478 |-----------|------|
8479 | Template | `maestro/templates/musehub/pages/blob.html` |
8480 | UI handler | `maestro/api/routes/musehub/ui.py` → `blob_page()` |
8481 | API endpoint | `maestro/api/routes/musehub/objects.py` → `get_blob_meta()` |
8482 | Pydantic model | `maestro/models/musehub.py` → `BlobMetaResponse` |
8483 | Tests | `tests/test_musehub_ui.py` — `test_blob_*` (7 tests) |
8484
8485 ---
8486
8487
8488 ## Score / Notation Renderer (issue #210)
8489
8490 ### Motivation
8491
8492 Musicians who read traditional notation cannot visualize Muse compositions as
8493 sheet music without exporting to MusicXML and opening a separate application.
8494 The score renderer bridges this gap by rendering standard music notation
8495 directly in the browser from quantized MIDI data.
8496
8497 ### Routes
8498
8499 | Route | Handler | Description |
8500 |-------|---------|-------------|
8501 | `GET /musehub/ui/{owner}/{repo_slug}/score/{ref}` | `score_page()` | Full score — all instrument parts |
8502 | `GET /musehub/ui/{owner}/{repo_slug}/score/{ref}/{path}` | `score_part_page()` | Single-part view filtered by instrument name |
8503
8504 No JWT is required to render the HTML shell. Auth is handled client-side via
8505 localStorage JWT, matching all other UI pages.
8506
8507 ### Notation Data Pipeline
8508
8509 ```
8510 convert_ref_to_notation(ref) # musehub_notation.py
8511 └─ _seed_from_ref(ref) # SHA-256 → deterministic int seed
8512 └─ _notes_for_track(seed, ...) # LCG pseudo-random note generation
8513 └─ NotationResult(tracks, tempo, key, time_sig)
8514
8515 └─ score.html (Jinja2 template)
8516 └─ renderScore() JS
8517 └─ drawStaffLines() + drawNote() → SVG
8518 ```
8519
8520 ### Server-Side Quantization (`musehub_notation.py`)
8521
8522 Key types:
8523
8524 | Type | Kind | Description |
8525 |------|------|-------------|
8526 | `NotationNote` | `TypedDict` | Single quantized note: `pitch_name`, `octave`, `duration`, `start_beat`, `velocity`, `track_id` |
8527 | `NotationTrack` | `TypedDict` | One instrument part: `track_id`, `clef`, `key_signature`, `time_signature`, `instrument`, `notes` |
8528 | `NotationDict` | `TypedDict` | JSON-serialisable form: `tracks`, `tempo`, `key`, `timeSig` |
8529 | `NotationResult` | `NamedTuple` | Internal result: `tracks`, `tempo`, `key`, `time_sig` |
8530
8531 Public API:
8532
8533 ```python
8534 from maestro.services.musehub_notation import convert_ref_to_notation, notation_result_to_dict
8535
8536 result = convert_ref_to_notation("main", num_tracks=3, num_bars=8)
8537 payload = notation_result_to_dict(result)
8538 # {"tracks": [...], "tempo": 120, "key": "C major", "timeSig": "4/4"}
8539 ```
8540
8541 `convert_ref_to_notation` is **deterministic** — the same `ref` always returns
8542 the same notation. Distinct refs produce different keys, tempos, and time
8543 signatures.
8544
8545 ### Client-Side SVG Renderer (`score.html`)
8546
8547 The template's `{% block page_script %}` implements a lightweight SVG renderer
8548 that draws staff lines, note heads, stems, flags, ledger lines, and accidentals
8549 without any external library dependency.
8550
8551 Key renderer functions:
8552
8553 | Function | Description |
8554 |----------|-------------|
8555 | `drawStaffLines(beatsPerBar, numBars)` | Draws 5 staff lines + bar lines, returns SVG prefix |
8556 | `drawClef(clef)` | Renders treble/bass clef label |
8557 | `drawTimeSig(timeSig, x)` | Renders time signature numerals |
8558 | `drawNote(note, clef, beatsPerBar, barWidth)` | Renders note head, stem, flag, ledger lines, accidental |
8559 | `renderTrackStaff(track)` | Assembles full staff SVG for one instrument part |
8560 | `renderScore()` | Wires track selector + meta panel + all staves |
8561 | `setTrack(id)` | Switches part selector and re-renders |
8562
8563 Note head style: filled ellipse (quarter/eighth), open ellipse (half/whole).
8564 Stem direction: up when note is at or below the middle staff line, down above.
8565
8566 ### VexFlow Decision
8567
8568 VexFlow.js was evaluated but not vendored in this implementation. The full
8569 minified library (~2 MB) would have increased page payload significantly for a
8570 feature used by a minority of users. The lightweight SVG renderer covers the
8571 required use cases (clefs, key/time signatures, notes, rests, beams, dynamics).
8572 If VexFlow integration is needed in future, the notation JSON endpoint
8573 (`/api/v1/musehub/repos/{id}/notation/{ref}`) is already designed to supply the
8574 quantized data that VexFlow's `EasyScore` API expects.
8575
8576 ### JSON Content Negotiation
8577
8578 The score page requests:
8579 ```
8580 GET /api/v1/musehub/repos/{repo_id}/notation/{ref}
8581 ```
8582 Response: `{ "tracks": [...], "tempo": int, "key": str, "timeSig": str }`
8583
8584 If the notation endpoint is unavailable, the renderer falls back to a minimal
8585 client-side stub (C-major chord, 4 beats) so the page always displays
8586 something meaningful.
8587
8588 ### Implementation
8589
8590 | Layer | File |
8591 |-------|------|
8592 | Service | `maestro/services/musehub_notation.py` — `convert_ref_to_notation()`, `notation_result_to_dict()` |
8593 | HTML routes | `maestro/api/routes/musehub/ui.py` — `score_page()`, `score_part_page()` |
8594 | Template | `maestro/templates/musehub/pages/score.html` |
8595 | Tests (service) | `tests/test_musehub_notation.py` — 19 tests |
8596 | Tests (UI) | `tests/test_musehub_ui.py` — `test_score_*` (9 tests) |
8597
8598 ---
8599
8600 ## Issue Tracker Enhancements (Phase 9 — Issue #218)
8601
8602 ### Overview
8603
8604 The Muse Hub issue tracker supports full collaborative workflows beyond the initial title/body/close MVP. This section documents the enhanced issue detail capabilities added in phase 9.
8605
8606 ### Features
8607
8608 **Threaded Discussion**
8609
8610 Issues support threaded comments via `POST /issues/{number}/comments`. Top-level comments have `parentId: null`; replies set `parentId` to the target comment UUID. The UI builds the thread tree client-side from the flat chronological list.
8611
8612 **Musical Context Linking**
8613
8614 Comment bodies are scanned at write time for musical context references using the pattern `type:value`:
8615
8616 | Syntax | Type | Example |
8617 |--------|------|---------|
8618 | `track:bass` | track | References the bass track |
8619 | `section:chorus` | section | References the chorus section |
8620 | `beats:16-24` | beats | References a beat range |
8621
8622 Parsed refs are stored in `musehub_issue_comments.musical_refs` (JSON array) and returned in `IssueCommentResponse.musicalRefs`. The issue detail UI renders them as coloured badges.
8623
8624 **Assignees**
8625
8626 A single collaborator can be assigned per issue via `POST /issues/{number}/assign`. The `assignee` field is a free-form display name. Pass `"assignee": null` to unassign.
8627
8628 **Milestones**
8629
8630 Milestones group issues into named goals (e.g. "Album v1.0"). Create milestones at `POST /milestones`, then link issues via `POST /issues/{number}/milestone?milestone_id=<uuid>`. Each milestone response includes live `openIssues` and `closedIssues` counts computed from the current state of linked issues.
8631
8632 **State Transitions**
8633
8634 Issues may be reopened after closing: `POST /issues/{number}/reopen`. Both close and reopen fire webhook events with `action: "opened"/"closed"`.
8635
8636 **Edit Capability**
8637
8638 `PATCH /issues/{number}` allows partial updates to title, body, and labels.
8639
8640 ### Database Schema
8641
8642 | Table | Purpose |
8643 |-------|---------|
8644 | `musehub_milestones` | Per-repo milestone definitions |
8645 | `musehub_issues` | Extended with `assignee`, `milestone_id`, `updated_at` |
8646 | `musehub_issue_comments` | Threaded comments with `parent_id` and `musical_refs` |
8647
8648 ### Implementation
8649
8650 | Layer | File |
8651 |-------|------|
8652 | DB models | `maestro/db/musehub_models.py` — `MusehubMilestone`, `MusehubIssueComment`, extended `MusehubIssue` |
8653 | Pydantic models | `maestro/models/musehub.py` — `MilestoneCreate/Response`, `IssueCommentCreate/Response`, `IssueUpdate`, `IssueAssignRequest`, `MusicalRef` |
8654 | Service | `maestro/services/musehub_issues.py` — comment CRUD, milestone CRUD, assign, reopen, update |
8655 | Routes | `maestro/api/routes/musehub/issues.py` — 12 endpoints |
8656 | UI template | `maestro/templates/musehub/pages/issue_detail.html` — threaded UI |
8657 | Migration | `alembic/versions/0001_consolidated_schema.py` — new tables and columns |
8658 | Tests | `tests/test_musehub_issues.py` — 12 new tests covering all acceptance criteria |
8659
8660 ---
8661
8662
8663 ## muse rerere
8664
8665 ### Purpose
8666
8667 In parallel multi-branch Muse workflows, identical merge conflicts appear repeatedly — the same MIDI region modified in the same structural way on two independent branches. `muse rerere` (reuse recorded resolutions) records conflict shapes and their resolutions so they can be applied automatically on subsequent merges.
8668
8669 The fingerprint is **transposition-invariant**: two conflicts with the same structural shape but different absolute pitches are treated as the same conflict, allowing a resolution recorded in one key to be applied in another.
8670
8671 ### Cache Layout
8672
8673 ```
8674 .muse/rr-cache/<sha256-hash>/
8675 conflict — serialised conflict fingerprint (JSON)
8676 postimage — serialised resolution (JSON, written only after resolve)
8677 ```
8678
8679 Entries marked `[R]` (in `muse rerere list`) have a `postimage`; entries marked `[C]` are conflict-only (awaiting resolution).
8680
8681 ### Commands
8682
8683 | Command | Description |
8684 |---------|-------------|
8685 | `muse rerere` | Auto-apply any cached resolution for current merge conflicts |
8686 | `muse rerere list` | Show all conflict fingerprints in the rr-cache (`[R]` = resolved, `[C]` = conflict-only) |
8687 | `muse rerere forget <hash>` | Remove a single cached entry from the rr-cache |
8688 | `muse rerere clear` | Purge the entire rr-cache |
8689
8690 ### Example Output
8691
8692 ```
8693 $ muse merge feature/harmony-rework
8694 CONFLICT (note): Both sides modified note at pitch=64 beat=3.0 in region r1
8695 ✅ muse rerere: resolved 1 conflict(s) using rerere (hash a3f7c2e1…)
8696
8697 $ muse rerere list
8698 rr-cache (2 entries):
8699 [R] a3f7c2e1d9b4...
8700 [C] 88f02c3a71e4...
8701
8702 $ muse rerere forget a3f7c2e1d9b4...
8703 ✅ Forgot rerere entry a3f7c2e1d9b4…
8704
8705 $ muse rerere clear
8706 ✅ Cleared 1 rr-cache entry.
8707 ```
8708
8709 ### Hook Integration
8710
8711 `build_merge_checkout_plan()` in `maestro/services/muse_merge.py` automatically calls `record_conflict()` and `apply_rerere()` whenever conflicts are detected and a `repo_root` is provided. On a cache hit it logs:
8712
8713 ```
8714 ✅ muse rerere: resolved N conflict(s) using rerere.
8715 ```
8716
8717 ### Result Types
8718
8719 | Function | Return Type | Description |
8720 |----------|-------------|-------------|
8721 | `record_conflict(repo_root, conflicts)` | `str` | SHA-256 hex fingerprint identifying the conflict shape |
8722 | `record_resolution(repo_root, hash, resolution)` | `None` | Writes `postimage` to cache; raises `FileNotFoundError` if hash not found |
8723 | `apply_rerere(repo_root, conflicts)` | `tuple[int, JSONObject \| None]` | `(n_resolved, resolution)` — `n_resolved` is 0 on miss or `len(conflicts)` on hit |
8724 | `list_rerere(repo_root)` | `list[str]` | Sorted list of all fingerprint hashes in the cache |
8725 | `forget_rerere(repo_root, hash)` | `bool` | `True` if removed, `False` if not found (idempotent) |
8726 | `clear_rerere(repo_root)` | `int` | Count of entries removed |
8727
8728 The conflict dict type (`ConflictDict`) has fields: `region_id: str`, `type: str`, `description: str`. The resolution type is `JSONObject` (`dict[str, JSONValue]`) — intentionally opaque to support arbitrary resolution strategies.
8729
8730 ### Agent Use Case
8731
8732 `muse rerere` is designed for AI agents running parallel Muse branch workflows. When an agent resolves a merge conflict (e.g. choosing the harmonic arrangement from `feature/harmony` over `feature/rhythm` for a contested region), it records the resolution via `record_resolution()`. Subsequent agents encountering structurally identical conflicts — even in a different key — receive the cached resolution automatically through `apply_rerere()`, eliminating repeated human intervention.
8733
8734 **Typical agent workflow:**
8735 1. `build_merge_checkout_plan()` encounters conflicts → `record_conflict()` is called automatically.
8736 2. Agent examines conflicts and calls `record_resolution()` with the chosen strategy.
8737 3. On the next parallel merge with the same conflict shape: `apply_rerere()` returns the resolution, the agent logs a single `✅ resolved N conflict(s) using rerere` line, and proceeds.
8738 4. Use `muse rerere list` to audit cached resolutions; `muse rerere forget <hash>` or `muse rerere clear` when resolutions are stale.
8739
8740 ### Implementation
8741
8742 | Layer | File |
8743 |-------|------|
8744 | Service | `maestro/services/muse_rerere.py` — fingerprinting, cache storage, all CRUD functions |
8745 | CLI commands | `maestro/muse_cli/commands/rerere.py` — `rerere`, `rerere list`, `rerere forget`, `rerere clear` |
8746 | CLI app | `maestro/muse_cli/app.py` — registered as `muse rerere` sub-app |
8747 | Merge hook | `maestro/services/muse_merge.py` — auto-apply after conflict detection |
8748 | Tests | `tests/test_muse_rerere.py` — full unit test coverage |
8749
8750 ---
8751