copilot-liku-cli 0.0.4 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/QUICKSTART.md +24 -0
  2. package/README.md +85 -33
  3. package/package.json +23 -14
  4. package/scripts/postinstall.js +63 -0
  5. package/src/cli/commands/window.js +66 -0
  6. package/src/main/agents/base-agent.js +15 -7
  7. package/src/main/agents/builder.js +211 -0
  8. package/src/main/agents/index.js +12 -4
  9. package/src/main/agents/orchestrator.js +40 -0
  10. package/src/main/agents/producer.js +891 -0
  11. package/src/main/agents/researcher.js +78 -0
  12. package/src/main/agents/state-manager.js +134 -2
  13. package/src/main/agents/trace-writer.js +83 -0
  14. package/src/main/agents/verifier.js +201 -0
  15. package/src/main/ai-service.js +673 -66
  16. package/src/main/index.js +682 -110
  17. package/src/main/inspect-service.js +24 -1
  18. package/src/main/python-bridge.js +395 -0
  19. package/src/main/system-automation.js +934 -133
  20. package/src/main/ui-automation/core/ui-provider.js +99 -0
  21. package/src/main/ui-automation/core/uia-host.js +214 -0
  22. package/src/main/ui-automation/index.js +30 -0
  23. package/src/main/ui-automation/interactions/element-click.js +6 -6
  24. package/src/main/ui-automation/interactions/high-level.js +28 -6
  25. package/src/main/ui-automation/interactions/index.js +21 -0
  26. package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
  27. package/src/main/ui-automation/window/index.js +6 -0
  28. package/src/main/ui-automation/window/manager.js +173 -26
  29. package/src/main/ui-watcher.js +420 -56
  30. package/src/main/visual-awareness.js +18 -1
  31. package/src/native/windows-uia/Program.cs +89 -0
  32. package/src/native/windows-uia/build.ps1 +24 -0
  33. package/src/native/windows-uia-dotnet/Program.cs +920 -0
  34. package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
  35. package/src/native/windows-uia-dotnet/build.ps1 +24 -0
  36. package/src/renderer/chat/chat.js +943 -671
  37. package/src/renderer/chat/index.html +39 -4
  38. package/src/renderer/chat/preload.js +8 -1
  39. package/src/renderer/overlay/overlay.js +157 -8
  40. package/src/renderer/overlay/preload.js +4 -0
  41. package/src/shared/inspect-types.js +82 -6
  42. package/ARCHITECTURE.md +0 -411
  43. package/CONFIGURATION.md +0 -302
  44. package/CONTRIBUTING.md +0 -225
  45. package/ELECTRON_README.md +0 -121
  46. package/PROJECT_STATUS.md +0 -229
  47. package/TESTING.md +0 -274
@@ -0,0 +1,891 @@
1
+ /**
2
+ * Producer Agent
3
+ *
4
+ * Orchestrates "agentic producer" flow:
5
+ * 1) Draft Score Plan from prompt (schema-guided).
6
+ * 2) Generate music via JSON-RPC gateway.
7
+ * 3) Run critics to quality-gate the result.
8
+ * 4) Refine the plan and retry (bounded attempts).
9
+ */
10
+
11
+ const { BaseAgent, AgentRole, AgentCapabilities } = require('./base-agent');
12
+ const { PythonBridge } = require('../python-bridge');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const DEFAULT_MAX_ITERATIONS = 2;
17
+ const DEFAULT_BPM = 90;
18
+ const DEFAULT_KEY = 'C';
19
+ const DEFAULT_MODE = 'minor';
20
+ const DEFAULT_TIME_SIGNATURE = [4, 4];
21
+ const DEFAULT_DIRECTOR_MODEL = 'claude-sonnet-4.5';
22
+ const DEFAULT_PRODUCER_MODEL = 'gpt-4.1';
23
+ const DEFAULT_VERIFIER_MODEL = 'claude-sonnet-4.5';
24
+
25
+ class ProducerAgent extends BaseAgent {
26
+ constructor(options = {}) {
27
+ super({
28
+ ...options,
29
+ role: AgentRole.PRODUCER,
30
+ name: options.name || 'producer',
31
+ description: 'Creates score plans, generates music, and runs quality critics',
32
+ capabilities: [
33
+ AgentCapabilities.SEARCH,
34
+ AgentCapabilities.READ,
35
+ AgentCapabilities.EXECUTE,
36
+ AgentCapabilities.TODO,
37
+ AgentCapabilities.HANDOFF
38
+ ]
39
+ });
40
+
41
+ this.pythonBridge = null;
42
+ this._scorePlanSchemaCache = null;
43
+ }
44
+
45
+ getSystemPrompt() {
46
+ return `You are the PRODUCER agent in a multi-agent music system.
47
+
48
+ # ROLE
49
+ - Generate a valid Score Plan (score_plan_v1) for MUSE.
50
+ - Keep plans musically coherent and production-aware.
51
+ - Return JSON only (no markdown) when asked to output a plan.
52
+
53
+ # QUALITY
54
+ - Prefer clear section structures and instrument roles.
55
+ - Use musically sensible BPM, key, mode, and arrangement.
56
+
57
+ # SAFETY
58
+ - Do not remove features or disable existing behavior.
59
+ - Keep outputs deterministic and schema-compliant.`;
60
+ }
61
+
62
+ async process(task, context = {}) {
63
+ const prompt = this._extractPrompt(task);
64
+ const maxIterations = Number(context.maxIterations || DEFAULT_MAX_ITERATIONS);
65
+ const allowCriticGateFailure = Boolean(
66
+ context.allowCriticGateFailure ||
67
+ context.generationOnlySuccess ||
68
+ context.allowQualityGateBypass
69
+ );
70
+ const referenceInput = this._resolveReferenceInput(prompt, context);
71
+ const modelPolicy = this._resolveModelPolicy(context);
72
+
73
+ const builder = this.orchestrator?.getBuilder?.();
74
+ const verifier = this.orchestrator?.getVerifier?.();
75
+ if (!builder) {
76
+ return { success: false, error: 'Producer requires Builder agent access' };
77
+ }
78
+ if (!verifier) {
79
+ return { success: false, error: 'Producer requires Verifier agent access' };
80
+ }
81
+
82
+ const referenceProfile = await this._analyzeReference(referenceInput);
83
+ let scorePlan = await this._createScorePlan(prompt, referenceProfile, modelPolicy);
84
+
85
+ const planningTelemetry = {
86
+ roleModels: {
87
+ director: modelPolicy.director,
88
+ producer: modelPolicy.producer,
89
+ verifier: modelPolicy.verifier
90
+ },
91
+ referenceUsed: !!referenceProfile,
92
+ referenceSource: referenceInput || null,
93
+ timestamp: new Date().toISOString()
94
+ };
95
+
96
+ this.log('info', 'Producer model policy selected', planningTelemetry);
97
+ const phaseStates = [];
98
+ this._pushPhaseState(phaseStates, 'producer_start', 0.02, 'Producer orchestration started');
99
+
100
+ const validationTelemetry = [];
101
+
102
+ const initialValidation = this._prepareValidatedScorePlan(scorePlan, prompt, 'initial');
103
+ scorePlan = initialValidation.plan;
104
+ validationTelemetry.push(initialValidation);
105
+ this._pushPhaseState(phaseStates, 'score_plan_validation', 0.12, initialValidation.validBefore ? 'Initial score plan validated' : 'Initial score plan required fallback');
106
+
107
+ scorePlan = this._normalizeScorePlan(scorePlan, prompt);
108
+
109
+ let lastResult = null;
110
+ let lastCritics = null;
111
+ let lastOutputAnalysis = null;
112
+ const preflightTelemetry = [];
113
+
114
+ for (let attempt = 1; attempt <= maxIterations; attempt++) {
115
+ this.log('info', 'Producer attempt starting', { attempt, maxIterations });
116
+ this._pushPhaseState(phaseStates, `attempt_${attempt}_start`, 0.15 + ((attempt - 1) * (0.7 / Math.max(1, maxIterations))), `Attempt ${attempt}/${maxIterations} started`);
117
+
118
+ const attemptValidation = this._prepareValidatedScorePlan(scorePlan, prompt, `attempt_${attempt}`);
119
+ scorePlan = attemptValidation.plan;
120
+ validationTelemetry.push(attemptValidation);
121
+ this._pushPhaseState(phaseStates, `attempt_${attempt}_validation`, 0.2 + ((attempt - 1) * (0.7 / Math.max(1, maxIterations))), attemptValidation.validBefore ? 'Attempt plan validated' : 'Attempt plan fallback applied');
122
+
123
+ const preflight = await verifier.preflightScorePlanGate(scorePlan, {
124
+ prompt,
125
+ model: modelPolicy.verifier
126
+ });
127
+ preflightTelemetry.push({ attempt, ...preflight });
128
+ this._pushPhaseState(phaseStates, `attempt_${attempt}_preflight`, 0.25 + ((attempt - 1) * (0.7 / Math.max(1, maxIterations))), preflight.passed ? 'Preflight gate passed' : 'Preflight gate failed');
129
+
130
+ if (!preflight.passed) {
131
+ this.log('warn', 'Preflight gate failed before generation', {
132
+ attempt,
133
+ issues: preflight.issues
134
+ });
135
+
136
+ if (attempt < maxIterations) {
137
+ const syntheticCritic = {
138
+ report: {
139
+ summary: `Preflight gate failed: ${(preflight.issues || []).slice(0, 5).join('; ')}`
140
+ }
141
+ };
142
+ scorePlan = await this._refineScorePlan(prompt, scorePlan, syntheticCritic, referenceProfile, modelPolicy);
143
+ scorePlan = this._normalizeScorePlan(scorePlan, prompt);
144
+ continue;
145
+ }
146
+
147
+ return {
148
+ success: false,
149
+ terminalOutcome: 'PRECHECK_FAILED',
150
+ response: this._formatFailureResponse(scorePlan, lastResult, lastCritics, maxIterations, {
151
+ preflight,
152
+ outputAnalysis: lastOutputAnalysis
153
+ }),
154
+ scorePlan,
155
+ generation: lastResult,
156
+ critics: lastCritics,
157
+ outputAnalysis: lastOutputAnalysis,
158
+ planningTelemetry,
159
+ validationTelemetry,
160
+ preflightTelemetry,
161
+ phaseStates
162
+ };
163
+ }
164
+
165
+ lastResult = await builder.generateMusicFromScorePlan(scorePlan, {
166
+ prompt,
167
+ trackProgress: true
168
+ });
169
+ this._pushPhaseState(phaseStates, `attempt_${attempt}_generation`, 0.55 + ((attempt - 1) * (0.35 / Math.max(1, maxIterations))), 'Generation run completed');
170
+
171
+ if (!lastResult || !lastResult.midi_path) {
172
+ this.log('error', 'Music generation failed', { attempt, result: lastResult });
173
+ return {
174
+ success: false,
175
+ terminalOutcome: 'GENERATION_FAILED',
176
+ error: 'Generation failed or missing midi_path',
177
+ attempt,
178
+ result: lastResult,
179
+ planningTelemetry,
180
+ validationTelemetry,
181
+ preflightTelemetry,
182
+ phaseStates
183
+ };
184
+ }
185
+
186
+ lastCritics = await verifier.runMusicCritics(lastResult.midi_path, scorePlan.genre);
187
+ this._pushPhaseState(phaseStates, `attempt_${attempt}_critics`, 0.72 + ((attempt - 1) * (0.2 / Math.max(1, maxIterations))), lastCritics?.passed ? 'Critics passed' : 'Critics failed');
188
+
189
+ if (lastResult.audio_path) {
190
+ try {
191
+ lastOutputAnalysis = await verifier.analyzeRenderedOutput(
192
+ lastResult.audio_path,
193
+ scorePlan.genre || 'pop'
194
+ );
195
+ this._pushPhaseState(phaseStates, `attempt_${attempt}_output_analysis`, 0.82 + ((attempt - 1) * (0.16 / Math.max(1, maxIterations))), 'Output analysis complete');
196
+ } catch (error) {
197
+ lastOutputAnalysis = {
198
+ passed: false,
199
+ error: error.message
200
+ };
201
+ this._pushPhaseState(phaseStates, `attempt_${attempt}_output_analysis`, 0.82 + ((attempt - 1) * (0.16 / Math.max(1, maxIterations))), `Output analysis failed: ${error.message}`);
202
+ }
203
+ }
204
+
205
+ if (lastCritics.passed) {
206
+ this._pushPhaseState(phaseStates, 'producer_complete', 1.0, 'Producer completed successfully');
207
+ return {
208
+ success: true,
209
+ terminalOutcome: 'COMPLETED_SUCCESS',
210
+ response: this._formatSuccessResponse(scorePlan, lastResult, lastCritics, attempt, {
211
+ outputAnalysis: lastOutputAnalysis,
212
+ preflight: preflightTelemetry[preflightTelemetry.length - 1] || null
213
+ }),
214
+ scorePlan,
215
+ generation: lastResult,
216
+ critics: lastCritics,
217
+ outputAnalysis: lastOutputAnalysis,
218
+ planningTelemetry,
219
+ validationTelemetry,
220
+ preflightTelemetry,
221
+ phaseStates
222
+ };
223
+ }
224
+
225
+ if (allowCriticGateFailure && lastResult && lastResult.midi_path) {
226
+ this._pushPhaseState(phaseStates, 'producer_complete', 1.0, 'Producer completed with critic-gate bypass');
227
+ return {
228
+ success: true,
229
+ terminalOutcome: 'COMPLETED_WITH_CRITIC_FAIL_ACCEPTED',
230
+ response: this._formatSuccessResponse(scorePlan, lastResult, lastCritics, attempt, {
231
+ outputAnalysis: lastOutputAnalysis,
232
+ preflight: preflightTelemetry[preflightTelemetry.length - 1] || null,
233
+ criticGateBypassed: true
234
+ }),
235
+ scorePlan,
236
+ generation: lastResult,
237
+ critics: lastCritics,
238
+ outputAnalysis: lastOutputAnalysis,
239
+ planningTelemetry,
240
+ validationTelemetry,
241
+ preflightTelemetry,
242
+ phaseStates
243
+ };
244
+ }
245
+
246
+ if (attempt < maxIterations) {
247
+ scorePlan = await this._refineScorePlan(prompt, scorePlan, lastCritics, referenceProfile, modelPolicy);
248
+ scorePlan = this._normalizeScorePlan(scorePlan, prompt);
249
+ }
250
+ }
251
+
252
+ return {
253
+ success: false,
254
+ terminalOutcome: 'COMPLETED_WITH_CRITIC_FAIL',
255
+ response: this._formatFailureResponse(scorePlan, lastResult, lastCritics, maxIterations, {
256
+ preflight: preflightTelemetry[preflightTelemetry.length - 1] || null,
257
+ outputAnalysis: lastOutputAnalysis,
258
+ suggestBypass: true
259
+ }),
260
+ scorePlan,
261
+ generation: lastResult,
262
+ critics: lastCritics,
263
+ outputAnalysis: lastOutputAnalysis,
264
+ planningTelemetry,
265
+ validationTelemetry,
266
+ preflightTelemetry,
267
+ phaseStates
268
+ };
269
+ }
270
+
271
+ _pushPhaseState(target, step, percent, message, extra = {}) {
272
+ target.push({
273
+ step,
274
+ percent: Math.max(0, Math.min(1, Number(percent) || 0)),
275
+ message,
276
+ timestamp: new Date().toISOString(),
277
+ ...extra
278
+ });
279
+ }
280
+
281
+ async ensurePythonBridge() {
282
+ if (!this.pythonBridge) {
283
+ this.pythonBridge = PythonBridge.getShared();
284
+ }
285
+ if (!this.pythonBridge.isRunning) {
286
+ await this.pythonBridge.start();
287
+ }
288
+ return this.pythonBridge;
289
+ }
290
+
291
+ _extractPrompt(task) {
292
+ if (!task) return '';
293
+ if (typeof task === 'string') return task.trim();
294
+ if (typeof task.prompt === 'string') return task.prompt.trim();
295
+ if (typeof task.description === 'string') return task.description.trim();
296
+ return '';
297
+ }
298
+
299
+ _schemaPath() {
300
+ return path.resolve(__dirname, '..', '..', '..', '..', 'MUSE', 'docs', 'muse-specs', 'schemas', 'score_plan.v1.schema.json');
301
+ }
302
+
303
+ _loadSchema() {
304
+ try {
305
+ const schemaPath = this._schemaPath();
306
+ return fs.readFileSync(schemaPath, 'utf-8');
307
+ } catch (error) {
308
+ this.log('warn', 'Failed to load score plan schema', { error: error.message });
309
+ return null;
310
+ }
311
+ }
312
+
313
+ _loadScorePlanSchema() {
314
+ if (this._scorePlanSchemaCache) {
315
+ return this._scorePlanSchemaCache;
316
+ }
317
+ try {
318
+ const schemaText = this._loadSchema();
319
+ if (!schemaText) return null;
320
+ this._scorePlanSchemaCache = JSON.parse(schemaText);
321
+ return this._scorePlanSchemaCache;
322
+ } catch (error) {
323
+ this.log('warn', 'Failed to parse score plan schema JSON', { error: error.message });
324
+ return null;
325
+ }
326
+ }
327
+
328
+ async _createScorePlan(prompt, referenceProfile = null, modelPolicy = null) {
329
+ const schemaText = this._loadSchema();
330
+ const referenceContext = this._formatReferenceContext(referenceProfile);
331
+ const policy = modelPolicy || { director: DEFAULT_DIRECTOR_MODEL, producer: DEFAULT_PRODUCER_MODEL };
332
+
333
+ const directorGuidance = await this._draftDirectorGuidance(prompt, referenceProfile, policy.director);
334
+
335
+ const baseInstruction = `Create a score_plan_v1 JSON for this prompt.
336
+ Prompt: ${prompt}
337
+
338
+ ${referenceContext}
339
+
340
+ Director guidance (creative intent):
341
+ ${directorGuidance}
342
+
343
+ Rules:
344
+ - Output JSON ONLY (no markdown).
345
+ - Must satisfy required fields in the schema.
346
+ - Keep instruments realistic and varied.
347
+ `;
348
+
349
+ const promptWithSchema = schemaText
350
+ ? `${baseInstruction}\nSchema:\n${schemaText}`
351
+ : baseInstruction;
352
+
353
+ const response = await this.chat(promptWithSchema, { model: policy.producer });
354
+ const jsonText = this._extractJson(response.text);
355
+ if (!jsonText) {
356
+ this.log('warn', 'Failed to parse score plan JSON, falling back');
357
+ return {};
358
+ }
359
+ try {
360
+ return JSON.parse(jsonText);
361
+ } catch (error) {
362
+ this.log('warn', 'Score plan JSON parse error', { error: error.message });
363
+ return {};
364
+ }
365
+ }
366
+
367
+ async _refineScorePlan(prompt, previousPlan, critics, referenceProfile = null, modelPolicy = null) {
368
+ const schemaText = this._loadSchema();
369
+ const criticSummary = critics?.report?.summary || 'Critics failed without a summary.';
370
+ const referenceContext = this._formatReferenceContext(referenceProfile);
371
+ const policy = modelPolicy || { director: DEFAULT_DIRECTOR_MODEL, producer: DEFAULT_PRODUCER_MODEL };
372
+ const baseInstruction = `Refine the previous score_plan_v1 JSON to address critics.
373
+ Prompt: ${prompt}
374
+ Critic summary: ${criticSummary}
375
+
376
+ ${referenceContext}
377
+
378
+ Rules:
379
+ - Output JSON ONLY (no markdown).
380
+ - Preserve the prompt and keep schema validity.
381
+ `;
382
+
383
+ const promptWithSchema = schemaText
384
+ ? `${baseInstruction}\nPrevious plan:\n${JSON.stringify(previousPlan, null, 2)}\nSchema:\n${schemaText}`
385
+ : `${baseInstruction}\nPrevious plan:\n${JSON.stringify(previousPlan, null, 2)}`;
386
+
387
+ const response = await this.chat(promptWithSchema, { model: policy.producer });
388
+ const jsonText = this._extractJson(response.text);
389
+ if (!jsonText) {
390
+ return previousPlan;
391
+ }
392
+ try {
393
+ return JSON.parse(jsonText);
394
+ } catch (_error) {
395
+ return previousPlan;
396
+ }
397
+ }
398
+
399
+ _normalizeScorePlan(plan, prompt) {
400
+ const normalized = (plan && typeof plan === 'object') ? { ...plan } : {};
401
+ normalized.schema_version = 'score_plan_v1';
402
+ normalized.prompt = (normalized.prompt && String(normalized.prompt).trim()) || prompt || 'Music generation';
403
+
404
+ const bpm = Number(normalized.bpm);
405
+ normalized.bpm = Number.isFinite(bpm) ? Math.min(220, Math.max(30, bpm)) : DEFAULT_BPM;
406
+
407
+ const key = typeof normalized.key === 'string' ? normalized.key.trim() : DEFAULT_KEY;
408
+ normalized.key = /^[A-G](#|b)?$/.test(key) ? key : DEFAULT_KEY;
409
+
410
+ const mode = typeof normalized.mode === 'string' ? normalized.mode : DEFAULT_MODE;
411
+ const allowedModes = new Set(['major', 'minor', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'locrian']);
412
+ normalized.mode = allowedModes.has(mode) ? mode : DEFAULT_MODE;
413
+
414
+ if (!Array.isArray(normalized.time_signature) || normalized.time_signature.length !== 2) {
415
+ normalized.time_signature = DEFAULT_TIME_SIGNATURE;
416
+ }
417
+
418
+ if (!Array.isArray(normalized.sections) || normalized.sections.length === 0) {
419
+ normalized.sections = [
420
+ { name: 'Intro', type: 'intro', bars: 8, energy: 0.2, tension: 0.2 },
421
+ { name: 'Verse', type: 'verse', bars: 16, energy: 0.35, tension: 0.3 },
422
+ { name: 'Chorus', type: 'chorus', bars: 16, energy: 0.6, tension: 0.5 },
423
+ { name: 'Outro', type: 'outro', bars: 8, energy: 0.2, tension: 0.2 }
424
+ ];
425
+ }
426
+
427
+ if (!Array.isArray(normalized.tracks) || normalized.tracks.length === 0) {
428
+ normalized.tracks = [
429
+ { role: 'pad', instrument: 'Atmospheric Pad', density: 0.7 },
430
+ { role: 'strings', instrument: 'Warm Strings', density: 0.5 },
431
+ { role: 'keys', instrument: 'Soft Piano', density: 0.4 },
432
+ { role: 'bass', instrument: 'Sub Bass', density: 0.3 },
433
+ { role: 'fx', instrument: 'Drone FX', density: 0.2 }
434
+ ];
435
+ }
436
+
437
+ return normalized;
438
+ }
439
+
440
+ _prepareValidatedScorePlan(plan, prompt, stage = 'unknown') {
441
+ const normalized = this._normalizeScorePlan(plan, prompt);
442
+ const schema = this._loadScorePlanSchema();
443
+ const sanitized = this._sanitizeScorePlanToSchemaSubset(normalized, schema);
444
+ const before = this._validateScorePlanStrict(sanitized);
445
+
446
+ if (before.valid) {
447
+ return {
448
+ stage,
449
+ validBefore: true,
450
+ validAfter: true,
451
+ fallbackApplied: false,
452
+ errorsBefore: [],
453
+ errorsAfter: [],
454
+ plan: sanitized
455
+ };
456
+ }
457
+
458
+ const fallbackPlan = this._buildFallbackScorePlan(prompt, sanitized);
459
+ const fallbackSanitized = this._sanitizeScorePlanToSchemaSubset(fallbackPlan, schema);
460
+ const after = this._validateScorePlanStrict(fallbackSanitized);
461
+
462
+ if (!after.valid) {
463
+ this.log('warn', 'Fallback score plan still failed strict validation', {
464
+ stage,
465
+ errors: after.errors
466
+ });
467
+ }
468
+
469
+ return {
470
+ stage,
471
+ validBefore: false,
472
+ validAfter: after.valid,
473
+ fallbackApplied: true,
474
+ errorsBefore: before.errors,
475
+ errorsAfter: after.errors,
476
+ plan: fallbackSanitized
477
+ };
478
+ }
479
+
480
+ _sanitizeScorePlanToSchemaSubset(plan, _schema = null) {
481
+ const src = (plan && typeof plan === 'object') ? plan : {};
482
+
483
+ const topAllowed = new Set([
484
+ 'schema_version', 'request_id', 'prompt', 'bpm', 'key', 'mode',
485
+ 'time_signature', 'genre', 'mood', 'influences', 'seed', 'duration_bars',
486
+ 'sections', 'chord_map', 'tension_curve', 'cue_points', 'tracks', 'constraints'
487
+ ]);
488
+
489
+ const out = {};
490
+ for (const [key, value] of Object.entries(src)) {
491
+ if (topAllowed.has(key)) out[key] = value;
492
+ }
493
+
494
+ if (Array.isArray(out.time_signature)) {
495
+ out.time_signature = out.time_signature.slice(0, 2).map(v => Number(v));
496
+ }
497
+
498
+ if (Array.isArray(out.sections)) {
499
+ out.sections = out.sections
500
+ .filter(s => s && typeof s === 'object')
501
+ .map(s => ({
502
+ name: s.name,
503
+ type: s.type,
504
+ bars: Number(s.bars),
505
+ energy: s.energy !== undefined ? Number(s.energy) : undefined,
506
+ tension: s.tension !== undefined ? Number(s.tension) : undefined
507
+ }));
508
+ }
509
+
510
+ if (Array.isArray(out.tracks)) {
511
+ out.tracks = out.tracks
512
+ .filter(t => t && typeof t === 'object')
513
+ .map(t => ({
514
+ role: t.role,
515
+ instrument: t.instrument,
516
+ pattern_hint: t.pattern_hint,
517
+ octave: t.octave !== undefined ? Number(t.octave) : undefined,
518
+ density: t.density !== undefined ? Number(t.density) : undefined,
519
+ activation: Array.isArray(t.activation)
520
+ ? t.activation
521
+ .filter(a => a && typeof a === 'object')
522
+ .map(a => ({ section: a.section, active: !!a.active }))
523
+ : undefined
524
+ }));
525
+ }
526
+
527
+ if (Array.isArray(out.chord_map)) {
528
+ out.chord_map = out.chord_map
529
+ .filter(c => c && typeof c === 'object')
530
+ .map(c => ({ bar: Number(c.bar), chord: c.chord }));
531
+ }
532
+
533
+ if (Array.isArray(out.cue_points)) {
534
+ out.cue_points = out.cue_points
535
+ .filter(c => c && typeof c === 'object')
536
+ .map(c => ({
537
+ bar: Number(c.bar),
538
+ type: c.type,
539
+ intensity: c.intensity !== undefined ? Number(c.intensity) : undefined
540
+ }));
541
+ }
542
+
543
+ if (out.constraints && typeof out.constraints === 'object') {
544
+ out.constraints = {
545
+ avoid_instruments: Array.isArray(out.constraints.avoid_instruments) ? out.constraints.avoid_instruments : undefined,
546
+ avoid_drums: Array.isArray(out.constraints.avoid_drums) ? out.constraints.avoid_drums : undefined,
547
+ max_polyphony: out.constraints.max_polyphony !== undefined ? Number(out.constraints.max_polyphony) : undefined
548
+ };
549
+ }
550
+
551
+ const pruneUndefined = (obj) => {
552
+ if (Array.isArray(obj)) return obj.map(pruneUndefined);
553
+ if (obj && typeof obj === 'object') {
554
+ const cleaned = {};
555
+ for (const [k, v] of Object.entries(obj)) {
556
+ if (v !== undefined) cleaned[k] = pruneUndefined(v);
557
+ }
558
+ return cleaned;
559
+ }
560
+ return obj;
561
+ };
562
+
563
+ return pruneUndefined(out);
564
+ }
565
+
566
+ _validateScorePlanStrict(plan) {
567
+ const errors = [];
568
+ const allowedModes = new Set(['major', 'minor', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'locrian']);
569
+ const allowedSectionTypes = new Set(['intro', 'verse', 'pre_chorus', 'chorus', 'drop', 'bridge', 'breakdown', 'outro']);
570
+ const allowedTrackRoles = new Set(['drums', 'bass', 'keys', 'lead', 'strings', 'fx', 'pad']);
571
+ const allowedCueTypes = new Set(['fill', 'build', 'drop', 'breakdown']);
572
+
573
+ const required = ['schema_version', 'prompt', 'bpm', 'key', 'mode', 'sections', 'tracks'];
574
+ for (const key of required) {
575
+ if (plan[key] === undefined || plan[key] === null) {
576
+ errors.push(`Missing required field: ${key}`);
577
+ }
578
+ }
579
+
580
+ if (plan.schema_version !== 'score_plan_v1') {
581
+ errors.push('schema_version must be score_plan_v1');
582
+ }
583
+
584
+ if (typeof plan.prompt !== 'string' || !plan.prompt.trim()) {
585
+ errors.push('prompt must be a non-empty string');
586
+ }
587
+
588
+ if (typeof plan.bpm !== 'number' || Number.isNaN(plan.bpm) || plan.bpm < 30 || plan.bpm > 220) {
589
+ errors.push('bpm must be a number in [30,220]');
590
+ }
591
+
592
+ if (typeof plan.key !== 'string' || !/^[A-G](#|b)?$/.test(plan.key)) {
593
+ errors.push('key must match ^[A-G](#|b)?$');
594
+ }
595
+
596
+ if (!allowedModes.has(plan.mode)) {
597
+ errors.push('mode must be one of the allowed modes');
598
+ }
599
+
600
+ if (plan.time_signature !== undefined) {
601
+ const ts = plan.time_signature;
602
+ if (!Array.isArray(ts) || ts.length !== 2 || !Number.isInteger(ts[0]) || !Number.isInteger(ts[1]) || ts[0] < 1 || ts[1] < 1) {
603
+ errors.push('time_signature must be [int>=1, int>=1]');
604
+ }
605
+ }
606
+
607
+ if (!Array.isArray(plan.sections) || plan.sections.length < 1) {
608
+ errors.push('sections must be a non-empty array');
609
+ } else {
610
+ plan.sections.forEach((s, i) => {
611
+ if (!s || typeof s !== 'object') {
612
+ errors.push(`sections[${i}] must be an object`);
613
+ return;
614
+ }
615
+ if (typeof s.name !== 'string' || !s.name) errors.push(`sections[${i}].name required`);
616
+ if (!allowedSectionTypes.has(s.type)) errors.push(`sections[${i}].type invalid`);
617
+ if (!Number.isInteger(s.bars) || s.bars < 1) errors.push(`sections[${i}].bars must be int>=1`);
618
+ if (s.energy !== undefined && (typeof s.energy !== 'number' || s.energy < 0 || s.energy > 1)) {
619
+ errors.push(`sections[${i}].energy must be in [0,1]`);
620
+ }
621
+ if (s.tension !== undefined && (typeof s.tension !== 'number' || s.tension < 0 || s.tension > 1)) {
622
+ errors.push(`sections[${i}].tension must be in [0,1]`);
623
+ }
624
+ });
625
+ }
626
+
627
+ if (!Array.isArray(plan.tracks) || plan.tracks.length < 1) {
628
+ errors.push('tracks must be a non-empty array');
629
+ } else {
630
+ plan.tracks.forEach((t, i) => {
631
+ if (!t || typeof t !== 'object') {
632
+ errors.push(`tracks[${i}] must be an object`);
633
+ return;
634
+ }
635
+ if (!allowedTrackRoles.has(t.role)) errors.push(`tracks[${i}].role invalid`);
636
+ if (typeof t.instrument !== 'string' || !t.instrument) errors.push(`tracks[${i}].instrument required`);
637
+ if (t.density !== undefined && (typeof t.density !== 'number' || t.density < 0 || t.density > 1)) {
638
+ errors.push(`tracks[${i}].density must be in [0,1]`);
639
+ }
640
+ if (t.activation !== undefined) {
641
+ if (!Array.isArray(t.activation)) {
642
+ errors.push(`tracks[${i}].activation must be an array`);
643
+ } else {
644
+ t.activation.forEach((a, j) => {
645
+ if (!a || typeof a !== 'object') {
646
+ errors.push(`tracks[${i}].activation[${j}] must be object`);
647
+ return;
648
+ }
649
+ if (typeof a.section !== 'string' || !a.section) errors.push(`tracks[${i}].activation[${j}].section required`);
650
+ if (typeof a.active !== 'boolean') errors.push(`tracks[${i}].activation[${j}].active must be boolean`);
651
+ });
652
+ }
653
+ }
654
+ });
655
+ }
656
+
657
+ if (plan.chord_map !== undefined) {
658
+ if (!Array.isArray(plan.chord_map)) {
659
+ errors.push('chord_map must be an array');
660
+ } else {
661
+ plan.chord_map.forEach((c, i) => {
662
+ if (!c || typeof c !== 'object') {
663
+ errors.push(`chord_map[${i}] must be object`);
664
+ return;
665
+ }
666
+ if (!Number.isInteger(c.bar) || c.bar < 1) errors.push(`chord_map[${i}].bar must be int>=1`);
667
+ if (typeof c.chord !== 'string' || !c.chord) errors.push(`chord_map[${i}].chord required`);
668
+ });
669
+ }
670
+ }
671
+
672
+ if (plan.cue_points !== undefined) {
673
+ if (!Array.isArray(plan.cue_points)) {
674
+ errors.push('cue_points must be an array');
675
+ } else {
676
+ plan.cue_points.forEach((c, i) => {
677
+ if (!c || typeof c !== 'object') {
678
+ errors.push(`cue_points[${i}] must be object`);
679
+ return;
680
+ }
681
+ if (!Number.isInteger(c.bar) || c.bar < 1) errors.push(`cue_points[${i}].bar must be int>=1`);
682
+ if (!allowedCueTypes.has(c.type)) errors.push(`cue_points[${i}].type invalid`);
683
+ if (c.intensity !== undefined && (typeof c.intensity !== 'number' || c.intensity < 0 || c.intensity > 1)) {
684
+ errors.push(`cue_points[${i}].intensity must be in [0,1]`);
685
+ }
686
+ });
687
+ }
688
+ }
689
+
690
+ if (plan.constraints !== undefined) {
691
+ const c = plan.constraints;
692
+ if (!c || typeof c !== 'object' || Array.isArray(c)) {
693
+ errors.push('constraints must be an object');
694
+ } else if (c.max_polyphony !== undefined && (!Number.isInteger(c.max_polyphony) || c.max_polyphony < 1)) {
695
+ errors.push('constraints.max_polyphony must be int>=1');
696
+ }
697
+ }
698
+
699
+ return { valid: errors.length === 0, errors };
700
+ }
701
+
702
+ _buildFallbackScorePlan(prompt, candidate = {}) {
703
+ const safePrompt = (candidate.prompt && String(candidate.prompt).trim()) || prompt || 'Music generation';
704
+ return {
705
+ schema_version: 'score_plan_v1',
706
+ prompt: safePrompt,
707
+ bpm: DEFAULT_BPM,
708
+ key: DEFAULT_KEY,
709
+ mode: DEFAULT_MODE,
710
+ time_signature: DEFAULT_TIME_SIGNATURE,
711
+ genre: typeof candidate.genre === 'string' ? candidate.genre : undefined,
712
+ mood: typeof candidate.mood === 'string' ? candidate.mood : undefined,
713
+ sections: [
714
+ { name: 'Intro', type: 'intro', bars: 8, energy: 0.2, tension: 0.2 },
715
+ { name: 'Verse', type: 'verse', bars: 16, energy: 0.35, tension: 0.3 },
716
+ { name: 'Chorus', type: 'chorus', bars: 16, energy: 0.6, tension: 0.5 },
717
+ { name: 'Outro', type: 'outro', bars: 8, energy: 0.2, tension: 0.2 }
718
+ ],
719
+ tracks: [
720
+ { role: 'pad', instrument: 'Atmospheric Pad', density: 0.7 },
721
+ { role: 'strings', instrument: 'Warm Strings', density: 0.5 },
722
+ { role: 'keys', instrument: 'Soft Piano', density: 0.4 },
723
+ { role: 'bass', instrument: 'Sub Bass', density: 0.3 },
724
+ { role: 'fx', instrument: 'Drone FX', density: 0.2 }
725
+ ]
726
+ };
727
+ }
728
+
729
+ _extractJson(text) {
730
+ if (!text || typeof text !== 'string') return null;
731
+ const stripped = text.trim().replace(/^```json/i, '').replace(/^```/i, '').replace(/```$/i, '').trim();
732
+ if (stripped.startsWith('{') && stripped.endsWith('}')) {
733
+ return stripped;
734
+ }
735
+ const start = stripped.indexOf('{');
736
+ if (start === -1) return null;
737
+ let depth = 0;
738
+ for (let i = start; i < stripped.length; i++) {
739
+ const ch = stripped[i];
740
+ if (ch === '{') depth += 1;
741
+ if (ch === '}') {
742
+ depth -= 1;
743
+ if (depth === 0) {
744
+ return stripped.slice(start, i + 1);
745
+ }
746
+ }
747
+ }
748
+ return null;
749
+ }
750
+
751
+ _resolveReferenceInput(prompt, context = {}) {
752
+ if (context.referenceUrl && typeof context.referenceUrl === 'string') {
753
+ return context.referenceUrl.trim();
754
+ }
755
+ if (context.referencePath && typeof context.referencePath === 'string') {
756
+ return context.referencePath.trim();
757
+ }
758
+ if (context.reference && typeof context.reference === 'string') {
759
+ return context.reference.trim();
760
+ }
761
+ return this._extractFirstUrl(prompt);
762
+ }
763
+
764
+ _resolveModelPolicy(context = {}) {
765
+ const policy = context.modelPolicy && typeof context.modelPolicy === 'object'
766
+ ? context.modelPolicy
767
+ : {};
768
+
769
+ return {
770
+ director: policy.director || context.directorModel || DEFAULT_DIRECTOR_MODEL,
771
+ producer: policy.producer || context.producerModel || DEFAULT_PRODUCER_MODEL,
772
+ verifier: policy.verifier || context.verifierModel || DEFAULT_VERIFIER_MODEL
773
+ };
774
+ }
775
+
776
+ _extractFirstUrl(text) {
777
+ if (!text || typeof text !== 'string') return null;
778
+ const match = text.match(/https?:\/\/[^\s)]+/i);
779
+ return match ? match[0] : null;
780
+ }
781
+
782
+ async _analyzeReference(referenceInput) {
783
+ if (!referenceInput) return null;
784
+ try {
785
+ const bridge = await this.ensurePythonBridge();
786
+ const key = /^https?:\/\//i.test(referenceInput) ? 'url' : 'file_path';
787
+ const profile = await bridge.call('analyze_reference', {
788
+ [key]: referenceInput,
789
+ include_genre_in_hints: false
790
+ }, 120000);
791
+ this.log('info', 'Reference analysis complete', {
792
+ source: referenceInput,
793
+ bpm: profile?.bpm,
794
+ key: profile?.key,
795
+ mode: profile?.mode
796
+ });
797
+ return profile;
798
+ } catch (error) {
799
+ this.log('warn', 'Reference analysis failed; continuing without it', {
800
+ source: referenceInput,
801
+ error: error.message
802
+ });
803
+ return null;
804
+ }
805
+ }
806
+
807
+ async _draftDirectorGuidance(prompt, referenceProfile, directorModel) {
808
+ const referenceContext = this._formatReferenceContext(referenceProfile);
809
+ const instruction = `You are the Director role. Produce concise creative direction for song planning (not JSON).
810
+ Prompt: ${prompt}
811
+
812
+ ${referenceContext}
813
+
814
+ Return 6-10 bullet points covering: form, energy arc, rhythm feel, harmony color, instrumentation priorities, and mix aesthetic.`;
815
+
816
+ try {
817
+ const response = await this.chat(instruction, { model: directorModel });
818
+ return response?.text || 'No director guidance available.';
819
+ } catch (error) {
820
+ this.log('warn', 'Director guidance failed; fallback to prompt-only planning', {
821
+ model: directorModel,
822
+ error: error.message
823
+ });
824
+ return 'Director guidance unavailable; use prompt and reference profile only.';
825
+ }
826
+ }
827
+
828
+ _formatReferenceContext(profile) {
829
+ if (!profile || typeof profile !== 'object') {
830
+ return 'Reference profile: none.';
831
+ }
832
+
833
+ const compact = {
834
+ source: profile.source,
835
+ title: profile.title,
836
+ bpm: profile.bpm,
837
+ key: profile.key,
838
+ mode: profile.mode,
839
+ estimated_genre: profile.estimated_genre,
840
+ style_tags: profile.style_tags,
841
+ prompt_hints: profile.prompt_hints,
842
+ generation_params: profile.generation_params
843
+ };
844
+
845
+ return `Reference profile (ground truth from Python audio analysis):\n${JSON.stringify(compact, null, 2)}\nUse it to guide tempo/key/feel, but keep the final score plan coherent with the user prompt.`;
846
+ }
847
+
848
+ _formatSuccessResponse(plan, generation, critics, attempt, extras = {}) {
849
+ const title = generation.title || generation.output_name || generation.output_filename || 'Generated track';
850
+ const midiPath = generation.midi_path || 'unknown';
851
+ const audioPath = generation.audio_path || generation.wav_path || 'unknown';
852
+ const criticsSummary = critics?.report?.summary || 'Critics passed.';
853
+ const preflightStatus = extras?.preflight?.passed === false ? 'FAIL' : 'PASS';
854
+ const outputScore = extras?.outputAnalysis && typeof extras.outputAnalysis.genre_match_score !== 'undefined'
855
+ ? extras.outputAnalysis.genre_match_score
856
+ : 'n/a';
857
+ const outputPass = extras?.outputAnalysis && typeof extras.outputAnalysis.passed !== 'undefined'
858
+ ? extras.outputAnalysis.passed
859
+ : 'n/a';
860
+ const criticBypassLine = extras?.criticGateBypassed ? '\nCritic Gate Bypass: enabled (generation accepted despite critic failure).' : '';
861
+ return `Producer completed in ${attempt} attempt(s).
862
+ Title: ${title}
863
+ Prompt: ${plan.prompt}
864
+ Key/Mode: ${plan.key} ${plan.mode}
865
+ BPM: ${plan.bpm}
866
+ MIDI: ${midiPath}
867
+ Audio: ${audioPath}
868
+ Preflight Gate: ${preflightStatus}
869
+ Critics: ${criticsSummary}
870
+ Output Analysis: passed=${outputPass}, genre_match_score=${outputScore}${criticBypassLine}`;
871
+ }
872
+
873
+ _formatFailureResponse(plan, generation, critics, attempts, extras = {}) {
874
+ const criticsSummary = critics?.report?.summary || 'Critics failed.';
875
+ const preflightStatus = extras?.preflight?.passed === false ? 'FAIL' : 'n/a';
876
+ const outputScore = extras?.outputAnalysis && typeof extras.outputAnalysis.genre_match_score !== 'undefined'
877
+ ? extras.outputAnalysis.genre_match_score
878
+ : 'n/a';
879
+ const bypassHint = extras?.suggestBypass
880
+ ? '\nTip: Use /produce --accept-generation <prompt> to accept generated output even when critics fail.'
881
+ : '';
882
+ return `Producer failed after ${attempts} attempt(s).
883
+ Prompt: ${plan?.prompt || 'unknown'}
884
+ Last result: ${generation?.midi_path || 'no midi'}
885
+ Preflight Gate: ${preflightStatus}
886
+ Critics: ${criticsSummary}
887
+ Output Analysis Score: ${outputScore}${bypassHint}`;
888
+ }
889
+ }
890
+
891
+ module.exports = { ProducerAgent };