livepilot 1.9.14 → 1.9.16

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 (108) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +82 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +8 -8
  6. package/livepilot/.Codex-plugin/plugin.json +2 -2
  7. package/livepilot/.claude-plugin/plugin.json +2 -2
  8. package/livepilot/agents/livepilot-producer/AGENT.md +243 -49
  9. package/livepilot/skills/livepilot-core/SKILL.md +81 -6
  10. package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
  11. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  12. package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
  13. package/livepilot/skills/livepilot-release/SKILL.md +13 -13
  14. package/m4l_device/livepilot_bridge.js +32 -15
  15. package/mcp_server/__init__.py +1 -1
  16. package/mcp_server/connection.py +24 -2
  17. package/mcp_server/curves.py +14 -6
  18. package/mcp_server/evaluation/__init__.py +1 -0
  19. package/mcp_server/evaluation/fabric.py +575 -0
  20. package/mcp_server/evaluation/feature_extractors.py +84 -0
  21. package/mcp_server/evaluation/policy.py +67 -0
  22. package/mcp_server/evaluation/tools.py +53 -0
  23. package/mcp_server/m4l_bridge.py +9 -1
  24. package/mcp_server/memory/__init__.py +11 -2
  25. package/mcp_server/memory/anti_memory.py +78 -0
  26. package/mcp_server/memory/promotion.py +94 -0
  27. package/mcp_server/memory/session_memory.py +108 -0
  28. package/mcp_server/memory/taste_memory.py +158 -0
  29. package/mcp_server/memory/technique_store.py +27 -18
  30. package/mcp_server/memory/tools.py +112 -0
  31. package/mcp_server/mix_engine/__init__.py +1 -0
  32. package/mcp_server/mix_engine/critics.py +299 -0
  33. package/mcp_server/mix_engine/models.py +152 -0
  34. package/mcp_server/mix_engine/planner.py +103 -0
  35. package/mcp_server/mix_engine/state_builder.py +316 -0
  36. package/mcp_server/mix_engine/tools.py +220 -0
  37. package/mcp_server/performance_engine/__init__.py +1 -0
  38. package/mcp_server/performance_engine/models.py +148 -0
  39. package/mcp_server/performance_engine/planner.py +267 -0
  40. package/mcp_server/performance_engine/safety.py +165 -0
  41. package/mcp_server/performance_engine/tools.py +183 -0
  42. package/mcp_server/project_brain/__init__.py +6 -0
  43. package/mcp_server/project_brain/arrangement_graph.py +64 -0
  44. package/mcp_server/project_brain/automation_graph.py +72 -0
  45. package/mcp_server/project_brain/builder.py +123 -0
  46. package/mcp_server/project_brain/capability_graph.py +64 -0
  47. package/mcp_server/project_brain/models.py +282 -0
  48. package/mcp_server/project_brain/refresh.py +86 -0
  49. package/mcp_server/project_brain/role_graph.py +103 -0
  50. package/mcp_server/project_brain/session_graph.py +51 -0
  51. package/mcp_server/project_brain/tools.py +144 -0
  52. package/mcp_server/reference_engine/__init__.py +1 -0
  53. package/mcp_server/reference_engine/gap_analyzer.py +239 -0
  54. package/mcp_server/reference_engine/models.py +105 -0
  55. package/mcp_server/reference_engine/profile_builder.py +149 -0
  56. package/mcp_server/reference_engine/tactic_router.py +117 -0
  57. package/mcp_server/reference_engine/tools.py +236 -0
  58. package/mcp_server/runtime/__init__.py +1 -0
  59. package/mcp_server/runtime/action_ledger.py +117 -0
  60. package/mcp_server/runtime/action_ledger_models.py +91 -0
  61. package/mcp_server/runtime/action_tools.py +57 -0
  62. package/mcp_server/runtime/capability_state.py +219 -0
  63. package/mcp_server/runtime/safety_kernel.py +339 -0
  64. package/mcp_server/runtime/safety_tools.py +42 -0
  65. package/mcp_server/runtime/tools.py +67 -0
  66. package/mcp_server/server.py +17 -0
  67. package/mcp_server/sound_design/__init__.py +1 -0
  68. package/mcp_server/sound_design/critics.py +297 -0
  69. package/mcp_server/sound_design/models.py +147 -0
  70. package/mcp_server/sound_design/planner.py +104 -0
  71. package/mcp_server/sound_design/tools.py +297 -0
  72. package/mcp_server/tools/_agent_os_engine.py +947 -0
  73. package/mcp_server/tools/_composition_engine.py +1530 -0
  74. package/mcp_server/tools/_conductor.py +199 -0
  75. package/mcp_server/tools/_conductor_budgets.py +222 -0
  76. package/mcp_server/tools/_evaluation_contracts.py +91 -0
  77. package/mcp_server/tools/_form_engine.py +416 -0
  78. package/mcp_server/tools/_motif_engine.py +351 -0
  79. package/mcp_server/tools/_planner_engine.py +516 -0
  80. package/mcp_server/tools/_research_engine.py +542 -0
  81. package/mcp_server/tools/_research_provider.py +185 -0
  82. package/mcp_server/tools/_snapshot_normalizer.py +49 -0
  83. package/mcp_server/tools/agent_os.py +448 -0
  84. package/mcp_server/tools/analyzer.py +18 -0
  85. package/mcp_server/tools/automation.py +25 -10
  86. package/mcp_server/tools/composition.py +645 -0
  87. package/mcp_server/tools/devices.py +15 -1
  88. package/mcp_server/tools/midi_io.py +3 -1
  89. package/mcp_server/tools/motif.py +104 -0
  90. package/mcp_server/tools/planner.py +144 -0
  91. package/mcp_server/tools/research.py +223 -0
  92. package/mcp_server/tools/tracks.py +21 -6
  93. package/mcp_server/tools/transport.py +10 -2
  94. package/mcp_server/transition_engine/__init__.py +6 -0
  95. package/mcp_server/transition_engine/archetypes.py +167 -0
  96. package/mcp_server/transition_engine/critics.py +340 -0
  97. package/mcp_server/transition_engine/models.py +90 -0
  98. package/mcp_server/transition_engine/tools.py +291 -0
  99. package/mcp_server/translation_engine/__init__.py +5 -0
  100. package/mcp_server/translation_engine/critics.py +297 -0
  101. package/mcp_server/translation_engine/models.py +27 -0
  102. package/mcp_server/translation_engine/tools.py +108 -0
  103. package/package.json +2 -2
  104. package/remote_script/LivePilot/__init__.py +1 -1
  105. package/remote_script/LivePilot/arrangement.py +21 -3
  106. package/remote_script/LivePilot/clips.py +22 -6
  107. package/remote_script/LivePilot/notes.py +9 -1
  108. package/remote_script/LivePilot/server.py +6 -6
@@ -0,0 +1,108 @@
1
+ """Translation Engine MCP tools — 2 tools for playback robustness.
2
+
3
+ Each tool fetches data from Ableton via the shared connection,
4
+ then delegates to pure-computation critics.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from mcp.server.fastmcp import Context
10
+
11
+ from ..server import mcp
12
+ from .critics import build_translation_report, run_all_translation_critics
13
+
14
+
15
+ # ── Helpers ─────────────────────────────────────────────────────────
16
+
17
+
18
+ def _fetch_translation_data(ctx: Context) -> dict:
19
+ """Fetch mix snapshot data needed for translation analysis.
20
+
21
+ Builds snapshot from real available data:
22
+ - Spectrum from SpectralCache (direct access, not TCP)
23
+ - Stereo width estimated from track pan values
24
+ - Foreground detection from role inference
25
+ """
26
+ ableton = ctx.lifespan_context["ableton"]
27
+
28
+ # Get spectral data directly from SpectralCache
29
+ spectrum_bands = {}
30
+ try:
31
+ spectral = ctx.lifespan_context.get("spectral")
32
+ if spectral and spectral.is_connected:
33
+ spec_data = spectral.get("spectrum")
34
+ if spec_data and isinstance(spec_data["value"], dict):
35
+ spectrum_bands = spec_data["value"]
36
+ except Exception:
37
+ pass
38
+
39
+ # Estimate stereo width from track pans via session info
40
+ stereo_width = 0.0
41
+ center_strength = 0.5
42
+ has_foreground = True
43
+ foreground_masked = False
44
+ try:
45
+ session_info = ableton.send_command("get_session_info", {})
46
+ tracks = session_info.get("tracks", [])
47
+ if tracks:
48
+ # Pan may be at top level or nested under mixer.panning
49
+ def _get_pan(t: dict) -> float:
50
+ mixer = t.get("mixer")
51
+ if isinstance(mixer, dict):
52
+ return abs(mixer.get("panning", 0.0))
53
+ return abs(t.get("pan", 0.0))
54
+ pan_values = [_get_pan(t) for t in tracks if not t.get("muted", False)]
55
+ if pan_values:
56
+ # Wider pans = more stereo width
57
+ stereo_width = min(1.0, sum(pan_values) / max(len(pan_values), 1))
58
+ # Center strength = proportion of tracks near center
59
+ center_count = sum(1 for p in pan_values if p < 0.15)
60
+ center_strength = center_count / max(len(pan_values), 1)
61
+
62
+ # Simple foreground detection: at least one unmuted, non-quiet track
63
+ has_foreground = any(not t.get("muted", False) for t in tracks)
64
+ except Exception:
65
+ pass
66
+
67
+ return {
68
+ "stereo_width": stereo_width,
69
+ "center_strength": center_strength,
70
+ "sub_energy": spectrum_bands.get("sub", 0.0),
71
+ "low_energy": spectrum_bands.get("low", 0.0),
72
+ "low_mid_energy": spectrum_bands.get("low_mid", 0.0),
73
+ "high_energy": spectrum_bands.get("high", 0.0),
74
+ "presence_energy": spectrum_bands.get("presence", 0.0),
75
+ "has_foreground": has_foreground,
76
+ "foreground_masked": foreground_masked,
77
+ }
78
+
79
+
80
+ # ── MCP Tools ───────────────────────────────────────────────────────
81
+
82
+
83
+ @mcp.tool()
84
+ def check_translation(ctx: Context) -> dict:
85
+ """Check playback robustness — mono safety, small speakers, harshness.
86
+
87
+ Returns a full translation report with robustness classification
88
+ (robust/fragile/critical), boolean safety flags, and suggested
89
+ corrective moves.
90
+ """
91
+ mix_snapshot = _fetch_translation_data(ctx)
92
+ report = build_translation_report(mix_snapshot)
93
+ return report.to_dict()
94
+
95
+
96
+ @mcp.tool()
97
+ def get_translation_issues(ctx: Context) -> dict:
98
+ """Get just the translation issues without the full report.
99
+
100
+ Lighter than check_translation — returns only detected issues
101
+ from the 5 playback robustness critics.
102
+ """
103
+ mix_snapshot = _fetch_translation_data(ctx)
104
+ issues = run_all_translation_critics(mix_snapshot)
105
+ return {
106
+ "issues": [i.to_dict() for i in issues],
107
+ "issue_count": len(issues),
108
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.14",
3
+ "version": "1.9.16",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
+ "description": "Agentic production system for Ableton Live 12 — 236 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
6
6
  "author": "Pilot Studio",
7
7
  "license": "MIT",
8
8
  "type": "commonjs",
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
5
5
  when this script is selected in Preferences > Link, Tempo & MIDI.
6
6
  """
7
7
 
8
- __version__ = "1.9.14"
8
+ __version__ = "1.9.16"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from .server import LivePilotServer
@@ -352,6 +352,12 @@ def modify_arrangement_notes(song, params):
352
352
  note.velocity = float(mod["velocity"])
353
353
  if "probability" in mod:
354
354
  note.probability = float(mod["probability"])
355
+ if "mute" in mod:
356
+ note.mute = bool(mod["mute"])
357
+ if "velocity_deviation" in mod:
358
+ note.velocity_deviation = float(mod["velocity_deviation"])
359
+ if "release_velocity" in mod:
360
+ note.release_velocity = float(mod["release_velocity"])
355
361
  modified_count += 1
356
362
 
357
363
  song.begin_undo_step()
@@ -523,10 +529,20 @@ def set_arrangement_automation(song, params):
523
529
 
524
530
  # No fallback — direct envelope creation is the only safe approach.
525
531
  # Session-clip duplication can silently create overlapping clips.
532
+ #
533
+ # Known limitation: clips created via create_arrangement_clip
534
+ # (duplicate_clip_to_arrangement) often cannot have envelopes
535
+ # created programmatically. Workaround: record automation by
536
+ # arming the track and playing back with parameter changes,
537
+ # or use session-clip automation (set_clip_automation) and then
538
+ # record the session performance to arrangement.
526
539
  raise ValueError(
527
540
  "Cannot create automation envelope for parameter '%s' on this "
528
- "arrangement clip. Direct envelope access is not supported for "
529
- "this parameter type or Live version."
541
+ "arrangement clip. This is a known Live API limitation for "
542
+ "programmatically-created arrangement clips. Workarounds: "
543
+ "(1) use set_clip_automation on a session clip instead, "
544
+ "(2) record automation by arming the track during playback, "
545
+ "or (3) use the M4L bridge for deeper LOM envelope access."
530
546
  % parameter.name
531
547
  )
532
548
 
@@ -547,7 +563,9 @@ def transpose_arrangement_notes(song, params):
547
563
  clip = arr_clips[clip_index]
548
564
 
549
565
  from_time = float(params.get("from_time", 0.0))
550
- time_span = float(params.get("time_span", clip.length))
566
+ # Default span covers from from_time to end of clip, not the full clip length
567
+ default_span = max(0.0, clip.length - from_time) if clip.length > 0 else 32768.0
568
+ time_span = float(params.get("time_span", default_span))
551
569
 
552
570
  all_notes = clip.get_notes_extended(0, 128, from_time, time_span)
553
571
 
@@ -50,6 +50,11 @@ def create_clip(song, params):
50
50
  raise ValueError("Clip length must be > 0")
51
51
 
52
52
  clip_slot = get_clip_slot(song, track_index, clip_index)
53
+ if clip_slot.has_clip:
54
+ raise ValueError(
55
+ "Clip slot %d on track %d already has a clip. "
56
+ "Delete it first with delete_clip." % (clip_index, track_index)
57
+ )
53
58
  clip_slot.create_clip(length)
54
59
  clip = clip_slot.clip
55
60
 
@@ -147,12 +152,23 @@ def set_clip_loop(song, params):
147
152
  clip_index = int(params["clip_index"])
148
153
  clip = get_clip(song, track_index, clip_index)
149
154
 
150
- # Set end before start to avoid Live's loop_start < loop_end clamping.
151
- # Expanding the window first ensures the left edge can move freely.
152
- if "end" in params:
153
- clip.loop_end = float(params["end"])
154
- if "start" in params:
155
- clip.loop_start = float(params["start"])
155
+ # Conditional ordering to avoid Live's loop_start < loop_end clamping.
156
+ # When expanding the window, set the expanding edge first.
157
+ # When shrinking, set the contracting edge first.
158
+ new_end = float(params["end"]) if "end" in params else None
159
+ new_start = float(params["start"]) if "start" in params else None
160
+
161
+ if new_end is not None and new_end > clip.loop_end:
162
+ # Expanding right — set end first so start can move freely
163
+ clip.loop_end = new_end
164
+ if new_start is not None:
165
+ clip.loop_start = new_start
166
+ else:
167
+ # Shrinking or only changing start — set start first
168
+ if new_start is not None:
169
+ clip.loop_start = new_start
170
+ if new_end is not None:
171
+ clip.loop_end = new_end
156
172
  if "enabled" in params:
157
173
  clip.looping = bool(params["enabled"])
158
174
 
@@ -172,6 +172,12 @@ def modify_notes(song, params):
172
172
  note.velocity = float(mod["velocity"])
173
173
  if "probability" in mod:
174
174
  note.probability = float(mod["probability"])
175
+ if "mute" in mod:
176
+ note.mute = bool(mod["mute"])
177
+ if "velocity_deviation" in mod:
178
+ note.velocity_deviation = float(mod["velocity_deviation"])
179
+ if "release_velocity" in mod:
180
+ note.release_velocity = float(mod["release_velocity"])
175
181
  modified_count += 1
176
182
 
177
183
  # Pass the original NoteVector back — Boost.Python requires the C++ type
@@ -273,7 +279,9 @@ def transpose_notes(song, params):
273
279
  clip = get_clip(song, track_index, clip_index)
274
280
 
275
281
  from_time = float(params.get("from_time", 0.0))
276
- time_span = float(params.get("time_span", clip.length))
282
+ # Default span covers from from_time to end of clip, not the full clip length
283
+ default_span = max(0.0, clip.length - from_time) if clip.length > 0 else 32768.0
284
+ time_span = float(params.get("time_span", default_span))
277
285
 
278
286
  # Get notes — returns C++ NoteVector that must be passed back intact
279
287
  all_notes = clip.get_notes_extended(0, 128, from_time, time_span)
@@ -162,12 +162,12 @@ class LivePilotServer(object):
162
162
  pass
163
163
  continue
164
164
  self._client_connected = True
165
- self._client_thread = threading.Thread(
166
- target=self._run_client_session,
167
- args=(client, addr),
168
- )
169
- self._client_thread.daemon = True
170
- self._client_thread.start()
165
+ self._client_thread = threading.Thread(
166
+ target=self._run_client_session,
167
+ args=(client, addr),
168
+ )
169
+ self._client_thread.daemon = True
170
+ self._client_thread.start()
171
171
  except socket.timeout:
172
172
  continue
173
173
  except OSError: