livepilot 1.9.13 → 1.9.15

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 (105) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +51 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +7 -7
  6. package/bin/livepilot.js +32 -8
  7. package/installer/install.js +21 -2
  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 +243 -49
  11. package/livepilot/skills/livepilot-core/SKILL.md +81 -6
  12. package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
  13. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  14. package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
  15. package/livepilot/skills/livepilot-release/SKILL.md +13 -13
  16. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  17. package/m4l_device/livepilot_bridge.js +6 -3
  18. package/mcp_server/__init__.py +1 -1
  19. package/mcp_server/curves.py +11 -3
  20. package/mcp_server/evaluation/__init__.py +1 -0
  21. package/mcp_server/evaluation/fabric.py +575 -0
  22. package/mcp_server/evaluation/feature_extractors.py +84 -0
  23. package/mcp_server/evaluation/policy.py +67 -0
  24. package/mcp_server/evaluation/tools.py +53 -0
  25. package/mcp_server/memory/__init__.py +11 -2
  26. package/mcp_server/memory/anti_memory.py +78 -0
  27. package/mcp_server/memory/promotion.py +94 -0
  28. package/mcp_server/memory/session_memory.py +108 -0
  29. package/mcp_server/memory/taste_memory.py +158 -0
  30. package/mcp_server/memory/technique_store.py +2 -1
  31. package/mcp_server/memory/tools.py +112 -0
  32. package/mcp_server/mix_engine/__init__.py +1 -0
  33. package/mcp_server/mix_engine/critics.py +299 -0
  34. package/mcp_server/mix_engine/models.py +152 -0
  35. package/mcp_server/mix_engine/planner.py +103 -0
  36. package/mcp_server/mix_engine/state_builder.py +316 -0
  37. package/mcp_server/mix_engine/tools.py +214 -0
  38. package/mcp_server/performance_engine/__init__.py +1 -0
  39. package/mcp_server/performance_engine/models.py +148 -0
  40. package/mcp_server/performance_engine/planner.py +267 -0
  41. package/mcp_server/performance_engine/safety.py +162 -0
  42. package/mcp_server/performance_engine/tools.py +183 -0
  43. package/mcp_server/project_brain/__init__.py +6 -0
  44. package/mcp_server/project_brain/arrangement_graph.py +64 -0
  45. package/mcp_server/project_brain/automation_graph.py +72 -0
  46. package/mcp_server/project_brain/builder.py +123 -0
  47. package/mcp_server/project_brain/capability_graph.py +64 -0
  48. package/mcp_server/project_brain/models.py +282 -0
  49. package/mcp_server/project_brain/refresh.py +80 -0
  50. package/mcp_server/project_brain/role_graph.py +103 -0
  51. package/mcp_server/project_brain/session_graph.py +51 -0
  52. package/mcp_server/project_brain/tools.py +144 -0
  53. package/mcp_server/reference_engine/__init__.py +1 -0
  54. package/mcp_server/reference_engine/gap_analyzer.py +239 -0
  55. package/mcp_server/reference_engine/models.py +105 -0
  56. package/mcp_server/reference_engine/profile_builder.py +149 -0
  57. package/mcp_server/reference_engine/tactic_router.py +117 -0
  58. package/mcp_server/reference_engine/tools.py +235 -0
  59. package/mcp_server/runtime/__init__.py +1 -0
  60. package/mcp_server/runtime/action_ledger.py +117 -0
  61. package/mcp_server/runtime/action_ledger_models.py +84 -0
  62. package/mcp_server/runtime/action_tools.py +57 -0
  63. package/mcp_server/runtime/capability_state.py +218 -0
  64. package/mcp_server/runtime/safety_kernel.py +339 -0
  65. package/mcp_server/runtime/safety_tools.py +42 -0
  66. package/mcp_server/runtime/tools.py +64 -0
  67. package/mcp_server/server.py +23 -1
  68. package/mcp_server/sound_design/__init__.py +1 -0
  69. package/mcp_server/sound_design/critics.py +297 -0
  70. package/mcp_server/sound_design/models.py +147 -0
  71. package/mcp_server/sound_design/planner.py +104 -0
  72. package/mcp_server/sound_design/tools.py +297 -0
  73. package/mcp_server/tools/_agent_os_engine.py +947 -0
  74. package/mcp_server/tools/_composition_engine.py +1530 -0
  75. package/mcp_server/tools/_conductor.py +199 -0
  76. package/mcp_server/tools/_conductor_budgets.py +222 -0
  77. package/mcp_server/tools/_evaluation_contracts.py +91 -0
  78. package/mcp_server/tools/_form_engine.py +416 -0
  79. package/mcp_server/tools/_motif_engine.py +351 -0
  80. package/mcp_server/tools/_planner_engine.py +516 -0
  81. package/mcp_server/tools/_research_engine.py +542 -0
  82. package/mcp_server/tools/_research_provider.py +185 -0
  83. package/mcp_server/tools/_snapshot_normalizer.py +49 -0
  84. package/mcp_server/tools/agent_os.py +440 -0
  85. package/mcp_server/tools/analyzer.py +18 -0
  86. package/mcp_server/tools/automation.py +25 -10
  87. package/mcp_server/tools/composition.py +563 -0
  88. package/mcp_server/tools/motif.py +104 -0
  89. package/mcp_server/tools/planner.py +144 -0
  90. package/mcp_server/tools/research.py +223 -0
  91. package/mcp_server/tools/tracks.py +18 -3
  92. package/mcp_server/tools/transport.py +10 -2
  93. package/mcp_server/transition_engine/__init__.py +6 -0
  94. package/mcp_server/transition_engine/archetypes.py +167 -0
  95. package/mcp_server/transition_engine/critics.py +340 -0
  96. package/mcp_server/transition_engine/models.py +90 -0
  97. package/mcp_server/transition_engine/tools.py +291 -0
  98. package/mcp_server/translation_engine/__init__.py +5 -0
  99. package/mcp_server/translation_engine/critics.py +297 -0
  100. package/mcp_server/translation_engine/models.py +27 -0
  101. package/mcp_server/translation_engine/tools.py +74 -0
  102. package/package.json +2 -2
  103. package/remote_script/LivePilot/__init__.py +1 -1
  104. package/remote_script/LivePilot/arrangement.py +12 -2
  105. package/requirements.txt +1 -1
@@ -0,0 +1,416 @@
1
+ """Form Engine — radical structural transformations for arrangements.
2
+
3
+ Reorder sections, expand loops, compress verbose arrangements, insert bridges,
4
+ and propose new section graphs from transformation commands.
5
+
6
+ Zero external dependencies beyond stdlib.
7
+
8
+ Design: spec at docs/specs/2026-04-08-phase2-4-roadmap.md, Round 4 (4.4).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from copy import deepcopy
14
+ from dataclasses import asdict, dataclass, field
15
+ from typing import Optional
16
+
17
+ from ._composition_engine import SectionNode, SectionType
18
+
19
+
20
+ # ── Transformation Types ─────────────────────────────────────────────
21
+
22
+ VALID_TRANSFORMATIONS = frozenset({
23
+ "insert_bridge_before_final_chorus",
24
+ "swap_verse_positions",
25
+ "extend_section",
26
+ "compress_section",
27
+ "insert_breakdown",
28
+ "duplicate_section",
29
+ "remove_section",
30
+ "reverse_section_order",
31
+ "split_section",
32
+ })
33
+
34
+
35
+ @dataclass
36
+ class FormTransformation:
37
+ """A proposed structural transformation with before/after section graphs."""
38
+ transformation: str
39
+ target_section_index: Optional[int]
40
+ before_sections: list[SectionNode]
41
+ after_sections: list[SectionNode]
42
+ description: str
43
+ bar_delta: int # how many bars added (positive) or removed (negative)
44
+
45
+ def to_dict(self) -> dict:
46
+ return {
47
+ "transformation": self.transformation,
48
+ "target_section_index": self.target_section_index,
49
+ "before_section_count": len(self.before_sections),
50
+ "after_section_count": len(self.after_sections),
51
+ "description": self.description,
52
+ "bar_delta": self.bar_delta,
53
+ "after_sections": [s.to_dict() for s in self.after_sections],
54
+ }
55
+
56
+
57
+ # ── Core Transform Functions ─────────────────────────────────────────
58
+
59
+ def transform_section_order(
60
+ sections: list[SectionNode],
61
+ transformation: str,
62
+ target_index: Optional[int] = None,
63
+ bars: int = 8,
64
+ ) -> FormTransformation:
65
+ """Apply a structural transformation to the section graph.
66
+
67
+ sections: current section graph
68
+ transformation: one of VALID_TRANSFORMATIONS
69
+ target_index: which section to transform (for targeted operations)
70
+ bars: how many bars (for extend/compress/insert)
71
+
72
+ Returns: FormTransformation with the proposed new section graph.
73
+ """
74
+ if transformation not in VALID_TRANSFORMATIONS:
75
+ raise ValueError(
76
+ f"Unknown transformation '{transformation}'. "
77
+ f"Valid: {sorted(VALID_TRANSFORMATIONS)}"
78
+ )
79
+
80
+ if not sections:
81
+ raise ValueError("Cannot transform empty section graph")
82
+
83
+ # Deep copy to avoid mutating originals
84
+ original = sections
85
+ new_sections = deepcopy(sections)
86
+
87
+ if transformation == "insert_bridge_before_final_chorus":
88
+ return _insert_bridge_before_final_chorus(original, new_sections, bars)
89
+
90
+ elif transformation == "swap_verse_positions":
91
+ return _swap_verse_positions(original, new_sections)
92
+
93
+ elif transformation == "extend_section":
94
+ if target_index is None:
95
+ raise ValueError("extend_section requires target_index")
96
+ return _extend_section(original, new_sections, target_index, bars)
97
+
98
+ elif transformation == "compress_section":
99
+ if target_index is None:
100
+ raise ValueError("compress_section requires target_index")
101
+ return _compress_section(original, new_sections, target_index, bars)
102
+
103
+ elif transformation == "insert_breakdown":
104
+ if target_index is None:
105
+ target_index = _find_best_breakdown_position(new_sections)
106
+ return _insert_breakdown(original, new_sections, target_index, bars)
107
+
108
+ elif transformation == "duplicate_section":
109
+ if target_index is None:
110
+ raise ValueError("duplicate_section requires target_index")
111
+ return _duplicate_section(original, new_sections, target_index)
112
+
113
+ elif transformation == "remove_section":
114
+ if target_index is None:
115
+ raise ValueError("remove_section requires target_index")
116
+ return _remove_section(original, new_sections, target_index)
117
+
118
+ elif transformation == "reverse_section_order":
119
+ return _reverse_section_order(original, new_sections)
120
+
121
+ elif transformation == "split_section":
122
+ if target_index is None:
123
+ raise ValueError("split_section requires target_index")
124
+ return _split_section(original, new_sections, target_index)
125
+
126
+ # Should not reach here due to validation above
127
+ raise ValueError(f"Unhandled transformation: {transformation}")
128
+
129
+
130
+ def _reindex_sections(sections: list[SectionNode]) -> list[SectionNode]:
131
+ """Re-assign section_ids and adjust bar positions to be contiguous."""
132
+ current_bar = 0
133
+ for i, section in enumerate(sections):
134
+ length = section.end_bar - section.start_bar
135
+ section.section_id = f"sec_{i:02d}"
136
+ section.start_bar = current_bar
137
+ section.end_bar = current_bar + length
138
+ current_bar += length
139
+ return sections
140
+
141
+
142
+ def _total_bars(sections: list[SectionNode]) -> int:
143
+ return sections[-1].end_bar if sections else 0
144
+
145
+
146
+ # ── Individual Transformations ───────────────────────────────────────
147
+
148
+ def _insert_bridge_before_final_chorus(
149
+ original: list[SectionNode],
150
+ sections: list[SectionNode],
151
+ bridge_bars: int,
152
+ ) -> FormTransformation:
153
+ """Insert a bridge section before the last chorus/drop."""
154
+ # Find the last chorus or drop
155
+ last_climax_idx = None
156
+ for i in range(len(sections) - 1, -1, -1):
157
+ if sections[i].section_type in (SectionType.CHORUS, SectionType.DROP):
158
+ last_climax_idx = i
159
+ break
160
+
161
+ if last_climax_idx is None:
162
+ # No chorus/drop found — insert before the last section
163
+ last_climax_idx = len(sections) - 1
164
+
165
+ # Create bridge section
166
+ insert_bar = sections[last_climax_idx].start_bar
167
+ bridge = SectionNode(
168
+ section_id="bridge_new",
169
+ start_bar=insert_bar,
170
+ end_bar=insert_bar + bridge_bars,
171
+ section_type=SectionType.BRIDGE,
172
+ confidence=0.9,
173
+ energy=0.4,
174
+ density=0.3,
175
+ tracks_active=sections[last_climax_idx].tracks_active[:3], # Sparse
176
+ name="Bridge",
177
+ )
178
+
179
+ sections.insert(last_climax_idx, bridge)
180
+ sections = _reindex_sections(sections)
181
+
182
+ return FormTransformation(
183
+ transformation="insert_bridge_before_final_chorus",
184
+ target_section_index=last_climax_idx,
185
+ before_sections=original,
186
+ after_sections=sections,
187
+ description=f"Inserted {bridge_bars}-bar bridge before final climax",
188
+ bar_delta=bridge_bars,
189
+ )
190
+
191
+
192
+ def _swap_verse_positions(
193
+ original: list[SectionNode],
194
+ sections: list[SectionNode],
195
+ ) -> FormTransformation:
196
+ """Swap the first two verse sections for variety."""
197
+ verse_indices = [i for i, s in enumerate(sections) if s.section_type == SectionType.VERSE]
198
+
199
+ if len(verse_indices) < 2:
200
+ return FormTransformation(
201
+ transformation="swap_verse_positions",
202
+ target_section_index=None,
203
+ before_sections=original,
204
+ after_sections=sections,
205
+ description="Not enough verses to swap (need at least 2)",
206
+ bar_delta=0,
207
+ )
208
+
209
+ i, j = verse_indices[0], verse_indices[1]
210
+ sections[i], sections[j] = sections[j], sections[i]
211
+ sections = _reindex_sections(sections)
212
+
213
+ return FormTransformation(
214
+ transformation="swap_verse_positions",
215
+ target_section_index=verse_indices[0],
216
+ before_sections=original,
217
+ after_sections=sections,
218
+ description=f"Swapped verses at positions {i} and {j}",
219
+ bar_delta=0,
220
+ )
221
+
222
+
223
+ def _extend_section(
224
+ original: list[SectionNode],
225
+ sections: list[SectionNode],
226
+ target_index: int,
227
+ bars: int,
228
+ ) -> FormTransformation:
229
+ if target_index < 0 or target_index >= len(sections):
230
+ raise ValueError(f"target_index {target_index} out of range")
231
+
232
+ sections[target_index].end_bar += bars
233
+ sections = _reindex_sections(sections)
234
+
235
+ return FormTransformation(
236
+ transformation="extend_section",
237
+ target_section_index=target_index,
238
+ before_sections=original,
239
+ after_sections=sections,
240
+ description=f"Extended section {target_index} by {bars} bars",
241
+ bar_delta=bars,
242
+ )
243
+
244
+
245
+ def _compress_section(
246
+ original: list[SectionNode],
247
+ sections: list[SectionNode],
248
+ target_index: int,
249
+ bars: int,
250
+ ) -> FormTransformation:
251
+ if target_index < 0 or target_index >= len(sections):
252
+ raise ValueError(f"target_index {target_index} out of range")
253
+
254
+ current_length = sections[target_index].end_bar - sections[target_index].start_bar
255
+ new_length = max(4, current_length - bars) # Minimum 4 bars
256
+ actual_reduction = current_length - new_length
257
+ sections[target_index].end_bar = sections[target_index].start_bar + new_length
258
+ sections = _reindex_sections(sections)
259
+
260
+ return FormTransformation(
261
+ transformation="compress_section",
262
+ target_section_index=target_index,
263
+ before_sections=original,
264
+ after_sections=sections,
265
+ description=f"Compressed section {target_index} by {actual_reduction} bars",
266
+ bar_delta=-actual_reduction,
267
+ )
268
+
269
+
270
+ def _find_best_breakdown_position(sections: list[SectionNode]) -> int:
271
+ """Find the best position for a breakdown — after the highest energy section."""
272
+ if not sections:
273
+ return 0
274
+ max_energy_idx = max(range(len(sections)), key=lambda i: sections[i].energy)
275
+ return min(max_energy_idx + 1, len(sections))
276
+
277
+
278
+ def _insert_breakdown(
279
+ original: list[SectionNode],
280
+ sections: list[SectionNode],
281
+ position: int,
282
+ bars: int,
283
+ ) -> FormTransformation:
284
+ position = min(position, len(sections))
285
+ insert_bar = sections[position].start_bar if position < len(sections) else _total_bars(sections)
286
+
287
+ # Breakdown inherits tracks from surrounding section but reduces
288
+ ref_tracks = sections[min(position, len(sections) - 1)].tracks_active
289
+ breakdown = SectionNode(
290
+ section_id="bd_new",
291
+ start_bar=insert_bar,
292
+ end_bar=insert_bar + bars,
293
+ section_type=SectionType.BREAKDOWN,
294
+ confidence=0.9,
295
+ energy=0.25,
296
+ density=0.2,
297
+ tracks_active=ref_tracks[:2], # Very sparse
298
+ name="Breakdown",
299
+ )
300
+
301
+ sections.insert(position, breakdown)
302
+ sections = _reindex_sections(sections)
303
+
304
+ return FormTransformation(
305
+ transformation="insert_breakdown",
306
+ target_section_index=position,
307
+ before_sections=original,
308
+ after_sections=sections,
309
+ description=f"Inserted {bars}-bar breakdown at position {position}",
310
+ bar_delta=bars,
311
+ )
312
+
313
+
314
+ def _duplicate_section(
315
+ original: list[SectionNode],
316
+ sections: list[SectionNode],
317
+ target_index: int,
318
+ ) -> FormTransformation:
319
+ if target_index < 0 or target_index >= len(sections):
320
+ raise ValueError(f"target_index {target_index} out of range")
321
+
322
+ source = sections[target_index]
323
+ length = source.end_bar - source.start_bar
324
+ duplicate = deepcopy(source)
325
+ duplicate.name = f"{source.name} (repeat)" if source.name else ""
326
+
327
+ sections.insert(target_index + 1, duplicate)
328
+ sections = _reindex_sections(sections)
329
+
330
+ return FormTransformation(
331
+ transformation="duplicate_section",
332
+ target_section_index=target_index,
333
+ before_sections=original,
334
+ after_sections=sections,
335
+ description=f"Duplicated section {target_index} ({length} bars)",
336
+ bar_delta=length,
337
+ )
338
+
339
+
340
+ def _remove_section(
341
+ original: list[SectionNode],
342
+ sections: list[SectionNode],
343
+ target_index: int,
344
+ ) -> FormTransformation:
345
+ if target_index < 0 or target_index >= len(sections):
346
+ raise ValueError(f"target_index {target_index} out of range")
347
+ if len(sections) <= 1:
348
+ raise ValueError("Cannot remove the only section")
349
+
350
+ removed = sections.pop(target_index)
351
+ removed_bars = removed.end_bar - removed.start_bar
352
+ sections = _reindex_sections(sections)
353
+
354
+ return FormTransformation(
355
+ transformation="remove_section",
356
+ target_section_index=target_index,
357
+ before_sections=original,
358
+ after_sections=sections,
359
+ description=f"Removed section {target_index} ({removed_bars} bars)",
360
+ bar_delta=-removed_bars,
361
+ )
362
+
363
+
364
+ def _reverse_section_order(
365
+ original: list[SectionNode],
366
+ sections: list[SectionNode],
367
+ ) -> FormTransformation:
368
+ sections.reverse()
369
+ sections = _reindex_sections(sections)
370
+
371
+ return FormTransformation(
372
+ transformation="reverse_section_order",
373
+ target_section_index=None,
374
+ before_sections=original,
375
+ after_sections=sections,
376
+ description=f"Reversed order of {len(sections)} sections",
377
+ bar_delta=0,
378
+ )
379
+
380
+
381
+ def _split_section(
382
+ original: list[SectionNode],
383
+ sections: list[SectionNode],
384
+ target_index: int,
385
+ ) -> FormTransformation:
386
+ if target_index < 0 or target_index >= len(sections):
387
+ raise ValueError(f"target_index {target_index} out of range")
388
+
389
+ source = sections[target_index]
390
+ length = source.end_bar - source.start_bar
391
+ if length < 8:
392
+ raise ValueError(f"Section too short to split ({length} bars, minimum 8)")
393
+
394
+ midpoint = source.start_bar + length // 2
395
+
396
+ # First half keeps original type
397
+ first_half = deepcopy(source)
398
+ first_half.end_bar = midpoint
399
+
400
+ # Second half
401
+ second_half = deepcopy(source)
402
+ second_half.start_bar = midpoint
403
+ second_half.name = f"{source.name} B" if source.name else ""
404
+
405
+ sections[target_index] = first_half
406
+ sections.insert(target_index + 1, second_half)
407
+ sections = _reindex_sections(sections)
408
+
409
+ return FormTransformation(
410
+ transformation="split_section",
411
+ target_section_index=target_index,
412
+ before_sections=original,
413
+ after_sections=sections,
414
+ description=f"Split section {target_index} into two {length // 2}-bar halves",
415
+ bar_delta=0,
416
+ )