livepilot 1.19.1 → 1.20.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,158 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.0 — Item C phased cutover: 10 new semantic moves + Director Phase 6 rewrite (April 24 2026)
4
+
5
+ Implements the plan in `docs/plans/v1.20-structural-plan.md`. Ships 10
6
+ new semantic moves across four family-themed commits, rewrites the
7
+ creative director's Phase 6 to make `apply_semantic_move` the default
8
+ execution surface with a documented + tracked escape hatch, and hardens
9
+ three systemic issues surfaced during live pressure testing. Registry:
10
+ 33 → 43 moves across the same 7 canonical families. Full test suite:
11
+ 2858 → 2985 pass (+127, zero regressions).
12
+
13
+ ### New semantic moves (10 total)
14
+
15
+ **Routing family (`mcp_server/semantic_moves/routing_moves.py`):**
16
+ - `build_send_chain` (device_creation, medium risk) — load an ordered
17
+ chain of devices onto a return track; the Basic Channel / dub-techno
18
+ / ambient send-architecture primitive. `protect: low_end=0.6`.
19
+ - `configure_send_architecture` (mix, low risk) — set send levels
20
+ across multiple source tracks in one move.
21
+ - `set_track_routing` (mix, medium risk) — rewire a track's output
22
+ routing, e.g. "Sends Only" for bus architectures.
23
+
24
+ **Device-mutation family:**
25
+ - `configure_device` (sound_design, low risk) — bulk-configure N
26
+ parameters on an existing device in a single undoable move. Takes
27
+ `param_overrides: dict` (preset library deferred to v1.21).
28
+ - `remove_device` (sound_design, medium risk, protects
29
+ `signal_integrity=0.9`) — destructive removal with a required
30
+ `reason` string auto-logged to session memory for audit.
31
+
32
+ **Content family:**
33
+ - `load_chord_source` (sound_design, low risk) — create+voice+name a
34
+ MIDI chord clip in one move; feeds `build_send_chain` return chains.
35
+ - `create_drum_rack_pad` (device_creation, low risk) — add one pad to
36
+ a Drum Rack, Dilla-style kit-at-a-time.
37
+
38
+ **Metadata family:**
39
+ - `configure_groove` (arrangement, low risk) — the Dilla-swing
40
+ primitive; assigns a groove + optionally tunes its timing_amount.
41
+ - `set_scene_metadata` (arrangement, low risk) — conditional
42
+ name/color/tempo in one move.
43
+ - `set_track_metadata` (mix, low risk) — bundled rename + color, since
44
+ the two are always paired in Phase 6 usage.
45
+
46
+ ### Director SKILL — Phase 6 rewrite
47
+
48
+ - **Decision table (authoritative):** each uncovered-pattern row now
49
+ points at a specific v1.20 move (e.g. "Set multiple params on a
50
+ device" → `configure_device`). 10 NEW rows marked explicitly.
51
+ - **Default execution surface**: `apply_semantic_move` +
52
+ `commit_experiment`, replacing the pre-v1.20 "raw tools + manual
53
+ `add_session_memory(move_executed)` marker" pattern.
54
+ - **Escape hatch policy** (v1.20 transitional state): when no move
55
+ covers the pattern, raw-tool execution is permitted only with the
56
+ three-call discipline — the raw call, an `add_session_memory(
57
+ category="move_executed")` marker, AND an `add_session_memory(
58
+ category="tech_debt")` log naming the uncovered pattern. Both
59
+ categories are mandatory; they serve different consumers (ledger
60
+ vs release planning).
61
+ - **New reference doc** `phase-6-execution.md` (349 lines) — full
62
+ contract (seed_args, compiled steps, risk/protect, typical caller)
63
+ for each of the 10 moves, plus a worked escape-hatch example.
64
+
65
+ ### Architectural extension (commit 1)
66
+
67
+ `apply_semantic_move(args: dict)` and `preview_semantic_move(args: dict)`
68
+ now accept user seed parameters that flow into the compiler's kernel as
69
+ `kernel["seed_args"]`. Pre-v1.20 moves are unaffected (they read only
70
+ from `session_info`); the new routing/content/metadata moves read from
71
+ `seed_args` for user targets like `return_track_index`, `device_chain`,
72
+ `notes`, `track_index`, etc.
73
+
74
+ ### Live-test hardening (bugs caught during the 6 pressure-test gate)
75
+
76
+ **Wire-format compiler fix.** `configure_device` and `set_track_routing`
77
+ initially emitted MCP-tool-input keys (`parameter_name`,
78
+ `output_routing_type`) which the MCP tool layer would normalize — but
79
+ compiled plans use the `remote_command` backend that goes directly to
80
+ `ableton.send_command()`, bypassing the MCP tool entirely. Ableton's
81
+ Remote Script reads wire-format keys (`name_or_index`, `output_type`)
82
+ exclusively. Fix: both compilers emit wire format. New regression suite
83
+ `tests/test_compiler_wire_format_parity.py` — 10 parametrized cases,
84
+ one per v1.20 move, asserting every compiled step's params match the
85
+ Remote Script handler's actual key inventory.
86
+
87
+ **Automatic ledger write.** `apply_semantic_move` in explore mode now
88
+ writes a LedgerEntry to `SessionLedger` (family, intent, per-step
89
+ actions, provisional `kept=True`, `score = success_fraction`). Returns
90
+ a `ledger_entry_id` in the response so callers can correlate with
91
+ post-hoc `evaluate_move` evaluation. Pre-v1.20 docs pointed
92
+ anti-repetition at `memory_list` which actually reads the persistent
93
+ technique library — wrong store. Director SKILL now points at
94
+ `get_action_ledger_summary`. `commit_experiment` auto-ledger is v1.21
95
+ scope.
96
+
97
+ **Session memory categories.** `_VALID_CATEGORIES` in
98
+ `mcp_server/memory/session_memory.py` now includes the three v1.20
99
+ director Phase 6 categories: `move_executed`, `tech_debt`, and
100
+ `override`. Pre-v1.20 categories preserved (backward compat);
101
+ arbitrary strings still rejected. 7 new contract tests.
102
+
103
+ ### New tests (+127 across the release)
104
+
105
+ - `tests/test_registry_uniqueness.py` (4) — guard against dict-insertion
106
+ collisions; baseline move count.
107
+ - `tests/test_apply_semantic_move_args.py` (13) — seed_args threading +
108
+ ledger-write contract for each mode.
109
+ - `tests/test_routing_moves.py` (21) — per-move + cross-family.
110
+ - `tests/test_device_mutation_moves.py` (15).
111
+ - `tests/test_content_moves.py` (14).
112
+ - `tests/test_metadata_moves.py` (15).
113
+ - `tests/test_director_move_coverage.py` (8) — SKILL ↔ registry drift
114
+ detection; phase-6-execution.md contract coverage.
115
+ - `tests/test_compiler_wire_format_parity.py` (10) — wire-format
116
+ invariant across all 10 v1.20 moves.
117
+ - `tests/test_v1_20_session_memory_categories.py` (7) — allowlist
118
+ contract.
119
+ - Various in-place additions (execution_router / mcp_dispatch
120
+ classifier entries for `add_session_memory` and `add_drum_rack_pad`;
121
+ test_device_creation_moves invariant generalized to admit
122
+ device-loading moves alongside Device Forge moves).
123
+
124
+ ### Live pressure-test results (the 6 plan §5 scenarios, all passing)
125
+
126
+ 1. `build_send_chain` on Return A with Echo + Auto Filter + Hybrid
127
+ Reverb — 4 steps, 4 successes.
128
+ 2. `configure_device` on the Reverb with dub-cathedral overrides
129
+ (Decay 25.5s, Room Size 339.89, Dry/Wet 40%, Predelay 8.19ms,
130
+ Diffusion 77%) — 5 params set in one batch_set_parameters call.
131
+ 3. `configure_send_architecture` on track 0 → Send A at 0.4 — single
132
+ step, success.
133
+ 4. `load_chord_source` on track 0 slot 0 with a C minor 7 voicing —
134
+ create_clip + add_notes + set_clip_name, 3/3 success.
135
+ 5. 4 moves in sequence → `get_action_ledger_summary` returns 4 entries
136
+ (engine=semantic_moves, mix+arrangement families), zero `tech_debt`
137
+ entries — automatic ledger write confirmed end-to-end.
138
+ 6. Escape hatch: raw `set_track_arm(track=2, armed=true)` + both
139
+ mandatory `add_session_memory` markers; `get_session_memory(
140
+ category="tech_debt")` returns the log entry as expected.
141
+
142
+ ### Scope / non-goals
143
+
144
+ Not in v1.20, explicitly deferred:
145
+ - Hard cutover (closing the escape hatch). v1.21 target, conditional
146
+ on zero `tech_debt` entries over one month of production use.
147
+ - Preset YAML library for `configure_device`. The move's
148
+ `param_overrides` dict already accepts a pre-resolved preset; the
149
+ library is the layer that produces those dicts.
150
+ - `commit_experiment` automatic ledger write. Tracked as tech-debt.
151
+ - Rewriting the existing 33 moves to a new shape. v1.22+.
152
+ - Director Phase 6 compiler that picks moves automatically from user
153
+ intent. Current Phase 6 is user/director-selected; auto-selection
154
+ is a separate feature.
155
+
3
156
  ## 1.19.1 — v1.19.0 polish (April 24 2026)
4
157
 
5
158
  Patch release addressing the three "Known gaps" documented at the
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.19.1"
2
+ __version__ = "1.20.0"
@@ -11,7 +11,23 @@ import uuid
11
11
  from dataclasses import dataclass, field
12
12
  from typing import Optional
13
13
 
14
- _VALID_CATEGORIES = {"observation", "hypothesis", "decision", "issue"}
14
+ _VALID_CATEGORIES = {
15
+ # Pre-v1.20 categories — observations, working hypotheses, decisions, issues.
16
+ "observation",
17
+ "hypothesis",
18
+ "decision",
19
+ "issue",
20
+ # v1.20 categories — creative director Phase 6 escape-hatch discipline.
21
+ # The director must write BOTH of these when executing a raw-tool call
22
+ # that no semantic move covers. See
23
+ # livepilot/skills/livepilot-creative-director/references/phase-6-execution.md
24
+ # §escape-hatch policy for the three-call contract.
25
+ "move_executed", # ledger marker (consumed by anti-repetition)
26
+ "tech_debt", # "pattern should be a semantic move" (consumed by release planning)
27
+ # v1.20 director override log — explicit user decisions to proceed
28
+ # despite a check_brief_compliance violation.
29
+ "override",
30
+ }
15
31
 
16
32
 
17
33
  @dataclass
@@ -20,7 +36,10 @@ class SessionMemoryEntry:
20
36
 
21
37
  id: str
22
38
  timestamp_ms: int
23
- category: str # "observation", "hypothesis", "decision", "issue"
39
+ # Allowed categories are declared in _VALID_CATEGORIES (module level).
40
+ # Current set: observation / hypothesis / decision / issue (pre-v1.20)
41
+ # plus move_executed / tech_debt / override (v1.20 director Phase 6).
42
+ category: str
24
43
  content: str
25
44
  engine: str # which engine created this
26
45
  confidence: float
@@ -93,7 +93,13 @@ def get_session_memory(
93
93
  def add_session_memory(
94
94
  ctx: Context, category: str, content: str, engine: str = "agent_os"
95
95
  ) -> dict:
96
- """Add an ephemeral session memory entry (observation, hypothesis, decision, issue)."""
96
+ """Add an ephemeral session memory entry.
97
+
98
+ Categories:
99
+ - observation / hypothesis / decision / issue (pre-v1.20)
100
+ - move_executed, tech_debt, override (v1.20 director Phase 6 —
101
+ escape-hatch discipline + anti-pattern override logging)
102
+ """
97
103
  store = _get_session_memory(ctx)
98
104
  try:
99
105
  entry_id = store.add(category=category, content=content, engine=engine)
@@ -54,6 +54,15 @@ MCP_TOOLS: frozenset[str] = frozenset({
54
54
  "set_miditool_target",
55
55
  "get_miditool_context",
56
56
  "list_miditool_generators",
57
+ # Session memory writes (v1.20) — MCP-side store in mcp_server/memory/tools.py.
58
+ # No TCP round-trip. Used by remove_device to audit destructive ops + by
59
+ # the director's escape-hatch tech_debt logging.
60
+ "add_session_memory",
61
+ # Drum-rack pad construction (v1.20) — async orchestrator in
62
+ # mcp_server/tools/analyzer.py:775 that composes insert_rack_chain +
63
+ # set_drum_chain_note + insert_device + replace_sample_native. Used by
64
+ # the create_drum_rack_pad semantic move.
65
+ "add_drum_rack_pad",
57
66
  })
58
67
 
59
68
 
@@ -122,6 +122,22 @@ async def _list_miditool_generators(params: dict, ctx: Any = None) -> dict:
122
122
  return await _call(list_miditool_generators, ctx, params)
123
123
 
124
124
 
125
+ # ── Session memory writes (v1.20) ─────────────────────────────────────────
126
+ #
127
+ # remove_device emits an add_session_memory step to log its audit reason.
128
+ # Director Phase 6's escape hatch also writes tech_debt entries through
129
+ # this path. In-process — no TCP, no bridge.
130
+
131
+ async def _add_session_memory(params: dict, ctx: Any = None) -> dict:
132
+ from ..memory.tools import add_session_memory
133
+ return await _call(add_session_memory, ctx, params)
134
+
135
+
136
+ async def _add_drum_rack_pad(params: dict, ctx: Any = None) -> dict:
137
+ from ..tools.analyzer import add_drum_rack_pad
138
+ return await _call(add_drum_rack_pad, ctx, params)
139
+
140
+
125
141
  def build_mcp_dispatch_registry() -> dict[str, Callable]:
126
142
  """Return the canonical registry of MCP-only tools for plan execution.
127
143
 
@@ -147,4 +163,9 @@ def build_mcp_dispatch_registry() -> dict[str, Callable]:
147
163
  "set_miditool_target": _set_miditool_target,
148
164
  "get_miditool_context": _get_miditool_context,
149
165
  "list_miditool_generators": _list_miditool_generators,
166
+ # v1.20 — session memory writes for remove_device audit + director
167
+ # escape-hatch tech_debt logging.
168
+ "add_session_memory": _add_session_memory,
169
+ # v1.20 — drum rack pad construction (async orchestrator).
170
+ "add_drum_rack_pad": _add_drum_rack_pad,
150
171
  }
@@ -6,6 +6,10 @@ from . import transition_moves # noqa: F401
6
6
  from . import sound_design_moves # noqa: F401
7
7
  from . import performance_moves # noqa: F401
8
8
  from . import device_creation_moves # noqa: F401
9
+ from . import routing_moves # noqa: F401 (v1.20)
10
+ from . import device_mutation_moves # noqa: F401 (v1.20)
11
+ from . import content_moves # noqa: F401 (v1.20)
12
+ from . import metadata_moves # noqa: F401 (v1.20)
9
13
  from ..sample_engine import moves as sample_moves # noqa: F401
10
14
 
11
15
  # Import compilers to auto-register them
@@ -15,3 +19,7 @@ from . import sound_design_compilers # noqa: F401
15
19
  from . import performance_compilers # noqa: F401
16
20
  from . import sample_compilers # noqa: F401
17
21
  from . import device_creation_compilers # noqa: F401
22
+ from . import routing_compilers # noqa: F401 (v1.20)
23
+ from . import device_mutation_compilers # noqa: F401 (v1.20)
24
+ from . import content_compilers # noqa: F401 (v1.20)
25
+ from . import metadata_compilers # noqa: F401 (v1.20)
@@ -0,0 +1,174 @@
1
+ """Compilers for content-family semantic moves (v1.20).
2
+
3
+ Pure functions. Validate seed_args rigorously and emit steps whose params
4
+ match the underlying tool signatures exactly (clip_index vs clip_slot
5
+ naming carefully preserved — mcp_server/tools/clips.py:117 uses
6
+ ``clip_index``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .compiler import CompiledPlan, CompiledStep, register_compiler
12
+ from .models import SemanticMove
13
+
14
+
15
+ _DEFAULT_CLIP_LENGTH_BEATS = 4.0
16
+
17
+
18
+ def _empty_plan(move: SemanticMove, warnings: list[str]) -> CompiledPlan:
19
+ return CompiledPlan(
20
+ move_id=move.move_id,
21
+ intent=move.intent,
22
+ steps=[],
23
+ risk_level=move.risk_level,
24
+ summary="; ".join(warnings) if warnings else "No plan compiled",
25
+ requires_approval=True,
26
+ warnings=warnings,
27
+ )
28
+
29
+
30
+ # ── load_chord_source ─────────────────────────────────────────────────────────
31
+
32
+
33
+ def _compile_load_chord_source(move: SemanticMove, kernel: dict) -> CompiledPlan:
34
+ args = kernel.get("seed_args") or {}
35
+ track_index = args.get("track_index")
36
+ clip_slot = args.get("clip_slot")
37
+ notes = args.get("notes")
38
+ name = args.get("name")
39
+ length_beats = args.get("length_beats", _DEFAULT_CLIP_LENGTH_BEATS)
40
+
41
+ if track_index is None or clip_slot is None or notes is None or name is None:
42
+ return _empty_plan(move, [
43
+ "load_chord_source requires seed_args.track_index + clip_slot + notes + name"
44
+ ])
45
+ if not isinstance(track_index, int) or not isinstance(clip_slot, int):
46
+ return _empty_plan(move, [
47
+ "track_index and clip_slot must be ints"
48
+ ])
49
+ if clip_slot < 0:
50
+ return _empty_plan(move, [f"clip_slot must be non-negative, got {clip_slot}"])
51
+ if not isinstance(notes, (list, tuple)) or not notes:
52
+ return _empty_plan(move, ["notes must be a non-empty list of note dicts"])
53
+ if not isinstance(name, str) or not name.strip():
54
+ return _empty_plan(move, ["name must be a non-empty string"])
55
+ try:
56
+ length_f = float(length_beats)
57
+ except (TypeError, ValueError):
58
+ return _empty_plan(move, [f"length_beats must be numeric, got {length_beats!r}"])
59
+ if length_f <= 0:
60
+ return _empty_plan(move, [f"length_beats must be > 0, got {length_f}"])
61
+
62
+ # Step order matters: create_clip must land before add_notes, and the
63
+ # name step runs last so mid-pass inspection sees the real name, not
64
+ # a default "Clip" Live would assign.
65
+ create_step = CompiledStep(
66
+ tool="create_clip",
67
+ params={
68
+ "track_index": track_index,
69
+ "clip_index": clip_slot,
70
+ "length": length_f,
71
+ },
72
+ description=f"Create MIDI clip at track {track_index} slot {clip_slot} ({length_f} beats)",
73
+ verify_after=True,
74
+ backend="remote_command",
75
+ )
76
+ add_step = CompiledStep(
77
+ tool="add_notes",
78
+ params={
79
+ "track_index": track_index,
80
+ "clip_index": clip_slot,
81
+ "notes": list(notes),
82
+ },
83
+ description=f"Add {len(notes)} note(s) to clip",
84
+ verify_after=True,
85
+ backend="remote_command",
86
+ )
87
+ name_step = CompiledStep(
88
+ tool="set_clip_name",
89
+ params={
90
+ "track_index": track_index,
91
+ "clip_index": clip_slot,
92
+ "name": name.strip(),
93
+ },
94
+ description=f"Name clip '{name.strip()}'",
95
+ verify_after=True,
96
+ backend="remote_command",
97
+ )
98
+
99
+ return CompiledPlan(
100
+ move_id=move.move_id,
101
+ intent=move.intent,
102
+ steps=[create_step, add_step, name_step],
103
+ risk_level=move.risk_level,
104
+ summary=(
105
+ f"Load chord source '{name.strip()}' at track {track_index} slot "
106
+ f"{clip_slot} ({len(notes)} notes, {length_f} beats)"
107
+ ),
108
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
109
+ warnings=[],
110
+ )
111
+
112
+
113
+ # ── create_drum_rack_pad ──────────────────────────────────────────────────────
114
+
115
+
116
+ def _compile_create_drum_rack_pad(move: SemanticMove, kernel: dict) -> CompiledPlan:
117
+ args = kernel.get("seed_args") or {}
118
+ track_index = args.get("track_index")
119
+ pad_note = args.get("pad_note")
120
+ file_path = args.get("file_path")
121
+ rack_device_index = args.get("rack_device_index") # optional
122
+ chain_name = args.get("chain_name") # optional
123
+
124
+ if track_index is None or pad_note is None or file_path is None:
125
+ return _empty_plan(move, [
126
+ "create_drum_rack_pad requires seed_args.track_index + pad_note + file_path"
127
+ ])
128
+ if not isinstance(track_index, int) or not isinstance(pad_note, int):
129
+ return _empty_plan(move, ["track_index and pad_note must be ints"])
130
+ if not 0 <= pad_note <= 127:
131
+ return _empty_plan(move, [f"pad_note must be MIDI 0-127, got {pad_note}"])
132
+ if not isinstance(file_path, str) or not file_path.strip():
133
+ return _empty_plan(move, ["file_path must be a non-empty absolute path string"])
134
+ if rack_device_index is not None and not isinstance(rack_device_index, int):
135
+ return _empty_plan(move, ["rack_device_index (when provided) must be int"])
136
+ if chain_name is not None and not isinstance(chain_name, str):
137
+ return _empty_plan(move, ["chain_name (when provided) must be str"])
138
+
139
+ params: dict = {
140
+ "track_index": track_index,
141
+ "pad_note": pad_note,
142
+ "file_path": file_path,
143
+ }
144
+ if rack_device_index is not None:
145
+ params["rack_device_index"] = rack_device_index
146
+ if chain_name is not None:
147
+ params["chain_name"] = chain_name
148
+
149
+ step = CompiledStep(
150
+ tool="add_drum_rack_pad",
151
+ params=params,
152
+ description=(
153
+ f"Add drum rack pad on track {track_index}: MIDI note {pad_note} → "
154
+ f"{file_path.rsplit('/', 1)[-1]}"
155
+ ),
156
+ verify_after=True,
157
+ backend="mcp_tool",
158
+ )
159
+
160
+ return CompiledPlan(
161
+ move_id=move.move_id,
162
+ intent=move.intent,
163
+ steps=[step],
164
+ risk_level=move.risk_level,
165
+ summary=step.description,
166
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
167
+ warnings=[],
168
+ )
169
+
170
+
171
+ # ── Register compilers ────────────────────────────────────────────────────────
172
+
173
+ register_compiler("load_chord_source", _compile_load_chord_source)
174
+ register_compiler("create_drum_rack_pad", _compile_create_drum_rack_pad)
@@ -0,0 +1,87 @@
1
+ """Content-domain semantic moves (v1.20) — create MIDI chord-source clips
2
+ and build drum rack pads one at a time.
3
+
4
+ Both moves take user targets via ``kernel["seed_args"]``. Content moves
5
+ complement the routing family: load_chord_source produces the source clip
6
+ that a build_send_chain return chain processes; create_drum_rack_pad is
7
+ the one-pad-at-a-time primitive for programming kits à la Dilla's
8
+ isolated-voice workflow.
9
+ """
10
+
11
+ from .models import SemanticMove
12
+ from .registry import register
13
+
14
+
15
+ LOAD_CHORD_SOURCE = SemanticMove(
16
+ move_id="load_chord_source",
17
+ family="sound_design",
18
+ intent=(
19
+ "Create a named MIDI chord clip in a specific slot — the single-source "
20
+ "feed for dub / ambient send architectures. Takes track_index, "
21
+ "clip_slot, notes (list of {pitch, start_time, duration, velocity}), "
22
+ "name, and optional length_beats (default 4.0) via seed_args."
23
+ ),
24
+ targets={"harmonic": 0.4, "depth": 0.3, "clarity": 0.3},
25
+ protect={"cohesion": 0.6},
26
+ risk_level="low",
27
+ plan_template=[
28
+ {
29
+ "tool": "create_clip",
30
+ "params": {"description": "Empty MIDI clip of length_beats"},
31
+ "description": "Create empty MIDI clip",
32
+ "backend": "remote_command",
33
+ },
34
+ {
35
+ "tool": "add_notes",
36
+ "params": {"description": "Add the chord voicing notes"},
37
+ "description": "Add chord voicing",
38
+ "backend": "remote_command",
39
+ },
40
+ {
41
+ "tool": "set_clip_name",
42
+ "params": {"description": "Name the clip so it's identifiable"},
43
+ "description": "Name the clip",
44
+ "backend": "remote_command",
45
+ },
46
+ ],
47
+ verification_plan=[
48
+ {
49
+ "tool": "get_clip_info",
50
+ "check": "clip exists at track/slot, has expected name + note count",
51
+ "backend": "remote_command",
52
+ },
53
+ ],
54
+ )
55
+
56
+ CREATE_DRUM_RACK_PAD_MOVE = SemanticMove(
57
+ move_id="create_drum_rack_pad",
58
+ family="device_creation",
59
+ intent=(
60
+ "Add one pad to a Drum Rack — kick, snare, hat, etc. Takes "
61
+ "track_index, pad_note (MIDI 0-127), file_path (absolute), and "
62
+ "optional rack_device_index + chain_name via seed_args. Wraps the "
63
+ "Live 12.4 native replace_sample_native flow."
64
+ ),
65
+ targets={"groove": 0.5, "punch": 0.3, "contrast": 0.2},
66
+ protect={"cohesion": 0.6},
67
+ risk_level="low",
68
+ plan_template=[
69
+ {
70
+ "tool": "add_drum_rack_pad",
71
+ "params": {"description": "Single atomic pad build + sample load"},
72
+ "description": "Build drum rack pad",
73
+ "backend": "mcp_tool",
74
+ },
75
+ ],
76
+ verification_plan=[
77
+ {
78
+ "tool": "get_rack_chains",
79
+ "check": "new chain exists on the rack, trigger note matches pad_note",
80
+ "backend": "remote_command",
81
+ },
82
+ ],
83
+ )
84
+
85
+
86
+ for _move in (LOAD_CHORD_SOURCE, CREATE_DRUM_RACK_PAD_MOVE):
87
+ register(_move)