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.
Files changed (58) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +25 -6
  3. package/bin/livepilot.js +4 -2
  4. package/installer/install.js +21 -12
  5. package/livepilot/.Codex-plugin/plugin.json +1 -1
  6. package/livepilot/.claude-plugin/plugin.json +1 -1
  7. package/livepilot/skills/livepilot-core/SKILL.md +8 -8
  8. package/livepilot/skills/livepilot-core/references/overview.md +2 -2
  9. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  10. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  11. package/m4l_device/livepilot_bridge.js +1 -1
  12. package/mcp_server/__init__.py +1 -1
  13. package/mcp_server/atlas/__init__.py +20 -1
  14. package/mcp_server/atlas/tools.py +83 -22
  15. package/mcp_server/composer/full/apply.py +9 -0
  16. package/mcp_server/composer/full/layer_planner.py +16 -6
  17. package/mcp_server/creative_constraints/engine.py +46 -0
  18. package/mcp_server/experiment/engine.py +10 -3
  19. package/mcp_server/grader/client.py +30 -2
  20. package/mcp_server/grader/tools.py +8 -2
  21. package/mcp_server/hook_hunter/analyzer.py +15 -2
  22. package/mcp_server/m4l_bridge.py +19 -21
  23. package/mcp_server/memory/taste_graph.py +16 -0
  24. package/mcp_server/memory/technique_store.py +23 -3
  25. package/mcp_server/mix_engine/state_builder.py +4 -2
  26. package/mcp_server/musical_intelligence/detectors.py +11 -2
  27. package/mcp_server/musical_intelligence/tools.py +15 -2
  28. package/mcp_server/preview_studio/engine.py +30 -1
  29. package/mcp_server/project_brain/tools.py +56 -52
  30. package/mcp_server/reference_engine/tools.py +22 -2
  31. package/mcp_server/runtime/live_version.py +27 -8
  32. package/mcp_server/runtime/safety_kernel.py +11 -0
  33. package/mcp_server/sample_engine/critics.py +7 -3
  34. package/mcp_server/sample_engine/slice_workflow.py +3 -2
  35. package/mcp_server/sample_engine/tools.py +53 -21
  36. package/mcp_server/song_brain/builder.py +17 -1
  37. package/mcp_server/sound_design/tools.py +1 -1
  38. package/mcp_server/splice_client/http_bridge.py +43 -1
  39. package/mcp_server/splice_client/models.py +7 -3
  40. package/mcp_server/synthesis_brain/adapters/analog.py +13 -1
  41. package/mcp_server/synthesis_brain/adapters/operator.py +5 -2
  42. package/mcp_server/synthesis_brain/adapters/wavetable.py +4 -4
  43. package/mcp_server/tools/_composition_engine/models.py +6 -0
  44. package/mcp_server/tools/_composition_engine/sections.py +4 -0
  45. package/mcp_server/tools/clips.py +5 -4
  46. package/mcp_server/tools/composition.py +5 -1
  47. package/mcp_server/tools/midi_io.py +40 -1
  48. package/mcp_server/tools/transport.py +1 -1
  49. package/mcp_server/user_corpus/plugin_engine/detector.py +66 -11
  50. package/mcp_server/user_corpus/runner.py +7 -1
  51. package/mcp_server/user_corpus/scanners/amxd.py +24 -13
  52. package/mcp_server/user_corpus/tools.py +45 -9
  53. package/mcp_server/wonder_mode/tools.py +66 -27
  54. package/package.json +1 -1
  55. package/remote_script/LivePilot/__init__.py +1 -1
  56. package/remote_script/LivePilot/server.py +23 -3
  57. package/requirements.txt +1 -1
  58. 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
- - **Known gap (v1.25):** `KnowledgePack.atlas_candidates_per_role` is an empty stub the agent currently falls through to `search_browser` filename matching instead of consulting the indexed atlas. This is BUG-FULL-MODE-24 and is the headline feature of v1.25.
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
- - `atlas_candidates_per_role` — **stubbed in v1.25.0**, will be populated in v1.25
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 [`livepilot.mcpb`](https://github.com/dreamrec/LivePilot/releases/latest) and double-click it.
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
- claude plugin add github:dreamrec/LivePilot/plugin
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/plugin
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 Remote Script host (default: 127.0.0.1)");
827
- console.log(" LIVE_MCP_PORT Remote Script port (default: 9878)");
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
 
@@ -46,23 +46,32 @@ function _assertSafeInstallPath(resolvedPath, candidates) {
46
46
  "/Applications/Ableton",
47
47
  "C:\\ProgramData\\Ableton",
48
48
  ];
49
- // The detected Ableton candidate paths are always considered safe
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 (path.resolve(c.path).startsWith(path.resolve(resolvedPath))) {
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
- const safe = allowedPrefixes.some((p) => resolvedPath.startsWith(path.resolve(p)));
59
- if (!safe) {
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.0",
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.0",
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.0):
202
- - **mix** — `tighten_low_end`, `widen_stereo`, `make_punchier`, `darken_without_losing_width`, `reduce_repetition_fatigue`, `make_kick_bass_lock`, `reduce_foreground_competition`
203
- - **arrangement** — `refresh_repeated_section`, plus structural moves defined alongside mix
204
- - **transition** — `create_buildup_tension`, `smooth_scene_handoff`, `increase_contrast_before_payoff`, `bridge_sections`, `increase_forward_motion`, `open_chorus`, `create_breakdown`
205
- - **sound_design** — `add_warmth`, `add_texture`, `shape_transients`, `add_space`
206
- - **performance** — `recover_energy`, `decompress_tension`, `safe_spotlight`, `emergency_simplify`
207
- - **device_creation** — `create_chaos_modulator`, `create_feedback_resonator`, `create_wavefolder_effect`, `create_bitcrusher_effect`, `create_karplus_string`, `create_stochastic_texture`, `create_fdn_reverb` (procedural M4L device generation)
208
- - **sample** — `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`)
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.0 — Architecture & Tool Reference
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 (30) — Real-time DSP analysis (requires LivePilot Analyzer M4L device on master track)
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.0:**
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.0";
37
+ var VERSION = "1.27.1";
38
38
 
39
39
  // ── State ──────────────────────────────────────────────────────────────────
40
40
 
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.27.0"
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
- self._by_name[dev_name.lower()] = dev
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
- # Search all non-`packs` namespaces; `packs` is already in the bundled atlas
113
- for ns in idx.list_namespaces():
114
- if ns == "packs":
115
- continue
116
- overlay_results.extend(idx.search(query, namespace=ns, limit=limit))
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
- return entry
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
- for ns in idx.list_namespaces():
262
- if ns == "packs":
263
- continue
264
- hits = idx.search(query, namespace=ns, limit=5)
265
- for entry in hits:
266
- overlay_suggestions.append({
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
- "source": f"user_overlay:{entry.namespace}",
273
- "note": "Load via search_browser or extension_atlas_get; no Live URI.",
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
- # Reload if not already loaded
666
- import mcp_server.atlas as atlas_mod
667
- if atlas_mod._atlas_instance is None:
668
- atlas_mod._atlas_instance = AtlasManager(atlas_path)
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": atlas_mod._atlas_instance.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": [_serialize_overlay_entry(e) for e in matches],
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
- raise ValueError(
412
- "plan_sections() requires explicit section_template in v1.24+. "
413
- "SECTION_TEMPLATES was removed — the LLM provides form, not the "
414
- "framework. Pass a section_template list or use the v1.24 full-mode "
415
- "compose flow which is LLM-creative."
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 all successful steps back to checkpoint. Undo is a remote_command,
251
- # route it through the normal ableton.send_command path for simplicity.
252
- for _ in range(steps_executed):
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: