livepilot 1.19.0 → 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,212 @@
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
+
156
+ ## 1.19.1 — v1.19.0 polish (April 24 2026)
157
+
158
+ Patch release addressing the three "Known gaps" documented at the
159
+ end of the v1.19.0 CHANGELOG entry. All three were cosmetic or
160
+ observability issues — no correctness changes. 3 new tests + 1
161
+ pre-existing test tolerance widened. Test suite 2854 → 2858 pass.
162
+
163
+ ### Fixes
164
+
165
+ - **#1 `baseline_transport` not exposed via `compare_experiments`.**
166
+ The field was populated internally on `ExperimentSet` (verified
167
+ by unit tests) but `compare_experiments`' MCP response omitted
168
+ it — operators had no surface-level path to verify the
169
+ between-branch drift fix was actually firing. Now present on
170
+ every response (`None` when the experiment hasn't run yet, so
171
+ clients can rely on key presence and check
172
+ `result["baseline_transport"] is None` without `in` guards).
173
+
174
+ - **#2 Tempo warning midpoint rounds to int while range is exact.**
175
+ Pre-v1.19.1 `compile_hybrid_brief` with disjoint tempo ranges
176
+ reported warning text "midpoint 108 BPM" while the returned
177
+ range was 105-110 (centered on 107.5). Two rounding
178
+ conventions — human-facing text rounded to `:0f`, machine-facing
179
+ range kept the exact float. Fix: `:g` format in the warning
180
+ produces the shortest accurate representation (107.5 stays
181
+ "107.5"; 128.0 renders as "128") so both surfaces agree.
182
+
183
+ - **#3 `weights` display full float precision.**
184
+ Uniform 3-packet hybrids rendered weights as
185
+ `0.3333333333333333` — noisy output that contrasted with
186
+ `evaluation_bias.target_dimensions` values already being
187
+ rounded to 4 decimal places. Weights are now rounded to 4 dp
188
+ in the response dict (`[0.3333, 0.3333, 0.3333]`). Internal
189
+ computation still uses full precision; only the output is
190
+ rounded.
191
+
192
+ ### Tests added
193
+
194
+ - `test_compare_experiments_surfaces_baseline_transport` — round-trip
195
+ seed a distinctive baseline on ExperimentSet, assert
196
+ `compare_experiments` surfaces all fields (is_playing, song_time,
197
+ track_states, captured_at_ms).
198
+ - `test_compare_experiments_baseline_none_when_not_captured` — fresh
199
+ experiment has `baseline_transport: None` in the response rather
200
+ than an omitted key.
201
+ - `test_tempo_warning_midpoint_matches_range_center` — regex-parse
202
+ the warning text and assert its numeric midpoint matches the
203
+ returned range's center within 0.01 BPM.
204
+ - `test_weights_rounded_to_4dp` — uniform 3-packet weights must be
205
+ representable at 4 dp precision (`round(w, 4) == w`).
206
+
207
+ Test suite: 2858 pass, 1 skipped. Zero regressions. `sync_metadata
208
+ --check` clean.
209
+
3
210
  ## 1.19.0 — Experiment baseline + hybrid packet compilation (April 24 2026)
4
211
 
5
212
  Minor version bump. Ships two of the three open items documented in
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.19.0"
2
+ __version__ = "1.20.0"
@@ -333,9 +333,15 @@ def _compile_from_packets(
333
333
  f"{name or 'packet'} {lo:.0f}-{hi:.0f}"
334
334
  for lo, hi, name in tempo_ranges
335
335
  )
336
+ # v1.19.1 #2 — :g format keeps warning midpoint consistent with
337
+ # the returned range center. Pre-v1.19.1 used :.0f (int-rounded)
338
+ # so BC+Dilla reported 'midpoint 108 BPM' while range was
339
+ # 105-110 centered on 107.5 — two rounding conventions.
340
+ # :g gives the shortest accurate representation: 107.5 stays
341
+ # "107.5", 128.0 becomes "128".
336
342
  warnings.append(
337
343
  f"Tempo ranges don't overlap ({range_desc}) — defaulting "
338
- f"to midpoint {midpoint:.0f} BPM. Specify which anchor "
344
+ f"to midpoint {midpoint:g} BPM. Specify which anchor "
339
345
  f"you want or pick a single packet."
340
346
  )
341
347
 
@@ -346,7 +352,10 @@ def _compile_from_packets(
346
352
  return {
347
353
  "type": "hybrid",
348
354
  "source_packets": list(packet_ids),
349
- "weights": list(weights),
355
+ # v1.19.1 #3 — round weights to 4 dp for clean display, matching the
356
+ # convention target_dimensions already uses. Pre-v1.19.1 uniform
357
+ # 3-packet weights rendered as 0.3333333333333333 — noisy output.
358
+ "weights": [round(w, 4) for w in weights],
350
359
  "name": hybrid_name,
351
360
  "sonic_identity": sonic_identity,
352
361
  "reach_for": reach_for,
@@ -601,10 +601,21 @@ def compare_experiments(
601
601
  "evaluation": b.evaluation,
602
602
  }
603
603
 
604
+ # v1.19.1 #1 — surface baseline_transport for operator observability.
605
+ # Always present in the response (None when not captured) so clients
606
+ # can `result["baseline_transport"] is None` instead of checking for
607
+ # key presence first. Populated during run_experiment's first pass.
608
+ baseline_dict = (
609
+ experiment.baseline_transport.to_dict()
610
+ if experiment.baseline_transport is not None
611
+ else None
612
+ )
613
+
604
614
  return {
605
615
  "experiment_id": experiment_id,
606
616
  "request": experiment.request_text,
607
617
  "branch_count": experiment.branch_count,
618
+ "baseline_transport": baseline_dict,
608
619
  "ranking": [
609
620
  {
610
621
  "rank": i + 1,
@@ -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)