livepilot 1.9.23 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +119 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +144 -13
  6. package/bin/livepilot.js +87 -0
  7. package/installer/codex.js +147 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/skills/livepilot-core/SKILL.md +21 -4
  11. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  12. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  13. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  14. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  15. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  16. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  17. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  18. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  19. package/livepilot/skills/livepilot-core/references/overview.md +13 -9
  20. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  21. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  22. package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
  23. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  24. package/livepilot/skills/livepilot-release/SKILL.md +19 -5
  25. package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
  26. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  27. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  28. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  29. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
  30. package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
  31. package/livepilot.mcpb +0 -0
  32. package/m4l_device/livepilot_bridge.js +1 -1
  33. package/manifest.json +2 -2
  34. package/mcp_server/__init__.py +1 -1
  35. package/mcp_server/atlas/__init__.py +357 -0
  36. package/mcp_server/atlas/device_atlas.json +44067 -0
  37. package/mcp_server/atlas/enrichments/__init__.py +111 -0
  38. package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
  39. package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
  40. package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
  41. package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
  42. package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
  43. package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
  44. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
  45. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
  46. package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
  47. package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
  48. package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
  49. package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
  50. package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
  51. package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
  52. package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
  53. package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
  54. package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
  55. package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
  56. package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
  57. package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
  58. package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
  59. package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
  60. package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
  61. package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
  62. package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
  63. package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
  64. package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
  65. package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
  66. package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
  67. package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
  68. package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
  69. package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
  70. package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
  71. package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
  72. package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
  73. package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
  74. package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
  75. package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
  76. package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
  77. package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
  78. package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
  79. package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
  80. package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
  81. package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
  82. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
  83. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
  84. package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
  85. package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
  86. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
  87. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
  88. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
  89. package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
  90. package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
  91. package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
  92. package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
  93. package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
  94. package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
  95. package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
  96. package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
  97. package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
  98. package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
  99. package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
  100. package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
  101. package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
  102. package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
  103. package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
  104. package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
  105. package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
  106. package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
  107. package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
  108. package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
  109. package/mcp_server/atlas/scanner.py +236 -0
  110. package/mcp_server/atlas/tools.py +224 -0
  111. package/mcp_server/composer/__init__.py +1 -0
  112. package/mcp_server/composer/engine.py +452 -0
  113. package/mcp_server/composer/layer_planner.py +427 -0
  114. package/mcp_server/composer/prompt_parser.py +329 -0
  115. package/mcp_server/composer/tools.py +201 -0
  116. package/mcp_server/connection.py +53 -8
  117. package/mcp_server/corpus/__init__.py +377 -0
  118. package/mcp_server/device_forge/__init__.py +1 -0
  119. package/mcp_server/device_forge/builder.py +377 -0
  120. package/mcp_server/device_forge/models.py +142 -0
  121. package/mcp_server/device_forge/templates.py +483 -0
  122. package/mcp_server/device_forge/tools.py +162 -0
  123. package/mcp_server/hook_hunter/analyzer.py +23 -0
  124. package/mcp_server/hook_hunter/models.py +1 -0
  125. package/mcp_server/hook_hunter/tools.py +4 -2
  126. package/mcp_server/m4l_bridge.py +1 -0
  127. package/mcp_server/memory/taste_graph.py +68 -1
  128. package/mcp_server/memory/tools.py +15 -4
  129. package/mcp_server/musical_intelligence/detectors.py +14 -1
  130. package/mcp_server/musical_intelligence/tools.py +11 -8
  131. package/mcp_server/persistence/__init__.py +1 -0
  132. package/mcp_server/persistence/base_store.py +82 -0
  133. package/mcp_server/persistence/project_store.py +106 -0
  134. package/mcp_server/persistence/taste_store.py +122 -0
  135. package/mcp_server/preview_studio/models.py +1 -0
  136. package/mcp_server/preview_studio/tools.py +56 -13
  137. package/mcp_server/runtime/capability.py +66 -0
  138. package/mcp_server/runtime/capability_probe.py +137 -0
  139. package/mcp_server/runtime/execution_router.py +143 -0
  140. package/mcp_server/runtime/live_version.py +102 -0
  141. package/mcp_server/runtime/remote_commands.py +87 -0
  142. package/mcp_server/runtime/tools.py +18 -4
  143. package/mcp_server/sample_engine/__init__.py +1 -0
  144. package/mcp_server/sample_engine/analyzer.py +216 -0
  145. package/mcp_server/sample_engine/critics.py +390 -0
  146. package/mcp_server/sample_engine/models.py +193 -0
  147. package/mcp_server/sample_engine/moves.py +127 -0
  148. package/mcp_server/sample_engine/planner.py +186 -0
  149. package/mcp_server/sample_engine/sources.py +540 -0
  150. package/mcp_server/sample_engine/techniques.py +908 -0
  151. package/mcp_server/sample_engine/tools.py +442 -0
  152. package/mcp_server/semantic_moves/__init__.py +3 -0
  153. package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
  154. package/mcp_server/semantic_moves/mix_moves.py +41 -41
  155. package/mcp_server/semantic_moves/performance_moves.py +13 -13
  156. package/mcp_server/semantic_moves/sample_compilers.py +372 -0
  157. package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
  158. package/mcp_server/semantic_moves/tools.py +18 -17
  159. package/mcp_server/semantic_moves/transition_moves.py +16 -16
  160. package/mcp_server/server.py +51 -0
  161. package/mcp_server/services/__init__.py +1 -0
  162. package/mcp_server/services/motif_service.py +67 -0
  163. package/mcp_server/session_continuity/tracker.py +29 -1
  164. package/mcp_server/song_brain/builder.py +28 -1
  165. package/mcp_server/song_brain/models.py +4 -0
  166. package/mcp_server/song_brain/tools.py +20 -2
  167. package/mcp_server/sound_design/critics.py +89 -1
  168. package/mcp_server/splice_client/__init__.py +1 -0
  169. package/mcp_server/splice_client/client.py +347 -0
  170. package/mcp_server/splice_client/models.py +96 -0
  171. package/mcp_server/splice_client/protos/__init__.py +1 -0
  172. package/mcp_server/splice_client/protos/app_pb2.py +319 -0
  173. package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
  174. package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
  175. package/mcp_server/tools/arrangement.py +69 -0
  176. package/mcp_server/tools/automation.py +15 -2
  177. package/mcp_server/tools/devices.py +117 -6
  178. package/mcp_server/tools/notes.py +37 -4
  179. package/mcp_server/wonder_mode/diagnosis.py +5 -0
  180. package/mcp_server/wonder_mode/engine.py +85 -1
  181. package/mcp_server/wonder_mode/tools.py +6 -1
  182. package/package.json +12 -2
  183. package/remote_script/LivePilot/__init__.py +8 -1
  184. package/remote_script/LivePilot/arrangement.py +114 -0
  185. package/remote_script/LivePilot/browser.py +56 -1
  186. package/remote_script/LivePilot/devices.py +236 -6
  187. package/remote_script/LivePilot/mixing.py +8 -3
  188. package/remote_script/LivePilot/server.py +5 -1
  189. package/remote_script/LivePilot/transport.py +3 -0
  190. package/remote_script/LivePilot/version_detect.py +78 -0
  191. package/scripts/sync_metadata.py +132 -0
@@ -14,12 +14,12 @@ ADD_WARMTH = SemanticMove(
14
14
  protect={"clarity": 0.6, "punch": 0.5},
15
15
  risk_level="low",
16
16
  compile_plan=[
17
- {"tool": "set_device_parameter", "params": {"description": "Add Saturator drive +2-4dB for harmonic warmth"}, "description": "Add saturation"},
18
- {"tool": "set_device_parameter", "params": {"description": "Boost EQ low-mid shelf +1-2dB"}, "description": "Low-mid warmth"},
17
+ {"tool": "set_device_parameter", "params": {"description": "Add Saturator drive +2-4dB for harmonic warmth"}, "description": "Add saturation", "backend": "remote_command"},
18
+ {"tool": "set_device_parameter", "params": {"description": "Boost EQ low-mid shelf +1-2dB"}, "description": "Low-mid warmth", "backend": "remote_command"},
19
19
  ],
20
20
  verification_plan=[
21
- {"tool": "get_master_spectrum", "check": "low-mid energy increased, high-mid stable"},
22
- {"tool": "get_track_meters", "check": "target track producing audio"},
21
+ {"tool": "get_master_spectrum", "check": "low-mid energy increased, high-mid stable", "backend": "mcp_tool"},
22
+ {"tool": "get_track_meters", "check": "target track producing audio", "backend": "remote_command"},
23
23
  ],
24
24
  )
25
25
 
@@ -31,11 +31,11 @@ ADD_TEXTURE = SemanticMove(
31
31
  protect={"clarity": 0.6},
32
32
  risk_level="medium",
33
33
  compile_plan=[
34
- {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff for organic texture"}, "description": "Organic filter motion"},
35
- {"tool": "set_track_send", "params": {"description": "Increase delay send for spatial texture"}, "description": "Spatial texture via delay"},
34
+ {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff for organic texture"}, "description": "Organic filter motion", "backend": "mcp_tool"},
35
+ {"tool": "set_track_send", "params": {"description": "Increase delay send for spatial texture"}, "description": "Spatial texture via delay", "backend": "remote_command"},
36
36
  ],
37
37
  verification_plan=[
38
- {"tool": "get_track_meters", "check": "track producing audio with variation"},
38
+ {"tool": "get_track_meters", "check": "track producing audio with variation", "backend": "remote_command"},
39
39
  ],
40
40
  )
41
41
 
@@ -47,11 +47,11 @@ SHAPE_TRANSIENTS = SemanticMove(
47
47
  protect={"warmth": 0.5},
48
48
  risk_level="low",
49
49
  compile_plan=[
50
- {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor attack time (faster = sharper transients, slower = rounder)"}, "description": "Shape attack"},
51
- {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor release for rhythmic pumping"}, "description": "Shape release"},
50
+ {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor attack time (faster = sharper transients, slower = rounder)"}, "description": "Shape attack", "backend": "remote_command"},
51
+ {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor release for rhythmic pumping"}, "description": "Shape release", "backend": "remote_command"},
52
52
  ],
53
53
  verification_plan=[
54
- {"tool": "get_track_meters", "check": "track producing audio with expected transient character"},
54
+ {"tool": "get_track_meters", "check": "track producing audio with expected transient character", "backend": "remote_command"},
55
55
  ],
56
56
  )
57
57
 
@@ -63,13 +63,13 @@ ADD_SPACE = SemanticMove(
63
63
  protect={"punch": 0.6, "clarity": 0.5},
64
64
  risk_level="low",
65
65
  compile_plan=[
66
- {"tool": "set_track_send", "params": {"description": "Increase reverb send to 25-35%"}, "description": "Add reverb depth"},
67
- {"tool": "set_track_send", "params": {"description": "Add subtle delay send 10-15%"}, "description": "Add delay texture"},
68
- {"tool": "set_track_pan", "params": {"description": "Widen pan slightly for spatial presence"}, "description": "Widen spatial field"},
66
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send to 25-35%"}, "description": "Add reverb depth", "backend": "remote_command"},
67
+ {"tool": "set_track_send", "params": {"description": "Add subtle delay send 10-15%"}, "description": "Add delay texture", "backend": "remote_command"},
68
+ {"tool": "set_track_pan", "params": {"description": "Widen pan slightly for spatial presence"}, "description": "Widen spatial field", "backend": "remote_command"},
69
69
  ],
70
70
  verification_plan=[
71
- {"tool": "get_track_meters", "check": "stereo output present, no phase cancellation"},
72
- {"tool": "analyze_mix", "check": "stereo.mono_risk is false"},
71
+ {"tool": "get_track_meters", "check": "stereo output present, no phase cancellation", "backend": "remote_command"},
72
+ {"tool": "analyze_mix", "check": "stereo.mono_risk is false", "backend": "mcp_tool"},
73
73
  ],
74
74
  )
75
75
 
@@ -177,24 +177,25 @@ def apply_semantic_move(
177
177
  result["note"] = "Awaiting approval — present the plan to the user, then execute steps individually"
178
178
  return result
179
179
 
180
- # explore mode — execute immediately
180
+ # explore mode — execute through unified router
181
+ from ..runtime.execution_router import execute_plan_steps
182
+
183
+ step_dicts = [
184
+ {"tool": step.tool, "params": step.params, "description": step.description}
185
+ for step in plan.steps
186
+ ]
187
+ exec_results = execute_plan_steps(step_dicts, ableton=ableton, ctx=ctx)
188
+
181
189
  executed_steps = []
182
- for step in plan.steps:
183
- try:
184
- tool_result = ableton.send_command(step.tool, step.params)
185
- executed_steps.append({
186
- "tool": step.tool,
187
- "description": step.description,
188
- "result": tool_result,
189
- "ok": True,
190
- })
191
- except Exception as exc:
192
- executed_steps.append({
193
- "tool": step.tool,
194
- "description": step.description,
195
- "error": str(exc),
196
- "ok": False,
197
- })
190
+ for i, er in enumerate(exec_results):
191
+ executed_steps.append({
192
+ "tool": er.tool,
193
+ "backend": er.backend,
194
+ "description": step_dicts[i].get("description", ""),
195
+ "result": er.result if er.ok else None,
196
+ "error": er.error if not er.ok else None,
197
+ "ok": er.ok,
198
+ })
198
199
 
199
200
  result = plan.to_dict()
200
201
  result["executed"] = True
@@ -11,12 +11,12 @@ INCREASE_FORWARD_MOTION = SemanticMove(
11
11
  protect={"clarity": 0.6},
12
12
  risk_level="low",
13
13
  compile_plan=[
14
- {"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising filter cutoff over 4 bars"}, "description": "Rising filter sweep"},
15
- {"tool": "set_track_volume", "params": {"description": "Push rhythm elements +5-8%"}, "description": "Push rhythm forward"},
16
- {"tool": "apply_automation_shape", "params": {"curve_type": "linear", "description": "Rising reverb send for anticipation"}, "description": "Build reverb wash"},
14
+ {"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising filter cutoff over 4 bars"}, "description": "Rising filter sweep", "backend": "mcp_tool"},
15
+ {"tool": "set_track_volume", "params": {"description": "Push rhythm elements +5-8%"}, "description": "Push rhythm forward", "backend": "remote_command"},
16
+ {"tool": "apply_automation_shape", "params": {"curve_type": "linear", "description": "Rising reverb send for anticipation"}, "description": "Build reverb wash", "backend": "mcp_tool"},
17
17
  ],
18
18
  verification_plan=[
19
- {"tool": "get_track_meters", "check": "energy increasing, all tracks alive"},
19
+ {"tool": "get_track_meters", "check": "energy increasing, all tracks alive", "backend": "remote_command"},
20
20
  ],
21
21
  )
22
22
 
@@ -28,13 +28,13 @@ OPEN_CHORUS = SemanticMove(
28
28
  protect={"clarity": 0.6, "cohesion": 0.5},
29
29
  risk_level="medium",
30
30
  compile_plan=[
31
- {"tool": "set_track_volume", "params": {"description": "Push all melodic tracks +10-15%"}, "description": "Push chorus energy"},
32
- {"tool": "set_track_pan", "params": {"description": "Widen stereo field on chords/pads"}, "description": "Widen stereo"},
33
- {"tool": "set_track_send", "params": {"description": "Increase reverb/delay sends for spaciousness"}, "description": "Add space"},
31
+ {"tool": "set_track_volume", "params": {"description": "Push all melodic tracks +10-15%"}, "description": "Push chorus energy", "backend": "remote_command"},
32
+ {"tool": "set_track_pan", "params": {"description": "Widen stereo field on chords/pads"}, "description": "Widen stereo", "backend": "remote_command"},
33
+ {"tool": "set_track_send", "params": {"description": "Increase reverb/delay sends for spaciousness"}, "description": "Add space", "backend": "remote_command"},
34
34
  ],
35
35
  verification_plan=[
36
- {"tool": "get_track_meters", "check": "overall energy increased, stereo field wider"},
37
- {"tool": "analyze_mix", "check": "no clipping, stereo.mono_risk is false"},
36
+ {"tool": "get_track_meters", "check": "overall energy increased, stereo field wider", "backend": "remote_command"},
37
+ {"tool": "analyze_mix", "check": "no clipping, stereo.mono_risk is false", "backend": "mcp_tool"},
38
38
  ],
39
39
  )
40
40
 
@@ -46,12 +46,12 @@ CREATE_BREAKDOWN = SemanticMove(
46
46
  protect={"cohesion": 0.5},
47
47
  risk_level="medium",
48
48
  compile_plan=[
49
- {"tool": "set_track_volume", "params": {"description": "Pull drums to 20-30%"}, "description": "Strip drums"},
50
- {"tool": "set_track_volume", "params": {"description": "Pull bass to 30-40%"}, "description": "Reduce bass"},
51
- {"tool": "set_track_send", "params": {"description": "Increase reverb send on remaining elements"}, "description": "Add reverb depth"},
49
+ {"tool": "set_track_volume", "params": {"description": "Pull drums to 20-30%"}, "description": "Strip drums", "backend": "remote_command"},
50
+ {"tool": "set_track_volume", "params": {"description": "Pull bass to 30-40%"}, "description": "Reduce bass", "backend": "remote_command"},
51
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send on remaining elements"}, "description": "Add reverb depth", "backend": "remote_command"},
52
52
  ],
53
53
  verification_plan=[
54
- {"tool": "get_track_meters", "check": "energy significantly reduced, at least one element still prominent"},
54
+ {"tool": "get_track_meters", "check": "energy significantly reduced, at least one element still prominent", "backend": "remote_command"},
55
55
  ],
56
56
  )
57
57
 
@@ -63,11 +63,11 @@ BRIDGE_SECTIONS = SemanticMove(
63
63
  protect={"clarity": 0.6},
64
64
  risk_level="low",
65
65
  compile_plan=[
66
- {"tool": "apply_automation_shape", "params": {"curve_type": "cosine", "description": "Gentle filter sweep across bridge"}, "description": "Bridge filter motion"},
67
- {"tool": "set_track_volume", "params": {"description": "Gentle volume crossfade between section elements"}, "description": "Crossfade elements"},
66
+ {"tool": "apply_automation_shape", "params": {"curve_type": "cosine", "description": "Gentle filter sweep across bridge"}, "description": "Bridge filter motion", "backend": "mcp_tool"},
67
+ {"tool": "set_track_volume", "params": {"description": "Gentle volume crossfade between section elements"}, "description": "Crossfade elements", "backend": "remote_command"},
68
68
  ],
69
69
  verification_plan=[
70
- {"tool": "get_track_meters", "check": "smooth level transition, no dropouts"},
70
+ {"tool": "get_track_meters", "check": "smooth level transition, no dropouts", "backend": "remote_command"},
71
71
  ],
72
72
  )
73
73
 
@@ -40,6 +40,40 @@ def _identify_port_holder(port: int) -> str | None:
40
40
  return None
41
41
 
42
42
 
43
+ def _master_has_livepilot_analyzer(ableton: AbletonConnection) -> bool:
44
+ """Check whether the analyzer device is currently on the master track."""
45
+ try:
46
+ track = ableton.send_command("get_master_track")
47
+ except Exception:
48
+ return False
49
+
50
+ devices = track.get("devices", []) if isinstance(track, dict) else []
51
+ for device in devices:
52
+ normalized = " ".join(
53
+ str(device.get("name") or "").replace("_", " ").replace("-", " ").lower().split()
54
+ )
55
+ if normalized == "livepilot analyzer":
56
+ return True
57
+ return False
58
+
59
+
60
+ async def _warm_analyzer_bridge(
61
+ ableton: AbletonConnection,
62
+ spectral: SpectralCache,
63
+ timeout: float = 3.0,
64
+ ) -> None:
65
+ """Give the analyzer stream a short startup window before first use."""
66
+ if not _master_has_livepilot_analyzer(ableton):
67
+ return
68
+
69
+ loop = asyncio.get_running_loop()
70
+ deadline = loop.time() + max(timeout, 0.0)
71
+ while loop.time() < deadline:
72
+ if spectral.is_connected:
73
+ return
74
+ await asyncio.sleep(0.05)
75
+
76
+
43
77
  @asynccontextmanager
44
78
  async def lifespan(server):
45
79
  """Create and yield the shared AbletonConnection + M4L bridge."""
@@ -80,6 +114,8 @@ async def lifespan(server):
80
114
  }
81
115
 
82
116
  try:
117
+ if bridge_state["transport"] is not None:
118
+ await _warm_analyzer_bridge(ableton, spectral)
83
119
  yield {
84
120
  "ableton": ableton,
85
121
  "spectral": spectral,
@@ -140,6 +176,10 @@ from .stuckness_detector import tools as stuckness_tools # noqa: F401, E40
140
176
  from .wonder_mode import tools as wonder_mode_tools # noqa: F401, E402
141
177
  from .session_continuity import tools as session_cont_tools # noqa: F401, E402
142
178
  from .creative_constraints import tools as constraints_tools # noqa: F401, E402
179
+ from .device_forge import tools as device_forge_tools # noqa: F401, E402
180
+ from .sample_engine import tools as sample_engine_tools # noqa: F401, E402
181
+ from .atlas import tools as atlas_tools # noqa: F401, E402
182
+ from .composer import tools as composer_tools # noqa: F401, E402
143
183
 
144
184
 
145
185
  # ---------------------------------------------------------------------------
@@ -170,6 +210,14 @@ def _coerce_schema_property(prop: dict) -> None:
170
210
  # Recurse into array items so list[int]/list[float] params also accept strings
171
211
  if "items" in prop and isinstance(prop["items"], dict):
172
212
  _coerce_schema_property(prop["items"])
213
+ if "properties" in prop and isinstance(prop["properties"], dict):
214
+ for nested in prop["properties"].values():
215
+ if isinstance(nested, dict):
216
+ _coerce_schema_property(nested)
217
+ if "$defs" in prop and isinstance(prop["$defs"], dict):
218
+ for nested in prop["$defs"].values():
219
+ if isinstance(nested, dict):
220
+ _coerce_schema_property(nested)
173
221
 
174
222
 
175
223
  def _get_all_tools():
@@ -202,6 +250,9 @@ def _patch_tool_schemas() -> None:
202
250
  if name == "ctx":
203
251
  continue # skip the Context parameter
204
252
  _coerce_schema_property(prop)
253
+ for definition in tool.parameters.get("$defs", {}).values():
254
+ if isinstance(definition, dict):
255
+ _coerce_schema_property(definition)
205
256
 
206
257
 
207
258
  _patch_tool_schemas()
@@ -0,0 +1 @@
1
+ """Shared services consumed by multiple domains."""
@@ -0,0 +1,67 @@
1
+ """Shared motif service — one entry point for all motif consumers.
2
+
3
+ SongBrain, HookHunter, and musical_intelligence all import from here
4
+ instead of making ad-hoc calls to the motif engine or TCP.
5
+
6
+ Pure computation — no I/O. Callers provide pre-fetched data.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Optional
12
+
13
+
14
+ def get_motif_data(
15
+ notes_by_track: dict[int, list[dict]],
16
+ ) -> dict:
17
+ """Extract motif data from pre-fetched notes.
18
+
19
+ Args:
20
+ notes_by_track: {track_index: [note_dicts]} from get_notes calls
21
+
22
+ Returns:
23
+ Motif analysis dict with motifs, motif_count, tracks_analyzed.
24
+ Returns empty result if no notes or engine unavailable.
25
+ """
26
+ if not notes_by_track:
27
+ return {"motifs": [], "motif_count": 0, "tracks_analyzed": 0}
28
+
29
+ try:
30
+ from ..tools import _motif_engine as motif_engine
31
+ motifs = motif_engine.detect_motifs(notes_by_track)
32
+ return {
33
+ "motifs": [m.to_dict() for m in motifs],
34
+ "motif_count": len(motifs),
35
+ "tracks_analyzed": len(notes_by_track),
36
+ }
37
+ except Exception:
38
+ return {"motifs": [], "motif_count": 0, "tracks_analyzed": 0}
39
+
40
+
41
+ def fetch_notes_from_ableton(ableton, tracks: list[dict], max_clips: int = 8) -> dict[int, list[dict]]:
42
+ """Fetch notes from Ableton for motif analysis.
43
+
44
+ This is the I/O helper — calls get_notes through valid TCP commands.
45
+ Callers pass the ableton connection; this function does the fetching.
46
+ """
47
+ notes_by_track: dict[int, list[dict]] = {}
48
+ for track in tracks:
49
+ t_idx = track.get("index", 0)
50
+ if not track.get("has_midi_input", False) and not any(
51
+ kw in track.get("name", "").lower()
52
+ for kw in ("midi", "synth", "bass", "lead", "pad", "keys", "piano", "chord")
53
+ ):
54
+ continue
55
+ track_notes = []
56
+ for clip_idx in range(max_clips):
57
+ try:
58
+ result = ableton.send_command("get_notes", {
59
+ "track_index": t_idx,
60
+ "clip_index": clip_idx,
61
+ })
62
+ track_notes.extend(result.get("notes", []))
63
+ except Exception:
64
+ pass
65
+ if track_notes:
66
+ notes_by_track[t_idx] = track_notes
67
+ return notes_by_track
@@ -23,14 +23,22 @@ from .models import (
23
23
  _story = SessionStory()
24
24
  _threads: dict[str, CreativeThread] = {}
25
25
  _turns: list[TurnResolution] = []
26
+ _project_store = None # Optional PersistentProjectStore
27
+
28
+
29
+ def set_project_store(store) -> None:
30
+ """Attach a persistent project store for flush-on-write."""
31
+ global _project_store
32
+ _project_store = store
26
33
 
27
34
 
28
35
  def reset_story() -> None:
29
36
  """Reset session story (for testing)."""
30
- global _story, _threads, _turns
37
+ global _story, _threads, _turns, _project_store
31
38
  _story = SessionStory()
32
39
  _threads = {}
33
40
  _turns = []
41
+ _project_store = None
34
42
 
35
43
 
36
44
  # ── Session story ─────────────────────────────────────────────────
@@ -117,6 +125,13 @@ def record_turn_resolution(
117
125
  else:
118
126
  _story.mood_arc.append("neutral")
119
127
 
128
+ # Flush to persistent store
129
+ if _project_store is not None:
130
+ try:
131
+ _project_store.save_turn(turn.to_dict())
132
+ except Exception:
133
+ pass
134
+
120
135
  return turn
121
136
 
122
137
 
@@ -138,6 +153,14 @@ def open_thread(description: str, domain: str = "", priority: float = 0.5) -> Cr
138
153
  last_touched_ms=now,
139
154
  )
140
155
  _threads[thread_id] = thread
156
+
157
+ # Flush to persistent store
158
+ if _project_store is not None:
159
+ try:
160
+ _project_store.save_thread(thread.to_dict())
161
+ except Exception:
162
+ pass
163
+
141
164
  return thread
142
165
 
143
166
 
@@ -147,6 +170,11 @@ def resolve_thread(thread_id: str) -> Optional[CreativeThread]:
147
170
  if thread:
148
171
  thread.status = "resolved"
149
172
  thread.last_touched_ms = int(time.time() * 1000)
173
+ if _project_store is not None:
174
+ try:
175
+ _project_store.save_thread(thread.to_dict())
176
+ except Exception:
177
+ pass
150
178
  return thread
151
179
 
152
180
 
@@ -74,16 +74,43 @@ def build_song_brain(
74
74
 
75
75
  drift_risk = _estimate_drift_risk(recent_moves, sacred)
76
76
 
77
+ # Evidence-weighted confidence adjustment
78
+ # Weights: motif=0.4, composition=0.2, role_graph=0.15, scenes=0.15, recent_moves=0.1
79
+ evidence_weights = {
80
+ "motif_data": 0.4,
81
+ "composition_analysis": 0.2,
82
+ "role_graph": 0.15,
83
+ "scenes": 0.15,
84
+ "recent_moves": 0.1,
85
+ }
86
+ evidence_score = sum(
87
+ weight for source, weight in evidence_weights.items()
88
+ if built_from.get(source, False)
89
+ )
90
+ # Adjust identity confidence by evidence availability
91
+ adjusted_confidence = round(identity_confidence * (0.4 + 0.6 * evidence_score), 3)
92
+
93
+ evidence_breakdown = {
94
+ "raw_confidence": identity_confidence,
95
+ "evidence_score": round(evidence_score, 3),
96
+ "adjusted_confidence": adjusted_confidence,
97
+ "sources": {
98
+ source: {"available": built_from.get(source, False), "weight": weight}
99
+ for source, weight in evidence_weights.items()
100
+ },
101
+ }
102
+
77
103
  return SongBrain(
78
104
  brain_id=brain_id,
79
105
  identity_core=identity_core,
80
- identity_confidence=identity_confidence,
106
+ identity_confidence=adjusted_confidence,
81
107
  sacred_elements=sacred,
82
108
  section_purposes=sections,
83
109
  energy_arc=energy_arc,
84
110
  identity_drift_risk=drift_risk,
85
111
  payoff_targets=payoff_targets,
86
112
  open_questions=open_questions,
113
+ evidence_breakdown=evidence_breakdown,
87
114
  built_from=built_from,
88
115
  )
89
116
 
@@ -84,6 +84,9 @@ class SongBrain:
84
84
  # Open questions the song has not resolved
85
85
  open_questions: list[OpenQuestion] = field(default_factory=list)
86
86
 
87
+ # Evidence breakdown — what data informed each inference
88
+ evidence_breakdown: dict = field(default_factory=dict)
89
+
87
90
  # Metadata
88
91
  built_from: dict = field(default_factory=dict) # what data sources contributed
89
92
 
@@ -98,6 +101,7 @@ class SongBrain:
98
101
  "identity_drift_risk": self.identity_drift_risk,
99
102
  "payoff_targets": self.payoff_targets,
100
103
  "open_questions": [q.to_dict() for q in self.open_questions],
104
+ "evidence_breakdown": self.evidence_breakdown,
101
105
  "built_from": self.built_from,
102
106
  }
103
107
 
@@ -88,9 +88,11 @@ def _fetch_session_data(ctx: Context) -> dict:
88
88
  except Exception:
89
89
  pass
90
90
 
91
- # Motif data — from the motif engine if notes exist
91
+ # Motif data — via shared motif service (pure-Python, not TCP)
92
92
  try:
93
- data["motif_data"] = ableton.send_command("get_motif_graph")
93
+ from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
94
+ notes_by_track = fetch_notes_from_ableton(ableton, data.get("tracks", []))
95
+ data["motif_data"] = get_motif_data(notes_by_track)
94
96
  except Exception:
95
97
  pass # Motif graph requires notes in clips; empty is valid
96
98
 
@@ -147,6 +149,21 @@ def build_song_brain(ctx: Context) -> dict:
147
149
  Returns the full SongBrain as a dict.
148
150
  """
149
151
  data = _fetch_session_data(ctx)
152
+
153
+ # Capability reporting — what data was actually available
154
+ from ..runtime.capability import build_capability
155
+ cap = build_capability(
156
+ required=["session_info", "scenes", "tracks", "motif_data", "composition_analysis", "role_graph"],
157
+ available={
158
+ "session_info": bool(data.get("session_info", {}).get("tempo")),
159
+ "scenes": bool(data.get("scenes")),
160
+ "tracks": bool(data.get("tracks")),
161
+ "motif_data": bool(data.get("motif_data")),
162
+ "composition_analysis": bool(data.get("composition_analysis")),
163
+ "role_graph": bool(data.get("role_graph")),
164
+ },
165
+ )
166
+
150
167
  brain = builder.build_song_brain(
151
168
  session_info=data["session_info"],
152
169
  scenes=data["scenes"],
@@ -161,6 +178,7 @@ def build_song_brain(ctx: Context) -> dict:
161
178
  return {
162
179
  **brain.to_dict(),
163
180
  "summary": brain.summary,
181
+ "capability": cap.to_dict(),
164
182
  }
165
183
 
166
184
 
@@ -281,17 +281,105 @@ def run_layer_overlap_critic(
281
281
  return issues
282
282
 
283
283
 
284
+ # ── Corpus Intelligence Critic ──────────────────────────────────────
285
+
286
+
287
+ def run_corpus_critic(
288
+ patch: PatchModel,
289
+ goal: TimbralGoalVector,
290
+ ) -> list[SoundDesignIssue]:
291
+ """Use the device-knowledge corpus to flag missed opportunities.
292
+
293
+ Checks each device in the chain against the corpus for known
294
+ techniques, parameter sweet spots, and creative possibilities
295
+ that the current patch doesn't exploit.
296
+ """
297
+ try:
298
+ from ..corpus import get_corpus
299
+ except ImportError:
300
+ return []
301
+
302
+ corpus = get_corpus()
303
+ if not corpus.emotional_recipes and not corpus.device_knowledge:
304
+ return [] # Corpus not loaded
305
+
306
+ issues: list[SoundDesignIssue] = []
307
+
308
+ # Check if any device in the chain has corpus knowledge
309
+ for block in patch.blocks:
310
+ dk = corpus.get_device(block.device_name)
311
+ if dk and dk.techniques and block.block_type == "oscillator":
312
+ # Oscillator with known techniques — suggest if patch is simple
313
+ has_character_block = any(
314
+ b.block_type in ("saturation", "spectral")
315
+ for b in patch.blocks
316
+ )
317
+ if not has_character_block and len(dk.techniques) > 2:
318
+ issues.append(SoundDesignIssue(
319
+ issue_type="corpus_technique_available",
320
+ critic="corpus",
321
+ severity=0.25,
322
+ confidence=0.6,
323
+ affected_blocks=[block.device_name],
324
+ evidence=(
325
+ f"Corpus has {len(dk.techniques)} known techniques "
326
+ f"for {block.device_name} but chain lacks character "
327
+ f"processing (saturation/spectral). First technique: "
328
+ f"{dk.techniques[0][:80]}"
329
+ ),
330
+ recommended_moves=["modulation_injection", "filter_contour"],
331
+ ))
332
+
333
+ # Check if goal maps to a known emotional recipe
334
+ emotion_map = {
335
+ "warmth": ("warmth", goal.warmth),
336
+ "brightness": ("euphoria", goal.brightness),
337
+ "instability": ("tension", goal.instability),
338
+ "softness": ("melancholy", goal.softness),
339
+ }
340
+ for quality, (emotion_key, goal_value) in emotion_map.items():
341
+ if goal_value > 0.3:
342
+ recipe = corpus.suggest_for_emotion(emotion_key)
343
+ if recipe and recipe.techniques:
344
+ # Check if any corpus technique device is in the chain
345
+ chain_names_lower = {d.lower() for d in patch.device_chain}
346
+ recipe_devices = set()
347
+ for tech in recipe.techniques:
348
+ # Extract bold device names from technique strings
349
+ for match in re.finditer(r"\*\*(.+?)\*\*", tech):
350
+ recipe_devices.add(match.group(1).lower())
351
+
352
+ missing = recipe_devices - chain_names_lower
353
+ if missing and len(missing) <= 3:
354
+ issues.append(SoundDesignIssue(
355
+ issue_type="corpus_emotion_opportunity",
356
+ critic="corpus",
357
+ severity=0.2,
358
+ confidence=0.5,
359
+ affected_blocks=list(missing)[:3],
360
+ evidence=(
361
+ f"Goal wants {quality}={goal_value:.2f}. "
362
+ f"Corpus '{recipe.emotion}' recipe suggests "
363
+ f"devices not in chain: {', '.join(list(missing)[:3])}"
364
+ ),
365
+ recommended_moves=["filter_contour", "modulation_injection"],
366
+ ))
367
+
368
+ return issues
369
+
370
+
284
371
  # ── Run all critics ──────────────────────────────────────────────────
285
372
 
286
373
 
287
374
  def run_all_sound_design_critics(
288
375
  state: SoundDesignState,
289
376
  ) -> list[SoundDesignIssue]:
290
- """Run all five critics and aggregate issues."""
377
+ """Run all six critics and aggregate issues."""
291
378
  issues: list[SoundDesignIssue] = []
292
379
  issues.extend(run_static_timbre_critic(state.patch, state.goal))
293
380
  issues.extend(run_weak_identity_critic(state.patch))
294
381
  issues.extend(run_masking_role_critic(state.patch, state.layers))
295
382
  issues.extend(run_modulation_flatness_critic(state.patch))
296
383
  issues.extend(run_layer_overlap_critic(state.layers))
384
+ issues.extend(run_corpus_critic(state.patch, state.goal))
297
385
  return issues
@@ -0,0 +1 @@
1
+ """Splice gRPC client — connect to Splice desktop's local API for sample search and download."""