livepilot 1.20.2 → 1.21.0

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.
@@ -200,9 +200,166 @@ def _compile_emergency_simplify(move: SemanticMove, kernel: dict) -> CompiledPla
200
200
  )
201
201
 
202
202
 
203
+ def _compile_configure_record_readiness(move: SemanticMove, kernel: dict) -> CompiledPlan:
204
+ """Compile configure_record_readiness.
205
+
206
+ seed_args:
207
+ track_index: int — required; must be >= 0 (return tracks can't be armed)
208
+ armed: bool — required
209
+ exclusive: bool — optional, default False
210
+
211
+ Steps:
212
+ exclusive=True + armed=True
213
+ → N+1 steps: set_track_arm(other_idx, arm=False) for every
214
+ regular track ≠ target, then set_track_arm(target, arm=True).
215
+ — Emulates Ableton's exclusive-arm mode manually. Cannot use
216
+ ``set_exclusive_arm`` directly: ``song.exclusive_arm`` has
217
+ no Python setter in Live 12.4 (property getter only — a
218
+ pre-existing v1.20.3 Remote Script bug surfaced during v1.21's
219
+ live-test pre-flight). The manual disarm loop produces the
220
+ same user-facing outcome (target is the single armed track)
221
+ without depending on the broken toggle.
222
+ else
223
+ → [set_track_arm(track_index, arm=armed)]
224
+
225
+ Wire-format discipline: emit `arm` (not `armed`). The remote_command
226
+ backend bypasses the MCP tool rename layer (``tools/tracks.py:317``
227
+ renames ``armed → arm`` before send_command), so the compiler must
228
+ emit ``arm`` directly. See remote_script/LivePilot/tracks.py:263
229
+ for the Remote Script handler.
230
+ """
231
+ args = kernel.get("seed_args") or {}
232
+ track_index = args.get("track_index")
233
+ armed = args.get("armed")
234
+ exclusive = args.get("exclusive", False)
235
+
236
+ # Required-seed-args
237
+ if track_index is None or armed is None:
238
+ return CompiledPlan(
239
+ move_id=move.move_id,
240
+ intent=move.intent,
241
+ summary="missing required seed_args",
242
+ warnings=[
243
+ "configure_record_readiness requires seed_args.track_index "
244
+ "(int) and seed_args.armed (bool). Example: "
245
+ "apply_semantic_move(\"configure_record_readiness\", "
246
+ "mode=\"explore\", args={\"track_index\": 0, \"armed\": True})"
247
+ ],
248
+ )
249
+ if not isinstance(track_index, int):
250
+ return CompiledPlan(
251
+ move_id=move.move_id, intent=move.intent,
252
+ summary="invalid track_index type",
253
+ warnings=[f"track_index must be int, got {type(track_index).__name__}"],
254
+ )
255
+ if not isinstance(armed, bool):
256
+ return CompiledPlan(
257
+ move_id=move.move_id, intent=move.intent,
258
+ summary="invalid armed type",
259
+ warnings=[f"armed must be bool, got {type(armed).__name__}"],
260
+ )
261
+ if not isinstance(exclusive, bool):
262
+ return CompiledPlan(
263
+ move_id=move.move_id, intent=move.intent,
264
+ summary="invalid exclusive type",
265
+ warnings=[f"exclusive must be bool, got {type(exclusive).__name__}"],
266
+ )
267
+
268
+ # Contradiction: exclusive requires armed
269
+ if exclusive and not armed:
270
+ return CompiledPlan(
271
+ move_id=move.move_id, intent=move.intent,
272
+ summary="contradictory exclusive+armed",
273
+ warnings=[
274
+ "exclusive=True requires armed=True (the point of exclusive "
275
+ "is to become the single armed track); to disarm individually "
276
+ "call configure_record_readiness with exclusive=False"
277
+ ],
278
+ )
279
+
280
+ # Return-track constraint (Ableton's handler rejects negative indices)
281
+ if track_index < 0:
282
+ return CompiledPlan(
283
+ move_id=move.move_id, intent=move.intent,
284
+ summary="return tracks cannot be armed",
285
+ warnings=[
286
+ f"Cannot arm a return track (track_index={track_index}). "
287
+ "Ableton's set_track_arm handler rejects negative indices "
288
+ "(remote_script/LivePilot/tracks.py:261). Provide a regular "
289
+ "track index (>= 0)."
290
+ ],
291
+ )
292
+
293
+ steps: list[CompiledStep] = []
294
+ if exclusive and armed:
295
+ # Manual emulation of Ableton's exclusive-arm mode (set_exclusive_arm
296
+ # handler is broken in Live 12.4 per above docstring). Emit N+1
297
+ # steps: disarm every other regular track, then arm target.
298
+ all_tracks = kernel.get("session_info", {}).get("tracks", []) or []
299
+ if not all_tracks:
300
+ return CompiledPlan(
301
+ move_id=move.move_id, intent=move.intent,
302
+ summary="exclusive mode requires session_info.tracks",
303
+ warnings=[
304
+ "configure_record_readiness exclusive=True requires "
305
+ "session_info.tracks to know which other tracks to disarm. "
306
+ "apply_semantic_move builds session_info automatically; "
307
+ "direct compiler callers must supply it explicitly."
308
+ ],
309
+ )
310
+ for track in all_tracks:
311
+ idx = track.get("index")
312
+ if idx is None or idx == track_index:
313
+ continue
314
+ # Skip return / master — can't be armed anyway, and Ableton's
315
+ # set_track_arm rejects negative indices (tracks.py:261).
316
+ if track.get("type") in ("return", "master"):
317
+ continue
318
+ if isinstance(idx, int) and idx < 0:
319
+ continue
320
+ name = track.get("name", f"track {idx}")
321
+ steps.append(CompiledStep(
322
+ tool="set_track_arm",
323
+ params={"track_index": idx, "arm": False},
324
+ description=f"Disarm {name} (exclusive-arm emulation)",
325
+ backend="remote_command",
326
+ ))
327
+ steps.append(CompiledStep(
328
+ tool="set_track_arm",
329
+ params={"track_index": track_index, "arm": True},
330
+ description=(
331
+ f"Arm track {track_index} "
332
+ f"(exclusive — single-armed, {len(steps)} other(s) disarmed)"
333
+ ),
334
+ backend="remote_command",
335
+ ))
336
+ summary = (
337
+ f"Exclusive-arm track {track_index} — "
338
+ f"{len(steps)-1} other regular track(s) disarmed first"
339
+ )
340
+ else:
341
+ steps.append(CompiledStep(
342
+ tool="set_track_arm",
343
+ params={"track_index": track_index, "arm": armed},
344
+ description=f"{'Arm' if armed else 'Disarm'} track {track_index}",
345
+ backend="remote_command",
346
+ ))
347
+ summary = f"{'Arm' if armed else 'Disarm'} track {track_index}"
348
+
349
+ return CompiledPlan(
350
+ move_id=move.move_id,
351
+ intent=move.intent,
352
+ steps=steps,
353
+ risk_level=move.risk_level,
354
+ summary=summary,
355
+ requires_approval=False, # Performance moves execute immediately
356
+ )
357
+
358
+
203
359
  # ── Register ────────────────────────────────────────────────────────────────
204
360
 
205
361
  register_compiler("recover_energy", _compile_recover_energy)
206
362
  register_compiler("decompress_tension", _compile_decompress_tension)
207
363
  register_compiler("safe_spotlight", _compile_safe_spotlight)
208
364
  register_compiler("emergency_simplify", _compile_emergency_simplify)
365
+ register_compiler("configure_record_readiness", _compile_configure_record_readiness)
@@ -76,6 +76,51 @@ EMERGENCY_SIMPLIFY = SemanticMove(
76
76
  ],
77
77
  )
78
78
 
79
+ # v1.21: configure_record_readiness — closes the tech_debt entry from
80
+ # v1.20 live test 6 (raw set_track_arm without a semantic-move wrapper).
81
+ # seed_args: {track_index: int, armed: bool, exclusive?: bool = False}.
82
+ # Note: `armed` here is the *ergonomic* seed_arg name — the compiler
83
+ # translates it to the wire-format key `arm` per remote_script/LivePilot/
84
+ # tracks.py:263. See _compile_configure_record_readiness.
85
+ CONFIGURE_RECORD_READINESS = SemanticMove(
86
+ move_id="configure_record_readiness",
87
+ family="performance",
88
+ intent=(
89
+ "Arm or disarm a track for recording. When exclusive=True, disarms "
90
+ "all other regular tracks then arms the target — the standard "
91
+ "one-take recording setup (Live 12.4's `song.exclusive_arm` toggle "
92
+ "is read-only from Python, so the compiler emulates the mode via "
93
+ "a manual disarm loop)."
94
+ ),
95
+ targets={},
96
+ protect={"signal_integrity": 0.7},
97
+ risk_level="low",
98
+ required_capabilities=["session"],
99
+ plan_template=[
100
+ # Informational — compiler builds concrete steps from seed_args.
101
+ {
102
+ "tool": "set_track_arm",
103
+ "params": {"description": "Arm or disarm the target track"},
104
+ "description": "Toggle track arm",
105
+ "backend": "remote_command",
106
+ },
107
+ ],
108
+ verification_plan=[
109
+ {
110
+ "tool": "get_track_info",
111
+ "check": "track's arm field matches requested value",
112
+ "backend": "remote_command",
113
+ },
114
+ ],
115
+ )
116
+
117
+
79
118
  # Register all performance moves
80
- for _move in [RECOVER_ENERGY, DECOMPRESS_TENSION, SAFE_SPOTLIGHT, EMERGENCY_SIMPLIFY]:
119
+ for _move in [
120
+ RECOVER_ENERGY,
121
+ DECOMPRESS_TENSION,
122
+ SAFE_SPOTLIGHT,
123
+ EMERGENCY_SIMPLIFY,
124
+ CONFIGURE_RECORD_READINESS, # v1.21
125
+ ]:
81
126
  register(_move)
@@ -397,11 +397,14 @@ async def apply_semantic_move(
397
397
  success_count = sum(1 for s in executed_steps if s["ok"])
398
398
  failure_count = sum(1 for s in executed_steps if not s["ok"])
399
399
 
400
- # v1.20: write the executed move to the SessionLedger so
401
- # get_last_move / memory_list / anti-repetition-rules can see it
402
- # WITHOUT requiring the director to call add_session_memory
403
- # manually. Best-effort a ledger write failure must not fail
404
- # the overall move.
400
+ # store_purpose: writer
401
+ # v1.20: apply_semantic_move is the canonical semantic-moves writer
402
+ # to the SessionLedger. Downstream anti-repetition / stuckness /
403
+ # song-brain readers (annotated store_purpose: anti_repetition) consume
404
+ # entries this block writes. commit_experiment (v1.21) mirrors this
405
+ # pattern with a "composer|experiment" engine tag instead of
406
+ # "semantic_moves". Best-effort — a ledger write failure must not
407
+ # fail the overall move.
405
408
  ledger_entry_id: Optional[str] = None
406
409
  try:
407
410
  from ..runtime.action_ledger import SessionLedger
@@ -139,6 +139,12 @@ def _fetch_session_data(ctx: Context) -> dict:
139
139
  except Exception as exc:
140
140
  logger.debug("_fetch_session_data failed: %s", exc)
141
141
 
142
+ # store_purpose: anti_repetition
143
+ # song_brain's _fetch_session_data surfaces recent moves into the
144
+ # brain's context so section analysis can detect repeated work
145
+ # patterns. Recency signal — NOT the persistent technique library.
146
+ # Correct store: SessionLedger.get_recent_moves (v1.20 director SKILL
147
+ # previously pointed at memory_list for this, which was wrong).
142
148
  # Recent moves — from session-scoped action ledger
143
149
  try:
144
150
  from ..runtime.action_ledger import SessionLedger
@@ -27,6 +27,10 @@ def _get_action_history(ctx: Context) -> list[dict]:
27
27
  repeated undos, local-tweaking, loop-without-structure detection.
28
28
  Falls back to empty list when no ledger data exists (graceful degradation).
29
29
  """
30
+ # store_purpose: anti_repetition
31
+ # Stuckness detection reads recent_moves to spot repeated undos,
32
+ # local-tweaking loops, and loop-without-structure patterns.
33
+ # Recency signal — NOT the persistent technique library.
30
34
  try:
31
35
  from ..runtime.action_ledger import SessionLedger
32
36
  ledger = ctx.lifespan_context.get("action_ledger")
@@ -14,6 +14,12 @@ from typing import Any, Optional
14
14
  from .models import QUALITY_DIMENSIONS, _clamp
15
15
 
16
16
 
17
+ # store_purpose: technique_library
18
+ # analyze_outcome_history consumes payloads from the persistent
19
+ # technique library (memory_list(type="outcome")) — NOT recency data.
20
+ # Taste-inference work reads accumulated outcome records, unlike
21
+ # anti-repetition which reads SessionLedger.get_recent_moves.
22
+
17
23
  # ── Outcome Memory Analysis (Round 1) ────────────────────────────────
18
24
  def analyze_outcome_history(outcomes: list[dict]) -> dict:
19
25
  """Analyze accumulated outcome memories to identify user taste patterns.
@@ -1913,3 +1913,128 @@ async def compressor_set_sidechain(
1913
1913
  params["source_channel"] = str(source_channel)
1914
1914
  ableton = ctx.lifespan_context["ableton"]
1915
1915
  return ableton.send_command("set_compressor_sidechain", params)
1916
+
1917
+
1918
+ # ──────────────────────────────────────────────────────────────────────
1919
+ # v1.20.3 — ensure_analyzer_on_master
1920
+ #
1921
+ # Motivated by the v1.20.1 live-test campaign operator-error (see
1922
+ # ~/Desktop/DREAM AI/demo Project/REPORT.md). The LLM operator had a
1923
+ # clear global-memory instruction to load LivePilot_Analyzer on master
1924
+ # proactively on a fresh session, and missed it — leaving analyzer-
1925
+ # gated moves brittle. This tool closes that class of error by making
1926
+ # the load idempotent + automatable.
1927
+
1928
+ _ANALYZER_DEVICE_NAME = "LivePilot_Analyzer"
1929
+
1930
+
1931
+ def _load_analyzer_impl(ctx, track_index: int, device_name: str,
1932
+ allow_duplicate: bool = False) -> dict:
1933
+ """Indirection so tests can monkeypatch the load call without having
1934
+ to fake the full find_and_load_device MCP-tool machinery. Production
1935
+ calls straight through to the existing tool."""
1936
+ from .devices import find_and_load_device
1937
+ return find_and_load_device(
1938
+ ctx,
1939
+ track_index=track_index,
1940
+ device_name=device_name,
1941
+ allow_duplicate=allow_duplicate,
1942
+ )
1943
+
1944
+
1945
+ @mcp.tool()
1946
+ def ensure_analyzer_on_master(ctx: Context) -> dict:
1947
+ """Idempotent pre-flight: load LivePilot_Analyzer on master if missing.
1948
+
1949
+ Safe to call at the start of any session or before any move that
1950
+ declares analyzer dependency. Calling it repeatedly is cheap —
1951
+ subsequent calls short-circuit via a single get_master_track read.
1952
+
1953
+ CLAUDE.md invariant: "LivePilot_Analyzer must be LAST on master."
1954
+ This tool reports whether the invariant holds via ``is_last_on_master``;
1955
+ it does NOT move the device (that's a user action in Ableton's GUI).
1956
+
1957
+ Return shape:
1958
+ - status: one of {"already_loaded", "loaded", "install_required", "failed"}
1959
+ - device_index: int — position of the analyzer on master (when present)
1960
+ - is_last_on_master: bool — True when analyzer is the last device
1961
+ - duplicate_count: int — 2+ when multiple analyzers exist (shouldn't)
1962
+ - warning: str | None — surfaces last-on-master violations
1963
+ - hint: str — actionable next step when status != "already_loaded"/"loaded"
1964
+ - error: str | None — present on status="failed"
1965
+ """
1966
+ ableton = ctx.lifespan_context["ableton"]
1967
+
1968
+ # 1. Inspect the master chain for an existing analyzer.
1969
+ try:
1970
+ master = ableton.send_command("get_master_track")
1971
+ except Exception as exc:
1972
+ return {
1973
+ "status": "failed",
1974
+ "error": f"Could not read master track: {exc}",
1975
+ "hint": "Verify MCP connection to Ableton; retry with get_session_info first.",
1976
+ }
1977
+
1978
+ devices = (master or {}).get("devices") or []
1979
+ matches = [d for d in devices if d.get("name") == _ANALYZER_DEVICE_NAME]
1980
+
1981
+ if matches:
1982
+ # 2. Already loaded — build a status report without side effects.
1983
+ first = matches[0]
1984
+ device_index = first.get("index")
1985
+ is_last = False
1986
+ if devices:
1987
+ last_name = devices[-1].get("name")
1988
+ is_last = (last_name == _ANALYZER_DEVICE_NAME)
1989
+
1990
+ result: dict = {
1991
+ "status": "already_loaded",
1992
+ "device_index": device_index,
1993
+ "is_last_on_master": is_last,
1994
+ "duplicate_count": len(matches),
1995
+ }
1996
+ if len(matches) > 1:
1997
+ result["warning"] = (
1998
+ f"{len(matches)} instances of {_ANALYZER_DEVICE_NAME} on master — "
1999
+ "only one is needed. Remove extras in Ableton's GUI."
2000
+ )
2001
+ elif not is_last:
2002
+ result["warning"] = (
2003
+ f"{_ANALYZER_DEVICE_NAME} is not the LAST device on master. "
2004
+ "CLAUDE.md invariant requires it to come after ALL effects so "
2005
+ "it reads the final output, not pre-effect signal. "
2006
+ "Move it to the end of the master chain in Ableton's GUI."
2007
+ )
2008
+ return result
2009
+
2010
+ # 3. Not on master — try loading from the Ableton browser.
2011
+ try:
2012
+ loaded = _load_analyzer_impl(
2013
+ ctx,
2014
+ track_index=-1000, # master convention
2015
+ device_name=_ANALYZER_DEVICE_NAME,
2016
+ allow_duplicate=False,
2017
+ )
2018
+ except Exception as exc:
2019
+ # Typical path: device not in browser (user hasn't installed via
2020
+ # install_m4l_device yet).
2021
+ return {
2022
+ "status": "install_required",
2023
+ "error": str(exc),
2024
+ "hint": (
2025
+ "LivePilot_Analyzer not found in Ableton's browser. Install "
2026
+ "first with install_m4l_device(source_path="
2027
+ "\"<repo>/m4l_device/LivePilot_Analyzer.amxd\") — that copies "
2028
+ "the .amxd into ~/Music/Ableton/User Library/Presets/Audio "
2029
+ "Effects/Max Audio Effect/. Then call ensure_analyzer_on_master "
2030
+ "again to complete the load."
2031
+ ),
2032
+ }
2033
+
2034
+ device_index = (loaded or {}).get("device_index")
2035
+ return {
2036
+ "status": "loaded",
2037
+ "device_index": device_index,
2038
+ "is_last_on_master": True, # fresh load always lands at the end
2039
+ "duplicate_count": 1,
2040
+ }
@@ -150,6 +150,13 @@ def _generate_replay_steps(technique: dict) -> list[str]:
150
150
  return ["Replay the technique from the stored payload"]
151
151
 
152
152
 
153
+ # store_purpose: mcp_tool_definition
154
+ # memory_list is the MCP tool for browsing the persistent technique
155
+ # library (memory_learn-populated). Callers that use its output for
156
+ # anti-repetition recency have the v1.20 store-confusion BUG: correct
157
+ # pattern is SessionLedger.get_recent_moves or get_action_ledger_summary.
158
+ # The test tests/test_ledger_readers.py::TestAntiRepetitionUsesLedgerNotMemoryList
159
+ # enforces this invariant across the codebase.
153
160
  @mcp.tool()
154
161
  def memory_list(
155
162
  ctx: Context,
@@ -148,6 +148,10 @@ def _get_active_constraints():
148
148
 
149
149
  def _get_ledger_entries(ctx: Context) -> list[dict]:
150
150
  """Get recent action ledger entries as dicts."""
151
+ # store_purpose: anti_repetition
152
+ # Wonder Mode's rescue trigger reads recent_moves to feed the
153
+ # stuckness detector — classic recency signal, NOT the persistent
154
+ # technique library. Correct store: SessionLedger.get_recent_moves.
151
155
  try:
152
156
  from ..runtime.action_ledger import SessionLedger
153
157
  ledger: SessionLedger = ctx.lifespan_context.setdefault(
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.20.2",
3
+ "version": "1.21.0",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 429 tools, 53 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
5
+ "description": "Agentic production system for Ableton Live 12 — 430 tools, 53 domains, 43 semantic moves. Device atlas (1305 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, 12 creative intelligence engines",
6
6
  "author": "Pilot Studio",
7
7
  "license": "BSL-1.1",
8
8
  "type": "commonjs",
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
5
5
  when this script is selected in Preferences > Link, Tempo & MIDI.
6
6
  """
7
7
 
8
- __version__ = "1.20.2"
8
+ __version__ = "1.21.0"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
package/server.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.dreamrec/livepilot",
4
- "description": "429-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
4
+ "description": "430-tool agentic MCP production system for Ableton Live 12 — 53 domains, 43 semantic moves, device atlas (1305 devices), Splice intelligence (gRPC + GraphQL), 9-band spectral perception auto-loaded, Creative Director skill, technique memory, 12 creative engines",
5
5
  "repository": {
6
6
  "url": "https://github.com/dreamrec/LivePilot",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.20.2",
9
+ "version": "1.21.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.20.2",
14
+ "version": "1.21.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }