livepilot 1.10.0 → 1.10.2

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 (55) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +214 -0
  4. package/CONTRIBUTING.md +2 -2
  5. package/LICENSE +62 -21
  6. package/README.md +264 -286
  7. package/livepilot/.Codex-plugin/plugin.json +2 -2
  8. package/livepilot/.claude-plugin/plugin.json +2 -2
  9. package/livepilot/skills/livepilot-arrangement/SKILL.md +18 -1
  10. package/livepilot/skills/livepilot-core/SKILL.md +5 -5
  11. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  12. package/livepilot/skills/livepilot-devices/SKILL.md +23 -2
  13. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  14. package/livepilot/skills/livepilot-release/SKILL.md +21 -17
  15. package/livepilot/skills/livepilot-sample-engine/SKILL.md +2 -1
  16. package/livepilot/skills/livepilot-wonder/SKILL.md +8 -6
  17. package/livepilot.mcpb +0 -0
  18. package/m4l_device/LivePilot_Analyzer.adv +0 -0
  19. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  20. package/m4l_device/livepilot_bridge.js +1 -1
  21. package/manifest.json +4 -4
  22. package/mcp_server/__init__.py +1 -1
  23. package/mcp_server/composer/engine.py +249 -169
  24. package/mcp_server/composer/sample_resolver.py +153 -0
  25. package/mcp_server/composer/tools.py +97 -87
  26. package/mcp_server/memory/taste_accessors.py +47 -0
  27. package/mcp_server/preview_studio/engine.py +9 -2
  28. package/mcp_server/preview_studio/tools.py +78 -35
  29. package/mcp_server/project_brain/tools.py +34 -0
  30. package/mcp_server/runtime/execution_router.py +180 -38
  31. package/mcp_server/runtime/mcp_dispatch.py +46 -0
  32. package/mcp_server/runtime/remote_commands.py +4 -1
  33. package/mcp_server/runtime/tools.py +55 -32
  34. package/mcp_server/sample_engine/moves.py +12 -12
  35. package/mcp_server/sample_engine/slice_workflow.py +190 -0
  36. package/mcp_server/sample_engine/tools.py +104 -1
  37. package/mcp_server/semantic_moves/device_creation_moves.py +7 -7
  38. package/mcp_server/semantic_moves/mix_moves.py +8 -8
  39. package/mcp_server/semantic_moves/models.py +7 -7
  40. package/mcp_server/semantic_moves/performance_moves.py +4 -4
  41. package/mcp_server/semantic_moves/sample_compilers.py +14 -9
  42. package/mcp_server/semantic_moves/sound_design_moves.py +4 -4
  43. package/mcp_server/semantic_moves/tools.py +63 -10
  44. package/mcp_server/semantic_moves/transition_moves.py +4 -4
  45. package/mcp_server/server.py +20 -1
  46. package/mcp_server/session_continuity/tracker.py +4 -1
  47. package/mcp_server/tools/_conductor.py +16 -0
  48. package/mcp_server/tools/_planner_engine.py +24 -0
  49. package/mcp_server/tools/analyzer.py +2 -0
  50. package/mcp_server/tools/planner.py +3 -0
  51. package/mcp_server/wonder_mode/engine.py +59 -13
  52. package/mcp_server/wonder_mode/tools.py +33 -1
  53. package/package.json +8 -8
  54. package/remote_script/LivePilot/__init__.py +1 -1
  55. package/remote_script/LivePilot/devices.py +10 -0
@@ -1,34 +1,48 @@
1
- """Unified execution router for compiled plan steps.
1
+ """Unified async execution router for compiled plan steps.
2
2
 
3
3
  Classifies each step by backend (remote_command, mcp_tool, bridge_command)
4
- and dispatches to the correct execution path. Replaces the pattern of
5
- sending everything through ableton.send_command() blindly.
4
+ and dispatches through the correct transport. Async-only there is no
5
+ sync path. Callers that need to execute plans live inside an async tool
6
+ and await execute_plan_steps_async.
6
7
 
7
8
  Step backends:
8
- remote_command — valid Remote Script handler, goes through TCP
9
- bridge_command — M4L bridge handler, goes through TCP (requires bridge)
10
- mcp_toolMCP-layer Python function, called directly
11
- unknown not a valid target anywhere
9
+ remote_command — valid Remote Script handler, goes through the sync TCP
10
+ client (ableton.send_command)
11
+ bridge_commandM4L bridge handler, goes through the async UDP M4L bridge
12
+ client (bridge.send_command), NOT through ableton
13
+ mcp_tool — in-process Python function, dispatched via an mcp_registry
14
+ dict supplied by the server lifespan
15
+ unknown — not a valid target anywhere; returns a clear error
16
+
17
+ Step-result binding:
18
+ Any step may carry an optional step_id. Later steps may reference an
19
+ earlier result by setting a param to {"$from_step": "<id>", "path": "a.b"}.
20
+ Resolved recursively BEFORE dispatch.
12
21
  """
13
22
 
14
23
  from __future__ import annotations
15
24
 
25
+ import inspect
16
26
  from dataclasses import dataclass
17
- from typing import Any, Optional
27
+ from typing import Any, Awaitable, Callable, Optional
18
28
 
19
29
  from .remote_commands import BRIDGE_COMMANDS, REMOTE_COMMANDS
20
30
 
21
31
 
22
32
  # MCP-only tools that exist as Python functions but NOT as TCP handlers.
23
33
  # These must be called through direct import, not ableton.send_command().
34
+ # NOTE: capture_audio is a BRIDGE command (livepilot_bridge.js:146), not MCP.
35
+ # It used to be duplicated here; removed to keep classification unambiguous.
24
36
  MCP_TOOLS: frozenset[str] = frozenset({
25
37
  "apply_automation_shape",
26
38
  "apply_gesture_template",
27
39
  "analyze_mix",
28
40
  "get_master_spectrum",
29
41
  "get_emotional_arc",
30
- "capture_audio",
31
42
  "get_motif_graph",
43
+ # Sample-engine workflow tools — async Python that orchestrates multiple
44
+ # sub-commands (search_browser + load_browser_item + bridge.replace_simpler_sample).
45
+ "load_sample_to_simpler",
32
46
  # Device Forge tools (MCP-only, no TCP handler)
33
47
  "generate_m4l_effect",
34
48
  "install_m4l_device",
@@ -66,17 +80,75 @@ def classify_step(tool: str) -> str:
66
80
  return "unknown"
67
81
 
68
82
 
69
- def execute_step(
83
+ # ── Step-result binding ─────────────────────────────────────────────────
84
+
85
+ def _resolve_binding(binding: dict, step_results: dict) -> Any:
86
+ """Resolve a {"$from_step": step_id, "path": "a.b.c"} binding.
87
+
88
+ Raises ValueError with a clear message on missing step_id or missing key.
89
+ """
90
+ step_id = binding["$from_step"]
91
+ path = binding.get("path", "")
92
+
93
+ if step_id not in step_results:
94
+ available = sorted(step_results.keys())
95
+ raise ValueError(
96
+ f"Step binding failed: step_id '{step_id}' not found. "
97
+ f"Available: {available or '(no earlier results)'}"
98
+ )
99
+
100
+ current = step_results[step_id]
101
+ if not isinstance(current, dict):
102
+ raise ValueError(
103
+ f"Step binding failed: result of '{step_id}' is "
104
+ f"{type(current).__name__}, not a dict"
105
+ )
106
+
107
+ if not path:
108
+ return current
109
+
110
+ for segment in path.split("."):
111
+ if not isinstance(current, dict) or segment not in current:
112
+ keys = list(current.keys()) if isinstance(current, dict) else type(current).__name__
113
+ raise ValueError(
114
+ f"Step binding failed: path '{path}' not found in result of "
115
+ f"'{step_id}'. Available at this level: {keys}"
116
+ )
117
+ current = current[segment]
118
+
119
+ return current
120
+
121
+
122
+ def _resolve_params(params: Any, step_results: dict) -> Any:
123
+ """Recursively walk params and resolve any $from_step bindings."""
124
+ if isinstance(params, dict):
125
+ if "$from_step" in params:
126
+ return _resolve_binding(params, step_results)
127
+ return {k: _resolve_params(v, step_results) for k, v in params.items()}
128
+ if isinstance(params, list):
129
+ return [_resolve_params(v, step_results) for v in params]
130
+ return params
131
+
132
+
133
+ # ── Async execution path ────────────────────────────────────────────────
134
+
135
+ async def _execute_step_async(
70
136
  tool: str,
71
137
  params: dict,
72
- ableton: Any = None,
73
- ctx: Any = None,
74
- declared_backend: str | None = None,
138
+ ableton: Any,
139
+ bridge: Any,
140
+ mcp_registry: dict[str, Callable],
141
+ ctx: Any,
142
+ declared_backend: Optional[str] = None,
75
143
  ) -> ExecutionResult:
76
- """Execute a single plan step through the correct backend."""
77
- backend = declared_backend if declared_backend in ("remote_command", "bridge_command", "mcp_tool") else classify_step(tool)
144
+ """Dispatch a single step through the correct transport, async-aware."""
145
+ backend = (
146
+ declared_backend
147
+ if declared_backend in ("remote_command", "bridge_command", "mcp_tool")
148
+ else classify_step(tool)
149
+ )
78
150
 
79
- if backend in ("remote_command", "bridge_command"):
151
+ if backend == "remote_command":
80
152
  if ableton is None:
81
153
  return ExecutionResult(
82
154
  ok=False, backend=backend, tool=tool,
@@ -84,45 +156,95 @@ def execute_step(
84
156
  )
85
157
  try:
86
158
  result = ableton.send_command(tool, params)
159
+ if isinstance(result, dict) and "error" in result:
160
+ return ExecutionResult(ok=False, backend=backend, tool=tool, error=result["error"])
87
161
  return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
88
162
  except Exception as e:
89
163
  return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
90
164
 
91
- elif backend == "mcp_tool":
92
- # MCP tools require direct Python dispatch.
93
- # For now, return a clear error — full MCP dispatch is wired per-tool
94
- # in the callers (apply_semantic_move, render_preview_variant).
95
- return ExecutionResult(
96
- ok=False, backend=backend, tool=tool,
97
- error=f"MCP tool '{tool}' requires direct Python dispatch — "
98
- f"not executable through TCP. Use the MCP layer directly.",
99
- )
165
+ if backend == "bridge_command":
166
+ if bridge is None:
167
+ return ExecutionResult(
168
+ ok=False, backend=backend, tool=tool,
169
+ error="M4L bridge unavailable — cannot dispatch bridge command",
170
+ )
171
+ try:
172
+ # M4LBridge.send_command accepts (command, *args) and OSC-encodes
173
+ # each arg positionally. Plan authors construct params dicts in
174
+ # the order the bridge command expects; we unpack by insertion
175
+ # order (Python 3.7+ guarantees this). This keeps plans readable
176
+ # while matching the real bridge's positional wire format.
177
+ positional = list(params.values()) if params else []
178
+ call = bridge.send_command(tool, *positional)
179
+ result = await call if inspect.isawaitable(call) else call
180
+ if isinstance(result, dict) and "error" in result:
181
+ return ExecutionResult(ok=False, backend=backend, tool=tool, error=result["error"])
182
+ return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
183
+ except Exception as e:
184
+ return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
100
185
 
101
- else:
102
- return ExecutionResult(
103
- ok=False, backend="unknown", tool=tool,
104
- error=f"Unknown tool '{tool}' — not a Remote Script command, "
105
- f"bridge command, or registered MCP tool",
106
- )
186
+ if backend == "mcp_tool":
187
+ fn = mcp_registry.get(tool) if mcp_registry else None
188
+ if fn is None:
189
+ return ExecutionResult(
190
+ ok=False, backend=backend, tool=tool,
191
+ error=(
192
+ f"MCP tool '{tool}' not registered in async router dispatch map. "
193
+ f"Add it to mcp_server.runtime.mcp_dispatch.build_mcp_dispatch_registry()."
194
+ ),
195
+ )
196
+ try:
197
+ sig = inspect.signature(fn)
198
+ kwargs = {"ctx": ctx} if "ctx" in sig.parameters else {}
199
+ call = fn(params, **kwargs)
200
+ result = await call if inspect.isawaitable(call) else call
201
+ if isinstance(result, dict) and "error" in result:
202
+ return ExecutionResult(ok=False, backend=backend, tool=tool, error=result["error"])
203
+ return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
204
+ except Exception as e:
205
+ return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
206
+
207
+ return ExecutionResult(
208
+ ok=False, backend="unknown", tool=tool,
209
+ error=(
210
+ f"Unknown tool '{tool}' — not a Remote Script command, "
211
+ f"bridge command, or registered MCP tool"
212
+ ),
213
+ )
107
214
 
108
215
 
109
- def execute_plan_steps(
216
+ async def execute_plan_steps_async(
110
217
  steps: list[dict],
111
218
  ableton: Any = None,
219
+ bridge: Any = None,
220
+ mcp_registry: Optional[dict[str, Callable]] = None,
112
221
  ctx: Any = None,
113
222
  stop_on_failure: bool = True,
114
223
  ) -> list[ExecutionResult]:
115
- """Execute a list of plan steps, returning results for each.
224
+ """Async plan executor with step-result binding and correct bridge transport.
116
225
 
117
- Stops on first failure by default. Set stop_on_failure=False
118
- to continue past errors (useful for best-effort execution).
226
+ Supports three backends:
227
+ - remote_command via ableton.send_command (sync TCP client)
228
+ - bridge_command via bridge.send_command (async UDP M4L bridge client)
229
+ - mcp_tool via mcp_registry[tool](params, ctx=ctx)
230
+
231
+ Step-result binding:
232
+ Any step may carry an optional "step_id". Later steps may reference an
233
+ earlier result by setting a param to {"$from_step": "<id>", "path": "a.b"}.
234
+ The router walks params recursively and resolves bindings before dispatch.
235
+ Missing ids or missing paths fail that step with a clear error.
236
+
237
+ stop_on_failure: Stop the plan on the first failing step (default). Set to
238
+ False for best-effort execution (each result still recorded).
119
239
  """
120
240
  results: list[ExecutionResult] = []
241
+ step_results: dict[str, Any] = {}
242
+ mcp_registry = mcp_registry or {}
121
243
 
122
244
  for step in steps:
123
245
  tool = step.get("tool") or step.get("command", "")
124
- params = step.get("params") or step.get("args", {})
125
- # Honor declared backend from step annotations (PR5) if present
246
+ raw_params = step.get("params") or step.get("args", {}) or {}
247
+ step_id = step.get("step_id")
126
248
  declared_backend = step.get("backend")
127
249
 
128
250
  if not tool:
@@ -134,9 +256,29 @@ def execute_plan_steps(
134
256
  break
135
257
  continue
136
258
 
137
- result = execute_step(tool, params, ableton=ableton, ctx=ctx, declared_backend=declared_backend)
259
+ # Resolve any $from_step bindings in params BEFORE dispatch.
260
+ try:
261
+ params = _resolve_params(raw_params, step_results)
262
+ except ValueError as e:
263
+ results.append(ExecutionResult(
264
+ ok=False, backend="binding", tool=tool, error=str(e),
265
+ ))
266
+ if stop_on_failure:
267
+ break
268
+ continue
269
+
270
+ result = await _execute_step_async(
271
+ tool, params,
272
+ ableton=ableton, bridge=bridge,
273
+ mcp_registry=mcp_registry, ctx=ctx,
274
+ declared_backend=declared_backend,
275
+ )
138
276
  results.append(result)
139
277
 
278
+ # Record successful step result for future bindings
279
+ if result.ok and step_id and isinstance(result.result, dict):
280
+ step_results[step_id] = result.result
281
+
140
282
  if not result.ok and stop_on_failure:
141
283
  break
142
284
 
@@ -0,0 +1,46 @@
1
+ """Registry of in-process MCP tools callable from the async execution router.
2
+
3
+ These tools live as Python async functions in the MCP server — not TCP Remote
4
+ Script handlers and not M4L bridge commands. Plans that want to invoke them
5
+ go through this registry so the async router can dispatch them in-process.
6
+
7
+ Each entry is a thin wrapper around the real MCP tool import, keeping the
8
+ module cheap to import (no heavy server wiring until a caller actually
9
+ dispatches an MCP step).
10
+
11
+ To add a new in-process tool to plans:
12
+ 1. Add the tool name to MCP_TOOLS in execution_router.py so classify_step
13
+ returns "mcp_tool" for it.
14
+ 2. Add an _adapter function here that imports the real implementation and
15
+ adapts its kwargs from a plan-style params dict.
16
+ 3. Register the adapter in build_mcp_dispatch_registry.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any, Callable
22
+
23
+
24
+ async def _load_sample_to_simpler(params: dict, ctx: Any = None) -> dict:
25
+ """Adapter for mcp_server.tools.analyzer.load_sample_to_simpler.
26
+
27
+ Accepts the plan-step params dict and unpacks into the real tool's kwargs.
28
+ """
29
+ from ..tools.analyzer import load_sample_to_simpler
30
+ return await load_sample_to_simpler(
31
+ ctx,
32
+ track_index=int(params["track_index"]),
33
+ file_path=str(params["file_path"]),
34
+ device_index=int(params.get("device_index", 0)),
35
+ )
36
+
37
+
38
+ def build_mcp_dispatch_registry() -> dict[str, Callable]:
39
+ """Return the canonical registry of MCP-only tools for plan execution.
40
+
41
+ Callers (typically the server lifespan init) should call this once and
42
+ pass the registry to execute_plan_steps_async via the mcp_registry kwarg.
43
+ """
44
+ return {
45
+ "load_sample_to_simpler": _load_sample_to_simpler,
46
+ }
@@ -80,7 +80,10 @@ BRIDGE_COMMANDS: frozenset[str] = frozenset({
80
80
  "remove_warp_marker", "capture_audio", "capture_stop",
81
81
  "check_flucoma", "scrub_clip", "stop_scrub", "get_display_values",
82
82
  "get_plugin_params", "map_plugin_param", "get_plugin_presets",
83
- "load_sample_to_simpler",
83
+ # NOTE: load_sample_to_simpler used to live here, but it's actually an
84
+ # async Python MCP tool in mcp_server/tools/analyzer.py, not a bridge
85
+ # command. It has no case in livepilot_bridge.js and no @register handler
86
+ # in remote_script. See mcp_server/runtime/execution_router.MCP_TOOLS.
84
87
  })
85
88
 
86
89
  # Combined: all valid send_command targets
@@ -110,11 +110,16 @@ def get_session_kernel(
110
110
  memory_ok=True,
111
111
  )
112
112
 
113
- # Optional subcomponents — degrade gracefully
114
- ledger_summary = {}
115
- taste_graph = {}
116
- anti_prefs = []
117
- session_mem = []
113
+ # Optional subcomponents — degrade gracefully, but reach into the SAME
114
+ # session-scoped stores the public memory tools read/write via
115
+ # ctx.lifespan_context.setdefault(...). Creating fresh stores here meant
116
+ # users who recorded anti-preferences, session memory, or taste signals
117
+ # through the MCP tools always saw an empty kernel.
118
+ ledger_summary: dict = {}
119
+ taste_graph: dict = {}
120
+ anti_prefs: list = []
121
+ session_mem: list = []
122
+ kernel_warnings: list[str] = []
118
123
 
119
124
  try:
120
125
  from .action_ledger import SessionLedger
@@ -122,38 +127,42 @@ def get_session_kernel(
122
127
  if ledger is None:
123
128
  ledger = SessionLedger()
124
129
  ctx.lifespan_context["action_ledger"] = ledger
125
- if ledger:
126
- recent = ledger.get_recent_moves(limit=10)
127
- ledger_summary = {
128
- "total_moves": len(ledger._entries),
129
- "memory_candidate_count": len(ledger.get_memory_candidates()),
130
- "last_move": ledger.get_last_move().to_dict() if ledger.get_last_move() else None,
131
- "recent_moves": [entry.to_dict() for entry in recent],
132
- }
133
- except Exception:
134
- pass
135
-
130
+ recent = ledger.get_recent_moves(limit=10)
131
+ ledger_summary = {
132
+ "total_moves": len(ledger._entries),
133
+ "memory_candidate_count": len(ledger.get_memory_candidates()),
134
+ "last_move": ledger.get_last_move().to_dict() if ledger.get_last_move() else None,
135
+ "recent_moves": [entry.to_dict() for entry in recent],
136
+ }
137
+ except Exception as e:
138
+ kernel_warnings.append(f"ledger_unavailable: {e}")
139
+
140
+ # Taste graph + anti-prefs — share stores via lifespan_context, use the
141
+ # canonical build_taste_graph() so consumers see dimension_weights shape.
136
142
  try:
143
+ from ..memory.taste_graph import build_taste_graph
137
144
  from ..memory.taste_memory import TasteMemoryStore
138
- taste_store = TasteMemoryStore()
139
- taste_graph = {d.name: d.to_dict() for d in taste_store._dims.values()
140
- if d.evidence_count > 0}
141
- except Exception:
142
- pass
143
-
144
- try:
145
145
  from ..memory.anti_memory import AntiMemoryStore
146
- anti_store = AntiMemoryStore()
147
- anti_prefs = anti_store.list_all()
148
- except Exception:
149
- pass
146
+ from ..persistence.taste_store import PersistentTasteStore
147
+ taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
148
+ anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
149
+ persistent = ctx.lifespan_context.setdefault("persistent_taste", PersistentTasteStore())
150
+ graph = build_taste_graph(
151
+ taste_store=taste_store,
152
+ anti_store=anti_store,
153
+ persistent_store=persistent,
154
+ )
155
+ taste_graph = graph.to_dict()
156
+ anti_prefs = [p.to_dict() for p in anti_store.get_anti_preferences()]
157
+ except Exception as e:
158
+ kernel_warnings.append(f"taste_graph_unavailable: {e}")
150
159
 
151
160
  try:
152
161
  from ..memory.session_memory import SessionMemoryStore
153
- mem_store = SessionMemoryStore()
154
- session_mem = mem_store.recent(limit=10)
155
- except Exception:
156
- pass
162
+ mem_store = ctx.lifespan_context.setdefault("session_memory", SessionMemoryStore())
163
+ session_mem = [entry.to_dict() for entry in mem_store.get_recent(limit=10)]
164
+ except Exception as e:
165
+ kernel_warnings.append(f"session_memory_unavailable: {e}")
157
166
 
158
167
  kernel = build_session_kernel(
159
168
  session_info=session_info,
@@ -167,4 +176,18 @@ def get_session_kernel(
167
176
  anti_preferences=anti_prefs,
168
177
  )
169
178
 
170
- return kernel.to_dict()
179
+ # Populate routing hints from conductor when request context is available
180
+ if request_text.strip():
181
+ try:
182
+ from ..tools._conductor import classify_request
183
+ plan = classify_request(request_text)
184
+ kernel.recommended_engines = [r.engine for r in plan.routes[:3]]
185
+ kernel.recommended_workflow = plan.workflow_mode
186
+ except Exception as e:
187
+ kernel_warnings.append(f"conductor_routing_unavailable: {e}")
188
+
189
+ result_dict = kernel.to_dict()
190
+ if kernel_warnings:
191
+ # Additive — callers can ignore; debug-mode introspection benefits.
192
+ result_dict["warnings"] = kernel_warnings
193
+ return result_dict
@@ -14,8 +14,8 @@ SAMPLE_CHOP_RHYTHM = SemanticMove(
14
14
  targets={"groove": 0.5, "novelty": 0.3, "punch": 0.2},
15
15
  protect={"clarity": 0.6, "coherence": 0.5},
16
16
  risk_level="medium",
17
- compile_plan=[
18
- {"tool": "load_sample_to_simpler", "params": {"description": "Load sample into Simpler for slicing"}, "description": "Load into Simpler", "backend": "bridge_command"},
17
+ plan_template=[
18
+ {"tool": "load_sample_to_simpler", "params": {"description": "Load sample into Simpler for slicing"}, "description": "Load into Simpler", "backend": "mcp_tool"},
19
19
  {"tool": "set_simpler_playback_mode", "params": {"mode": "slice", "description": "Switch to slice mode for rhythmic chopping"}, "description": "Enable slice mode", "backend": "remote_command"},
20
20
  {"tool": "crop_simpler", "params": {"description": "Crop to rhythmically relevant region"}, "description": "Crop to useful region", "backend": "bridge_command"},
21
21
  ],
@@ -32,8 +32,8 @@ SAMPLE_TEXTURE_LAYER = SemanticMove(
32
32
  targets={"depth": 0.4, "motion": 0.3, "warmth": 0.3},
33
33
  protect={"clarity": 0.7, "punch": 0.5},
34
34
  risk_level="low",
35
- compile_plan=[
36
- {"tool": "load_sample_to_simpler", "params": {"description": "Load textural sample into Simpler"}, "description": "Load texture sample", "backend": "bridge_command"},
35
+ plan_template=[
36
+ {"tool": "load_sample_to_simpler", "params": {"description": "Load textural sample into Simpler"}, "description": "Load texture sample", "backend": "mcp_tool"},
37
37
  {"tool": "set_simpler_playback_mode", "params": {"mode": "classic", "description": "Classic mode for sustained texture playback"}, "description": "Classic playback", "backend": "remote_command"},
38
38
  {"tool": "set_device_parameter", "params": {"description": "Lower filter cutoff to sit beneath main elements"}, "description": "Filter for background placement", "backend": "remote_command"},
39
39
  {"tool": "set_track_send", "params": {"description": "Add reverb send for spatial depth"}, "description": "Reverb for depth", "backend": "remote_command"},
@@ -50,8 +50,8 @@ SAMPLE_VOCAL_GHOST = SemanticMove(
50
50
  targets={"novelty": 0.4, "depth": 0.3, "motion": 0.3},
51
51
  protect={"clarity": 0.5},
52
52
  risk_level="medium",
53
- compile_plan=[
54
- {"tool": "load_sample_to_simpler", "params": {"description": "Load vocal sample into Simpler"}, "description": "Load vocal", "backend": "bridge_command"},
53
+ plan_template=[
54
+ {"tool": "load_sample_to_simpler", "params": {"description": "Load vocal sample into Simpler"}, "description": "Load vocal", "backend": "mcp_tool"},
55
55
  {"tool": "reverse_simpler", "params": {"description": "Reverse for ghostly character"}, "description": "Reverse vocal", "backend": "bridge_command"},
56
56
  {"tool": "set_device_parameter", "params": {"description": "Detune -5 to -12 semitones for haunting pitch"}, "description": "Pitch down for ghost effect", "backend": "remote_command"},
57
57
  {"tool": "set_track_send", "params": {"description": "Heavy reverb send 40-60% for wash"}, "description": "Reverb wash", "backend": "remote_command"},
@@ -68,9 +68,9 @@ SAMPLE_BREAK_LAYER = SemanticMove(
68
68
  targets={"groove": 0.4, "punch": 0.3, "novelty": 0.3},
69
69
  protect={"coherence": 0.6, "clarity": 0.5},
70
70
  risk_level="medium",
71
- compile_plan=[
71
+ plan_template=[
72
72
  {"tool": "create_midi_track", "params": {"description": "New track for break layer"}, "description": "Create break track", "backend": "remote_command"},
73
- {"tool": "load_sample_to_simpler", "params": {"description": "Load breakbeat into Simpler"}, "description": "Load break", "backend": "bridge_command"},
73
+ {"tool": "load_sample_to_simpler", "params": {"description": "Load breakbeat into Simpler"}, "description": "Load break", "backend": "mcp_tool"},
74
74
  {"tool": "set_simpler_playback_mode", "params": {"mode": "slice", "slice_by": "transient", "description": "Slice by transients for individual hits"}, "description": "Slice break by transients", "backend": "remote_command"},
75
75
  {"tool": "set_track_volume", "params": {"description": "Set break layer volume below main drums"}, "description": "Balance break level", "backend": "remote_command"},
76
76
  ],
@@ -87,8 +87,8 @@ SAMPLE_RESAMPLE_DESTROY = SemanticMove(
87
87
  targets={"novelty": 0.5, "motion": 0.3, "groove": 0.2},
88
88
  protect={"coherence": 0.4},
89
89
  risk_level="high",
90
- compile_plan=[
91
- {"tool": "load_sample_to_simpler", "params": {"description": "Load sample for destruction"}, "description": "Load source material", "backend": "bridge_command"},
90
+ plan_template=[
91
+ {"tool": "load_sample_to_simpler", "params": {"description": "Load sample for destruction"}, "description": "Load source material", "backend": "mcp_tool"},
92
92
  {"tool": "warp_simpler", "params": {"description": "Extreme warp settings for time-stretch artifacts"}, "description": "Warp for artifacts", "backend": "bridge_command"},
93
93
  {"tool": "set_device_parameter", "params": {"description": "Add Redux or bitcrusher for lo-fi destruction"}, "description": "Bitcrush/reduce", "backend": "remote_command"},
94
94
  {"tool": "set_device_parameter", "params": {"description": "Saturator drive to maximum for harmonic distortion"}, "description": "Saturate heavily", "backend": "remote_command"},
@@ -105,8 +105,8 @@ SAMPLE_ONE_SHOT_ACCENT = SemanticMove(
105
105
  targets={"punch": 0.4, "groove": 0.3, "novelty": 0.3},
106
106
  protect={"clarity": 0.6, "coherence": 0.5},
107
107
  risk_level="low",
108
- compile_plan=[
109
- {"tool": "load_sample_to_simpler", "params": {"description": "Load one-shot into Simpler"}, "description": "Load one-shot", "backend": "bridge_command"},
108
+ plan_template=[
109
+ {"tool": "load_sample_to_simpler", "params": {"description": "Load one-shot into Simpler"}, "description": "Load one-shot", "backend": "mcp_tool"},
110
110
  {"tool": "set_simpler_playback_mode", "params": {"mode": "one_shot", "description": "One-shot mode for trigger playback"}, "description": "One-shot mode", "backend": "remote_command"},
111
111
  {"tool": "crop_simpler", "params": {"description": "Tight crop around the transient"}, "description": "Crop to transient", "backend": "bridge_command"},
112
112
  ],