livepilot 1.27.0 → 1.27.1
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.
- package/CHANGELOG.md +48 -0
- package/README.md +25 -6
- package/bin/livepilot.js +4 -2
- package/installer/install.js +21 -12
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/SKILL.md +8 -8
- package/livepilot/skills/livepilot-core/references/overview.md +2 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +20 -1
- package/mcp_server/atlas/tools.py +83 -22
- package/mcp_server/composer/full/apply.py +9 -0
- package/mcp_server/composer/full/layer_planner.py +16 -6
- package/mcp_server/creative_constraints/engine.py +46 -0
- package/mcp_server/experiment/engine.py +10 -3
- package/mcp_server/grader/client.py +30 -2
- package/mcp_server/grader/tools.py +8 -2
- package/mcp_server/hook_hunter/analyzer.py +15 -2
- package/mcp_server/m4l_bridge.py +19 -21
- package/mcp_server/memory/taste_graph.py +16 -0
- package/mcp_server/memory/technique_store.py +23 -3
- package/mcp_server/mix_engine/state_builder.py +4 -2
- package/mcp_server/musical_intelligence/detectors.py +11 -2
- package/mcp_server/musical_intelligence/tools.py +15 -2
- package/mcp_server/preview_studio/engine.py +30 -1
- package/mcp_server/project_brain/tools.py +56 -52
- package/mcp_server/reference_engine/tools.py +22 -2
- package/mcp_server/runtime/live_version.py +27 -8
- package/mcp_server/runtime/safety_kernel.py +11 -0
- package/mcp_server/sample_engine/critics.py +7 -3
- package/mcp_server/sample_engine/slice_workflow.py +3 -2
- package/mcp_server/sample_engine/tools.py +53 -21
- package/mcp_server/song_brain/builder.py +17 -1
- package/mcp_server/sound_design/tools.py +1 -1
- package/mcp_server/splice_client/http_bridge.py +43 -1
- package/mcp_server/splice_client/models.py +7 -3
- package/mcp_server/synthesis_brain/adapters/analog.py +13 -1
- package/mcp_server/synthesis_brain/adapters/operator.py +5 -2
- package/mcp_server/synthesis_brain/adapters/wavetable.py +4 -4
- package/mcp_server/tools/_composition_engine/models.py +6 -0
- package/mcp_server/tools/_composition_engine/sections.py +4 -0
- package/mcp_server/tools/clips.py +5 -4
- package/mcp_server/tools/composition.py +5 -1
- package/mcp_server/tools/midi_io.py +40 -1
- package/mcp_server/tools/transport.py +1 -1
- package/mcp_server/user_corpus/plugin_engine/detector.py +66 -11
- package/mcp_server/user_corpus/runner.py +7 -1
- package/mcp_server/user_corpus/scanners/amxd.py +24 -13
- package/mcp_server/user_corpus/tools.py +45 -9
- package/mcp_server/wonder_mode/tools.py +66 -27
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/server.py +23 -3
- package/requirements.txt +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.27.1 — 2026-06-21
|
|
4
|
+
|
|
5
|
+
Maintenance release: 35 verified fixes from a deep multi-agent audit, recursive installed-plugin scanning, and Windows-CI hardening. No change to the tool surface (467 tools / 56 domains).
|
|
6
|
+
|
|
7
|
+
### Restored — tools that were silently broken
|
|
8
|
+
- `augment_with_samples`, `get_composition_plan`, `propose_composer_branches` — crashed or returned nothing since the v1.24 refactor removed section templates; they now degrade to a single full-length section.
|
|
9
|
+
- `check_clip_key_consistency` — always returned `"unknown"` (removed FastMCP `.fn` accessor).
|
|
10
|
+
- `compare_phrase_renders` — returned an identical empty critique; now analyzes each render.
|
|
11
|
+
|
|
12
|
+
### Fixed — correctness
|
|
13
|
+
- Reference engine: project spectrum and stereo width are now populated (gap analysis previously ran against an all-zero project).
|
|
14
|
+
- `infer_section_purposes`: drops are no longer mislabeled as tension.
|
|
15
|
+
- Grader: master/return/group tracks identified by their real field names (group containers were inflating track counts).
|
|
16
|
+
- Hook salience: the memorability boost no longer fires on every candidate.
|
|
17
|
+
- Mix: the `flat_dynamics` critic is now reachable (`over_compressed` is a 3–6 dB band).
|
|
18
|
+
- Wavetable adapter uses the real `Osc 1 Pos` parameter name.
|
|
19
|
+
- Simpler slice playback uses the correct base note (36+N); `vibe_fit` energy proxy normalized.
|
|
20
|
+
- Harmony and phrase-grid analysis read from the correct clip slot.
|
|
21
|
+
- `exclude_globs` now match files inside named directories.
|
|
22
|
+
- A single shared technique store so in-session saves are visible to recall/search.
|
|
23
|
+
- Read-only prefix matcher no longer misclassifies mutating tools as safe.
|
|
24
|
+
- Atlas id/name collisions no longer shadow entries; the overlay index is no longer rescanned per namespace.
|
|
25
|
+
- AMXD device-type map recognizes MIDI Tool devices (Live 12.1+).
|
|
26
|
+
- `apply_full_plan_v2` postflight no longer deletes a reused existing track.
|
|
27
|
+
- Fatigue level no longer diluted by low-severity issues; energy-arc no longer desyncs on skipped scenes; `create_preview_set` no longer silently overwrites an existing set.
|
|
28
|
+
|
|
29
|
+
### Fixed — installed-plugin scanning (#44)
|
|
30
|
+
- The plugin scanner now recurses into vendor subfolders (e.g. `VST3/<Vendor>/Plugin.vst3`); nested plugins are found and vendor folders are no longer emitted as junk inventory records.
|
|
31
|
+
|
|
32
|
+
### Fixed — safety and resources
|
|
33
|
+
- Splice `is_free` misclassification that could bypass credit/quota gating.
|
|
34
|
+
- Experiment rollback undoes only `remote_command` steps (no longer reverts unrelated edits).
|
|
35
|
+
- Timed-out write commands are dropped instead of re-executing later on the main thread.
|
|
36
|
+
- Order-tolerant M4L bridge chunk reassembly (no permanent response loss on UDP reordering).
|
|
37
|
+
- Installer install-path guard hardened; Splice bearer token gated to splice.com HTTPS hosts; non-numeric Live version strings tolerated instead of crashing the capability probe.
|
|
38
|
+
|
|
39
|
+
### Performance
|
|
40
|
+
- Blocking sample I/O (SQLite, file decode, FFT, network) moved off the asyncio event loop.
|
|
41
|
+
- Capped previously-unbounded tool responses (`atlas_device_info`, `extension_atlas_search`, corpus synthesis briefs, piano-roll matrix, plugin `sdk_metadata`).
|
|
42
|
+
- Deduped redundant session round-trips (`enter_wonder_mode`, `build_project_brain`).
|
|
43
|
+
|
|
44
|
+
### CI and tooling
|
|
45
|
+
- Fixed the Windows CI matrix (cp1252 `UnicodeEncodeError` in verifier scripts; POSIX-path test fixture).
|
|
46
|
+
- `build_mcpb.sh` enforces `.mcpbignore`; portable dev scripts.
|
|
47
|
+
|
|
48
|
+
### Dependencies
|
|
49
|
+
- fastmcp, soundfile 0.14.0, grpcio 1.81.1, protobuf 7.35.1.
|
|
50
|
+
|
|
3
51
|
## v1.27.0 — 2026-06-16
|
|
4
52
|
|
|
5
53
|
Probe-first Live 12.4 capability release.
|
package/README.md
CHANGED
|
@@ -38,6 +38,20 @@
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
+
## What's New in v1.27.1
|
|
42
|
+
|
|
43
|
+
A maintenance release that fixes 35 issues from a deep audit and restores tools that had silently broken:
|
|
44
|
+
|
|
45
|
+
- **Restored tools** — `augment_with_samples`, `get_composition_plan`, `propose_composer_branches`, `check_clip_key_consistency`, and `compare_phrase_renders` were crashing or returning empty results; they now work.
|
|
46
|
+
- **Recursive plugin scanning (#44)** — the User Corpus scanner now finds plugins nested in vendor subfolders (e.g. `VST3/<Vendor>/Plugin.vst3`) instead of only the top level.
|
|
47
|
+
- **Truer analysis** — reference-gap, hook-salience, dynamics, drop-detection, harmony-slot, and grader fixes that previously produced fabricated or empty results.
|
|
48
|
+
- **Safer & faster** — Splice credit-gating fixed, blocking sample I/O moved off the event loop, unbounded tool responses capped, and the M4L bridge no longer loses responses on UDP reordering.
|
|
49
|
+
- **v1.27.0** added two read-only Live 12.4 capability-probe tools (`probe_link_audio`, `probe_stem_workflow`).
|
|
50
|
+
|
|
51
|
+
Full details in the [CHANGELOG](CHANGELOG.md).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
41
55
|
## What LivePilot Does
|
|
42
56
|
|
|
43
57
|
Most MCP servers are tool collections — they execute commands. LivePilot is an **agentic production system**. It has eight layers that work together:
|
|
@@ -473,7 +487,7 @@ Full track with song form: intro, verse, hook, breakdown, outro. Uses a two-phas
|
|
|
473
487
|
- Native arrangement clips via `create_native_arrangement_clip` (one clip per section, looped to fill section length)
|
|
474
488
|
- Zombie-track cleanup in postflight (removes tracks with no clips and no instrument device)
|
|
475
489
|
- Drum-role pitch repair ported from fast mode
|
|
476
|
-
- **
|
|
490
|
+
- **Atlas anchors:** role-level device knowledge is supplied through `KnowledgePack.atlas_anchors`, which consults the indexed atlas rather than filename matching. The older `atlas_candidates_per_role` field is a deprecated legacy stub, kept empty for back-compat.
|
|
477
491
|
- Invoke with: *"Write a full [genre] track at [tempo] BPM"* or *"Build a full arrangement"*
|
|
478
492
|
|
|
479
493
|
#### develop mode — `develop_apply`
|
|
@@ -492,7 +506,7 @@ All three modes share a `KnowledgePack` that provides structured creative contex
|
|
|
492
506
|
- `event_lexicon` — 42 structural events across 7 categories (drum density, harmonic, texture, vocal, rhythm feel, tension, fx gesture)
|
|
493
507
|
- `genre_context` — parses the 15-genre `genre-vocabularies.md` at load time
|
|
494
508
|
- `artist_context` — parses the ~25-producer `artist-vocabularies.md` at load time
|
|
495
|
-
- `
|
|
509
|
+
- `atlas_anchors` — indexed-atlas device anchors per role (replaced the legacy `atlas_candidates_per_role` stub)
|
|
496
510
|
|
|
497
511
|
#### Core composer tools
|
|
498
512
|
|
|
@@ -550,7 +564,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
|
|
|
550
564
|
|
|
551
565
|
### Easiest: Claude Desktop Extension (1 click)
|
|
552
566
|
|
|
553
|
-
Download
|
|
567
|
+
Download the latest `livepilot-<version>.mcpb` from the [Releases page](https://github.com/dreamrec/LivePilot/releases/latest) and double-click it.
|
|
554
568
|
Claude Desktop installs everything automatically. Then:
|
|
555
569
|
|
|
556
570
|
1. Open Ableton Live 12
|
|
@@ -584,10 +598,14 @@ Restart Ableton → Preferences → Link, Tempo & MIDI → Control Surface → *
|
|
|
584
598
|
<details>
|
|
585
599
|
<summary><strong>2. MCP Client</strong></summary>
|
|
586
600
|
|
|
587
|
-
**Claude Code
|
|
601
|
+
**Claude Code** — pick one (installing both registers the MCP server twice and collides on port 9878):
|
|
588
602
|
```bash
|
|
603
|
+
# MCP server only:
|
|
589
604
|
claude mcp add LivePilot -- npx livepilot
|
|
590
|
-
|
|
605
|
+
|
|
606
|
+
# Or the full plugin (MCP server + skills + slash commands):
|
|
607
|
+
claude plugin marketplace add github:dreamrec/LivePilot
|
|
608
|
+
claude plugin install livepilot@dreamrec-LivePilot
|
|
591
609
|
```
|
|
592
610
|
|
|
593
611
|
**Codex App:**
|
|
@@ -680,7 +698,8 @@ npx livepilot --install-codex-plugin
|
|
|
680
698
|
**Claude Code**
|
|
681
699
|
|
|
682
700
|
```bash
|
|
683
|
-
claude plugin add github:dreamrec/LivePilot
|
|
701
|
+
claude plugin marketplace add github:dreamrec/LivePilot
|
|
702
|
+
claude plugin install livepilot@dreamrec-LivePilot
|
|
684
703
|
```
|
|
685
704
|
|
|
686
705
|
| Command | What |
|
package/bin/livepilot.js
CHANGED
|
@@ -823,8 +823,10 @@ async function main() {
|
|
|
823
823
|
console.log(" --help Show this help");
|
|
824
824
|
console.log("");
|
|
825
825
|
console.log("Environment:");
|
|
826
|
-
console.log(" LIVE_MCP_HOST
|
|
827
|
-
console.log(" LIVE_MCP_PORT
|
|
826
|
+
console.log(" LIVE_MCP_HOST Remote Script host (default: 127.0.0.1)");
|
|
827
|
+
console.log(" LIVE_MCP_PORT Remote Script port (default: 9878)");
|
|
828
|
+
console.log(" LIVEPILOT_AUTO_INSTALL Auto-install Remote Script on launch (set to 'true')");
|
|
829
|
+
console.log(" LIVEPILOT_TCP_PORT Override Remote Script port (Desktop Extension/MCPB)");
|
|
828
830
|
return;
|
|
829
831
|
}
|
|
830
832
|
|
package/installer/install.js
CHANGED
|
@@ -46,23 +46,32 @@ function _assertSafeInstallPath(resolvedPath, candidates) {
|
|
|
46
46
|
"/Applications/Ableton",
|
|
47
47
|
"C:\\ProgramData\\Ableton",
|
|
48
48
|
];
|
|
49
|
-
//
|
|
49
|
+
// True iff `child` equals `root` or is a descendant of it. The path.sep
|
|
50
|
+
// suffix prevents `/Applications/Ableton-evil` from matching the
|
|
51
|
+
// `/Applications/Ableton` prefix (prefix-boundary check).
|
|
52
|
+
const within = (child, root) => {
|
|
53
|
+
const c = path.resolve(child);
|
|
54
|
+
const r = path.resolve(root);
|
|
55
|
+
return c === r || c.startsWith(r + path.sep);
|
|
56
|
+
};
|
|
57
|
+
// The destination must be a detected Ableton Remote Scripts dir, a descendant
|
|
58
|
+
// of one, or under a known-safe prefix. We deliberately do NOT treat an
|
|
59
|
+
// *ancestor* of a candidate as safe — that previously let
|
|
60
|
+
// LIVEPILOT_INSTALL_PATH=/ or /Applications slip through (candidate path
|
|
61
|
+
// "starts with" the broad parent), defeating the guard.
|
|
50
62
|
for (const c of candidates) {
|
|
51
|
-
if (
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (path.resolve(resolvedPath).startsWith(path.resolve(c.path))) {
|
|
63
|
+
if (within(resolvedPath, c.path)) {
|
|
55
64
|
return;
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
throw new InstallerAbort(
|
|
61
|
-
`LIVEPILOT_INSTALL_PATH=${resolvedPath} is outside permitted directories. ` +
|
|
62
|
-
`Refusing to install. Allowed roots: ${allowedPrefixes.join(", ")}`,
|
|
63
|
-
{ recoverable: false }
|
|
64
|
-
);
|
|
67
|
+
if (allowedPrefixes.some((p) => within(resolvedPath, p))) {
|
|
68
|
+
return;
|
|
65
69
|
}
|
|
70
|
+
throw new InstallerAbort(
|
|
71
|
+
`LIVEPILOT_INSTALL_PATH=${resolvedPath} is outside permitted directories. ` +
|
|
72
|
+
`Refusing to install. Allowed roots: ${allowedPrefixes.join(", ")}`,
|
|
73
|
+
{ recoverable: false }
|
|
74
|
+
);
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.1",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 \u2014 467 tools, 56 domains, 44 semantic moves, device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pilot Studio"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.1",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 \u2014 467 tools, 56 domains, 44 semantic moves, device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pilot Studio"
|
|
@@ -198,14 +198,14 @@ Use when the user wants options, variants, or is stuck ("surprise me", "try some
|
|
|
198
198
|
**Rule of thumb**: if the user asked for a specific fix, Flow A. If they asked "what would you do?" or mentioned feel/vibe without parameters, Flow B.
|
|
199
199
|
|
|
200
200
|
### Semantic Moves
|
|
201
|
-
High-level musical intents that compile to deterministic tool sequences. 7 families (44 moves as of v1.27.
|
|
202
|
-
- **mix** — `tighten_low_end`, `widen_stereo`, `make_punchier`, `darken_without_losing_width`, `reduce_repetition_fatigue`, `make_kick_bass_lock`, `
|
|
203
|
-
- **
|
|
204
|
-
- **
|
|
205
|
-
- **
|
|
206
|
-
- **performance** — `recover_energy`, `decompress_tension`, `safe_spotlight`, `emergency_simplify`
|
|
207
|
-
- **
|
|
208
|
-
- **
|
|
201
|
+
High-level musical intents that compile to deterministic tool sequences. 7 families (44 moves as of v1.27.1):
|
|
202
|
+
- **mix** (9) — `tighten_low_end`, `widen_stereo`, `make_punchier`, `darken_without_losing_width`, `reduce_repetition_fatigue`, `make_kick_bass_lock`, `set_track_routing`, `set_track_metadata`, `configure_send_architecture`
|
|
203
|
+
- **device_creation** (9) — `create_chaos_modulator`, `create_feedback_resonator`, `create_wavefolder_effect`, `create_bitcrusher_effect`, `create_karplus_string`, `create_stochastic_texture`, `create_fdn_reverb`, `create_drum_rack_pad`, `build_send_chain` (procedural M4L device generation)
|
|
204
|
+
- **sound_design** (7) — `add_warmth`, `add_texture`, `shape_transients`, `add_space`, `configure_device`, `remove_device`, `load_chord_source`
|
|
205
|
+
- **sample** (6) — `sample_chop_rhythm`, `sample_texture_layer`, `sample_vocal_ghost`, `sample_break_layer`, `sample_resample_destroy`, `sample_one_shot_accent` (registered from `sample_engine/moves.py`)
|
|
206
|
+
- **performance** (5) — `recover_energy`, `decompress_tension`, `safe_spotlight`, `emergency_simplify`, `configure_record_readiness`
|
|
207
|
+
- **arrangement** (4) — `create_buildup_tension`, `smooth_scene_handoff`, `configure_groove`, `set_scene_metadata`
|
|
208
|
+
- **transition** (4) — `bridge_sections`, `increase_forward_motion`, `open_chorus`, `create_breakdown`
|
|
209
209
|
|
|
210
210
|
Use `list_semantic_moves(domain="mix")` to discover available moves.
|
|
211
211
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# LivePilot v1.27.
|
|
1
|
+
# LivePilot v1.27.1 — Architecture & Tool Reference
|
|
2
2
|
|
|
3
3
|
Agentic production system for Ableton Live 12. 467 tools across 56 domains. Device atlas (5264 devices, 120 enriched, 47 with aesthetic-tagged `signature_techniques`), spectral perception (M4L analyzer with 9-band FFT — sub_low / sub / low / low_mid / mid / high_mid / high / presence / air), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O, **LIVE Splice describe-a-sound + variations via captured GraphQL endpoints (v1.17)**, drum-rack pad-by-pad construction, live dead-device detection via meter sampling, role-aware Simpler defaults, session-record arrangement-automation workaround.
|
|
4
4
|
|
|
@@ -218,7 +218,7 @@ This turns "set EQ band 3 to -4 dB" into "cut 400 Hz by 4 dB, then read the spec
|
|
|
218
218
|
| `memory_update` | Updates name, tags, or qualities | `technique_id`, `name`, `tags`, `qualities` |
|
|
219
219
|
| `memory_delete` | Removes technique (backs up first) | `technique_id` |
|
|
220
220
|
|
|
221
|
-
### Analyzer (
|
|
221
|
+
### Analyzer (38) — Real-time DSP analysis (requires LivePilot Analyzer M4L device on master track)
|
|
222
222
|
|
|
223
223
|
| Tool | What it does | Key params |
|
|
224
224
|
|------|-------------|------------|
|
|
@@ -173,7 +173,7 @@ Live 12.4 introduces a new capability tier that unlocks native LOM access for sa
|
|
|
173
173
|
**Tool signatures:** unchanged. Callers do not need to detect the tier —
|
|
174
174
|
routing is transparent.
|
|
175
175
|
|
|
176
|
-
**Probe-first 12.4 surfaces in v1.27.
|
|
176
|
+
**Probe-first 12.4 surfaces in v1.27.1:**
|
|
177
177
|
- `probe_link_audio()` reports observed peer/input/routing visibility and
|
|
178
178
|
returns `manual_only`, `readable`, `routable`, or `unavailable`.
|
|
179
179
|
- `probe_stem_workflow()` reports observed callable paths and returns
|
|
Binary file
|
|
@@ -34,7 +34,7 @@ outlets = 2; // 0: to udpsend (responses), 1: to buffer~/status
|
|
|
34
34
|
// Single source of truth for the bridge version — bumped alongside the
|
|
35
35
|
// rest of the release manifest. Surfaced in the UI via messnamed("livepilot_version", ...)
|
|
36
36
|
// so the frozen .amxd visibly reports which build it was last exported from.
|
|
37
|
-
var VERSION = "1.27.
|
|
37
|
+
var VERSION = "1.27.1";
|
|
38
38
|
|
|
39
39
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
40
40
|
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.27.
|
|
2
|
+
__version__ = "1.27.1"
|
|
@@ -7,9 +7,12 @@ suggestion, chain building, and device comparison.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
+
import logging
|
|
10
11
|
import os
|
|
11
12
|
from typing import Any, Dict, List, Optional
|
|
12
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class AtlasManager:
|
|
15
18
|
"""In-memory device atlas with indexed lookups."""
|
|
@@ -47,10 +50,26 @@ class AtlasManager:
|
|
|
47
50
|
dev_category = dev.get("category", "")
|
|
48
51
|
|
|
49
52
|
if dev_id:
|
|
53
|
+
if dev_id in self._by_id and self._by_id[dev_id] is not dev:
|
|
54
|
+
logger.warning(
|
|
55
|
+
"atlas: duplicate device id %r shadows a prior entry "
|
|
56
|
+
"(name=%r); last-wins", dev_id, dev_name,
|
|
57
|
+
)
|
|
50
58
|
self._by_id[dev_id] = dev
|
|
51
59
|
if dev_name:
|
|
52
|
-
|
|
60
|
+
name_key = dev_name.lower()
|
|
61
|
+
if name_key in self._by_name and self._by_name[name_key] is not dev:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"atlas: duplicate device name %r shadows a prior entry "
|
|
64
|
+
"(id=%r); last-wins", dev_name, dev_id,
|
|
65
|
+
)
|
|
66
|
+
self._by_name[name_key] = dev
|
|
53
67
|
if dev_uri:
|
|
68
|
+
if dev_uri in self._by_uri and self._by_uri[dev_uri] is not dev:
|
|
69
|
+
logger.warning(
|
|
70
|
+
"atlas: duplicate device uri %r shadows a prior entry "
|
|
71
|
+
"(id=%r); last-wins", dev_uri, dev_id,
|
|
72
|
+
)
|
|
54
73
|
self._by_uri[dev_uri] = dev
|
|
55
74
|
|
|
56
75
|
# Category index
|
|
@@ -109,11 +109,14 @@ def atlas_search(ctx: Context, query: str, category: str = "all", limit: int = 1
|
|
|
109
109
|
try:
|
|
110
110
|
from .overlays import get_overlay_index
|
|
111
111
|
idx = get_overlay_index()
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
# Single pass over the overlay index (namespace=None scans all
|
|
113
|
+
# namespaces at once), then drop `packs` client-side — `packs` is
|
|
114
|
+
# already covered by the bundled atlas. Avoids re-scanning the whole
|
|
115
|
+
# index once per namespace.
|
|
116
|
+
overlay_results = [
|
|
117
|
+
e for e in idx.search(query, namespace=None, limit=limit * 2)
|
|
118
|
+
if e.namespace != "packs"
|
|
119
|
+
]
|
|
117
120
|
except Exception: # noqa: BLE001 — never fail atlas_search over an overlay glitch
|
|
118
121
|
pass
|
|
119
122
|
|
|
@@ -177,10 +180,13 @@ def atlas_search(ctx: Context, query: str, category: str = "all", limit: int = 1
|
|
|
177
180
|
|
|
178
181
|
|
|
179
182
|
@mcp.tool()
|
|
180
|
-
def atlas_device_info(ctx: Context, device_id: str) -> dict:
|
|
183
|
+
def atlas_device_info(ctx: Context, device_id: str, verbose: bool = True) -> dict:
|
|
181
184
|
"""Get complete atlas knowledge about a device — parameters, recipes, pairings, gotchas.
|
|
182
185
|
|
|
183
186
|
device_id: the atlas ID or device name (e.g., "drift", "Compressor", "808_core_kit")
|
|
187
|
+
verbose: when True (default) return the full raw atlas record. Set False for a
|
|
188
|
+
compact summary (capped description, tag/technique counts) — useful
|
|
189
|
+
when you only need to identify the device, not read every recipe.
|
|
184
190
|
"""
|
|
185
191
|
atlas = _get_atlas()
|
|
186
192
|
if atlas is None:
|
|
@@ -189,7 +195,29 @@ def atlas_device_info(ctx: Context, device_id: str) -> dict:
|
|
|
189
195
|
entry = atlas.lookup(device_id)
|
|
190
196
|
if entry is None:
|
|
191
197
|
return {"error": f"Device '{device_id}' not found in atlas", "suggestion": "Use atlas_search to find devices"}
|
|
192
|
-
|
|
198
|
+
if verbose:
|
|
199
|
+
return entry
|
|
200
|
+
# Compact mode: the common case doesn't need the full multi-KB record.
|
|
201
|
+
# Truncate the longest free-text fields the way atlas_search already caps
|
|
202
|
+
# sonic_description, and report counts for list-heavy fields so the caller
|
|
203
|
+
# knows to re-request verbose for the full detail.
|
|
204
|
+
summary: dict = {
|
|
205
|
+
"id": entry.get("id", ""),
|
|
206
|
+
"name": entry.get("name", ""),
|
|
207
|
+
"uri": entry.get("uri", ""),
|
|
208
|
+
"category": entry.get("category", ""),
|
|
209
|
+
"enriched": bool(entry.get("enriched", False)),
|
|
210
|
+
"sonic_description": (
|
|
211
|
+
entry.get("sonic_description") or entry.get("description", "")
|
|
212
|
+
)[:_ATLAS_SEARCH_DESCRIPTION_CHAR_CAP],
|
|
213
|
+
"character_tags": (
|
|
214
|
+
entry.get("character_tags") or entry.get("tags", [])
|
|
215
|
+
)[:8],
|
|
216
|
+
"sweet_spot": str(entry.get("sweet_spot", ""))[:_ATLAS_SEARCH_DESCRIPTION_CHAR_CAP],
|
|
217
|
+
}
|
|
218
|
+
summary.update(_surface_enriched_fields(entry))
|
|
219
|
+
summary["verbose_available"] = True
|
|
220
|
+
return summary
|
|
193
221
|
|
|
194
222
|
|
|
195
223
|
@mcp.tool()
|
|
@@ -258,20 +286,20 @@ def atlas_chain_suggest(ctx: Context, role: str, genre: str = "") -> dict:
|
|
|
258
286
|
query_parts.append(genre)
|
|
259
287
|
query = " ".join(query_parts)
|
|
260
288
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
289
|
+
hits = [
|
|
290
|
+
e for e in idx.search(query, namespace=None, limit=10)
|
|
291
|
+
if e.namespace != "packs"
|
|
292
|
+
]
|
|
293
|
+
for entry in hits:
|
|
294
|
+
overlay_suggestions.append({
|
|
267
295
|
"namespace": entry.namespace,
|
|
268
296
|
"entity_id": entry.entity_id,
|
|
269
297
|
"name": entry.name,
|
|
270
298
|
"description": (entry.description or "")[:120],
|
|
271
299
|
"tags": list(entry.tags)[:5],
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
300
|
+
"source": f"user_overlay:{entry.namespace}",
|
|
301
|
+
"note": "Load via search_browser or extension_atlas_get; no Live URI.",
|
|
302
|
+
})
|
|
275
303
|
except Exception: # noqa: BLE001 — never fail chain_suggest over an overlay glitch
|
|
276
304
|
pass
|
|
277
305
|
|
|
@@ -662,14 +690,17 @@ def scan_full_library(
|
|
|
662
690
|
if not force and os.path.exists(atlas_path):
|
|
663
691
|
age = time.time() - os.path.getmtime(atlas_path)
|
|
664
692
|
if age < 86400:
|
|
665
|
-
#
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
693
|
+
# Use the public, thread-safe loader rather than poking the
|
|
694
|
+
# legacy `_atlas_instance` cell directly: get_atlas() loads
|
|
695
|
+
# lazily via the Singleton holder and is the only path the rest
|
|
696
|
+
# of the module actually reads back from. The old direct
|
|
697
|
+
# AtlasManager(atlas_path) construction wrote a cell get_atlas()
|
|
698
|
+
# never consults.
|
|
699
|
+
from . import get_atlas
|
|
669
700
|
return {
|
|
670
701
|
"status": "already_exists",
|
|
671
702
|
"age_hours": round(age / 3600, 1),
|
|
672
|
-
"device_count":
|
|
703
|
+
"device_count": get_atlas().device_count,
|
|
673
704
|
"message": "Atlas is recent. Use force=True to rescan.",
|
|
674
705
|
}
|
|
675
706
|
|
|
@@ -796,6 +827,34 @@ def _serialize_overlay_entry(entry) -> dict:
|
|
|
796
827
|
}
|
|
797
828
|
|
|
798
829
|
|
|
830
|
+
_OVERLAY_DESCRIPTION_PREVIEW = 240
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _serialize_overlay_entry_lite(entry) -> dict:
|
|
834
|
+
"""Lightweight search-result serializer: drops the full YAML `body`.
|
|
835
|
+
|
|
836
|
+
The `body` of an overlay entry can be ~17 KB; inlining it for every one of
|
|
837
|
+
up to `limit` matches makes a single extension_atlas_search return hundreds
|
|
838
|
+
of KB. The search path only needs identity + a description preview so the
|
|
839
|
+
caller can decide which entry to fetch in full via extension_atlas_get.
|
|
840
|
+
"""
|
|
841
|
+
description = entry.description or ""
|
|
842
|
+
truncated = len(description) > _OVERLAY_DESCRIPTION_PREVIEW
|
|
843
|
+
return {
|
|
844
|
+
"namespace": entry.namespace,
|
|
845
|
+
"entity_type": entry.entity_type,
|
|
846
|
+
"entity_id": entry.entity_id,
|
|
847
|
+
"name": entry.name,
|
|
848
|
+
"description": (
|
|
849
|
+
description[:_OVERLAY_DESCRIPTION_PREVIEW] + "…"
|
|
850
|
+
if truncated else description
|
|
851
|
+
),
|
|
852
|
+
"tags": entry.tags,
|
|
853
|
+
"artists": entry.artists,
|
|
854
|
+
"requires_box": entry.requires_box,
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
|
|
799
858
|
@mcp.tool()
|
|
800
859
|
def extension_atlas_search(ctx: Context, query: str,
|
|
801
860
|
namespace: str = "",
|
|
@@ -822,7 +881,9 @@ def extension_atlas_search(ctx: Context, query: str,
|
|
|
822
881
|
"namespace": namespace or None,
|
|
823
882
|
"entity_type": entity_type or None,
|
|
824
883
|
"count": len(matches),
|
|
825
|
-
"results": [
|
|
884
|
+
"results": [_serialize_overlay_entry_lite(e) for e in matches],
|
|
885
|
+
"note": "Search results omit the full YAML body; call "
|
|
886
|
+
"extension_atlas_get(namespace, entity_id) for the complete entry.",
|
|
826
887
|
}
|
|
827
888
|
|
|
828
889
|
|
|
@@ -719,6 +719,15 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
719
719
|
continue
|
|
720
720
|
else:
|
|
721
721
|
track_index = int(track_index)
|
|
722
|
+
# BUG-FIX (P2 zombie-cleanup): a reused/existing track must also be
|
|
723
|
+
# registered as "applied". Otherwise the postflight zombie-cleanup
|
|
724
|
+
# (which excludes only applied_track_indices) sees a reused track
|
|
725
|
+
# that currently has no session clips / instrument — e.g. one given
|
|
726
|
+
# only arrangement clips — as an empty leftover and DELETES it.
|
|
727
|
+
# Reused tracks must never be eligible for cleanup, and postflight
|
|
728
|
+
# monitoring must be restored on them too.
|
|
729
|
+
if track_index >= 0 and track_index not in applied_track_indices:
|
|
730
|
+
applied_track_indices.append(track_index)
|
|
722
731
|
|
|
723
732
|
# Optional instrument load
|
|
724
733
|
instrument = track_spec.get("instrument") or {}
|
|
@@ -408,12 +408,22 @@ def plan_sections(
|
|
|
408
408
|
Returns a list of dicts: {name, bars, layers, start_bar}.
|
|
409
409
|
"""
|
|
410
410
|
if section_template is None:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
411
|
+
# v1.24 removed the built-in SECTION_TEMPLATES registry (form is the
|
|
412
|
+
# LLM's job, not the framework's). But the deterministic scaffolding
|
|
413
|
+
# tools (augment_with_samples, get_composition_plan,
|
|
414
|
+
# propose_composer_branches) and wonder_mode still call this without a
|
|
415
|
+
# template. Degrade to a SINGLE full-length section containing every
|
|
416
|
+
# selected role — this is NOT a form template (no genre-keyed
|
|
417
|
+
# multi-section sequence, no module-level bar-count registry), it just
|
|
418
|
+
# lets those tools emit a working skeleton. Callers that want real form
|
|
419
|
+
# pass an explicit section_template.
|
|
420
|
+
roles = _select_roles(intent)
|
|
421
|
+
return [{
|
|
422
|
+
"name": "Full",
|
|
423
|
+
"bars": max(4, int(intent.duration_bars or 64)),
|
|
424
|
+
"layers": list(roles),
|
|
425
|
+
"start_bar": 0,
|
|
426
|
+
}]
|
|
417
427
|
|
|
418
428
|
template = section_template
|
|
419
429
|
|
|
@@ -72,9 +72,28 @@ def validate_plan_against_constraints(
|
|
|
72
72
|
session_info = session_info or {}
|
|
73
73
|
violations: list[str] = []
|
|
74
74
|
warnings: list[str] = []
|
|
75
|
+
unenforced: list[str] = []
|
|
75
76
|
|
|
76
77
|
steps = plan.get("steps", [])
|
|
77
78
|
|
|
79
|
+
# Modes that carry no structural step-level rule (taste/heuristic only).
|
|
80
|
+
# They were previously skipped silently — surface them as warnings so a
|
|
81
|
+
# caller can't mistake "no violations" for "validated against this mode".
|
|
82
|
+
advisory_modes = {
|
|
83
|
+
"mood_shift_without_new_fx": (
|
|
84
|
+
"shift the mood with existing tools — adding effects is discouraged "
|
|
85
|
+
"but not auto-detected here"
|
|
86
|
+
),
|
|
87
|
+
"make_it_stranger_but_keep_the_hook": (
|
|
88
|
+
"push novelty while preserving the hook — not enforceable from plan "
|
|
89
|
+
"steps alone"
|
|
90
|
+
),
|
|
91
|
+
"club_translation_safe": (
|
|
92
|
+
"keep changes club/DJ-friendly — judged by mix/tempo character, not "
|
|
93
|
+
"individual steps"
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
for constraint in constraint_set.constraints:
|
|
79
98
|
if constraint == "no_new_tracks":
|
|
80
99
|
for step in steps:
|
|
@@ -96,10 +115,37 @@ def validate_plan_against_constraints(
|
|
|
96
115
|
if step.get("action", "") in mix_actions:
|
|
97
116
|
violations.append(f"Step modifies mix ({step['action']}) — violates arrangement_only")
|
|
98
117
|
|
|
118
|
+
elif constraint == "use_loaded_devices_only":
|
|
119
|
+
load_actions = {"load_browser_item", "insert_device", "find_and_load_device",
|
|
120
|
+
"load_device_by_uri", "load_sample_to_simpler",
|
|
121
|
+
"replace_simpler_sample", "insert_rack_chain"}
|
|
122
|
+
for step in steps:
|
|
123
|
+
if step.get("action", "") in load_actions:
|
|
124
|
+
violations.append(
|
|
125
|
+
f"Step loads a new device ({step['action']}) — violates use_loaded_devices_only"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
elif constraint == "performance_safe_creative":
|
|
129
|
+
unsafe_actions = {"create_midi_track", "create_audio_track", "create_return_track",
|
|
130
|
+
"delete_track", "delete_device", "delete_clip", "delete_scene"}
|
|
131
|
+
for step in steps:
|
|
132
|
+
if step.get("action", "") in unsafe_actions:
|
|
133
|
+
violations.append(
|
|
134
|
+
f"Step is unsafe during live performance ({step['action']}) — "
|
|
135
|
+
f"violates performance_safe_creative"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
elif constraint in advisory_modes:
|
|
139
|
+
unenforced.append(constraint)
|
|
140
|
+
warnings.append(
|
|
141
|
+
f"{constraint} is advisory: {advisory_modes[constraint]}"
|
|
142
|
+
)
|
|
143
|
+
|
|
99
144
|
return {
|
|
100
145
|
"valid": len(violations) == 0,
|
|
101
146
|
"violations": violations,
|
|
102
147
|
"warnings": warnings,
|
|
148
|
+
"unenforced_constraints": unenforced,
|
|
103
149
|
"constraint_count": len(constraint_set.constraints),
|
|
104
150
|
}
|
|
105
151
|
|
|
@@ -244,12 +244,19 @@ async def run_branch_async(
|
|
|
244
244
|
]
|
|
245
245
|
|
|
246
246
|
steps_executed = sum(1 for r in exec_results if r.ok)
|
|
247
|
+
# Only remote_command steps land on Ableton's linear undo stack. Bridge
|
|
248
|
+
# (M4L/OSC) and mcp_tool mutations do NOT, so issuing one `undo` per
|
|
249
|
+
# successful step over-undoes and walks back unrelated prior user edits.
|
|
250
|
+
# Count undos from remote_command successes alone.
|
|
251
|
+
undo_count = sum(
|
|
252
|
+
1 for r in exec_results if r.ok and r.backend == "remote_command"
|
|
253
|
+
)
|
|
247
254
|
branch.executed_at_ms = int(time.time() * 1000)
|
|
248
255
|
branch.after_snapshot = capture_fn()
|
|
249
256
|
|
|
250
|
-
# Undo
|
|
251
|
-
#
|
|
252
|
-
for _ in range(
|
|
257
|
+
# Undo only the remote_command steps back to checkpoint. Undo is itself a
|
|
258
|
+
# remote_command, routed through the normal ableton.send_command path.
|
|
259
|
+
for _ in range(undo_count):
|
|
253
260
|
try:
|
|
254
261
|
ableton.send_command("undo", {})
|
|
255
262
|
except Exception as exc:
|