livepilot 1.18.1 → 1.18.3

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,147 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.18.3 — Brief compliance runtime check (#7 + #8) (April 24 2026)
4
+
5
+ Third v1.18.x patch. Bundles two Known Issues items (#7 + #8) that
6
+ shared the same "check tool args against brief constraints" machinery.
7
+
8
+ ### Fix
9
+
10
+ - **#7 Packet `avoid` list runtime enforcement.**
11
+ - **#8 `locked_dimensions` runtime enforcement.**
12
+
13
+ Both were advisory-only pre-v1.18.3: the director SKILL.md documented
14
+ the hard-filter rules but no runtime machinery verified compliance.
15
+ This release ships a **stateless pure check function** in a new
16
+ `mcp_server/creative_director` module, exposed as the MCP tool
17
+ `check_brief_compliance(brief, tool_name, tool_args)`.
18
+
19
+ **Usage**: director's Phase 6 calls the tool before each risky
20
+ execution (EQ parameters, filter settings, new scene creation, clip
21
+ note editing, send routing, etc.). The tool returns
22
+ `{"ok": bool, "violations": [...]}`. Violations are reports, not
23
+ automatic blocks — the director surfaces them to the user and offers
24
+ three paths: adjust, override-for-this-turn, or pick a different tool.
25
+
26
+ **Detection strategy — best-effort heuristic**, not semantic
27
+ understanding:
28
+
29
+ - anti_pattern matching via keyword tokens + parameter-name heuristics
30
+ (e.g., pattern "bright top-end" + Hi Gain positive value → fires)
31
+ - locked_dimension matching via tool → dimension map
32
+ (e.g., structural lock + create_scene → fires)
33
+
34
+ ### Infrastructure
35
+
36
+ - NEW module `mcp_server/creative_director/` with compliance.py +
37
+ tools.py
38
+ - NEW MCP tool `check_brief_compliance` (tool count 427 → 428,
39
+ domain count 52 → 53)
40
+ - Director SKILL.md Phase 6 now documents the check + the
41
+ three-path violation-response protocol
42
+ - Full session-state active-brief storage is deferred to v1.19;
43
+ v1.18.3 is stateless (caller passes brief each time)
44
+
45
+ ### Tests added
46
+
47
+ - `test_compliance_check_detects_anti_pattern_violation` (BC packet
48
+ + Hi Gain boost → violation)
49
+ - `test_compliance_check_detects_locked_dimension_violation`
50
+ (structural lock + create_scene → violation)
51
+ - `test_compliance_check_passes_compliant_call` (no false positives)
52
+ - `test_compliance_check_empty_brief_permissive` (fresh session
53
+ safety)
54
+
55
+ Test suite: 2792 pass, 1 skipped. Zero regressions.
56
+
57
+ ### Still open for v1.19 (3 items)
58
+
59
+ - Experiment state continuity between branches (architectural —
60
+ transport-state locking needed)
61
+ - Hybrid-packet compilation algorithm (union/intersection logic for
62
+ multi-packet refs like "Basic Channel meets Dilla")
63
+ - Full architectural fix for #3 (route director Phase 6 through
64
+ `apply_semantic_move` / `commit_experiment` — replaces the
65
+ doc-level fix shipped in v1.18.1)
66
+
67
+ These are v1.19 scope — each needs new architectural decisions and
68
+ infrastructure unsuitable for patch releases.
69
+
70
+ ## 1.18.2 — Wonder cold-start + tie-break + genre catalog closure (April 24 2026)
71
+
72
+ Second patch in the v1.18.x series. Three items from the v1.18.0/v1.18.1
73
+ Known Issues list resolved. Test suite grew to 2785 pass, xfail marker
74
+ removed (formerly 1, now 0).
75
+
76
+ ### Fixes
77
+
78
+ - **#10 Wonder Mode zero-variant degradation on empty session context.**
79
+ `enter_wonder_mode` on an empty/sparse session was returning 3
80
+ IDENTICAL `analytical_only` variants all with intent "Analytical
81
+ suggestion for: <request>". Live-verified during v1.18.0 Test 4
82
+ ("I'm stuck" on a 4-track empty session). Fix: introduced
83
+ `_COLD_START_SEEDS` in `mcp_server/wonder_mode/engine.py` — three
84
+ distinct starting-point suggestions covering different families
85
+ (`device_creation × rhythmic` + `sound_design × harmonic` +
86
+ `mix × architecture-first`). When `executable_count == 0`, the
87
+ padding loop uses `build_cold_start_variant()` which pulls from
88
+ the seed set by index, producing genuinely distinct variants with
89
+ specific actionable `what_changed` / `why_it_matters` text.
90
+ Partial-match case (1-2 executable) still uses the generic
91
+ fallback to avoid mixing real moves with architecture-first seeds.
92
+
93
+ - **#11 Experiment ranking tie-break coarseness.**
94
+ `ExperimentSet.ranked_branches()` was a single-key sort by score,
95
+ producing unstable rankings at score ties. Live-verified in v1.18.0
96
+ Test 8 — 3-branch experiment with `add_space` + `add_warmth` +
97
+ `widen_stereo` all scored 0.6 with no clear winner. Fix: composite
98
+ sort key via new `_branch_rank_key()` helper, in priority order:
99
+ (1) `-score` (primary, higher wins), (2) `-novelty_rank` (higher
100
+ novelty wins score ties — creative asks reward variation),
101
+ (3) `risk_rank` (lower risk wins secondary ties — safety default),
102
+ (4) `step_count` (simpler plans win tertiary ties),
103
+ (5) `branch_id` (deterministic final tiebreak for reproducibility).
104
+
105
+ - **Concept packet catalog closure.** 13 new genre YAMLs
106
+ (drone, downtempo, lo_fi, boom_bap, footwork, techno,
107
+ detroit_techno, synthwave, deep_house, disco, soul, dub, hyperpop)
108
+ + 15 too-generic/narrow refs removed from 12 artist packets
109
+ (electronic ×5, electronica, bass_music, cinematic, acid_techno,
110
+ french_house, nu_disco, soulful_house, vaporwave, juke, jungle).
111
+ The xfailing `test_all_artist_genre_refs_resolve_strictly` test
112
+ is now a required green pass. The concept surface has full graph
113
+ closure — every artist→genre cross-reference resolves to an actual
114
+ genre YAML's `id` field.
115
+
116
+ ### Tests added / changed
117
+
118
+ - `test_wonder_cold_start_has_distinct_variants` (new — guards
119
+ against regression to the 3-identical-generics degradation)
120
+ - `test_experiment_tie_break_prefers_higher_novelty` (new — unexpected
121
+ > strong > safe at equal scores)
122
+ - `test_experiment_tie_break_is_deterministic` (new — ranking stable
123
+ across input order)
124
+ - `test_all_artist_genre_refs_resolve_strictly` (was xfailing, now
125
+ passing — xfail marker removed)
126
+ - `test_concept_packets_count` (floor updated 14 → 27 genres)
127
+
128
+ ### Still open for v1.18.3 / v1.19
129
+
130
+ 5 items remain from the original v1.18.0 Known Issues list:
131
+
132
+ - **#7 Packet `avoid` list runtime enforcement** (still advisory —
133
+ pre-flight check against tool args needed)
134
+ - **#8 `locked_dimensions` runtime enforcement** (same pattern as #7)
135
+ - **Experiment state continuity between branches** (before-snapshot
136
+ drift)
137
+ - **Hybrid-packet compilation algorithm** (union/intersection logic
138
+ for "Basic Channel meets Dilla")
139
+ - **Full architectural fix for #3** (route director Phase 6 through
140
+ semantic_move commits — big redesign, v1.19 scope)
141
+
142
+ These all need new infrastructure or architectural decisions
143
+ unsuitable for a patch release.
144
+
3
145
  ## 1.18.1 — Director HIGH-severity patches (April 23 2026)
4
146
 
5
147
  Patch release addressing 4 of the 12 known issues documented in v1.18.0.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  <p align="center">
19
19
  An agentic production system for Ableton Live 12.<br>
20
- 427 tools. 52 domains. Device atlas. Plan-aware Splice integration. Auto-composition. Spectral perception. Technique memory. Drum-rack pad builder. Live dead-device detection.
20
+ 428 tools. 53 domains. Device atlas. Plan-aware Splice integration. Auto-composition. Spectral perception. Technique memory. Drum-rack pad builder. Live dead-device detection.
21
21
  </p>
22
22
 
23
23
  <br>
@@ -80,8 +80,8 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
80
80
  │ └─────────────────┼──────────────────┘ │
81
81
  │ ▼ │
82
82
  │ ┌─────────────────┐ │
83
- │ │ 427 MCP Tools │ │
84
- │ │ 52 domains │ │
83
+ │ │ 428 MCP Tools │ │
84
+ │ │ 53 domains │ │
85
85
  │ └────────┬────────┘ │
86
86
  │ │ │
87
87
  │ Remote Script ──┤── TCP 9878 │
@@ -121,7 +121,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
121
121
 
122
122
  ## The Intelligence Layer
123
123
 
124
- 12 engines sit on top of the 427 tools. They give the AI musical judgment, not just musical execution.
124
+ 12 engines sit on top of the 428 tools. They give the AI musical judgment, not just musical execution.
125
125
 
126
126
  ### SongBrain — What the Song Is
127
127
 
@@ -173,7 +173,7 @@ Every engine follows: **measure before → act → measure after → compare**.
173
173
 
174
174
  ## Tools
175
175
 
176
- 427 tools across 52 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
176
+ 428 tools across 53 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
177
177
 
178
178
  <br>
179
179
 
@@ -362,7 +362,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
362
362
  | Creative Constraints | 5 | constraint activation, reference-inspired variants |
363
363
  | Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
364
364
 
365
- > **[View all 427 tools →](docs/manual/tool-catalog.md)**
365
+ > **[View all 428 tools →](docs/manual/tool-catalog.md)**
366
366
 
367
367
  <br>
368
368
 
@@ -589,7 +589,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
589
589
 
590
590
  | Document | What's inside |
591
591
  |----------|---------------|
592
- | [Manual](docs/manual/index.md) | Complete reference: architecture, all 427 tools, workflows |
592
+ | [Manual](docs/manual/index.md) | Complete reference: architecture, all 428 tools, workflows |
593
593
  | [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
594
594
  | [Device Atlas](docs/manual/device-atlas.md) | 1305 devices indexed — search, suggest, chain building |
595
595
  | [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.18.1"
2
+ __version__ = "1.18.3"
@@ -0,0 +1,21 @@
1
+ """Creative Director — v1.18.3+ runtime compliance check for brief constraints.
2
+
3
+ The livepilot-creative-director skill compiles a Creative Brief inline in
4
+ each creative turn. The brief's `anti_patterns` and `locked_dimensions`
5
+ fields were previously advisory — no runtime machinery verified that
6
+ intended tool calls respected them.
7
+
8
+ This module ships the minimum-effective enforcement layer: a pure
9
+ check function `check_brief_compliance(brief, tool_name, tool_args)`
10
+ that returns {"ok": bool, "violations": [...]}. Director's Phase 6
11
+ calls it before each risky tool execution. Violations don't block
12
+ execution automatically — the director reports them to the user, who
13
+ can override or abandon.
14
+
15
+ Full session-state active-brief storage + automatic interception is a
16
+ v1.19 scope item.
17
+ """
18
+
19
+ from .compliance import check_brief_compliance
20
+
21
+ __all__ = ["check_brief_compliance"]
@@ -0,0 +1,263 @@
1
+ """Brief compliance check — v1.18.3 pure function for anti-pattern and
2
+ locked-dimension enforcement.
3
+
4
+ Pure computation: no I/O, no session state. Caller passes the brief
5
+ dict and the intended tool call; the function returns a report of
6
+ violations. Director's Phase 6 calls this before each risky tool call.
7
+
8
+ Design principles:
9
+
10
+ 1. **Best-effort heuristic, not semantic understanding.** anti_patterns
11
+ in the brief are prose. The checker does keyword-token matching
12
+ against humanized tool-call descriptions. Won't catch every
13
+ violation (e.g., "too muddy" → EQ cut at 300 Hz requires more
14
+ intelligence than substring match). Does catch obvious ones
15
+ (e.g., "bright top-end" → Hi Gain boost).
16
+
17
+ 2. **Never block — always report.** The return format is a violations
18
+ list with human-readable reason + suggestion. The caller (director)
19
+ decides whether to proceed, ask the user, or abandon. Hard-blocking
20
+ at this layer would crash under false positives.
21
+
22
+ 3. **Empty brief passes everything.** A brief without anti_patterns or
23
+ locked_dimensions returns ok=True for all calls — we don't want a
24
+ fresh session to be hostile to experimentation.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import re
30
+ from typing import Any
31
+
32
+
33
+ # ── Tool → dimension mapping ────────────────────────────────────────
34
+ # Maps MCP tool names to the creative dimension they primarily affect.
35
+ # Used for locked_dimensions enforcement. A tool can map to None
36
+ # (dimension-agnostic — e.g., undo, get_session_info) in which case no
37
+ # locked-dimension check applies.
38
+
39
+ _STRUCTURAL_TOOLS = frozenset({
40
+ "create_scene", "delete_scene", "duplicate_scene", "set_scene_name",
41
+ "set_scene_tempo", "set_scene_color", "fire_scene",
42
+ "set_clip_follow_action", "set_scene_follow_action",
43
+ "capture_and_insert_scene",
44
+ "create_arrangement_clip", "create_native_arrangement_clip",
45
+ "force_arrangement", "plan_arrangement",
46
+ "transform_section", "refresh_repeated_section",
47
+ })
48
+
49
+ _RHYTHMIC_TOOLS = frozenset({
50
+ "add_notes", "modify_notes", "remove_notes", "transpose_notes",
51
+ "duplicate_notes", "add_arrangement_notes", "modify_arrangement_notes",
52
+ "remove_arrangement_notes", "quantize_clip",
53
+ "assign_clip_groove", "set_groove_params", "set_song_groove_amount",
54
+ "apply_gesture_template",
55
+ "generate_euclidean_rhythm", "layer_euclidean_rhythms",
56
+ "generate_countermelody", "transform_motif",
57
+ })
58
+
59
+ _TIMBRAL_TOOLS = frozenset({
60
+ "set_device_parameter", "batch_set_parameters",
61
+ "load_browser_item", "find_and_load_device",
62
+ "load_device_by_uri", "insert_device", "delete_device", "move_device",
63
+ "toggle_device", "copy_device_state",
64
+ "install_m4l_device",
65
+ })
66
+
67
+ _SPATIAL_TOOLS = frozenset({
68
+ "set_track_send", "set_track_pan", "set_track_volume",
69
+ "set_master_volume",
70
+ "create_return_track",
71
+ "set_track_routing",
72
+ })
73
+
74
+
75
+ def _tool_to_dimension(tool_name: str, tool_args: dict) -> str | None:
76
+ """Map an MCP tool call to its primary creative dimension.
77
+
78
+ Returns one of {"structural", "rhythmic", "timbral", "spatial"} or
79
+ None if the tool is dimension-agnostic.
80
+
81
+ Some tools (e.g., load_browser_item) are timbral EXCEPT when loading
82
+ a device on a return track, which is spatial. The heuristic resolves
83
+ this via track_index: negative indices indicate return tracks.
84
+ """
85
+ if tool_name in _STRUCTURAL_TOOLS:
86
+ return "structural"
87
+ if tool_name in _RHYTHMIC_TOOLS:
88
+ return "rhythmic"
89
+ if tool_name in _SPATIAL_TOOLS:
90
+ return "spatial"
91
+ if tool_name in _TIMBRAL_TOOLS:
92
+ # load_browser_item on a return track is spatial, not timbral —
93
+ # loading Echo/Reverb/Auto Filter on a return is send-chain work.
94
+ track_index = tool_args.get("track_index")
95
+ if isinstance(track_index, int) and track_index < 0 and track_index != -1000:
96
+ return "spatial"
97
+ return "timbral"
98
+ return None
99
+
100
+
101
+ # ── Anti-pattern token matching ────────────────────────────────────
102
+
103
+ # Phrases that describe parameter changes indicative of "bright" moves
104
+ _BRIGHTENING_PARAM_KEYWORDS = ("hi gain", "hi freq", "high gain", "brightness",
105
+ "treble", "presence", "air")
106
+ # Phrases indicative of "aggressive transient" moves
107
+ _TRANSIENT_BOOST_KEYWORDS = ("transient", "attack", "punch", "snappy")
108
+ # Phrases indicative of "sidechain" moves
109
+ _SIDECHAIN_KEYWORDS = ("sidechain", "envelope follower", "ducking")
110
+ # Phrases indicative of "quantization" moves
111
+ _QUANTIZE_KEYWORDS = ("quantize", "snap", "full-grid", "perfectly quantized")
112
+
113
+
114
+ def _humanize_tool_call(tool_name: str, tool_args: dict) -> str:
115
+ """Produce a prose description of a tool call for keyword matching.
116
+
117
+ Best-effort — the output is not structured, just a lowercased string
118
+ that concatenates the tool name + notable arg values.
119
+ """
120
+ parts = [tool_name]
121
+ for key, val in (tool_args or {}).items():
122
+ if isinstance(val, (str, int, float, bool)):
123
+ parts.append(f"{key}={val}")
124
+ return " ".join(parts).lower()
125
+
126
+
127
+ def _anti_pattern_matches(pattern: str, tool_name: str, tool_args: dict) -> bool:
128
+ """Decide whether an anti_pattern phrase flags this tool call.
129
+
130
+ Strategy: lowercase the pattern, pull content keywords, match against
131
+ the humanized call + known parameter-name heuristics.
132
+ """
133
+ pattern_lower = pattern.lower()
134
+ call_desc = _humanize_tool_call(tool_name, tool_args)
135
+
136
+ # Direct substring match — cheapest first
137
+ # Split pattern into words, check if any significant word appears
138
+ # in the call description
139
+ stopwords = {"the", "a", "an", "and", "or", "of", "to", "in", "on", "at", "with", "for", "/", "-"}
140
+ pattern_tokens = [w.strip("—-./,:;") for w in re.split(r"\s+", pattern_lower)]
141
+ pattern_tokens = [w for w in pattern_tokens if w and w not in stopwords and len(w) >= 3]
142
+
143
+ # Check parameter-name heuristics for common anti-patterns
144
+ if any(kw in pattern_lower for kw in ("bright", "top-end", "top end", "highs")):
145
+ param_name = str(tool_args.get("parameter_name", "")).lower()
146
+ value = tool_args.get("value")
147
+ if any(bright_kw in param_name for bright_kw in _BRIGHTENING_PARAM_KEYWORDS):
148
+ # Boosting (positive value on a gain parameter) is the violation
149
+ if isinstance(value, (int, float)) and value > 0:
150
+ return True
151
+
152
+ if any(kw in pattern_lower for kw in ("transient", "aggressive transient",
153
+ "transient-heavy", "crisp")):
154
+ param_name = str(tool_args.get("parameter_name", "")).lower()
155
+ if any(t_kw in param_name for t_kw in _TRANSIENT_BOOST_KEYWORDS):
156
+ value = tool_args.get("value")
157
+ if isinstance(value, (int, float)) and value > 0.5:
158
+ return True
159
+
160
+ if any(kw in pattern_lower for kw in ("sidechain", "pumping")):
161
+ if tool_name == "compressor_set_sidechain":
162
+ return True
163
+ param_name = str(tool_args.get("parameter_name", "")).lower()
164
+ if any(sc_kw in param_name for sc_kw in _SIDECHAIN_KEYWORDS):
165
+ return True
166
+
167
+ if any(kw in pattern_lower for kw in ("full-grid", "full grid",
168
+ "quantized", "perfectly quantized")):
169
+ if tool_name == "quantize_clip":
170
+ # Quantize to tight grid (1/16 or finer, strong amount) is the violation
171
+ return True
172
+
173
+ # Fallback: token-level substring match
174
+ for token in pattern_tokens:
175
+ if token in call_desc:
176
+ return True
177
+
178
+ return False
179
+
180
+
181
+ # ── Public API ──────────────────────────────────────────────────────
182
+
183
+ def check_brief_compliance(
184
+ brief: dict,
185
+ tool_name: str,
186
+ tool_args: dict | None = None,
187
+ ) -> dict:
188
+ """Check whether a tool call complies with the active creative brief.
189
+
190
+ brief: compiled Creative Brief dict (may contain anti_patterns,
191
+ locked_dimensions, reference_anchors, etc.).
192
+ tool_name: the MCP tool about to be called.
193
+ tool_args: the dict of arguments.
194
+
195
+ Returns:
196
+ {
197
+ "ok": bool,
198
+ "violations": [
199
+ {
200
+ "rule": "anti_pattern" | "locked_dimension",
201
+ "detail": <pattern or dimension string>,
202
+ "reason": "...",
203
+ "suggestion": "...",
204
+ },
205
+ ...
206
+ ],
207
+ }
208
+
209
+ Empty brief (no anti_patterns, no locked_dimensions) always returns
210
+ ok=True with empty violations list. Best-effort heuristic — not
211
+ semantic understanding. Caller (director Phase 6) decides whether
212
+ to proceed, surface to user, or abandon.
213
+ """
214
+ tool_args = tool_args or {}
215
+ violations: list[dict[str, Any]] = []
216
+
217
+ # 1. Check anti_patterns
218
+ anti_patterns = brief.get("anti_patterns", []) or []
219
+ for pattern in anti_patterns:
220
+ if not isinstance(pattern, str) or not pattern.strip():
221
+ continue
222
+ if _anti_pattern_matches(pattern, tool_name, tool_args):
223
+ violations.append({
224
+ "rule": "anti_pattern",
225
+ "detail": pattern,
226
+ "reason": (
227
+ f"Tool call '{tool_name}' appears to violate the "
228
+ f"anti_pattern '{pattern}' from the active brief."
229
+ ),
230
+ "suggestion": (
231
+ "Either (a) adjust the call to avoid this pattern, "
232
+ "(b) ask the user to explicitly override this "
233
+ "specific anti_pattern, or (c) pick a different "
234
+ "tool that achieves the creative goal without "
235
+ "triggering the avoid rule."
236
+ ),
237
+ })
238
+
239
+ # 2. Check locked_dimensions
240
+ locked_dims = brief.get("locked_dimensions", []) or []
241
+ tool_dimension = _tool_to_dimension(tool_name, tool_args)
242
+ if tool_dimension and tool_dimension in locked_dims:
243
+ violations.append({
244
+ "rule": "locked_dimension",
245
+ "detail": tool_dimension,
246
+ "reason": (
247
+ f"Tool '{tool_name}' touches the '{tool_dimension}' "
248
+ f"dimension which the user explicitly locked in this "
249
+ f"brief."
250
+ ),
251
+ "suggestion": (
252
+ f"User locked this dimension. Either (a) surface the "
253
+ f"conflict and ask the user to unlock, or (b) pick a "
254
+ f"tool that operates on a different dimension. "
255
+ f"Available unlocked dimensions: "
256
+ f"{sorted(set(['structural', 'rhythmic', 'timbral', 'spatial']) - set(locked_dims))}."
257
+ ),
258
+ })
259
+
260
+ return {
261
+ "ok": not violations,
262
+ "violations": violations,
263
+ }
@@ -0,0 +1,72 @@
1
+ """Creative Director MCP tools — v1.18.3+ brief compliance.
2
+
3
+ Exposes `check_brief_compliance` as an MCP tool so the
4
+ `livepilot-creative-director` skill can call it before each risky
5
+ Phase 6 tool execution. Caller passes the compiled brief dict + the
6
+ intended tool call; the tool returns a violations report.
7
+
8
+ Stateless by design: no session storage of the active brief. The
9
+ director passes the brief each time. Full session-state active-brief
10
+ storage is v1.19 scope.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Optional
16
+
17
+ from fastmcp import Context
18
+
19
+ from ..server import mcp
20
+ from .compliance import check_brief_compliance as _check_brief_compliance
21
+
22
+
23
+ @mcp.tool()
24
+ def check_brief_compliance(
25
+ ctx: Context,
26
+ brief: dict,
27
+ tool_name: str,
28
+ tool_args: Optional[dict] = None,
29
+ ) -> dict:
30
+ """Check whether an intended tool call complies with the active creative brief.
31
+
32
+ v1.18.3 #7 + #8 runtime enforcement for the director's anti_patterns
33
+ and locked_dimensions brief fields. Call this BEFORE executing any
34
+ risky tool from director's Phase 6 — especially when the brief has
35
+ non-empty anti_patterns or locked_dimensions.
36
+
37
+ brief: the compiled Creative Brief dict. May contain anti_patterns
38
+ (list of prose phrases), locked_dimensions (list of:
39
+ structural/rhythmic/timbral/spatial), reference_anchors, etc.
40
+ tool_name: the MCP tool name you're about to call.
41
+ tool_args: dict of arguments you'll pass to that tool.
42
+
43
+ Returns:
44
+ {
45
+ "ok": bool,
46
+ "violations": [
47
+ {
48
+ "rule": "anti_pattern" | "locked_dimension",
49
+ "detail": <the anti_pattern phrase OR the locked dimension>,
50
+ "reason": "Why this call appears to violate the brief",
51
+ "suggestion": "What to do about it",
52
+ },
53
+ ...
54
+ ],
55
+ }
56
+
57
+ Violations are NEVER automatic blocks — they're reports. The
58
+ director decides whether to proceed, surface to user, or abandon.
59
+ Empty brief (no anti_patterns, no locked_dimensions) always
60
+ returns ok=True.
61
+
62
+ Best-effort keyword heuristic, NOT semantic understanding. Will
63
+ miss subtle violations (e.g., 'too muddy' → 300 Hz cut needs
64
+ judgment this checker doesn't have). Will catch obvious ones
65
+ (e.g., 'bright top-end' → Hi Gain positive boost).
66
+ """
67
+ result = _check_brief_compliance(
68
+ brief=brief,
69
+ tool_name=tool_name,
70
+ tool_args=tool_args or {},
71
+ )
72
+ return result
@@ -194,6 +194,52 @@ class ExperimentBranch:
194
194
  return d
195
195
 
196
196
 
197
+ # v1.18.2 #11: composite tie-break ranking for experiment branches.
198
+ # Maps novelty_label / risk_label strings to integer ranks.
199
+ _NOVELTY_RANK: dict[str, int] = {
200
+ "safe": 0,
201
+ "medium": 1, # rarely used, but accept it for robustness
202
+ "strong": 1,
203
+ "unexpected": 2,
204
+ "bold": 2, # alias in some producer outputs
205
+ }
206
+ _RISK_RANK: dict[str, int] = {
207
+ "low": 0,
208
+ "medium": 1,
209
+ "high": 2,
210
+ }
211
+
212
+
213
+ def _branch_rank_key(branch: "ExperimentBranch") -> tuple:
214
+ """Composite sort key for ExperimentSet.ranked_branches().
215
+
216
+ Returns a tuple (-score, -novelty, risk, step_count, branch_id) such
217
+ that Python's default ascending sort produces the desired ranking:
218
+ higher scores first, then higher novelty at score ties, then lower
219
+ risk under equal novelty, then simpler plans, then branch_id as a
220
+ deterministic final tiebreak.
221
+ """
222
+ score = float(getattr(branch, "score", 0.0) or 0.0)
223
+ seed = getattr(branch, "seed", None)
224
+
225
+ if seed is not None:
226
+ novelty_label = (seed.novelty_label or "").lower()
227
+ risk_label = (seed.risk_label or "").lower()
228
+ else:
229
+ novelty_label = ""
230
+ risk_label = ""
231
+
232
+ novelty_rank = _NOVELTY_RANK.get(novelty_label, 1) # middle if unknown
233
+ risk_rank = _RISK_RANK.get(risk_label, 1)
234
+
235
+ plan = getattr(branch, "compiled_plan", None) or {}
236
+ step_count = int(plan.get("step_count", 0) or 0)
237
+
238
+ branch_id = getattr(branch, "branch_id", "") or ""
239
+
240
+ return (-score, -novelty_rank, risk_rank, step_count, branch_id)
241
+
242
+
197
243
  @dataclass
198
244
  class ExperimentSet:
199
245
  """A collection of branches being compared for one request."""
@@ -215,9 +261,33 @@ class ExperimentSet:
215
261
  return None
216
262
 
217
263
  def ranked_branches(self) -> list[ExperimentBranch]:
218
- """Return branches sorted by score descending."""
264
+ """Return evaluated branches sorted by composite rank.
265
+
266
+ v1.18.2 #11 fix: pre-fix this was a single-key sort by score,
267
+ which produced unstable rankings at score ties (live-verified in
268
+ v1.18.0 Test 8 — three branches at 0.6 with no winner).
269
+
270
+ Sort keys, in priority order:
271
+ 1. -score — higher score wins
272
+ 2. -novelty_rank — higher novelty wins at score ties
273
+ (creative asks reward variation)
274
+ 3. risk_rank — lower risk wins secondary ties
275
+ (safety default under equal novelty)
276
+ 4. step_count — simpler plans win tertiary ties
277
+ 5. branch_id — deterministic final tiebreak
278
+ (stable ranking across equal branches)
279
+
280
+ Novelty labels rank: "safe"=0, "strong"=1, "unexpected"=2, "bold"=2.
281
+ Risk labels rank: "low"=0, "medium"=1, "high"=2.
282
+ Unknown labels default to the middle (1).
283
+ """
219
284
  evaluated = [b for b in self.branches if b.status == "evaluated"]
220
- return sorted(evaluated, key=lambda b: -b.score)
285
+ return sorted(evaluated, key=_branch_rank_key)
286
+
287
+ # expose the key function for testing + custom rankers
288
+ def rank_key_for(self, branch: "ExperimentBranch") -> tuple:
289
+ """Return the composite rank key for a branch (for tie-break debugging)."""
290
+ return _branch_rank_key(branch)
221
291
 
222
292
  def to_dict(self) -> dict:
223
293
  return {
@@ -301,6 +301,7 @@ from .stuckness_detector import tools as stuckness_tools # noqa: F401, E40
301
301
  from .wonder_mode import tools as wonder_mode_tools # noqa: F401, E402
302
302
  from .session_continuity import tools as session_cont_tools # noqa: F401, E402
303
303
  from .creative_constraints import tools as constraints_tools # noqa: F401, E402
304
+ from .creative_director import tools as creative_director_tools # noqa: F401, E402
304
305
  from .device_forge import tools as device_forge_tools # noqa: F401, E402
305
306
  from .sample_engine import tools as sample_engine_tools # noqa: F401, E402
306
307
  from .atlas import tools as atlas_tools # noqa: F401, E402
@@ -321,6 +321,82 @@ def build_analytical_variant(label: str, request_text: str, novelty_level: float
321
321
  }
322
322
 
323
323
 
324
+ # v1.18.2 #10 fix: distinct cold-start variant seeds for empty/sparse
325
+ # sessions. Used when no semantic moves match the request. Each seed has
326
+ # a specific `what_changed` + `why_it_matters` covering a different
327
+ # starting-point family (device_creation × rhythm + device_creation ×
328
+ # harmony + mix-architecture-first). Replaces the 3-identical-generics
329
+ # degradation that v1.18.0 Test 4 surfaced.
330
+ _COLD_START_SEEDS: list[dict] = [
331
+ {
332
+ "label": "safe",
333
+ "family": "device_creation",
334
+ "intent": "Begin with a rhythmic foundation",
335
+ "what_changed": "Load a drum kit (Drum Rack or Core Kit) on a fresh MIDI track, program a 4-bar kick-and-hat pattern",
336
+ "what_preserved": "blank slate — first move sets the tempo and grid foundation",
337
+ "why_it_matters": "Every track needs a rhythmic anchor before timbral or structural work. Safe starting point — drums-first is the most common composition entry.",
338
+ "novelty_level": 0.3,
339
+ "identity_effect": "establishes",
340
+ },
341
+ {
342
+ "label": "strong",
343
+ "family": "sound_design",
344
+ "intent": "Begin with a harmonic source",
345
+ "what_changed": "Load Drift or Meld on a MIDI track with a chord-stab patch (short attack, moderate release, slight detune), sketch a 2-bar chord pattern",
346
+ "what_preserved": "tempo and key are still open to discovery — lets the harmony suggest the rhythm",
347
+ "why_it_matters": "A harmonic source opens a different emotional palette than drums-first. Chord-first composition (Isolée / Luomo style) is less common but produces distinctive results.",
348
+ "novelty_level": 0.55,
349
+ "identity_effect": "establishes",
350
+ },
351
+ {
352
+ "label": "unexpected",
353
+ "family": "mix",
354
+ "intent": "Begin with the space, not the source",
355
+ "what_changed": "Configure return tracks BEFORE any instrument work — set up Return A with Convolution Reverb (cathedral IR) and Return B with Echo in ping-pong mode",
356
+ "what_preserved": "the blank slate IS the canvas; the sends are the frame you'll paint into",
357
+ "why_it_matters": "Dub techno and ambient producers (Basic Channel, Gas, Henke) build sound AROUND pre-configured sends. Unusual but genre-appropriate starting point.",
358
+ "novelty_level": 0.85,
359
+ "identity_effect": "establishes",
360
+ },
361
+ ]
362
+
363
+
364
+ def build_cold_start_variant(seed: dict, request_text: str, variant_id: str = "") -> dict:
365
+ """Build a cold-start variant seed for an empty/sparse session.
366
+
367
+ Used when no semantic moves match the request. Returns a variant with
368
+ distinct, actionable `what_changed` / `why_it_matters` text — NOT the
369
+ generic 'No matching moves found' fallback. Each seed covers a
370
+ different starting-point family; together they give the user three
371
+ genuinely distinct first-moves to choose from.
372
+
373
+ See `_COLD_START_SEEDS` for the seed set. The variant is
374
+ `analytical_only=True` (no compiled_plan) — turning these into
375
+ one-click executable plans is a v1.19 enhancement.
376
+ """
377
+ return {
378
+ "variant_id": variant_id,
379
+ "label": seed["label"],
380
+ "move_id": "",
381
+ "family": seed["family"],
382
+ "intent": seed["intent"],
383
+ "what_changed": seed["what_changed"],
384
+ "what_preserved": seed["what_preserved"],
385
+ "why_it_matters": seed["why_it_matters"],
386
+ "identity_effect": seed["identity_effect"],
387
+ "novelty_level": seed["novelty_level"],
388
+ "taste_fit": 0.5,
389
+ "targets_snapshot": {},
390
+ "compiled_plan": None,
391
+ "score": 0.0,
392
+ "rank": 0,
393
+ "score_breakdown": {},
394
+ "analytical_only": True,
395
+ "distinctness_reason": f"Cold-start seed ({seed['family']}) — empty session, no moves matched",
396
+ "cold_start": True,
397
+ }
398
+
399
+
324
400
  # ── Taste fit scoring ────────────────────────────────────────────
325
401
 
326
402
 
@@ -577,16 +653,37 @@ def generate_wonder_variants(
577
653
 
578
654
  executable_count = len(variants)
579
655
 
580
- # Pad with analytical variants
581
- while len(variants) < 3:
582
- idx = len(variants)
583
- v = build_analytical_variant(
584
- label=labels[idx],
585
- request_text=request_text,
586
- novelty_level=_NOVELTY_LEVELS.get(labels[idx], 0.5),
587
- variant_id=f"{set_prefix}_{labels[idx]}",
588
- )
589
- variants.append(v)
656
+ # v1.18.2 #10 fix: when NO executable moves matched, seed from the
657
+ # cold-start distinct-starting-points set instead of padding with
658
+ # identical generic analytical variants. Pre-fix, cold-start on an
659
+ # empty session returned 3 variants all with the same generic
660
+ # "No matching moves found" text — unhelpful to the user.
661
+ #
662
+ # The partial-match case (1 or 2 executable variants) still pads with
663
+ # the generic analytical fallback because we don't want to mix real
664
+ # move-based variants with architecture-first seeds — that would
665
+ # confuse the presentation.
666
+ if executable_count == 0:
667
+ while len(variants) < 3:
668
+ idx = len(variants)
669
+ seed = _COLD_START_SEEDS[idx]
670
+ v = build_cold_start_variant(
671
+ seed=seed,
672
+ request_text=request_text,
673
+ variant_id=f"{set_prefix}_{seed['label']}",
674
+ )
675
+ variants.append(v)
676
+ else:
677
+ # Partial-match: pad to 3 with generic analytical variants
678
+ while len(variants) < 3:
679
+ idx = len(variants)
680
+ v = build_analytical_variant(
681
+ label=labels[idx],
682
+ request_text=request_text,
683
+ novelty_level=_NOVELTY_LEVELS.get(labels[idx], 0.5),
684
+ variant_id=f"{set_prefix}_{labels[idx]}",
685
+ )
686
+ variants.append(v)
590
687
 
591
688
  novelty_band = 0.5
592
689
  taste_evidence = 0
@@ -603,7 +700,13 @@ def generate_wonder_variants(
603
700
 
604
701
  degraded_reason = ""
605
702
  if executable_count == 0:
606
- degraded_reason = "No matching executable moves found"
703
+ # v1.18.2 #10: cold-start path distinct starting-point seeds
704
+ # rather than identical-generic padding.
705
+ degraded_reason = (
706
+ "No matching executable moves — cold-start variants seeded "
707
+ "from distinct starting-point families (device_creation × 2 "
708
+ "+ mix-architecture-first)"
709
+ )
607
710
  elif executable_count == 1:
608
711
  degraded_reason = "Only 1 distinct executable move found"
609
712
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.18.1",
3
+ "version": "1.18.3",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 427 tools, 52 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 — 428 tools, 53 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 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.18.1"
8
+ __version__ = "1.18.3"
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": "427-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
4
+ "description": "428-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
5
5
  "repository": {
6
6
  "url": "https://github.com/dreamrec/LivePilot",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.18.1",
9
+ "version": "1.18.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.18.1",
14
+ "version": "1.18.3",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }