#!/usr/bin/env bash # Builds midi-episode11/ — a disposable MIDI-domain repo for Episode 11 # ("Muse Understands Music"). Re-run to regenerate; never hand-edit the # repo directory itself. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$HERE/midi-episode11" rm -rf "$REPO" mkdir -p "$REPO" cd "$REPO" export MUSE_ENABLE_MIDI=1 python3 - <<'PY' import mido from pathlib import Path TPB = 480 def build(path, bpm, notes): mid = mido.MidiFile(type=0, ticks_per_beat=TPB) track = mido.MidiTrack() mid.tracks.append(track) events = [(0, mido.MetaMessage("set_tempo", tempo=mido.bpm2tempo(bpm), time=0))] for start, pitch, velocity, duration in notes: events.append((start, mido.Message("note_on", channel=0, note=pitch, velocity=velocity, time=0))) events.append((start + duration, mido.Message("note_off", channel=0, note=pitch, velocity=0, time=0))) order = {"set_tempo": 0, "note_off": 1, "note_on": 2} events.sort(key=lambda item: (item[0], order.get(item[1].type, 9))) previous = 0 for tick, msg in events: track.append(msg.copy(time=tick - previous)) previous = tick track.append(mido.MetaMessage("end_of_track", time=0)) mid.save(path) # C - E - G - C', quarter notes at 120bpm BASE = [ (0, 60, 80, 480), (480, 64, 80, 480), (960, 67, 80, 480), (1440, 72, 80, 480), ] build(Path("phrase.mid"), 120, BASE) PY muse init --domain midi --json | python3 -m json.tool # NOTE: non-code domains have no staging concept — files are tracked the # moment they're on disk (muse status already shows "added": ["phrase.mid"] # before any `add` step). `muse code add` is code-domain only and errors here. muse commit -m "Add four-note phrase (C-E-G-C')" \ --agent-id claude-code --model-id claude-sonnet-5 --sign echo "--- domain-info (midi vs code contrast) ---" muse domain-info --json | python3 -m json.tool # Branch 1: change the third's pitch (E -> F) muse checkout -b feat/raise-third --intent "raise the third from E to F" --resumable python3 - <<'PY' import mido mid = mido.MidiFile("phrase.mid") for track in mid.tracks: for msg in track: if msg.type in ("note_on", "note_off") and msg.note == 64: msg.note = 65 mid.save("phrase.mid") PY muse commit -m "Raise the third from E to F" \ --agent-id claude-code --model-id claude-sonnet-5 --sign # Back to main, branch 2: raise velocity on every note muse switch main muse checkout -b feat/louder-melody --intent "raise velocity on the melody notes" --resumable python3 - <<'PY' import mido mid = mido.MidiFile("phrase.mid") for track in mid.tracks: for msg in track: if msg.type == "note_on": msg.velocity = 110 mid.save("phrase.mid") PY muse commit -m "Increase melody velocity to 110" \ --agent-id claude-code --model-id claude-sonnet-5 --sign # Fast-forward merge #1 (no conflict — only one side touched the file) muse switch main muse merge feat/raise-third # Merge #2 — genuine conflict: same note, different attribute changed on # each side (pitch on one branch, velocity on the other) muse merge feat/louder-melody --json | python3 -m json.tool || true echo "--- conflicts ---" muse conflicts --json | python3 -m json.tool # Manual resolution: incorporate BOTH sides' intent (F pitch + velocity 110) python3 - <<'PY' import mido mid = mido.MidiFile("phrase.mid") for track in mid.tracks: for msg in track: if msg.type == "note_on": msg.velocity = 110 mid.save("phrase.mid") PY muse resolve phrase.mid muse commit -m "Merge feat/louder-melody: F pitch + velocity 110 on all notes" \ --agent-id claude-code --model-id claude-sonnet-5 --sign muse branch -d feat/raise-third muse branch -d feat/louder-melody echo "--- final log ---" muse log --json | python3 -m json.tool echo "Demo repo built at $REPO"