livepilot 1.9.21 → 1.9.23

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 (110) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.mcpbignore +40 -0
  3. package/AGENTS.md +2 -2
  4. package/CHANGELOG.md +47 -0
  5. package/CONTRIBUTING.md +1 -1
  6. package/README.md +47 -72
  7. package/bin/livepilot.js +135 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
  11. package/livepilot/commands/arrange.md +42 -14
  12. package/livepilot/commands/beat.md +68 -21
  13. package/livepilot/commands/evaluate.md +23 -13
  14. package/livepilot/commands/mix.md +35 -11
  15. package/livepilot/commands/perform.md +31 -19
  16. package/livepilot/commands/sounddesign.md +38 -17
  17. package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
  18. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
  19. package/livepilot/skills/livepilot-core/SKILL.md +60 -4
  20. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
  21. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
  22. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
  23. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
  24. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
  25. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
  26. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
  27. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
  28. package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
  29. package/livepilot/skills/livepilot-core/references/overview.md +4 -4
  30. package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
  31. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
  32. package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
  33. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
  34. package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
  35. package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
  36. package/livepilot/skills/livepilot-release/SKILL.md +15 -15
  37. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
  38. package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
  39. package/livepilot.mcpb +0 -0
  40. package/m4l_device/livepilot_bridge.js +1 -1
  41. package/manifest.json +91 -0
  42. package/mcp_server/__init__.py +1 -1
  43. package/mcp_server/creative_constraints/__init__.py +6 -0
  44. package/mcp_server/creative_constraints/engine.py +277 -0
  45. package/mcp_server/creative_constraints/models.py +75 -0
  46. package/mcp_server/creative_constraints/tools.py +341 -0
  47. package/mcp_server/experiment/__init__.py +6 -0
  48. package/mcp_server/experiment/engine.py +213 -0
  49. package/mcp_server/experiment/models.py +120 -0
  50. package/mcp_server/experiment/tools.py +263 -0
  51. package/mcp_server/hook_hunter/__init__.py +5 -0
  52. package/mcp_server/hook_hunter/analyzer.py +342 -0
  53. package/mcp_server/hook_hunter/models.py +57 -0
  54. package/mcp_server/hook_hunter/tools.py +586 -0
  55. package/mcp_server/memory/taste_graph.py +261 -0
  56. package/mcp_server/memory/tools.py +88 -0
  57. package/mcp_server/mix_engine/critics.py +2 -2
  58. package/mcp_server/mix_engine/models.py +1 -1
  59. package/mcp_server/mix_engine/state_builder.py +2 -2
  60. package/mcp_server/musical_intelligence/__init__.py +8 -0
  61. package/mcp_server/musical_intelligence/detectors.py +421 -0
  62. package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
  63. package/mcp_server/musical_intelligence/tools.py +221 -0
  64. package/mcp_server/preview_studio/__init__.py +5 -0
  65. package/mcp_server/preview_studio/engine.py +280 -0
  66. package/mcp_server/preview_studio/models.py +73 -0
  67. package/mcp_server/preview_studio/tools.py +423 -0
  68. package/mcp_server/runtime/session_kernel.py +96 -0
  69. package/mcp_server/runtime/tools.py +90 -1
  70. package/mcp_server/semantic_moves/__init__.py +13 -0
  71. package/mcp_server/semantic_moves/compiler.py +116 -0
  72. package/mcp_server/semantic_moves/mix_compilers.py +291 -0
  73. package/mcp_server/semantic_moves/mix_moves.py +157 -0
  74. package/mcp_server/semantic_moves/models.py +46 -0
  75. package/mcp_server/semantic_moves/performance_compilers.py +208 -0
  76. package/mcp_server/semantic_moves/performance_moves.py +81 -0
  77. package/mcp_server/semantic_moves/registry.py +32 -0
  78. package/mcp_server/semantic_moves/resolvers.py +126 -0
  79. package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
  80. package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
  81. package/mcp_server/semantic_moves/tools.py +204 -0
  82. package/mcp_server/semantic_moves/transition_compilers.py +222 -0
  83. package/mcp_server/semantic_moves/transition_moves.py +76 -0
  84. package/mcp_server/server.py +10 -0
  85. package/mcp_server/session_continuity/__init__.py +6 -0
  86. package/mcp_server/session_continuity/models.py +86 -0
  87. package/mcp_server/session_continuity/tools.py +230 -0
  88. package/mcp_server/session_continuity/tracker.py +235 -0
  89. package/mcp_server/song_brain/__init__.py +6 -0
  90. package/mcp_server/song_brain/builder.py +477 -0
  91. package/mcp_server/song_brain/models.py +132 -0
  92. package/mcp_server/song_brain/tools.py +294 -0
  93. package/mcp_server/stuckness_detector/__init__.py +5 -0
  94. package/mcp_server/stuckness_detector/detector.py +400 -0
  95. package/mcp_server/stuckness_detector/models.py +66 -0
  96. package/mcp_server/stuckness_detector/tools.py +195 -0
  97. package/mcp_server/tools/_conductor.py +104 -6
  98. package/mcp_server/tools/analyzer.py +1 -1
  99. package/mcp_server/tools/devices.py +34 -0
  100. package/mcp_server/wonder_mode/__init__.py +6 -0
  101. package/mcp_server/wonder_mode/diagnosis.py +84 -0
  102. package/mcp_server/wonder_mode/engine.py +493 -0
  103. package/mcp_server/wonder_mode/session.py +114 -0
  104. package/mcp_server/wonder_mode/tools.py +285 -0
  105. package/package.json +2 -2
  106. package/remote_script/LivePilot/__init__.py +1 -1
  107. package/remote_script/LivePilot/browser.py +4 -1
  108. package/remote_script/LivePilot/devices.py +29 -0
  109. package/remote_script/LivePilot/tracks.py +11 -4
  110. package/scripts/generate_tool_catalog.py +131 -0
@@ -0,0 +1,266 @@
1
+ """Compilers for sound-design-domain semantic moves.
2
+
3
+ These prefer native Ableton devices. Volume/send adjustments are used
4
+ as safe fallbacks when device chain details aren't in the kernel.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .compiler import CompiledPlan, CompiledStep, register_compiler
10
+ from .models import SemanticMove
11
+ from . import resolvers
12
+
13
+
14
+ def _compile_add_warmth(move: SemanticMove, kernel: dict) -> CompiledPlan:
15
+ """Compile 'add_warmth': volume boost + reverb send for perceived warmth.
16
+
17
+ SAFETY: Never blindly set device parameters — device_index=0, parameter_index=0
18
+ can kill audio if the first device isn't a Saturator. Only adjust device params
19
+ when find_device_on_track confirms a Saturator is present.
20
+ """
21
+ steps = []
22
+ descriptions = []
23
+ warnings = []
24
+
25
+ # Target melodic or bass tracks for warmth
26
+ targets = resolvers.find_tracks_by_role(kernel, ["bass", "chords", "pad"])
27
+ if not targets:
28
+ targets = resolvers.find_tracks_by_role(kernel, ["lead"])
29
+
30
+ for t in targets[:2]:
31
+ idx = t["index"]
32
+ name = t["name"]
33
+
34
+ # Try to find a Saturator on the track (safe device adjustment)
35
+ saturator = resolvers.find_device_on_track(kernel, idx, "Saturator")
36
+ if saturator:
37
+ steps.append(CompiledStep(
38
+ tool="set_device_parameter",
39
+ params={
40
+ "track_index": idx,
41
+ "device_index": saturator["device_index"],
42
+ "parameter_index": 0,
43
+ "value": 0.3,
44
+ },
45
+ description=f"Gentle Saturator drive on {name}",
46
+ ))
47
+ descriptions.append(f"Saturate {name}")
48
+ else:
49
+ # No Saturator found — use volume + send instead of risky device params
50
+ warnings.append(f"No Saturator on {name} — using volume+reverb for warmth")
51
+
52
+ # Boost volume slightly for perceived warmth
53
+ steps.append(CompiledStep(
54
+ tool="set_track_volume",
55
+ params={"track_index": idx, "volume": 0.65},
56
+ description=f"Boost {name} slightly for warmth",
57
+ ))
58
+
59
+ # Add reverb send for depth/warmth perception
60
+ steps.append(CompiledStep(
61
+ tool="set_track_send",
62
+ params={"track_index": idx, "send_index": 0, "value": 0.25},
63
+ description=f"Add reverb warmth to {name}",
64
+ ))
65
+ descriptions.append(f"Warm {name}")
66
+
67
+ steps.append(CompiledStep(
68
+ tool="get_track_meters",
69
+ params={"include_stereo": True},
70
+ description="Verify warmth — tracks producing audio, no distortion",
71
+ ))
72
+
73
+ return CompiledPlan(
74
+ move_id=move.move_id,
75
+ intent=move.intent,
76
+ steps=steps,
77
+ before_reads=[{"tool": "get_master_spectrum", "params": {}}],
78
+ after_reads=[{"tool": "get_master_spectrum", "params": {}}],
79
+ risk_level="low",
80
+ summary="; ".join(descriptions) if descriptions else "No tracks for warmth",
81
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
82
+ warnings=warnings,
83
+ )
84
+
85
+
86
+ def _compile_add_texture(move: SemanticMove, kernel: dict) -> CompiledPlan:
87
+ """Compile 'add_texture': perlin filter motion + delay send."""
88
+ steps = []
89
+ descriptions = []
90
+
91
+ targets = resolvers.find_tracks_by_role(kernel, ["pad", "chords", "lead"])
92
+
93
+ for t in targets[:1]:
94
+ idx = t["index"]
95
+ name = t["name"]
96
+ steps.append(CompiledStep(
97
+ tool="apply_automation_shape",
98
+ params={
99
+ "track_index": idx,
100
+ "clip_index": 0,
101
+ "parameter_type": "device",
102
+ "device_index": 0,
103
+ "parameter_index": 0,
104
+ "curve_type": "perlin",
105
+ "center": 0.4,
106
+ "amplitude": 0.2,
107
+ "duration": 8,
108
+ "density": 16,
109
+ },
110
+ description=f"Perlin filter motion on {name} for organic texture",
111
+ ))
112
+ descriptions.append(f"Perlin filter on {name}")
113
+
114
+ # Add delay send
115
+ steps.append(CompiledStep(
116
+ tool="set_track_send",
117
+ params={"track_index": idx, "send_index": 1, "value": 0.20},
118
+ description=f"Add delay send on {name} for spatial texture",
119
+ ))
120
+ descriptions.append(f"Delay texture on {name}")
121
+
122
+ steps.append(CompiledStep(
123
+ tool="get_track_meters",
124
+ params={"include_stereo": True},
125
+ description="Verify texture — track active with variation",
126
+ ))
127
+
128
+ return CompiledPlan(
129
+ move_id=move.move_id,
130
+ intent=move.intent,
131
+ steps=steps,
132
+ risk_level="medium",
133
+ summary="; ".join(descriptions) if descriptions else "No tracks for texture",
134
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
135
+ )
136
+
137
+
138
+ def _compile_shape_transients(move: SemanticMove, kernel: dict) -> CompiledPlan:
139
+ """Compile 'shape_transients': push drum volume for punch, adjust sends.
140
+
141
+ SAFETY: Never blindly set device parameters. Only adjust Compressor params
142
+ when find_device_on_track confirms one exists. Otherwise use volume for punch.
143
+ """
144
+ steps = []
145
+ descriptions = []
146
+ warnings = []
147
+
148
+ drums = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
149
+ if not drums:
150
+ return CompiledPlan(
151
+ move_id=move.move_id,
152
+ intent=move.intent,
153
+ summary="No drum/percussion tracks found",
154
+ warnings=["No rhythm tracks for transient shaping"],
155
+ )
156
+
157
+ for dt in drums[:1]:
158
+ idx = dt["index"]
159
+ name = dt["name"]
160
+
161
+ # Try to find a Compressor on the track
162
+ compressor = resolvers.find_device_on_track(kernel, idx, "Compressor")
163
+ if compressor:
164
+ steps.append(CompiledStep(
165
+ tool="set_device_parameter",
166
+ params={
167
+ "track_index": idx,
168
+ "device_index": compressor["device_index"],
169
+ "parameter_index": 0,
170
+ "value": 0.2,
171
+ },
172
+ description=f"Faster Compressor attack on {name} for snap",
173
+ ))
174
+ descriptions.append(f"Shape {name} compressor")
175
+ else:
176
+ warnings.append(f"No Compressor on {name} — using volume push for punch")
177
+
178
+ # Push volume for transient punch regardless
179
+ steps.append(CompiledStep(
180
+ tool="set_track_volume",
181
+ params={"track_index": idx, "volume": 0.75},
182
+ description=f"Push {name} to 0.75 for transient punch",
183
+ ))
184
+ descriptions.append(f"Push {name} for punch")
185
+
186
+ # Reduce reverb send to tighten transients
187
+ steps.append(CompiledStep(
188
+ tool="set_track_send",
189
+ params={"track_index": idx, "send_index": 0, "value": 0.10},
190
+ description=f"Tighten reverb on {name} for cleaner transients",
191
+ ))
192
+
193
+ steps.append(CompiledStep(
194
+ tool="get_track_meters",
195
+ params={"include_stereo": True},
196
+ description="Verify transient character after shaping",
197
+ ))
198
+
199
+ return CompiledPlan(
200
+ move_id=move.move_id,
201
+ intent=move.intent,
202
+ steps=steps,
203
+ risk_level="low",
204
+ summary="; ".join(descriptions),
205
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
206
+ warnings=warnings,
207
+ )
208
+
209
+
210
+ def _compile_add_space(move: SemanticMove, kernel: dict) -> CompiledPlan:
211
+ """Compile 'add_space': reverb + delay + pan widening."""
212
+ steps = []
213
+ descriptions = []
214
+
215
+ targets = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
216
+
217
+ for t in targets[:2]:
218
+ idx = t["index"]
219
+ name = t["name"]
220
+ steps.append(CompiledStep(
221
+ tool="set_track_send",
222
+ params={"track_index": idx, "send_index": 0, "value": 0.30},
223
+ description=f"Add reverb depth to {name}",
224
+ ))
225
+ descriptions.append(f"Reverb on {name}")
226
+
227
+ # Widen one element
228
+ for t in targets[:1]:
229
+ steps.append(CompiledStep(
230
+ tool="set_track_pan",
231
+ params={"track_index": t["index"], "pan": -0.20},
232
+ description=f"Pan {t['name']} slightly left for spatial width",
233
+ ))
234
+ for t in targets[1:2]:
235
+ steps.append(CompiledStep(
236
+ tool="set_track_pan",
237
+ params={"track_index": t["index"], "pan": 0.20},
238
+ description=f"Pan {t['name']} slightly right for spatial width",
239
+ ))
240
+
241
+ descriptions.append("Widen spatial field")
242
+
243
+ steps.append(CompiledStep(
244
+ tool="get_track_meters",
245
+ params={"include_stereo": True},
246
+ description="Verify spatial depth — stereo present, no phase issues",
247
+ ))
248
+
249
+ return CompiledPlan(
250
+ move_id=move.move_id,
251
+ intent=move.intent,
252
+ steps=steps,
253
+ before_reads=[{"tool": "analyze_mix", "params": {}}],
254
+ after_reads=[{"tool": "analyze_mix", "params": {}}],
255
+ risk_level="low",
256
+ summary="; ".join(descriptions) if descriptions else "No tracks for space",
257
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
258
+ )
259
+
260
+
261
+ # ── Register ────────────────────────────────────────────────────────────────
262
+
263
+ register_compiler("add_warmth", _compile_add_warmth)
264
+ register_compiler("add_texture", _compile_add_texture)
265
+ register_compiler("shape_transients", _compile_shape_transients)
266
+ register_compiler("add_space", _compile_add_space)
@@ -0,0 +1,78 @@
1
+ """Sound-design-domain semantic moves — musical intents for timbre and texture.
2
+
3
+ These moves prefer native Ableton devices over third-party plugins.
4
+ """
5
+
6
+ from .models import SemanticMove
7
+ from .registry import register
8
+
9
+ ADD_WARMTH = SemanticMove(
10
+ move_id="add_warmth",
11
+ family="sound_design",
12
+ intent="Add warmth to a track or the mix — gentle saturation and low-mid boost",
13
+ targets={"warmth": 0.5, "depth": 0.3, "cohesion": 0.2},
14
+ protect={"clarity": 0.6, "punch": 0.5},
15
+ risk_level="low",
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"},
19
+ ],
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"},
23
+ ],
24
+ )
25
+
26
+ ADD_TEXTURE = SemanticMove(
27
+ move_id="add_texture",
28
+ family="sound_design",
29
+ intent="Add texture and movement to a static sound — modulation and grain",
30
+ targets={"motion": 0.4, "novelty": 0.3, "depth": 0.3},
31
+ protect={"clarity": 0.6},
32
+ risk_level="medium",
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"},
36
+ ],
37
+ verification_plan=[
38
+ {"tool": "get_track_meters", "check": "track producing audio with variation"},
39
+ ],
40
+ )
41
+
42
+ SHAPE_TRANSIENTS = SemanticMove(
43
+ move_id="shape_transients",
44
+ family="sound_design",
45
+ intent="Shape transient character — sharpen or soften attack for rhythmic clarity",
46
+ targets={"punch": 0.5, "clarity": 0.3, "groove": 0.2},
47
+ protect={"warmth": 0.5},
48
+ risk_level="low",
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"},
52
+ ],
53
+ verification_plan=[
54
+ {"tool": "get_track_meters", "check": "track producing audio with expected transient character"},
55
+ ],
56
+ )
57
+
58
+ ADD_SPACE = SemanticMove(
59
+ move_id="add_space",
60
+ family="sound_design",
61
+ intent="Add spatial depth — reverb, delay, and stereo enhancement without muddying",
62
+ targets={"depth": 0.5, "width": 0.3, "clarity": 0.2},
63
+ protect={"punch": 0.6, "clarity": 0.5},
64
+ risk_level="low",
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"},
69
+ ],
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"},
73
+ ],
74
+ )
75
+
76
+ # Register all sound design moves
77
+ for _move in [ADD_WARMTH, ADD_TEXTURE, SHAPE_TRANSIENTS, ADD_SPACE]:
78
+ register(_move)
@@ -0,0 +1,204 @@
1
+ """Semantic move MCP tools — propose, preview, and apply musical intents.
2
+
3
+ 3 tools:
4
+ list_semantic_moves — discover available moves by domain
5
+ preview_semantic_move — see what a move will do before applying
6
+ propose_next_best_move — AI-ranked suggestions based on current session state
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Optional
12
+
13
+ from fastmcp import Context
14
+
15
+ from ..server import mcp
16
+ from . import registry
17
+
18
+
19
+ @mcp.tool()
20
+ def list_semantic_moves(
21
+ ctx: Context,
22
+ domain: str = "",
23
+ style: str = "",
24
+ ) -> dict:
25
+ """List available semantic moves — high-level musical intents.
26
+
27
+ Semantic moves express WHAT to achieve musically, not HOW parametrically.
28
+ Each move compiles into a sequence of existing deterministic tools.
29
+
30
+ domain: filter by family (mix, arrangement, transition, sound_design, performance)
31
+ style: filter by genre/style (reserved for future use)
32
+
33
+ Returns: list of moves with move_id, family, intent, targets, risk_level.
34
+ """
35
+ moves = registry.list_moves(domain=domain, style=style)
36
+ return {"moves": moves, "count": len(moves), "available_domains": ["mix", "arrangement"]}
37
+
38
+
39
+ @mcp.tool()
40
+ def preview_semantic_move(
41
+ ctx: Context,
42
+ move_id: str,
43
+ ) -> dict:
44
+ """Preview what a semantic move will do before applying it.
45
+
46
+ Returns the full compile plan (tool sequence), verification plan,
47
+ targets, protection constraints, and risk level. Use this to understand
48
+ the impact before committing.
49
+ """
50
+ move = registry.get_move(move_id)
51
+ if not move:
52
+ available = [m["move_id"] for m in registry.list_moves()]
53
+ return {
54
+ "error": f"Unknown move_id: {move_id}",
55
+ "available_moves": available,
56
+ }
57
+
58
+ return move.to_full_dict()
59
+
60
+
61
+ @mcp.tool()
62
+ def propose_next_best_move(
63
+ ctx: Context,
64
+ request_text: str,
65
+ limit: int = 3,
66
+ ) -> dict:
67
+ """Propose the best semantic moves for a natural language request.
68
+
69
+ Analyzes the request text and ranks available semantic moves by
70
+ relevance. Returns up to `limit` suggestions with confidence scores.
71
+
72
+ request_text: what the user wants (e.g., "make this punchier",
73
+ "tighten the low end", "reduce repetition")
74
+ limit: max suggestions to return (default 3)
75
+ """
76
+ if not request_text.strip():
77
+ return {"error": "request_text cannot be empty"}
78
+
79
+ # Simple keyword matching for now — will be replaced by conductor
80
+ # routing + taste ranking in V2 Step 7
81
+ request_lower = request_text.lower()
82
+ all_moves = list(registry._REGISTRY.values())
83
+
84
+ scored = []
85
+ for move in all_moves:
86
+ score = 0.0
87
+ # Match keywords from intent and move_id
88
+ intent_lower = move.intent.lower()
89
+ move_words = set(move.move_id.replace("_", " ").split())
90
+ intent_words = set(intent_lower.split())
91
+ request_words = set(request_lower.split())
92
+
93
+ # Word overlap scoring
94
+ overlap = request_words & (move_words | intent_words)
95
+ score += len(overlap) * 0.3
96
+
97
+ # Dimension matching
98
+ for dim in move.targets:
99
+ if dim in request_lower:
100
+ score += 0.2
101
+
102
+ # Boost exact intent matches
103
+ if move.move_id.replace("_", " ") in request_lower:
104
+ score += 1.0
105
+
106
+ if score > 0:
107
+ scored.append((move, min(score, 1.0)))
108
+
109
+ # Sort by score descending
110
+ scored.sort(key=lambda x: -x[1])
111
+ top = scored[:limit]
112
+
113
+ suggestions = []
114
+ for move, score in top:
115
+ d = move.to_dict()
116
+ d["match_score"] = round(score, 3)
117
+ suggestions.append(d)
118
+
119
+ return {
120
+ "request": request_text,
121
+ "suggestions": suggestions,
122
+ "count": len(suggestions),
123
+ }
124
+
125
+
126
+ @mcp.tool()
127
+ def apply_semantic_move(
128
+ ctx: Context,
129
+ move_id: str,
130
+ mode: str = "improve",
131
+ ) -> dict:
132
+ """Compile and optionally execute a semantic move against the current session.
133
+
134
+ Resolves the move's intent into concrete, parameterized tool calls based
135
+ on the current session topology (track names, roles, devices).
136
+
137
+ mode controls behavior:
138
+ - "improve" / "finish": compile and RETURN the plan for user approval.
139
+ The agent should present the steps and ask "Shall I do it?"
140
+ - "explore": compile and EXECUTE immediately, capturing before/after.
141
+ - "observe" / "diagnose": compile only, never execute. Return the plan.
142
+
143
+ Returns: CompiledPlan with concrete steps, summary, and execution status.
144
+ """
145
+ from . import compiler
146
+
147
+ move = registry.get_move(move_id)
148
+ if not move:
149
+ return {"error": f"Unknown move_id: {move_id}"}
150
+
151
+ # Build a lightweight kernel from session info
152
+ ableton = ctx.lifespan_context["ableton"]
153
+ session_info = ableton.send_command("get_session_info")
154
+ kernel = {
155
+ "session_info": session_info,
156
+ "mode": mode,
157
+ "capability_state": {},
158
+ }
159
+
160
+ # Compile the move
161
+ plan = compiler.compile(move, kernel)
162
+
163
+ if not plan.executable:
164
+ result = plan.to_dict()
165
+ result["executed"] = False
166
+ return result
167
+
168
+ if mode in ("observe", "diagnose"):
169
+ result = plan.to_dict()
170
+ result["executed"] = False
171
+ result["note"] = f"Mode '{mode}' — plan compiled but not executed"
172
+ return result
173
+
174
+ if mode in ("improve", "finish"):
175
+ result = plan.to_dict()
176
+ result["executed"] = False
177
+ result["note"] = "Awaiting approval — present the plan to the user, then execute steps individually"
178
+ return result
179
+
180
+ # explore mode — execute immediately
181
+ 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
+ })
198
+
199
+ result = plan.to_dict()
200
+ result["executed"] = True
201
+ result["execution_results"] = executed_steps
202
+ result["success_count"] = sum(1 for s in executed_steps if s["ok"])
203
+ result["failure_count"] = sum(1 for s in executed_steps if not s["ok"])
204
+ return result