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,120 @@
1
+ """Experiment branch data models.
2
+
3
+ An ExperimentBranch represents one trial of a semantic move against the
4
+ current session state. Multiple branches form an experiment set that can
5
+ be compared and ranked.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Optional
13
+
14
+
15
+ @dataclass
16
+ class BranchSnapshot:
17
+ """Captured state before or after a branch experiment."""
18
+ spectrum: Optional[dict] = None
19
+ rms: Optional[float] = None
20
+ peak: Optional[float] = None
21
+ track_meters: Optional[list] = None
22
+ timestamp_ms: int = 0
23
+
24
+ def to_dict(self) -> dict:
25
+ d = {}
26
+ if self.spectrum is not None:
27
+ d["spectrum"] = self.spectrum
28
+ if self.rms is not None:
29
+ d["rms"] = self.rms
30
+ if self.peak is not None:
31
+ d["peak"] = self.peak
32
+ if self.track_meters is not None:
33
+ d["track_meters"] = self.track_meters
34
+ d["timestamp_ms"] = self.timestamp_ms
35
+ return d
36
+
37
+
38
+ @dataclass
39
+ class ExperimentBranch:
40
+ """One trial branch in an experiment set."""
41
+ branch_id: str
42
+ name: str
43
+ move_id: str
44
+ source_kernel_id: str = ""
45
+ status: str = "pending" # pending | running | evaluated | committed | discarded
46
+
47
+ # Compiled plan for this branch
48
+ compiled_plan: Optional[dict] = None
49
+
50
+ # Captured snapshots
51
+ before_snapshot: Optional[BranchSnapshot] = None
52
+ after_snapshot: Optional[BranchSnapshot] = None
53
+
54
+ # Evaluation results
55
+ evaluation: Optional[dict] = None
56
+ score: float = 0.0
57
+
58
+ # Metadata
59
+ created_at_ms: int = 0
60
+ executed_at_ms: int = 0
61
+
62
+ def to_dict(self) -> dict:
63
+ d = {
64
+ "branch_id": self.branch_id,
65
+ "name": self.name,
66
+ "move_id": self.move_id,
67
+ "status": self.status,
68
+ "score": self.score,
69
+ "created_at_ms": self.created_at_ms,
70
+ }
71
+ if self.compiled_plan:
72
+ d["step_count"] = self.compiled_plan.get("step_count", 0)
73
+ d["summary"] = self.compiled_plan.get("summary", "")
74
+ if self.before_snapshot:
75
+ d["before_snapshot"] = self.before_snapshot.to_dict()
76
+ if self.after_snapshot:
77
+ d["after_snapshot"] = self.after_snapshot.to_dict()
78
+ if self.evaluation:
79
+ d["evaluation"] = self.evaluation
80
+ return d
81
+
82
+
83
+ @dataclass
84
+ class ExperimentSet:
85
+ """A collection of branches being compared for one request."""
86
+ experiment_id: str
87
+ request_text: str
88
+ branches: list[ExperimentBranch] = field(default_factory=list)
89
+ status: str = "open" # open | evaluated | committed | discarded
90
+ winner_branch_id: Optional[str] = None
91
+ created_at_ms: int = 0
92
+
93
+ @property
94
+ def branch_count(self) -> int:
95
+ return len(self.branches)
96
+
97
+ def get_branch(self, branch_id: str) -> Optional[ExperimentBranch]:
98
+ for b in self.branches:
99
+ if b.branch_id == branch_id:
100
+ return b
101
+ return None
102
+
103
+ def ranked_branches(self) -> list[ExperimentBranch]:
104
+ """Return branches sorted by score descending."""
105
+ evaluated = [b for b in self.branches if b.status == "evaluated"]
106
+ return sorted(evaluated, key=lambda b: -b.score)
107
+
108
+ def to_dict(self) -> dict:
109
+ return {
110
+ "experiment_id": self.experiment_id,
111
+ "request_text": self.request_text,
112
+ "status": self.status,
113
+ "branch_count": self.branch_count,
114
+ "branches": [b.to_dict() for b in self.branches],
115
+ "winner_branch_id": self.winner_branch_id,
116
+ "ranking": [
117
+ {"branch_id": b.branch_id, "name": b.name, "score": b.score}
118
+ for b in self.ranked_branches()
119
+ ],
120
+ }
@@ -0,0 +1,263 @@
1
+ """Experiment MCP tools — create, run, compare, commit, discard experiments.
2
+
3
+ 5 tools for branch-based creative search:
4
+ create_experiment — set up branches from semantic moves
5
+ run_experiment — trial each branch (apply → capture → undo)
6
+ compare_experiments — rank branches by evaluation score
7
+ commit_experiment — re-apply the winner permanently
8
+ discard_experiment — throw away all branches
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+ from typing import Optional
15
+
16
+ from fastmcp import Context
17
+
18
+ from ..server import mcp
19
+ from . import engine
20
+ from .models import BranchSnapshot
21
+
22
+
23
+ def _get_ableton(ctx: Context):
24
+ return ctx.lifespan_context["ableton"]
25
+
26
+
27
+ def _capture_snapshot(ctx: Context) -> BranchSnapshot:
28
+ """Capture current session state as a BranchSnapshot."""
29
+ ableton = _get_ableton(ctx)
30
+ spectral = ctx.lifespan_context.get("spectral")
31
+
32
+ snapshot = BranchSnapshot(timestamp_ms=int(time.time() * 1000))
33
+
34
+ # Track meters (always available)
35
+ try:
36
+ meters = ableton.send_command("get_track_meters", {"include_stereo": True})
37
+ snapshot.track_meters = meters.get("tracks", [])
38
+ except Exception:
39
+ pass
40
+
41
+ # Spectral data (requires M4L analyzer)
42
+ if spectral and spectral.is_connected:
43
+ try:
44
+ spec = spectral.get("spectrum")
45
+ if spec:
46
+ snapshot.spectrum = spec.get("value", {})
47
+ except Exception:
48
+ pass
49
+
50
+ try:
51
+ rms_data = spectral.get("rms")
52
+ if rms_data:
53
+ snapshot.rms = rms_data.get("value")
54
+ except Exception:
55
+ pass
56
+
57
+ return snapshot
58
+
59
+
60
+ @mcp.tool()
61
+ def create_experiment(
62
+ ctx: Context,
63
+ request_text: str,
64
+ move_ids: Optional[list] = None,
65
+ limit: int = 3,
66
+ ) -> dict:
67
+ """Create an experiment set to compare multiple approaches.
68
+
69
+ If move_ids is provided, creates one branch per move.
70
+ Otherwise, uses propose_next_best_move to find candidates.
71
+
72
+ request_text: what the user wants (e.g., "make this punchier")
73
+ move_ids: specific moves to try (e.g., ["make_punchier", "tighten_low_end"])
74
+ limit: max branches when auto-proposing (default 3)
75
+
76
+ Returns: experiment set with branch IDs ready for run_experiment.
77
+ """
78
+ if not move_ids:
79
+ # Auto-propose moves from the registry
80
+ from ..semantic_moves import registry
81
+ from ..semantic_moves.tools import propose_next_best_move
82
+ # Use the propose function's logic directly
83
+ all_moves = list(registry._REGISTRY.values())
84
+ request_lower = request_text.lower()
85
+ scored = []
86
+ for move in all_moves:
87
+ score = 0.0
88
+ move_words = set(move.move_id.replace("_", " ").split())
89
+ intent_words = set(move.intent.lower().split())
90
+ request_words = set(request_lower.split())
91
+ overlap = request_words & (move_words | intent_words)
92
+ score += len(overlap) * 0.3
93
+ for dim in move.targets:
94
+ if dim in request_lower:
95
+ score += 0.2
96
+ if score > 0.1:
97
+ scored.append((move.move_id, score))
98
+ scored.sort(key=lambda x: -x[1])
99
+ move_ids = [m[0] for m, _ in scored[:limit]] if scored else []
100
+
101
+ if not move_ids:
102
+ return {"error": "No matching semantic moves found for this request"}
103
+
104
+ # Build kernel_id from session
105
+ ableton = _get_ableton(ctx)
106
+ session = ableton.send_command("get_session_info")
107
+ kernel_id = f"kern_{int(time.time())}"
108
+
109
+ experiment = engine.create_experiment(
110
+ request_text=request_text,
111
+ move_ids=move_ids,
112
+ kernel_id=kernel_id,
113
+ )
114
+
115
+ return experiment.to_dict()
116
+
117
+
118
+ @mcp.tool()
119
+ def run_experiment(
120
+ ctx: Context,
121
+ experiment_id: str,
122
+ ) -> dict:
123
+ """Run all pending branches in an experiment.
124
+
125
+ For each branch:
126
+ 1. Compile the semantic move against current session
127
+ 2. Capture before state
128
+ 3. Execute the compiled plan
129
+ 4. Capture after state
130
+ 5. Undo all changes (revert to checkpoint)
131
+ 6. Evaluate the branch
132
+
133
+ Branches run sequentially (Ableton has linear undo).
134
+ """
135
+ experiment = engine.get_experiment(experiment_id)
136
+ if not experiment:
137
+ return {"error": f"Experiment {experiment_id} not found"}
138
+
139
+ ableton = _get_ableton(ctx)
140
+
141
+ # Import compiler
142
+ from ..semantic_moves import registry, compiler
143
+
144
+ results = []
145
+ for branch in experiment.branches:
146
+ if branch.status != "pending":
147
+ continue
148
+
149
+ # Compile the move
150
+ move = registry.get_move(branch.move_id)
151
+ if not move:
152
+ branch.status = "evaluated"
153
+ branch.score = 0.0
154
+ branch.evaluation = {"error": f"Move {branch.move_id} not found"}
155
+ results.append(branch.to_dict())
156
+ continue
157
+
158
+ session_info = ableton.send_command("get_session_info")
159
+ kernel = {"session_info": session_info, "mode": "explore"}
160
+ plan = compiler.compile(move, kernel)
161
+ compiled_dict = plan.to_dict()
162
+
163
+ # Run the branch (apply → capture → undo)
164
+ engine.run_branch(
165
+ branch=branch,
166
+ ableton=ableton,
167
+ compiled_plan=compiled_dict,
168
+ capture_fn=lambda: _capture_snapshot(ctx),
169
+ )
170
+
171
+ # Evaluate
172
+ def eval_fn(before, after):
173
+ # Simple heuristic evaluation when spectral data isn't available
174
+ score = 0.5 # Neutral
175
+ if before.get("track_meters") and after.get("track_meters"):
176
+ # Check all tracks still alive
177
+ before_alive = sum(1 for t in before["track_meters"] if t.get("level", 0) > 0)
178
+ after_alive = sum(1 for t in after["track_meters"] if t.get("level", 0) > 0)
179
+ if after_alive >= before_alive:
180
+ score += 0.1
181
+ else:
182
+ score -= 0.2 # Lost a track
183
+
184
+ if before.get("spectrum") and after.get("spectrum"):
185
+ # Spectral balance improvement heuristic
186
+ score += 0.1 # Bonus for having spectral data
187
+
188
+ return {"score": round(score, 3), "keep_change": score > 0.45}
189
+
190
+ engine.evaluate_branch(branch, eval_fn)
191
+ results.append(branch.to_dict())
192
+
193
+ return {
194
+ "experiment_id": experiment_id,
195
+ "branches_run": len(results),
196
+ "results": results,
197
+ "ranking": [
198
+ {"branch_id": b.branch_id, "name": b.name, "score": b.score, "move_id": b.move_id}
199
+ for b in experiment.ranked_branches()
200
+ ],
201
+ }
202
+
203
+
204
+ @mcp.tool()
205
+ def compare_experiments(
206
+ ctx: Context,
207
+ experiment_id: str,
208
+ ) -> dict:
209
+ """Compare and rank all evaluated branches in an experiment.
210
+
211
+ Returns branches sorted by score with their evaluations and summaries.
212
+ """
213
+ experiment = engine.get_experiment(experiment_id)
214
+ if not experiment:
215
+ return {"error": f"Experiment {experiment_id} not found"}
216
+
217
+ ranked = experiment.ranked_branches()
218
+ return {
219
+ "experiment_id": experiment_id,
220
+ "request": experiment.request_text,
221
+ "branch_count": experiment.branch_count,
222
+ "ranking": [
223
+ {
224
+ "rank": i + 1,
225
+ "branch_id": b.branch_id,
226
+ "name": b.name,
227
+ "move_id": b.move_id,
228
+ "score": b.score,
229
+ "summary": b.compiled_plan.get("summary", "") if b.compiled_plan else "",
230
+ "evaluation": b.evaluation,
231
+ }
232
+ for i, b in enumerate(ranked)
233
+ ],
234
+ "winner": ranked[0].to_dict() if ranked else None,
235
+ }
236
+
237
+
238
+ @mcp.tool()
239
+ def commit_experiment(
240
+ ctx: Context,
241
+ experiment_id: str,
242
+ branch_id: str,
243
+ ) -> dict:
244
+ """Commit the winning branch — re-apply its moves permanently.
245
+
246
+ This executes the branch's compiled plan again, this time without undoing.
247
+ The experiment is marked as committed.
248
+ """
249
+ experiment = engine.get_experiment(experiment_id)
250
+ if not experiment:
251
+ return {"error": f"Experiment {experiment_id} not found"}
252
+
253
+ ableton = _get_ableton(ctx)
254
+ return engine.commit_branch(experiment, branch_id, ableton)
255
+
256
+
257
+ @mcp.tool()
258
+ def discard_experiment(
259
+ ctx: Context,
260
+ experiment_id: str,
261
+ ) -> dict:
262
+ """Discard an entire experiment — no changes are kept."""
263
+ return engine.discard_experiment(experiment_id)
@@ -0,0 +1,5 @@
1
+ """Hook Hunter — melodic/rhythmic/timbral salience analysis for Stage 2.
2
+
3
+ Identifies what the track is most "about," ranks hook candidates,
4
+ and provides phrase-level payoff scoring.
5
+ """