@vibeframe/cli 0.112.1 → 0.113.2

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/dist/index.js CHANGED
@@ -7066,8 +7066,8 @@ var init_api_keys = __esm({
7066
7066
  envVar: "OPENAI_API_KEY",
7067
7067
  label: "OpenAI",
7068
7068
  showInSetup: true,
7069
- setupDescription: "gpt-image-2 image gen ($, default since v0.56), Whisper transcribe, Agent",
7070
- envExampleComment: "OpenAI API Key (Whisper transcription, gpt-image-2 \u2014 default text-to-image since v0.56)",
7069
+ setupDescription: "gpt-image-2 image gen ($, default since v0.56), gpt-4o-mini-tts narration, Whisper transcribe, Agent",
7070
+ envExampleComment: "OpenAI API Key (Whisper transcription, gpt-4o-mini-tts narration, gpt-image-2 \u2014 default text-to-image since v0.56)",
7071
7071
  envExampleUrl: "https://platform.openai.com/api-keys",
7072
7072
  keyFormat: { prefix: /^sk-/, example: "sk-..." }
7073
7073
  });
@@ -9174,8 +9174,8 @@ var init_openai = __esm({
9174
9174
  id: "openai",
9175
9175
  label: "OpenAI",
9176
9176
  apiKey: "openai",
9177
- kinds: ["llm", "image", "transcription"],
9178
- resolverPriority: { image: 1 },
9177
+ kinds: ["llm", "image", "transcription", "speech"],
9178
+ resolverPriority: { image: 1, speech: 2 },
9179
9179
  commandsUnlocked: [
9180
9180
  "agent -p openai",
9181
9181
  "generate image -p openai",
@@ -11597,16 +11597,19 @@ var init_elevenlabs = __esm({
11597
11597
  });
11598
11598
 
11599
11599
  // ../ai-providers/dist/kokoro/KokoroProvider.js
11600
- async function loadKokoroFactory() {
11600
+ async function loadKokoroRuntime() {
11601
11601
  if (factoryOverride)
11602
- return factoryOverride;
11602
+ return { factory: factoryOverride, device: "cpu" };
11603
11603
  try {
11604
11604
  const mod = await import("kokoro-js");
11605
- return mod.KokoroTTS;
11605
+ return { factory: mod.KokoroTTS, device: "cpu" };
11606
11606
  } catch (err) {
11607
- const fallback2 = await loadKokoroFromWorkspace();
11608
- if (fallback2)
11609
- return fallback2;
11607
+ const workspace = await loadKokoroFromWorkspace();
11608
+ if (workspace)
11609
+ return { factory: workspace, device: "cpu" };
11610
+ const bundled = await loadBundledKokoroRuntime();
11611
+ if (bundled)
11612
+ return bundled;
11610
11613
  throw mapKokoroImportError(err);
11611
11614
  }
11612
11615
  }
@@ -11626,10 +11629,51 @@ async function loadKokoroFromWorkspace(baseDir = process.cwd()) {
11626
11629
  return null;
11627
11630
  }
11628
11631
  }
11632
+ async function loadBundledKokoroRuntime(runtimeDir = process.env.VIBE_KOKORO_RUNTIME) {
11633
+ const base = runtimeDir?.trim();
11634
+ if (!base)
11635
+ return null;
11636
+ try {
11637
+ const { readFile: readFile56, writeFile: writeFile61, mkdir: mkdir31 } = await import("node:fs/promises");
11638
+ const { join: join30, resolve: resolve86 } = await import("node:path");
11639
+ const { pathToFileURL } = await import("node:url");
11640
+ const { homedir: homedir8 } = await import("node:os");
11641
+ const transformersDir = resolve86(base, "node_modules", "@huggingface", "transformers");
11642
+ const ortDistDir = resolve86(base, "node_modules", "onnxruntime-web", "dist");
11643
+ const transformers = await import(pathToFileURL(join30(transformersDir, "dist", "transformers.web.js")).href);
11644
+ const cacheRoot = join30(homedir8(), ".cache", "vibeframe", "models");
11645
+ transformers.env.allowLocalModels = false;
11646
+ transformers.env.useCustomCache = true;
11647
+ transformers.env.customCache = {
11648
+ async match(key) {
11649
+ try {
11650
+ return new Response(await readFile56(join30(cacheRoot, encodeURIComponent(String(key)))));
11651
+ } catch {
11652
+ return void 0;
11653
+ }
11654
+ },
11655
+ async put(key, response) {
11656
+ try {
11657
+ const buf = Buffer.from(await response.arrayBuffer());
11658
+ await mkdir31(cacheRoot, { recursive: true });
11659
+ await writeFile61(join30(cacheRoot, encodeURIComponent(String(key))), buf);
11660
+ } catch {
11661
+ }
11662
+ }
11663
+ };
11664
+ transformers.env.backends.onnx.wasm.wasmPaths = pathToFileURL(ortDistDir + "/").href;
11665
+ const factory = await loadKokoroFromWorkspace(base);
11666
+ if (!factory)
11667
+ return null;
11668
+ return { factory, device: "cpu" };
11669
+ } catch {
11670
+ return null;
11671
+ }
11672
+ }
11629
11673
  function mapKokoroImportError(err) {
11630
11674
  const code = err instanceof Error && "code" in err ? err.code : void 0;
11631
11675
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
11632
- return new Error("Local Kokoro TTS is unavailable: optional dependency kokoro-js is not installed (its native dependencies may have failed to build on this platform). Install it with `npm i kokoro-js` \u2014 run it inside your workspace folder when using the Claude Desktop extension \u2014 or set ELEVENLABS_API_KEY to use the elevenlabs TTS provider instead.");
11676
+ return new Error("Local Kokoro TTS is unavailable: kokoro-js could not be loaded from the npm install, the workspace node_modules, or the bundled extension runtime. Set OPENAI_API_KEY or ELEVENLABS_API_KEY to use a cloud voice instead (ttsProvider: openai / elevenlabs), or install the engine locally with `npm i kokoro-js` in your workspace folder.");
11633
11677
  }
11634
11678
  return err instanceof Error ? err : new Error(String(err));
11635
11679
  }
@@ -11637,10 +11681,12 @@ function loadModel(progress) {
11637
11681
  if (modelPromise)
11638
11682
  return modelPromise;
11639
11683
  modelPromise = (async () => {
11640
- const factory = await loadKokoroFactory();
11641
- return factory.from_pretrained(KOKORO_MODEL_ID, {
11684
+ const runtime = await loadKokoroRuntime();
11685
+ const override = process.env.VIBE_ONNX_DEVICE;
11686
+ const device = override === "cpu" || override === "wasm" ? override : runtime.device;
11687
+ return runtime.factory.from_pretrained(KOKORO_MODEL_ID, {
11642
11688
  dtype: "q8",
11643
- device: "cpu",
11689
+ device,
11644
11690
  progress_callback: progress ? (raw) => progress(normaliseEvent(raw)) : void 0
11645
11691
  });
11646
11692
  })().catch((err) => {
@@ -11724,7 +11770,7 @@ var init_kokoro = __esm({
11724
11770
  label: "Kokoro (local)",
11725
11771
  apiKey: null,
11726
11772
  kinds: ["speech"],
11727
- resolverPriority: { speech: 2 }
11773
+ resolverPriority: { speech: 3 }
11728
11774
  });
11729
11775
  }
11730
11776
  });
@@ -11996,12 +12042,119 @@ var init_openai_image = __esm({
11996
12042
  }
11997
12043
  });
11998
12044
 
12045
+ // ../ai-providers/dist/openai-tts/OpenAiTtsProvider.js
12046
+ var OPENAI_TTS_VOICES, DEFAULT_VOICE, DEFAULT_MODEL2, TTS_429_MAX_RETRIES2, TTS_429_RETRY_DELAY_MS2, OpenAiTtsProvider, openaiTtsProvider;
12047
+ var init_OpenAiTtsProvider = __esm({
12048
+ "../ai-providers/dist/openai-tts/OpenAiTtsProvider.js"() {
12049
+ "use strict";
12050
+ OPENAI_TTS_VOICES = [
12051
+ "alloy",
12052
+ "ash",
12053
+ "ballad",
12054
+ "cedar",
12055
+ "coral",
12056
+ "echo",
12057
+ "fable",
12058
+ "marin",
12059
+ "nova",
12060
+ "onyx",
12061
+ "sage",
12062
+ "shimmer",
12063
+ "verse"
12064
+ ];
12065
+ DEFAULT_VOICE = "marin";
12066
+ DEFAULT_MODEL2 = "gpt-4o-mini-tts";
12067
+ TTS_429_MAX_RETRIES2 = 1;
12068
+ TTS_429_RETRY_DELAY_MS2 = 2e3;
12069
+ OpenAiTtsProvider = class {
12070
+ constructor() {
12071
+ this.id = "openai";
12072
+ this.name = "OpenAI TTS";
12073
+ this.description = "OpenAI cloud text-to-speech (gpt-4o-mini-tts)";
12074
+ this.capabilities = ["text-to-speech"];
12075
+ this.isAvailable = true;
12076
+ this.baseUrl = "https://api.openai.com/v1";
12077
+ }
12078
+ async initialize(config2) {
12079
+ this.apiKey = config2.apiKey;
12080
+ if (config2.baseUrl) {
12081
+ this.baseUrl = config2.baseUrl;
12082
+ }
12083
+ }
12084
+ isConfigured() {
12085
+ return !!this.apiKey;
12086
+ }
12087
+ async textToSpeech(text, options = {}) {
12088
+ if (!this.apiKey) {
12089
+ return { success: false, error: "OpenAI API key not configured" };
12090
+ }
12091
+ const voice = (options.voice ?? DEFAULT_VOICE).toLowerCase();
12092
+ if (!OPENAI_TTS_VOICES.includes(voice)) {
12093
+ return {
12094
+ success: false,
12095
+ error: `Unknown OpenAI voice "${options.voice}". Available voices: ${OPENAI_TTS_VOICES.join(", ")}.`
12096
+ };
12097
+ }
12098
+ try {
12099
+ let response;
12100
+ for (let attempt = 0; ; attempt++) {
12101
+ response = await fetch(`${this.baseUrl}/audio/speech`, {
12102
+ method: "POST",
12103
+ headers: {
12104
+ Authorization: `Bearer ${this.apiKey}`,
12105
+ "Content-Type": "application/json"
12106
+ },
12107
+ body: JSON.stringify({
12108
+ model: options.model ?? DEFAULT_MODEL2,
12109
+ input: text,
12110
+ voice,
12111
+ response_format: "mp3",
12112
+ ...options.speed !== void 0 && { speed: options.speed },
12113
+ ...options.instructions !== void 0 && { instructions: options.instructions }
12114
+ })
12115
+ });
12116
+ if (response.status === 429 && attempt < TTS_429_MAX_RETRIES2) {
12117
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, TTS_429_RETRY_DELAY_MS2 * (attempt + 1)));
12118
+ continue;
12119
+ }
12120
+ break;
12121
+ }
12122
+ if (!response.ok) {
12123
+ const error = await response.text();
12124
+ return { success: false, error: `OpenAI TTS failed: ${error}` };
12125
+ }
12126
+ const arrayBuffer = await response.arrayBuffer();
12127
+ return {
12128
+ success: true,
12129
+ audioBuffer: Buffer.from(arrayBuffer),
12130
+ characterCount: text.length
12131
+ };
12132
+ } catch (error) {
12133
+ return {
12134
+ success: false,
12135
+ error: error instanceof Error ? error.message : "Unknown error"
12136
+ };
12137
+ }
12138
+ }
12139
+ };
12140
+ openaiTtsProvider = new OpenAiTtsProvider();
12141
+ }
12142
+ });
12143
+
12144
+ // ../ai-providers/dist/openai-tts/index.js
12145
+ var init_openai_tts = __esm({
12146
+ "../ai-providers/dist/openai-tts/index.js"() {
12147
+ "use strict";
12148
+ init_OpenAiTtsProvider();
12149
+ }
12150
+ });
12151
+
11999
12152
  // ../ai-providers/dist/runway/RunwayProvider.js
12000
- var DEFAULT_MODEL2, RunwayProvider, runwayProvider;
12153
+ var DEFAULT_MODEL3, RunwayProvider, runwayProvider;
12001
12154
  var init_RunwayProvider = __esm({
12002
12155
  "../ai-providers/dist/runway/RunwayProvider.js"() {
12003
12156
  "use strict";
12004
- DEFAULT_MODEL2 = "gen4.5";
12157
+ DEFAULT_MODEL3 = "gen4.5";
12005
12158
  RunwayProvider = class _RunwayProvider {
12006
12159
  constructor() {
12007
12160
  this.id = "runway";
@@ -12044,7 +12197,7 @@ var init_RunwayProvider = __esm({
12044
12197
  "1:1": "960:960"
12045
12198
  };
12046
12199
  const apiRatio = ratioMap[options?.aspectRatio || "16:9"] || "1280:720";
12047
- const model = options?.model || DEFAULT_MODEL2;
12200
+ const model = options?.model || DEFAULT_MODEL3;
12048
12201
  if (!options?.referenceImage && model !== "gen4.5") {
12049
12202
  return {
12050
12203
  id: "",
@@ -12306,11 +12459,11 @@ var init_runway = __esm({
12306
12459
 
12307
12460
  // ../ai-providers/dist/kling/KlingProvider.js
12308
12461
  import { createHmac } from "node:crypto";
12309
- var DEFAULT_MODEL3, STD_MODE_MODELS, KlingProvider, klingProvider;
12462
+ var DEFAULT_MODEL4, STD_MODE_MODELS, KlingProvider, klingProvider;
12310
12463
  var init_KlingProvider = __esm({
12311
12464
  "../ai-providers/dist/kling/KlingProvider.js"() {
12312
12465
  "use strict";
12313
- DEFAULT_MODEL3 = "kling-v2-5-turbo";
12466
+ DEFAULT_MODEL4 = "kling-v2-5-turbo";
12314
12467
  STD_MODE_MODELS = ["kling-v2-5-turbo", "kling-v2-6", "kling-v3", "kling-v3-omni"];
12315
12468
  KlingProvider = class {
12316
12469
  constructor() {
@@ -12373,7 +12526,7 @@ var init_KlingProvider = __esm({
12373
12526
  }
12374
12527
  try {
12375
12528
  const token = this.generateToken();
12376
- const model = options?.model || DEFAULT_MODEL3;
12529
+ const model = options?.model || DEFAULT_MODEL4;
12377
12530
  let mode = options?.mode || "std";
12378
12531
  if (!STD_MODE_MODELS.includes(model) && mode === "std") {
12379
12532
  mode = "pro";
@@ -12744,11 +12897,11 @@ var init_kling = __esm({
12744
12897
  });
12745
12898
 
12746
12899
  // ../ai-providers/dist/grok/GrokProvider.js
12747
- var DEFAULT_MODEL4, GrokProvider, grokProvider;
12900
+ var DEFAULT_MODEL5, GrokProvider, grokProvider;
12748
12901
  var init_GrokProvider = __esm({
12749
12902
  "../ai-providers/dist/grok/GrokProvider.js"() {
12750
12903
  "use strict";
12751
- DEFAULT_MODEL4 = "grok-imagine-video";
12904
+ DEFAULT_MODEL5 = "grok-imagine-video";
12752
12905
  GrokProvider = class {
12753
12906
  constructor() {
12754
12907
  this.id = "grok";
@@ -12915,7 +13068,7 @@ var init_GrokProvider = __esm({
12915
13068
  try {
12916
13069
  const duration = Math.round(Math.min(15, Math.max(1, options?.duration || 5)));
12917
13070
  const body = {
12918
- model: DEFAULT_MODEL4,
13071
+ model: DEFAULT_MODEL5,
12919
13072
  prompt: prompt3,
12920
13073
  duration,
12921
13074
  aspect_ratio: options?.aspectRatio || "16:9"
@@ -17181,6 +17334,19 @@ var require_src = __commonJS({
17181
17334
  });
17182
17335
 
17183
17336
  // ../ai-providers/dist/fal/FalProvider.js
17337
+ function estimateSeedanceVideoCostUsd(opts) {
17338
+ const shortSide = SEEDANCE_SHORT_SIDE[opts.resolution ?? "720p"] ?? 720;
17339
+ const [a, b] = (opts.aspectRatio ?? "16:9").split(":").map(Number);
17340
+ const ratioFactor = a > 0 && b > 0 ? Math.max(a, b) / Math.min(a, b) : 16 / 9;
17341
+ const longSide = Math.round(shortSide * ratioFactor);
17342
+ const tokensPerSecond = shortSide * longSide * 24 / 1024;
17343
+ let usdPerSecond = tokensPerSecond / 1e3 * SEEDANCE_USD_PER_1K_TOKENS;
17344
+ if (opts.fast && shortSide <= 720)
17345
+ usdPerSecond *= SEEDANCE_FAST_FACTOR;
17346
+ if (opts.hasVideoReference)
17347
+ usdPerSecond *= SEEDANCE_VIDEO_REF_FACTOR;
17348
+ return Number((usdPerSecond * Math.max(0, opts.durationSec)).toFixed(2));
17349
+ }
17184
17350
  function formatFalError(error) {
17185
17351
  if (!(error instanceof Error))
17186
17352
  return String(error);
@@ -17290,7 +17456,7 @@ function normaliseDuration(value) {
17290
17456
  return "auto";
17291
17457
  return Math.max(4, Math.min(15, Math.round(value)));
17292
17458
  }
17293
- var import_client, ENDPOINT_TEXT_TO_VIDEO, ENDPOINT_IMAGE_TO_VIDEO, ENDPOINT_REFERENCE_TO_VIDEO, DEFAULT_VARIANT, VALID_RESOLUTIONS, VALID_ASPECTS, FalProvider, falProvider;
17459
+ var import_client, ENDPOINT_TEXT_TO_VIDEO, ENDPOINT_IMAGE_TO_VIDEO, ENDPOINT_REFERENCE_TO_VIDEO, DEFAULT_VARIANT, VALID_RESOLUTIONS, VALID_ASPECTS, SEEDANCE_USD_PER_1K_TOKENS, SEEDANCE_FAST_FACTOR, SEEDANCE_VIDEO_REF_FACTOR, SEEDANCE_SHORT_SIDE, FalProvider, falProvider;
17294
17460
  var init_FalProvider = __esm({
17295
17461
  "../ai-providers/dist/fal/FalProvider.js"() {
17296
17462
  "use strict";
@@ -17310,6 +17476,10 @@ var init_FalProvider = __esm({
17310
17476
  DEFAULT_VARIANT = "seedance-2.0";
17311
17477
  VALID_RESOLUTIONS = ["480p", "720p", "1080p"];
17312
17478
  VALID_ASPECTS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"];
17479
+ SEEDANCE_USD_PER_1K_TOKENS = 0.014;
17480
+ SEEDANCE_FAST_FACTOR = 0.8;
17481
+ SEEDANCE_VIDEO_REF_FACTOR = 0.6;
17482
+ SEEDANCE_SHORT_SIDE = { "480p": 480, "720p": 720, "1080p": 1080 };
17313
17483
  FalProvider = class {
17314
17484
  constructor() {
17315
17485
  this.id = "seedance";
@@ -18122,9 +18292,11 @@ __export(dist_exports2, {
18122
18292
  KOKORO_MODEL_ID: () => KOKORO_MODEL_ID,
18123
18293
  KlingProvider: () => KlingProvider,
18124
18294
  KokoroProvider: () => KokoroProvider,
18295
+ OPENAI_TTS_VOICES: () => OPENAI_TTS_VOICES,
18125
18296
  OllamaProvider: () => OllamaProvider,
18126
18297
  OpenAIImageProvider: () => OpenAIImageProvider,
18127
18298
  OpenAIProvider: () => OpenAIProvider,
18299
+ OpenAiTtsProvider: () => OpenAiTtsProvider,
18128
18300
  ReplicateProvider: () => ReplicateProvider,
18129
18301
  RunwayProvider: () => RunwayProvider,
18130
18302
  WhisperProvider: () => WhisperProvider,
@@ -18132,6 +18304,7 @@ __export(dist_exports2, {
18132
18304
  defineApiKey: () => defineApiKey,
18133
18305
  defineProvider: () => defineProvider,
18134
18306
  elevenLabsProvider: () => elevenLabsProvider,
18307
+ estimateSeedanceVideoCostUsd: () => estimateSeedanceVideoCostUsd,
18135
18308
  falProvider: () => falProvider,
18136
18309
  geminiProvider: () => geminiProvider,
18137
18310
  getAllApiKeys: () => getAllApiKeys,
@@ -18148,9 +18321,12 @@ __export(dist_exports2, {
18148
18321
  isGeminiTextModelAlias: () => isGeminiTextModelAlias,
18149
18322
  klingProvider: () => klingProvider,
18150
18323
  kokoroProvider: () => kokoroProvider,
18324
+ loadBundledKokoroRuntime: () => loadBundledKokoroRuntime,
18325
+ loadKokoroFromWorkspace: () => loadKokoroFromWorkspace,
18151
18326
  ollamaProvider: () => ollamaProvider,
18152
18327
  openaiImageProvider: () => openaiImageProvider,
18153
18328
  openaiProvider: () => openaiProvider,
18329
+ openaiTtsProvider: () => openaiTtsProvider,
18154
18330
  providerRegistry: () => providerRegistry,
18155
18331
  replicateProvider: () => replicateProvider,
18156
18332
  resolveGeminiTextModel: () => resolveGeminiTextModel,
@@ -18173,6 +18349,7 @@ var init_dist2 = __esm({
18173
18349
  init_elevenlabs();
18174
18350
  init_kokoro();
18175
18351
  init_openai_image();
18352
+ init_openai_tts();
18176
18353
  init_runway();
18177
18354
  init_kling();
18178
18355
  init_grok();
@@ -26347,10 +26524,22 @@ var init_api_key = __esm({
26347
26524
 
26348
26525
  // src/commands/_shared/tts-resolve.ts
26349
26526
  async function resolveTtsProvider(preferred = "auto") {
26350
- const choice = preferred === "auto" ? await getConfiguredApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro" : preferred;
26527
+ let choice = preferred;
26528
+ if (choice === "auto") {
26529
+ if (await getConfiguredApiKey("ELEVENLABS_API_KEY")) {
26530
+ choice = "elevenlabs";
26531
+ } else if (await getConfiguredApiKey("OPENAI_API_KEY")) {
26532
+ choice = "openai";
26533
+ } else {
26534
+ choice = "kokoro";
26535
+ }
26536
+ }
26351
26537
  if (choice === "elevenlabs") {
26352
26538
  return buildElevenLabs();
26353
26539
  }
26540
+ if (choice === "openai") {
26541
+ return buildOpenAi();
26542
+ }
26354
26543
  return buildKokoro();
26355
26544
  }
26356
26545
  async function buildElevenLabs() {
@@ -26366,6 +26555,19 @@ async function buildElevenLabs() {
26366
26555
  });
26367
26556
  return { provider: "elevenlabs", audioExtension: "mp3", call };
26368
26557
  }
26558
+ async function buildOpenAi() {
26559
+ const key = await getApiKey("OPENAI_API_KEY", "OpenAI");
26560
+ if (!key) {
26561
+ throw new TtsKeyMissingError("openai");
26562
+ }
26563
+ const provider = new OpenAiTtsProvider();
26564
+ await provider.initialize({ apiKey: key });
26565
+ const call = async (text, opts) => provider.textToSpeech(text, {
26566
+ voice: opts?.voice,
26567
+ speed: opts?.speed
26568
+ });
26569
+ return { provider: "openai", audioExtension: "mp3", call };
26570
+ }
26369
26571
  async function buildKokoro() {
26370
26572
  const provider = new KokoroProvider();
26371
26573
  await provider.initialize({});
@@ -26378,11 +26580,11 @@ async function buildKokoro() {
26378
26580
  }
26379
26581
  function parseTtsProviderName(value) {
26380
26582
  if (!value) return "auto";
26381
- if (value === "auto" || value === "elevenlabs" || value === "kokoro") {
26583
+ if (value === "auto" || value === "elevenlabs" || value === "openai" || value === "kokoro") {
26382
26584
  return value;
26383
26585
  }
26384
26586
  throw new Error(
26385
- `Invalid --tts: ${value}. Valid: auto, elevenlabs, kokoro.`
26587
+ `Invalid --tts: ${value}. Valid: auto, elevenlabs, openai, kokoro.`
26386
26588
  );
26387
26589
  }
26388
26590
  var TtsKeyMissingError;
@@ -26394,7 +26596,7 @@ var init_tts_resolve = __esm({
26394
26596
  TtsKeyMissingError = class extends Error {
26395
26597
  constructor(provider) {
26396
26598
  super(
26397
- provider === "elevenlabs" ? "ElevenLabs API key required (ELEVENLABS_API_KEY). Run 'vibe setup', set ELEVENLABS_API_KEY in .env, or pass --tts kokoro for local synthesis." : `Provider ${provider} is unavailable.`
26599
+ provider === "elevenlabs" ? "ElevenLabs API key required (ELEVENLABS_API_KEY). Run 'vibe setup', set ELEVENLABS_API_KEY in .env, or pass --tts kokoro for local synthesis." : provider === "openai" ? "OpenAI API key required (OPENAI_API_KEY). Run 'vibe setup', set OPENAI_API_KEY in .env, or pass --tts kokoro for local synthesis." : `Provider ${provider} is unavailable.`
26398
26600
  );
26399
26601
  this.provider = provider;
26400
26602
  this.name = "TtsKeyMissingError";
@@ -26821,6 +27023,43 @@ vibe host setup all --write # write project/app config
26821
27023
  vibe host doctor all --json # verify readiness
26822
27024
  \`\`\`
26823
27025
 
27026
+ ## Native host goal loop
27027
+
27028
+ For long-running work, use Codex Goal mode, Claude Code \`/goal\`, or the
27029
+ host's equivalent as the outer loop. VibeFrame should not run a competing
27030
+ project-level goal loop; it provides \`--json\` commands, dry runs, cost caps,
27031
+ \`build-report.json\`, \`review-report.json\`, \`nextActions\`, \`safeToAutoRun\`,
27032
+ \`requiresConfirmation\`, \`fixOwner\`, deterministic repair, and render
27033
+ inspection for the host agent to reason over.
27034
+
27035
+ Copy-paste examples:
27036
+
27037
+ \`\`\`text
27038
+ /goal Build this VibeFrame project into renders/final.mp4. Use --json for every
27039
+ vibe command, run --dry-run before paid operations, use --max-cost 5 for builds
27040
+ unless the user sets another budget, read build-report.json and
27041
+ review-report.json before deciding the next action, prefer nextActions before
27042
+ guessing, run only safeToAutoRun:true actions automatically, and ask before
27043
+ requiresConfirmation:true actions. Treat retryWith as the compatibility
27044
+ fallback, fixOwner:"vibe" as CLI repair work, and fixOwner:"host-agent" as
27045
+ STORYBOARD.md, DESIGN.md, or composition edits.
27046
+ Stop only when renders/final.mp4 exists, duration and aspect ratio match the
27047
+ brief, inspect render --cheap has no errors, any AI review score is >= 90 when AI review is requested, and every
27048
+ remaining host-agent issue is fixed, accepted with rationale, or reported as
27049
+ blocked.
27050
+ \`\`\`
27051
+
27052
+ \`\`\`text
27053
+ /goal Finish this VibeFrame render using Claude Code goal mode as the outer
27054
+ loop. Use vibe context/schema when unsure, --json everywhere, dry-run before
27055
+ paid operations, budget cap via --max-cost 5, nextActions before guessing,
27056
+ build-report.json/review-report.json as loop state, and safeToAutoRun,
27057
+ requiresConfirmation, and fixOwner to decide between vibe scene repair and
27058
+ host-agent edits. Stop only after final MP4,
27059
+ target duration/aspect ratio, clean render inspection, any AI review score >= 90 when AI review is requested, and
27060
+ no unresolved unacknowledged host-agent issues.
27061
+ \`\`\`
27062
+
26824
27063
  ## Skills \u2014 USE THESE FIRST
26825
27064
 
26826
27065
  @SKILL.md
@@ -27316,6 +27555,9 @@ function clean(text) {
27316
27555
  function humanise(id) {
27317
27556
  return id.split(/[-_]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
27318
27557
  }
27558
+ function sceneCompositionId(beatId) {
27559
+ return `scene-${beatId}`;
27560
+ }
27319
27561
  function slugifySceneName(name) {
27320
27562
  const normalised = name.normalize("NFKD").replace(/[̀-ͯ]/g, "");
27321
27563
  const slug = normalised.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -27352,10 +27594,16 @@ function fitKineticFontSize(words) {
27352
27594
  function kenBurnsTween(scope, dur, endScale = 1.08) {
27353
27595
  return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
27354
27596
  }
27597
+ function backdropMotionTweens(scope, dur, hasImage, endScale = 1.08) {
27598
+ const sweep = hasImage ? "" : `
27599
+ tl.fromTo('${scope} .ambient-sweep', { xPercent: -75, opacity: 0.18 }, { xPercent: 75, opacity: 0.62, duration: ${dur.toFixed(2)}, ease: 'sine.inOut' }, 0);`;
27600
+ return `${kenBurnsTween(scope, dur, endScale)}${sweep}`;
27601
+ }
27355
27602
  function buildPreset(input) {
27356
27603
  const id = input.id;
27357
27604
  const scope = `[data-composition-id="${id}"]`;
27358
- const headline = clean(input.headline) || humanise(id);
27605
+ const displayId = input.beatId ?? id;
27606
+ const headline = clean(input.headline) || humanise(displayId);
27359
27607
  const subhead = clean(input.subhead);
27360
27608
  const kicker = clean(input.kicker);
27361
27609
  const dur = input.duration;
@@ -27378,8 +27626,17 @@ function buildPreset(input) {
27378
27626
  position: absolute;
27379
27627
  inset: 0;
27380
27628
  background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 70%);
27629
+ overflow: hidden;
27630
+ }
27631
+ ${scope} .ambient-sweep {
27632
+ position: absolute;
27633
+ inset: -22%;
27634
+ display: block;
27635
+ background: linear-gradient(115deg, rgba(255,255,255,0) 20%, rgba(0,201,255,0.34) 48%, rgba(142,45,226,0.26) 58%, rgba(255,255,255,0) 78%);
27636
+ filter: blur(12px);
27637
+ transform: rotate(8deg);
27381
27638
  }`;
27382
- const backdropMarkup = `<div class="backdrop"></div>`;
27639
+ const backdropMarkup = hasImage ? `<div class="backdrop"></div>` : `<div class="backdrop"><div class="ambient-sweep"></div></div>`;
27383
27640
  switch (input.preset) {
27384
27641
  case "simple": {
27385
27642
  const transcript = input.transcript;
@@ -27411,7 +27668,7 @@ function buildPreset(input) {
27411
27668
  body: `${backdropMarkup}
27412
27669
  <div class="caption" id="caption">${captionInner}</div>`,
27413
27670
  timeline: `${crossfadeTweens(scope, dur)}
27414
- ${kenBurnsTween(scope, dur)}
27671
+ ${backdropMotionTweens(scope, dur, hasImage)}
27415
27672
  ${captionTween}`
27416
27673
  };
27417
27674
  }
@@ -27450,12 +27707,12 @@ function buildPreset(input) {
27450
27707
  // scene's own fade-in handles the visual transition. Backdrop
27451
27708
  // Ken-Burns fills what used to be 80% dead static frame.
27452
27709
  timeline: `${crossfadeTweens(scope, dur)}
27453
- ${kenBurnsTween(scope, dur)}
27710
+ ${backdropMotionTweens(scope, dur, hasImage)}
27454
27711
  tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
27455
27712
  };
27456
27713
  }
27457
27714
  case "explainer": {
27458
- const k = kicker || humanise(id).toUpperCase();
27715
+ const k = kicker || humanise(displayId).toUpperCase();
27459
27716
  const sub = subhead || "";
27460
27717
  const transcript = input.transcript;
27461
27718
  const useWordSync = !!(transcript && transcript.length > 0 && sub);
@@ -27497,7 +27754,7 @@ function buildPreset(input) {
27497
27754
  <div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
27498
27755
  </div>`,
27499
27756
  timeline: `${crossfadeTweens(scope, dur)}
27500
- ${kenBurnsTween(scope, dur)}
27757
+ ${backdropMotionTweens(scope, dur, hasImage)}
27501
27758
  tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
27502
27759
  tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
27503
27760
  ${subtitleTween}`
@@ -27538,12 +27795,12 @@ function buildPreset(input) {
27538
27795
  body: `${backdropMarkup}
27539
27796
  <div class="kinetic">${wordSpans}</div>`,
27540
27797
  timeline: `${crossfadeTweens(scope, dur)}
27541
- ${kenBurnsTween(scope, dur)}
27798
+ ${backdropMotionTweens(scope, dur, hasImage)}
27542
27799
  ${tweens}`
27543
27800
  };
27544
27801
  }
27545
27802
  case "product-shot": {
27546
- const label = kicker || humanise(id);
27803
+ const label = kicker || humanise(displayId);
27547
27804
  const headlineFontPx = fitFontSize(headline, 72, 40, 50);
27548
27805
  return {
27549
27806
  css: `${scope} {
@@ -27577,7 +27834,7 @@ function buildPreset(input) {
27577
27834
  <div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
27578
27835
  <div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
27579
27836
  timeline: `${crossfadeTweens(scope, dur)}
27580
- tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: 1.12, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);
27837
+ ${backdropMotionTweens(scope, dur, hasImage, 1.12)}
27581
27838
  tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
27582
27839
  tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
27583
27840
  tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
@@ -27638,9 +27895,10 @@ function emitSceneHtml(input) {
27638
27895
  if (input.width <= 0 || input.height <= 0) {
27639
27896
  throw new Error(`Invalid canvas dims: ${input.width}x${input.height}`);
27640
27897
  }
27641
- const id = input.id;
27898
+ const beatId = input.id;
27899
+ const id = sceneCompositionId(beatId);
27642
27900
  const dur = Number(input.duration.toFixed(3));
27643
- const parts = buildPreset({ ...input, id, duration: dur });
27901
+ const parts = buildPreset({ ...input, id, beatId, duration: dur });
27644
27902
  const audioBlock = input.audioPath ? `
27645
27903
  <audio
27646
27904
  id="narration"
@@ -27656,7 +27914,7 @@ function emitSceneHtml(input) {
27656
27914
  ${lottieLayer.markup}` : "";
27657
27915
  const lottieScript = lottieLayer ? `
27658
27916
  ${lottieLayer.script}` : "";
27659
- return `<template id="scene-${id}-template">
27917
+ return `<template id="${id}-template">
27660
27918
  <div data-composition-id="${id}" data-start="0" data-duration="${dur}" data-width="${input.width}" data-height="${input.height}">
27661
27919
  <style>
27662
27920
  ${parts.css}
@@ -27692,8 +27950,9 @@ function buildClipReference(opts) {
27692
27950
  const duration = Number(opts.duration.toFixed(3));
27693
27951
  const track = opts.trackIndex ?? 1;
27694
27952
  const src = opts.src ?? `compositions/scene-${opts.id}.html`;
27953
+ const compositionId = sceneCompositionId(opts.id);
27695
27954
  const zIndex = Math.max(1, 1e6 - Math.floor(start * 1e3));
27696
- return `<div class="clip" data-composition-id="${esc(opts.id)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}" style="z-index: ${zIndex};"></div>`;
27955
+ return `<div class="clip" data-composition-id="${esc(compositionId)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}" style="z-index: ${zIndex};"></div>`;
27697
27956
  }
27698
27957
  function insertClipIntoRoot(rootHtml, clip) {
27699
27958
  const clipDiv = buildClipReference(clip);
@@ -29171,6 +29430,39 @@ var init_install_skill = __esm({
29171
29430
  });
29172
29431
 
29173
29432
  // src/commands/_shared/storyboard-parse.ts
29433
+ function resolveCharacters(frontmatter) {
29434
+ const pool = frontmatter?.characters;
29435
+ if (!pool || typeof pool !== "object") return [];
29436
+ const resolved = [];
29437
+ for (const [name, spec] of Object.entries(pool)) {
29438
+ if (typeof spec === "string") {
29439
+ const prompt3 = spec.trim();
29440
+ if (prompt3) resolved.push({ name, prompt: prompt3 });
29441
+ continue;
29442
+ }
29443
+ if (spec && typeof spec === "object") {
29444
+ const image = typeof spec.image === "string" ? spec.image.trim() : "";
29445
+ if (image) {
29446
+ resolved.push({ name, imagePath: image });
29447
+ continue;
29448
+ }
29449
+ const description = typeof spec.description === "string" ? spec.description.trim() : "";
29450
+ if (description) resolved.push({ name, prompt: description });
29451
+ }
29452
+ }
29453
+ return resolved;
29454
+ }
29455
+ function beatCharacterNames(cues) {
29456
+ const raw = cues?.characters;
29457
+ if (typeof raw === "string") {
29458
+ const name = raw.trim();
29459
+ return name ? [name] : [];
29460
+ }
29461
+ if (Array.isArray(raw)) {
29462
+ return raw.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
29463
+ }
29464
+ return [];
29465
+ }
29174
29466
  function parseStoryboard(md) {
29175
29467
  const normalized = md.replace(/\r\n/g, "\n");
29176
29468
  const { frontmatter, remaining } = extractProjectFrontmatter(normalized);
@@ -29776,7 +30068,15 @@ ${finalDurationBullet}
29776
30068
  itself. For multi-phase beats, drive phase changes with GSAP \`autoAlpha\`
29777
30069
  inside full-window clips: animate phase A out (\`autoAlpha: 0\`), then
29778
30070
  phase B in, on the same timeline.
29779
- - If \`assets/backdrop-${ctx.beat.id}.png\` exists, use that local file as the
30071
+ - If \`assets/video-${ctx.beat.id}.mp4\` exists, it is an AI-generated clip for
30072
+ this beat \u2014 use it as the FULL-FRAME visual and prefer it over any backdrop
30073
+ image or CSS. Put one full-bleed \`<video>\` inside a full-window \`.clip\`:
30074
+ \`<video src="assets/video-${ctx.beat.id}.mp4" data-start="0" data-duration="${BEAT_DURATION}" data-media-start="0" muted playsinline style="width:100%;height:100%;object-fit:cover"></video>\`
30075
+ Use the exact path (no \`./\` or \`../\`). The framework seeks the clip to each
30076
+ timeline frame, so do NOT add transform tweens to the \`<video>\` or its
30077
+ ancestors; layer text/motion graphics on top instead.
30078
+ - If \`assets/backdrop-${ctx.beat.id}.png\` exists (and no video clip above),
30079
+ use that local file as the
29780
30080
  full-frame visual backdrop. The exact path string is
29781
30081
  \`assets/backdrop-${ctx.beat.id}.png\`; do NOT prefix it with \`../\` or \`./\`
29782
30082
  because fragments are mounted from the project root.
@@ -30694,6 +30994,101 @@ function uniqueRetryWith(items) {
30694
30994
  ...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
30695
30995
  ];
30696
30996
  }
30997
+ function commandReviewAction(command, opts) {
30998
+ const classified = classifyReviewCommand(command);
30999
+ return stripUndefined({
31000
+ id: actionId("command", command),
31001
+ kind: "command",
31002
+ label: opts.label ?? command,
31003
+ command,
31004
+ fixOwner: opts.fixOwner ?? classified.fixOwner,
31005
+ costTier: opts.costTier ?? classified.costTier,
31006
+ safeToAutoRun: opts.safeToAutoRun ?? classified.safeToAutoRun,
31007
+ requiresConfirmation: opts.requiresConfirmation ?? classified.requiresConfirmation,
31008
+ reason: opts.reason,
31009
+ sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
31010
+ });
31011
+ }
31012
+ function agentReviewAction(opts) {
31013
+ return stripUndefined({
31014
+ id: actionId("agent", opts.agentPrompt),
31015
+ kind: "agent",
31016
+ label: opts.label,
31017
+ agentPrompt: opts.agentPrompt,
31018
+ fixOwner: "host-agent",
31019
+ costTier: "unknown",
31020
+ safeToAutoRun: false,
31021
+ requiresConfirmation: false,
31022
+ reason: opts.reason,
31023
+ sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
31024
+ });
31025
+ }
31026
+ function manualReviewAction(opts) {
31027
+ return stripUndefined({
31028
+ id: actionId("manual", opts.label),
31029
+ kind: "manual",
31030
+ label: opts.label,
31031
+ fixOwner: opts.fixOwner ?? "host-agent",
31032
+ costTier: "unknown",
31033
+ safeToAutoRun: false,
31034
+ requiresConfirmation: false,
31035
+ reason: opts.reason,
31036
+ sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
31037
+ });
31038
+ }
31039
+ function reviewActionsFromRetryWith(retryWith, sourceIssueCodes = []) {
31040
+ return normalizeReviewActions(
31041
+ retryWith.map((item) => {
31042
+ if (/^codex\s+/i.test(item)) {
31043
+ return agentReviewAction({
31044
+ label: "Ask the host agent to fix review issues",
31045
+ agentPrompt: item,
31046
+ reason: "The review report contains host-agent-owned issues.",
31047
+ sourceIssueCodes
31048
+ });
31049
+ }
31050
+ return commandReviewAction(item, {
31051
+ reason: "Backward-compatible retryWith command.",
31052
+ sourceIssueCodes
31053
+ });
31054
+ })
31055
+ );
31056
+ }
31057
+ function normalizeReviewActions(actions) {
31058
+ const merged = /* @__PURE__ */ new Map();
31059
+ for (const action of actions) {
31060
+ const key = [action.kind, action.command ?? action.agentPrompt ?? action.label].join("\0");
31061
+ const existing = merged.get(key);
31062
+ if (!existing) {
31063
+ merged.set(key, {
31064
+ ...action,
31065
+ sourceIssueCodes: uniqueStrings(action.sourceIssueCodes ?? [])
31066
+ });
31067
+ continue;
31068
+ }
31069
+ merged.set(key, {
31070
+ ...existing,
31071
+ safeToAutoRun: existing.safeToAutoRun && action.safeToAutoRun,
31072
+ requiresConfirmation: existing.requiresConfirmation || action.requiresConfirmation,
31073
+ sourceIssueCodes: uniqueStrings([
31074
+ ...existing.sourceIssueCodes ?? [],
31075
+ ...action.sourceIssueCodes ?? []
31076
+ ])
31077
+ });
31078
+ }
31079
+ return [...merged.values()].map(
31080
+ (action) => stripUndefined({
31081
+ ...action,
31082
+ sourceIssueCodes: action.sourceIssueCodes && action.sourceIssueCodes.length > 0 ? action.sourceIssueCodes : void 0
31083
+ })
31084
+ );
31085
+ }
31086
+ function deriveNextReviewActions(opts) {
31087
+ return normalizeReviewActions([
31088
+ ...opts.issues.flatMap((issue) => issue.actions ?? []),
31089
+ ...reviewActionsFromRetryWith(opts.retryWith)
31090
+ ]);
31091
+ }
30697
31092
  function fixOwnerForIssue(issue) {
30698
31093
  if (issue.fixOwner) return issue.fixOwner;
30699
31094
  if (issue.code.startsWith("AI_REVIEW_")) return "host-agent";
@@ -30703,11 +31098,23 @@ function fixOwnerForIssue(issue) {
30703
31098
  }
30704
31099
  return "vibe";
30705
31100
  }
30706
- function normalizeReviewIssues(issues, fallbackOwner) {
30707
- return issues.map((issue) => ({
30708
- ...issue,
30709
- fixOwner: issue.fixOwner ?? fallbackOwner ?? fixOwnerForIssue(issue)
30710
- }));
31101
+ function normalizeReviewIssues(issues, opts) {
31102
+ const fallbackOwner = typeof opts === "string" ? opts : opts?.fallbackOwner;
31103
+ const projectDir = typeof opts === "string" ? void 0 : opts?.projectDir;
31104
+ return issues.map((issue) => {
31105
+ const normalized = {
31106
+ ...issue,
31107
+ fixOwner: issue.fixOwner ?? fallbackOwner ?? fixOwnerForIssue(issue)
31108
+ };
31109
+ const actions = normalizeReviewActions([
31110
+ ...issue.actions ?? [],
31111
+ ...projectDir ? defaultActionsForIssue(normalized, projectDir) : []
31112
+ ]);
31113
+ return stripUndefined({
31114
+ ...normalized,
31115
+ actions: actions.length > 0 ? actions : void 0
31116
+ });
31117
+ });
30711
31118
  }
30712
31119
  function summarizeReviewIssues(issues) {
30713
31120
  const normalized = normalizeReviewIssues(issues);
@@ -30723,7 +31130,11 @@ function summarizeReviewIssues(issues) {
30723
31130
  };
30724
31131
  }
30725
31132
  function buildReviewReport(opts) {
30726
- const issues = normalizeReviewIssues(opts.issues);
31133
+ const retryWith = uniqueRetryWith(opts.retryWith);
31134
+ const issues = normalizeReviewIssues(opts.issues, {
31135
+ projectDir: opts.project,
31136
+ retryWith
31137
+ });
30727
31138
  return stripUndefined({
30728
31139
  schemaVersion: "1",
30729
31140
  kind: "review",
@@ -30734,7 +31145,8 @@ function buildReviewReport(opts) {
30734
31145
  score: opts.score,
30735
31146
  issues,
30736
31147
  summary: summarizeReviewIssues(issues),
30737
- retryWith: uniqueRetryWith(opts.retryWith),
31148
+ nextActions: deriveNextReviewActions({ issues, retryWith, projectDir: opts.project }),
31149
+ retryWith,
30738
31150
  sourceReports: opts.sourceReports ?? [],
30739
31151
  reportPath: opts.reportPath
30740
31152
  });
@@ -30748,6 +31160,306 @@ async function writeReviewReport(path2, report) {
30748
31160
  function stripUndefined(value) {
30749
31161
  return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
30750
31162
  }
31163
+ function defaultActionsForIssue(issue, projectDir) {
31164
+ const code = issue.code;
31165
+ const beat = issue.beatId ?? issue.scene;
31166
+ const beatFlag = beat ? ` --beat ${beat}` : "";
31167
+ const sourceIssueCodes = [code];
31168
+ if (code === "PROJECT_NOT_FOUND" || code === "MISSING_STORYBOARD") {
31169
+ return [
31170
+ commandReviewAction(`vibe init ${projectDir} --from "<brief>" --json`, {
31171
+ label: "Initialize the project from a brief",
31172
+ fixOwner: "host-agent",
31173
+ reason: "The project cannot be inspected until the core project files exist.",
31174
+ sourceIssueCodes
31175
+ })
31176
+ ];
31177
+ }
31178
+ if (code === "MISSING_DESIGN" || code === "DESIGN_PLACEHOLDER_FIELD") {
31179
+ return [
31180
+ manualReviewAction({
31181
+ label: "Create or complete DESIGN.md",
31182
+ reason: "Visual direction needs host-agent or human input before VibeFrame can repair it.",
31183
+ sourceIssueCodes
31184
+ })
31185
+ ];
31186
+ }
31187
+ if (code === "BEAT_NOT_FOUND" || code.startsWith("STORYBOARD_")) {
31188
+ const actions = [
31189
+ commandReviewAction(`vibe storyboard validate ${projectDir} --json`, {
31190
+ label: "Validate the storyboard",
31191
+ fixOwner: "host-agent",
31192
+ reason: "The storyboard issue needs structured validation before editing.",
31193
+ sourceIssueCodes
31194
+ })
31195
+ ];
31196
+ if (code === "STORYBOARD_PLACEHOLDER_CUE") {
31197
+ actions.push(
31198
+ commandReviewAction(
31199
+ `vibe storyboard revise ${projectDir} --from "<make cues concrete>" --dry-run --json`,
31200
+ {
31201
+ label: "Preview a storyboard revision",
31202
+ fixOwner: "host-agent",
31203
+ reason: "Placeholder cues should be revised into concrete video intent.",
31204
+ sourceIssueCodes
31205
+ }
31206
+ )
31207
+ );
31208
+ }
31209
+ return actions;
31210
+ }
31211
+ if (code === "MISSING_ROOT_COMPOSITION" || code === "ROOT_SYNC_CHECK_FAILED") {
31212
+ return [
31213
+ commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
31214
+ label: "Sync the project root composition",
31215
+ reason: "The root composition is missing or could not be verified.",
31216
+ sourceIssueCodes
31217
+ })
31218
+ ];
31219
+ }
31220
+ if (code.startsWith("ROOT_")) {
31221
+ return issue.fixOwner === "host-agent" ? [
31222
+ agentReviewAction({
31223
+ label: "Repair the root composition shell",
31224
+ agentPrompt: "Fix host-agent-owned root composition issues from review-report.json",
31225
+ reason: "The root composition cannot be repaired deterministically.",
31226
+ sourceIssueCodes
31227
+ })
31228
+ ] : [
31229
+ commandReviewAction(`vibe scene repair ${projectDir} --json`, {
31230
+ label: "Repair deterministic scene/root issues",
31231
+ reason: "The root composition has deterministic sync issues.",
31232
+ sourceIssueCodes
31233
+ }),
31234
+ commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
31235
+ label: "Resync root composition after repair",
31236
+ reason: "The root composition should be synced after deterministic repair.",
31237
+ sourceIssueCodes
31238
+ })
31239
+ ];
31240
+ }
31241
+ if (code === "MISSING_COMPOSITION") {
31242
+ return [
31243
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage compose --json`, {
31244
+ label: beat ? `Compose beat ${beat}` : "Compose missing scenes",
31245
+ reason: "Missing scene compositions must be generated or authored before render.",
31246
+ sourceIssueCodes
31247
+ })
31248
+ ];
31249
+ }
31250
+ if (code === "MISSING_BUILD_REPORT") {
31251
+ return [
31252
+ commandReviewAction(`vibe build ${projectDir} --dry-run --json`, {
31253
+ label: "Preview the build plan",
31254
+ reason: "A dry-run build report is needed before choosing a paid or mutating build step.",
31255
+ sourceIssueCodes
31256
+ })
31257
+ ];
31258
+ }
31259
+ if (code === "BUILD_REPORT_BEAT_MISSING") {
31260
+ return [
31261
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
31262
+ label: beat ? `Sync build report for beat ${beat}` : "Sync the build report",
31263
+ reason: "The build report does not include the inspected beat.",
31264
+ sourceIssueCodes
31265
+ })
31266
+ ];
31267
+ }
31268
+ if (code === "MALFORMED_BUILD_REPORT") {
31269
+ return [
31270
+ commandReviewAction(`vibe build ${projectDir} --json`, {
31271
+ label: "Regenerate the build report",
31272
+ reason: "The existing build report cannot be parsed.",
31273
+ sourceIssueCodes
31274
+ })
31275
+ ];
31276
+ }
31277
+ if (code === "MISSING_REPORTED_ASSET" || code === "STALE_ASSET" || code === "UNKNOWN_ASSET_FRESHNESS") {
31278
+ return [
31279
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --force --json`, {
31280
+ label: beat ? `Regenerate assets for beat ${beat}` : "Regenerate stale or missing assets",
31281
+ reason: "Asset generation may call providers, so the host should confirm first.",
31282
+ sourceIssueCodes
31283
+ })
31284
+ ];
31285
+ }
31286
+ if (code === "MUSIC_CUE_NOT_READY") {
31287
+ return [
31288
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --json`, {
31289
+ label: beat ? `Generate music for beat ${beat}` : "Generate missing music assets",
31290
+ reason: "Music generation may call providers, so the host should confirm first.",
31291
+ sourceIssueCodes
31292
+ }),
31293
+ commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
31294
+ label: "Sync music into the root composition",
31295
+ reason: "Generated music needs root composition wiring.",
31296
+ sourceIssueCodes
31297
+ })
31298
+ ];
31299
+ }
31300
+ if (code.startsWith("SCENE_LINT_")) {
31301
+ return [
31302
+ commandReviewAction(`vibe scene repair ${projectDir} --json`, {
31303
+ label: "Repair deterministic scene lint issues",
31304
+ reason: "Scene lint reported an issue VibeFrame may be able to repair mechanically.",
31305
+ sourceIssueCodes
31306
+ })
31307
+ ];
31308
+ }
31309
+ if (code === "RENDER_NOT_FOUND" || code === "EMPTY_RENDER") {
31310
+ return [
31311
+ commandReviewAction(
31312
+ beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
31313
+ {
31314
+ label: beat ? `Render beat ${beat}` : "Render the project",
31315
+ reason: "A render file is required before render inspection can pass.",
31316
+ sourceIssueCodes
31317
+ }
31318
+ )
31319
+ ];
31320
+ }
31321
+ if (code === "NO_AUDIO_STREAM" || code === "DURATION_DRIFT") {
31322
+ return [
31323
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
31324
+ label: beat ? `Sync timing/audio for beat ${beat}` : "Sync timing and audio wiring",
31325
+ reason: "The render output no longer matches synced project state.",
31326
+ sourceIssueCodes
31327
+ }),
31328
+ commandReviewAction(
31329
+ beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
31330
+ {
31331
+ label: beat ? `Rerender beat ${beat}` : "Rerender the project",
31332
+ reason: "The render should be regenerated after sync repair.",
31333
+ sourceIssueCodes
31334
+ }
31335
+ )
31336
+ ];
31337
+ }
31338
+ if (code === "BLACK_FRAME_SEGMENT") {
31339
+ return issue.fixOwner === "host-agent" ? [
31340
+ agentReviewAction({
31341
+ label: beat ? `Fix black frames in beat ${beat}` : "Fix black frame segment",
31342
+ agentPrompt: "Fix black-frame issues from review-report.json, then rerender.",
31343
+ reason: "The issue likely needs scene, storyboard, or visual cue changes.",
31344
+ sourceIssueCodes
31345
+ })
31346
+ ] : [
31347
+ commandReviewAction(`vibe scene repair ${projectDir} --json`, {
31348
+ label: "Repair deterministic scene timing",
31349
+ reason: "Black frames outside a specific beat may be recoverable with scene repair.",
31350
+ sourceIssueCodes
31351
+ })
31352
+ ];
31353
+ }
31354
+ if (code === "STATIC_FRAME_SEGMENT") {
31355
+ return [
31356
+ agentReviewAction({
31357
+ label: beat ? `Add motion to beat ${beat}` : "Fix static visual hold",
31358
+ agentPrompt: "Fix static-frame issues from review-report.json, then rerender.",
31359
+ reason: "Static segments usually need creative scene, storyboard, or asset changes.",
31360
+ sourceIssueCodes
31361
+ })
31362
+ ];
31363
+ }
31364
+ if (code === "LONG_SILENCE") {
31365
+ return issue.fixOwner === "host-agent" ? [
31366
+ agentReviewAction({
31367
+ label: beat ? `Fix long silence in beat ${beat}` : "Fix long silence",
31368
+ agentPrompt: "Fix long-silence issues from review-report.json, then rerender.",
31369
+ reason: "The beat duration, narration, or music cue needs host-agent judgment.",
31370
+ sourceIssueCodes
31371
+ })
31372
+ ] : [
31373
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
31374
+ label: beat ? `Sync audio for beat ${beat}` : "Sync audio wiring",
31375
+ reason: "The silence may be caused by stale narration or music wiring.",
31376
+ sourceIssueCodes
31377
+ })
31378
+ ];
31379
+ }
31380
+ if (code.startsWith("AI_REVIEW_")) {
31381
+ return [
31382
+ agentReviewAction({
31383
+ label: "Fix AI review findings",
31384
+ agentPrompt: "Fix host-agent-owned AI review issues from review-report.json, then rerender.",
31385
+ reason: "AI review findings require storyboard, design, or composition judgment.",
31386
+ sourceIssueCodes
31387
+ })
31388
+ ];
31389
+ }
31390
+ if (code === "FFPROBE_UNAVAILABLE" || code === "FFMPEG_UNAVAILABLE") {
31391
+ return [
31392
+ manualReviewAction({
31393
+ label: "Install FFmpeg and rerun inspection",
31394
+ reason: "Local media inspection depends on ffmpeg/ffprobe being available.",
31395
+ fixOwner: "host-agent",
31396
+ sourceIssueCodes
31397
+ })
31398
+ ];
31399
+ }
31400
+ if (code === "FFPROBE_FAILED" || code === "NO_VIDEO_STREAM") {
31401
+ return [
31402
+ commandReviewAction(
31403
+ beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
31404
+ {
31405
+ label: beat ? `Rerender beat ${beat}` : "Rerender the project",
31406
+ reason: "The rendered media could not be probed as a valid video.",
31407
+ sourceIssueCodes
31408
+ }
31409
+ )
31410
+ ];
31411
+ }
31412
+ if (code === "ASPECT_MISMATCH") {
31413
+ return [
31414
+ manualReviewAction({
31415
+ label: "Check project aspect and render settings",
31416
+ reason: "Aspect mismatches can come from either config intent or render settings.",
31417
+ fixOwner: issue.fixOwner,
31418
+ sourceIssueCodes
31419
+ })
31420
+ ];
31421
+ }
31422
+ return [];
31423
+ }
31424
+ function classifyReviewCommand(command) {
31425
+ const lower = command.trim().toLowerCase();
31426
+ if (lower.startsWith("vibe status ")) return safe("free");
31427
+ if (lower.startsWith("vibe scene repair")) return safe("free");
31428
+ if (lower.startsWith("vibe storyboard validate") || lower.startsWith("vibe storyboard get") || lower.startsWith("vibe storyboard list")) {
31429
+ return safe("free", "host-agent");
31430
+ }
31431
+ if (lower.startsWith("vibe inspect project")) return safe("free");
31432
+ if (lower.startsWith("vibe inspect render")) {
31433
+ if (lower.includes("--ai")) return unsafe("low");
31434
+ return safe("free");
31435
+ }
31436
+ if (lower.startsWith("vibe render ")) return safe("free");
31437
+ if (lower.startsWith("vibe build ")) {
31438
+ if (lower.includes("--dry-run")) return safe("free");
31439
+ if (lower.includes("--stage sync") || lower.includes("--stage render")) return safe("free");
31440
+ return unsafe("unknown");
31441
+ }
31442
+ if (lower.startsWith("vibe generate video") || lower.startsWith("vibe edit fill-gaps") || lower.startsWith("vibe remix ")) {
31443
+ return unsafe("very-high");
31444
+ }
31445
+ if (lower.startsWith("vibe storyboard revise")) return unsafe("unknown", "host-agent");
31446
+ return unsafe("unknown");
31447
+ }
31448
+ function safe(costTier, fixOwner = "vibe") {
31449
+ return { fixOwner, costTier, safeToAutoRun: true, requiresConfirmation: false };
31450
+ }
31451
+ function unsafe(costTier, fixOwner = "vibe") {
31452
+ return { fixOwner, costTier, safeToAutoRun: false, requiresConfirmation: true };
31453
+ }
31454
+ function actionId(kind, value) {
31455
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
31456
+ return `${kind}:${slug || "action"}`;
31457
+ }
31458
+ function uniqueStrings(items) {
31459
+ return [
31460
+ ...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
31461
+ ];
31462
+ }
30751
31463
  var init_review_report = __esm({
30752
31464
  "src/commands/_shared/review-report.ts"() {
30753
31465
  "use strict";
@@ -30880,6 +31592,297 @@ var init_scene_repair = __esm({
30880
31592
  }
30881
31593
  });
30882
31594
 
31595
+ // src/commands/_shared/storyboard-edit.ts
31596
+ function validateStoryboardMarkdown(markdown) {
31597
+ const parsed = parseStoryboard(markdown);
31598
+ const sections = splitBeatSections(markdown);
31599
+ const issues = [];
31600
+ if (parsed.beats.length === 0) {
31601
+ issues.push({
31602
+ severity: "error",
31603
+ code: "NO_BEATS",
31604
+ message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
31605
+ });
31606
+ }
31607
+ const seen = /* @__PURE__ */ new Map();
31608
+ for (const beat of parsed.beats) {
31609
+ seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
31610
+ }
31611
+ for (const [id, count] of seen.entries()) {
31612
+ if (count > 1) {
31613
+ issues.push({
31614
+ severity: "error",
31615
+ code: "DUPLICATE_BEAT_ID",
31616
+ beatId: id,
31617
+ message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
31618
+ });
31619
+ }
31620
+ }
31621
+ for (const section of sections) {
31622
+ const cueBlock = readLeadingCueBlock(section.body);
31623
+ if (cueBlock?.error) {
31624
+ issues.push({
31625
+ severity: "error",
31626
+ code: "MALFORMED_CUE_YAML",
31627
+ beatId: section.id,
31628
+ message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
31629
+ });
31630
+ }
31631
+ }
31632
+ const characterPool = new Set(
31633
+ Object.keys(
31634
+ parsed.frontmatter?.characters ?? {}
31635
+ )
31636
+ );
31637
+ for (const beat of parsed.beats) {
31638
+ const cues = beat.cues ?? {};
31639
+ for (const [key, value] of Object.entries(cues)) {
31640
+ if (!ALLOWED_CUE_KEYS.has(key)) {
31641
+ issues.push({
31642
+ severity: "warning",
31643
+ code: "UNKNOWN_CUE",
31644
+ beatId: beat.id,
31645
+ message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
31646
+ });
31647
+ continue;
31648
+ }
31649
+ if (key === "characters") {
31650
+ const validShape = typeof value === "string" || Array.isArray(value) && value.every((v) => typeof v === "string");
31651
+ if (!validShape) {
31652
+ issues.push({
31653
+ severity: "error",
31654
+ code: "INVALID_CHARACTERS_VALUE",
31655
+ beatId: beat.id,
31656
+ message: `Beat "${beat.id}" cue "characters" must be a character name or a list of names.`
31657
+ });
31658
+ continue;
31659
+ }
31660
+ for (const name of beatCharacterNames({ characters: value })) {
31661
+ if (!characterPool.has(name)) {
31662
+ issues.push({
31663
+ severity: "warning",
31664
+ code: "UNKNOWN_CHARACTER",
31665
+ beatId: beat.id,
31666
+ message: `Beat "${beat.id}" references character "${name}" not defined in the frontmatter \`characters:\` pool.`
31667
+ });
31668
+ }
31669
+ }
31670
+ continue;
31671
+ }
31672
+ if (key === "duration") {
31673
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
31674
+ issues.push({
31675
+ severity: "error",
31676
+ code: "INVALID_DURATION",
31677
+ beatId: beat.id,
31678
+ message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
31679
+ });
31680
+ }
31681
+ continue;
31682
+ }
31683
+ if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
31684
+ issues.push({
31685
+ severity: "error",
31686
+ code: "INVALID_CUE_VALUE",
31687
+ beatId: beat.id,
31688
+ message: `Beat "${beat.id}" cue "${key}" must be a string.`
31689
+ });
31690
+ }
31691
+ }
31692
+ }
31693
+ for (const beat of parsed.beats) {
31694
+ if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
31695
+ issues.push({
31696
+ severity: "warning",
31697
+ code: "BEAT_DURATION_TOO_LONG",
31698
+ beatId: beat.id,
31699
+ message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
31700
+ });
31701
+ }
31702
+ }
31703
+ return {
31704
+ ok: !issues.some((i) => i.severity === "error"),
31705
+ beats: parsed.beats,
31706
+ issues
31707
+ };
31708
+ }
31709
+ function getStoryboardBeat(markdown, beatId) {
31710
+ return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
31711
+ }
31712
+ function upsertStoryboardBeat(markdown, opts) {
31713
+ const beatId = normalizeBeatId(opts.beatId);
31714
+ const sections = splitBeatSections(markdown);
31715
+ const existing = sections.find((section2) => section2.id === beatId);
31716
+ const cues = storyboardCuesFromInput(opts);
31717
+ if (existing) {
31718
+ let next = markdown;
31719
+ for (const [key, value] of Object.entries(cues)) {
31720
+ next = setStoryboardCue(next, { beatId, key, value });
31721
+ }
31722
+ return { markdown: next, action: "updated" };
31723
+ }
31724
+ const section = buildStoryboardBeatSection({ ...opts, beatId, cues });
31725
+ if (sections.length > 0 && isStarterStoryboard(markdown)) {
31726
+ const global2 = markdown.slice(0, sections[0].start).trimEnd();
31727
+ return {
31728
+ markdown: `${global2}
31729
+
31730
+ ${section}`,
31731
+ action: "replaced-starter"
31732
+ };
31733
+ }
31734
+ const prefix = markdown.trimEnd();
31735
+ return {
31736
+ markdown: `${prefix}${prefix ? "\n\n" : ""}${section}`,
31737
+ action: "appended"
31738
+ };
31739
+ }
31740
+ function setStoryboardCue(markdown, opts) {
31741
+ const section = findBeatSection(markdown, opts.beatId);
31742
+ if (!section) {
31743
+ throw new Error(`Beat "${opts.beatId}" not found.`);
31744
+ }
31745
+ if (!ALLOWED_CUE_KEYS.has(opts.key)) {
31746
+ throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
31747
+ }
31748
+ const cue = readLeadingCueBlock(section.body);
31749
+ if (cue?.error) {
31750
+ throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
31751
+ }
31752
+ const cues = cue?.value ? { ...cue.value } : {};
31753
+ if (opts.unset) {
31754
+ delete cues[opts.key];
31755
+ } else {
31756
+ cues[opts.key] = normalizeCueValue(opts.key, opts.value);
31757
+ }
31758
+ const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
31759
+ const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
31760
+ const nextBody = cueBlock + remainingBody.trimStart();
31761
+ const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
31762
+ return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
31763
+ }
31764
+ function moveStoryboardBeat(markdown, opts) {
31765
+ if (opts.beatId === opts.afterBeatId) return markdown;
31766
+ const sections = splitBeatSections(markdown);
31767
+ const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
31768
+ if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
31769
+ const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
31770
+ if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
31771
+ const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
31772
+ const chunks = sections.map((section) => section.raw);
31773
+ const [moving] = chunks.splice(movingIndex, 1);
31774
+ const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
31775
+ chunks.splice(adjustedAfterIndex + 1, 0, moving);
31776
+ return global2 + normalizeBeatChunks(chunks);
31777
+ }
31778
+ function normalizeBeatChunks(chunks) {
31779
+ return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
31780
+ }
31781
+ function normalizeBeatId(id) {
31782
+ return deriveBeatId(id);
31783
+ }
31784
+ function storyboardCuesFromInput(opts) {
31785
+ const cues = {};
31786
+ if (opts.duration !== void 0) cues.duration = opts.duration;
31787
+ if (opts.narration && opts.narration.trim()) cues.narration = opts.narration.trim();
31788
+ if (opts.backdrop && opts.backdrop.trim()) cues.backdrop = opts.backdrop.trim();
31789
+ return cues;
31790
+ }
31791
+ function buildStoryboardBeatSection(opts) {
31792
+ const title = normalizeTitle(opts.title ?? humanizeBeatId(opts.beatId));
31793
+ const cueBlock = Object.keys(opts.cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(opts.cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
31794
+ const body = opts.body?.trim() || "Scene created with `vibe scene add`. Edit this body if the build flow should recompose it.";
31795
+ return `## Beat ${opts.beatId} - ${title}
31796
+
31797
+ ${cueBlock}${body.trimEnd()}
31798
+ `;
31799
+ }
31800
+ function normalizeTitle(value) {
31801
+ return value.replace(/\s+/g, " ").trim() || "Scene";
31802
+ }
31803
+ function humanizeBeatId(id) {
31804
+ return id.split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || "Scene";
31805
+ }
31806
+ function isStarterStoryboard(markdown) {
31807
+ const parsed = parseStoryboard(markdown);
31808
+ if (parsed.beats.length !== 3) return false;
31809
+ const ids = parsed.beats.map((beat) => beat.id);
31810
+ if (ids.join(",") !== "hook,proof,close") return false;
31811
+ return markdown.includes("Introduce the promise in one crisp sentence.") && markdown.includes("Show the mechanism or proof point that makes the promise believable.") && markdown.includes("Close with the action the viewer should remember.");
31812
+ }
31813
+ function normalizeCueValue(key, value) {
31814
+ if (key === "duration") {
31815
+ const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
31816
+ if (!Number.isFinite(n) || n <= 0) {
31817
+ throw new Error("Cue duration must be a positive number.");
31818
+ }
31819
+ return n;
31820
+ }
31821
+ if (typeof value !== "string") return value;
31822
+ return value.trim();
31823
+ }
31824
+ function splitBeatSections(markdown) {
31825
+ const normalized = markdown.replace(/\r\n/g, "\n");
31826
+ const headings = [];
31827
+ HEADING_RE2.lastIndex = 0;
31828
+ let match;
31829
+ while ((match = HEADING_RE2.exec(normalized)) !== null) {
31830
+ headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
31831
+ }
31832
+ return headings.map((heading, index) => {
31833
+ const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
31834
+ return {
31835
+ id: deriveBeatId(heading.line),
31836
+ heading: heading.line,
31837
+ start: heading.start,
31838
+ headingEnd: heading.end,
31839
+ end,
31840
+ raw: normalized.slice(heading.start, end),
31841
+ body: normalized.slice(heading.end, end).trim()
31842
+ };
31843
+ });
31844
+ }
31845
+ function findBeatSection(markdown, beatId) {
31846
+ return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
31847
+ }
31848
+ function readLeadingCueBlock(body) {
31849
+ const match = body.match(LEADING_CUE_RE);
31850
+ if (!match) return null;
31851
+ try {
31852
+ const parsed = (0, import_yaml5.parse)(match[2]);
31853
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
31854
+ return { full: match[0], error: "cue block must parse to a YAML object" };
31855
+ }
31856
+ return { full: match[0], value: parsed };
31857
+ } catch (error) {
31858
+ return { full: match[0], error: error instanceof Error ? error.message : String(error) };
31859
+ }
31860
+ }
31861
+ var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
31862
+ var init_storyboard_edit = __esm({
31863
+ "src/commands/_shared/storyboard-edit.ts"() {
31864
+ "use strict";
31865
+ import_yaml5 = __toESM(require_dist2(), 1);
31866
+ init_storyboard_parse();
31867
+ STORYBOARD_CUE_KEYS = [
31868
+ "duration",
31869
+ "narration",
31870
+ "backdrop",
31871
+ "video",
31872
+ "motion",
31873
+ "voice",
31874
+ "music",
31875
+ "asset",
31876
+ "characters"
31877
+ ];
31878
+ HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
31879
+ LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
31880
+ ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
31881
+ STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
31882
+ MAX_RECOMMENDED_BEAT_SEC = 15;
31883
+ }
31884
+ });
31885
+
30883
31886
  // src/commands/scene.ts
30884
31887
  import { resolve as resolve17, relative as relative6, dirname as dirname4 } from "node:path";
30885
31888
  import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile12, access as access5, copyFile } from "node:fs/promises";
@@ -30929,6 +31932,21 @@ async function loadVibeProjectConfig(projectDir) {
30929
31932
  }
30930
31933
  };
30931
31934
  }
31935
+ function escapeRegExp3(value) {
31936
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31937
+ }
31938
+ function readClipStart(rootHtml, beatId) {
31939
+ const compositionId = sceneCompositionId(beatId);
31940
+ const match = rootHtml.match(
31941
+ new RegExp(
31942
+ `<div\\s+class="clip"[^>]*\\bdata-composition-id="${escapeRegExp3(compositionId)}"[^>]*\\bdata-start="([\\d.]+)"`,
31943
+ "i"
31944
+ )
31945
+ );
31946
+ if (!match) return void 0;
31947
+ const start = Number.parseFloat(match[1]);
31948
+ return Number.isFinite(start) ? start : void 0;
31949
+ }
30932
31950
  async function resolveNarrationText(value) {
30933
31951
  if (!value) return void 0;
30934
31952
  const looksLikePath = /\.[a-z]{2,4}$/i.test(value) || value.includes("/") || value.includes("\\");
@@ -30955,6 +31973,7 @@ async function executeSceneAdd(opts) {
30955
31973
  const id = slugifySceneName(opts.name);
30956
31974
  const sceneRel = `compositions/scene-${id}.html`;
30957
31975
  const scenePath = resolve17(projectDir, sceneRel);
31976
+ const storyboardAbsPath = resolve17(projectDir, "STORYBOARD.md");
30958
31977
  const errResult = (error) => ({
30959
31978
  success: false,
30960
31979
  id,
@@ -31013,7 +32032,7 @@ async function executeSceneAdd(opts) {
31013
32032
  throw error;
31014
32033
  }
31015
32034
  opts.onProgress?.(
31016
- resolution.provider === "kokoro" ? "Generating narration with Kokoro (local \u2014 first run downloads ~330MB)..." : "Generating narration with ElevenLabs..."
32035
+ resolution.provider === "kokoro" ? "Generating narration with Kokoro (local \u2014 first run downloads a ~90MB model)..." : resolution.provider === "openai" ? "Generating narration with OpenAI (gpt-4o-mini-tts)..." : "Generating narration with ElevenLabs..."
31017
32036
  );
31018
32037
  const tts = await resolution.call(narrationText, {
31019
32038
  voice: opts.voice,
@@ -31174,6 +32193,27 @@ async function executeSceneAdd(opts) {
31174
32193
  duration = fallback2;
31175
32194
  }
31176
32195
  duration = Number(duration.toFixed(2));
32196
+ let storyboardPath2;
32197
+ let storyboardSynced = false;
32198
+ let storyboardAction = "skipped";
32199
+ const shouldSyncStoryboard = opts.syncStoryboard !== false && await pathExists2(storyboardAbsPath);
32200
+ if (shouldSyncStoryboard) {
32201
+ opts.onProgress?.("Syncing STORYBOARD.md...");
32202
+ const before = await readFile13(storyboardAbsPath, "utf-8");
32203
+ const synced = upsertStoryboardBeat(before, {
32204
+ beatId: id,
32205
+ title: opts.headline,
32206
+ duration,
32207
+ narration: narrationText && !opts.skipAudio ? narrationText : void 0,
32208
+ backdrop: opts.visuals && !opts.skipImage ? opts.visuals : void 0
32209
+ });
32210
+ if (synced.markdown !== before) {
32211
+ await writeFile12(storyboardAbsPath, synced.markdown, "utf-8");
32212
+ }
32213
+ storyboardPath2 = relative6(process.cwd(), storyboardAbsPath) || storyboardAbsPath;
32214
+ storyboardSynced = true;
32215
+ storyboardAction = synced.action;
32216
+ }
31177
32217
  opts.onProgress?.("Emitting scene HTML...");
31178
32218
  const sceneHtml = emitSceneHtml({
31179
32219
  id,
@@ -31192,11 +32232,24 @@ async function executeSceneAdd(opts) {
31192
32232
  await mkdir7(dirname4(scenePath), { recursive: true });
31193
32233
  await writeFile12(scenePath, sceneHtml, "utf-8");
31194
32234
  opts.onProgress?.("Updating root composition...");
31195
- const start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
31196
- const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
31197
- const trackIndex = existingClipCount % 2 + 1;
31198
- const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
31199
- await writeFile12(rootPath, updated, "utf-8");
32235
+ let start = 0;
32236
+ if (storyboardSynced) {
32237
+ const syncPlan = await syncRootComposition({
32238
+ projectDir,
32239
+ rootRel,
32240
+ beats: await loadProjectRootSyncBeats(projectDir)
32241
+ });
32242
+ const blockingIssue = syncPlan.issues.find((issue) => issue.severity === "error");
32243
+ if (blockingIssue) return errResult(blockingIssue.message);
32244
+ const rootHtmlAfterSync = await readFile13(rootPath, "utf-8");
32245
+ start = readClipStart(rootHtmlAfterSync, id) ?? 0;
32246
+ } else {
32247
+ start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
32248
+ const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
32249
+ const trackIndex = existingClipCount % 2 + 1;
32250
+ const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
32251
+ await writeFile12(rootPath, updated, "utf-8");
32252
+ }
31200
32253
  const transcriptAbsPath = transcriptRelPath ? resolve17(projectDir, transcriptRelPath) : void 0;
31201
32254
  return {
31202
32255
  success: true,
@@ -31209,6 +32262,9 @@ async function executeSceneAdd(opts) {
31209
32262
  audioPath: audioAbsPath ? relative6(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
31210
32263
  imagePath: imageAbsPath ? relative6(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
31211
32264
  lottiePath: lottieAbsPath ? relative6(process.cwd(), lottieAbsPath) || lottieAbsPath : void 0,
32265
+ storyboardPath: storyboardPath2,
32266
+ storyboardSynced,
32267
+ storyboardAction,
31212
32268
  transcriptPath: transcriptAbsPath ? relative6(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
31213
32269
  transcriptWordCount
31214
32270
  };
@@ -31250,6 +32306,8 @@ var init_scene = __esm({
31250
32306
  init_compose_prompts();
31251
32307
  init_scene_submit();
31252
32308
  init_scene_repair();
32309
+ init_root_sync();
32310
+ init_storyboard_edit();
31253
32311
  sceneCommand = new Command("scene").description(
31254
32312
  "Lower-level scene authoring (add, lint, list-styles). For project flow use `vibe init` / `vibe build` / `vibe render`."
31255
32313
  ).addHelpText(
@@ -31534,11 +32592,14 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
31534
32592
  ).option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option(
31535
32593
  "--visuals <prompt>",
31536
32594
  "Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider"
31537
- ).option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
32595
+ ).option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option(
32596
+ "--no-storyboard",
32597
+ "Do not sync STORYBOARD.md; insert this scene directly into the root composition only"
32598
+ ).option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
31538
32599
  "--tts <provider>",
31539
- "TTS provider: auto, elevenlabs, kokoro (default auto \u2014 picks ElevenLabs when key set, else Kokoro local)",
32600
+ "TTS provider: auto, elevenlabs, openai, kokoro (default auto \u2014 ElevenLabs key > OpenAI key > Kokoro local)",
31540
32601
  "auto"
31541
- ).option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)").option(
32602
+ ).option("--voice <id>", "Voice id (ElevenLabs name/id, OpenAI voice like marin, or Kokoro id like af_heart)").option(
31542
32603
  "--no-audio",
31543
32604
  "Skip TTS even when --narration is provided (useful for tests/agent dry runs)"
31544
32605
  ).option("--no-image", "Skip image generation even when --visuals is provided").option(
@@ -31603,6 +32664,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
31603
32664
  kicker: options.kicker,
31604
32665
  project: options.project,
31605
32666
  insertInto: options.insertInto,
32667
+ syncStoryboard: options.storyboard !== false,
31606
32668
  imageProvider: options.imageProvider,
31607
32669
  tts,
31608
32670
  audio: options.audio,
@@ -31630,6 +32692,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
31630
32692
  kicker: options.kicker,
31631
32693
  projectDir: options.project,
31632
32694
  insertInto: options.insertInto,
32695
+ syncStoryboard: options.storyboard !== false,
31633
32696
  imageProvider: options.imageProvider,
31634
32697
  tts,
31635
32698
  voice: options.voice,
@@ -31669,6 +32732,13 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
31669
32732
  if (result.audioPath) console.log(source_default.green(" +"), result.audioPath);
31670
32733
  if (result.imagePath) console.log(source_default.green(" +"), result.imagePath);
31671
32734
  if (result.transcriptPath) console.log(source_default.green(" +"), result.transcriptPath);
32735
+ if (result.storyboardPath && result.storyboardSynced) {
32736
+ console.log(
32737
+ result.storyboardAction === "updated" ? source_default.yellow(" ~") : source_default.green(" +"),
32738
+ result.storyboardPath,
32739
+ source_default.dim(`(${result.storyboardAction})`)
32740
+ );
32741
+ }
31672
32742
  console.log(source_default.yellow(" ~"), result.rootPath, source_default.dim("(updated)"));
31673
32743
  console.log();
31674
32744
  console.log(source_default.bold.cyan("Composition"));
@@ -35477,6 +36547,10 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
35477
36547
  ...review?.retryWith ?? [],
35478
36548
  ...workflow.retryWith
35479
36549
  ];
36550
+ const nextActions = normalizeReviewActions([
36551
+ ...review?.nextActions ?? [],
36552
+ ...reviewActionsFromRetryWith([...build?.retryWith ?? [], ...workflow.retryWith])
36553
+ ]);
35480
36554
  return {
35481
36555
  schemaVersion: "1",
35482
36556
  kind: "project",
@@ -35488,6 +36562,7 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
35488
36562
  review,
35489
36563
  jobs,
35490
36564
  warnings,
36565
+ nextActions,
35491
36566
  retryWith: unique(retryWith)
35492
36567
  };
35493
36568
  }
@@ -35781,9 +36856,44 @@ async function readReviewSummary(projectDir) {
35781
36856
  },
35782
36857
  sourceReports: stringArray(report.sourceReports),
35783
36858
  updatedAt: info?.mtime.toISOString(),
36859
+ nextActions: reviewActionsFromUnknown(report.nextActions),
35784
36860
  retryWith: stringArray(report.retryWith)
35785
36861
  });
35786
36862
  }
36863
+ function reviewActionsFromUnknown(value) {
36864
+ if (!Array.isArray(value)) return [];
36865
+ return value.flatMap((item) => {
36866
+ if (!item || typeof item !== "object") return [];
36867
+ const record = item;
36868
+ const kind = stringEnum(record.kind, ["command", "agent", "manual"]);
36869
+ const fixOwner = stringEnum(record.fixOwner, ["vibe", "host-agent"]);
36870
+ const costTier = stringEnum(record.costTier, [
36871
+ "free",
36872
+ "low",
36873
+ "high",
36874
+ "very-high",
36875
+ "unknown"
36876
+ ]);
36877
+ if (!kind || !fixOwner || !costTier || typeof record.id !== "string" || typeof record.label !== "string" || typeof record.safeToAutoRun !== "boolean" || typeof record.requiresConfirmation !== "boolean" || typeof record.reason !== "string") {
36878
+ return [];
36879
+ }
36880
+ return [
36881
+ stripUndefined2({
36882
+ id: record.id,
36883
+ kind,
36884
+ label: record.label,
36885
+ command: typeof record.command === "string" ? record.command : void 0,
36886
+ agentPrompt: typeof record.agentPrompt === "string" ? record.agentPrompt : void 0,
36887
+ fixOwner,
36888
+ costTier,
36889
+ safeToAutoRun: record.safeToAutoRun,
36890
+ requiresConfirmation: record.requiresConfirmation,
36891
+ reason: record.reason,
36892
+ sourceIssueCodes: stringArray(record.sourceIssueCodes)
36893
+ })
36894
+ ];
36895
+ });
36896
+ }
35787
36897
  function summarizeJobs(records) {
35788
36898
  return {
35789
36899
  total: records.length,
@@ -35995,6 +37105,9 @@ function numberOr(fallback2, value) {
35995
37105
  function stringArray(value) {
35996
37106
  return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
35997
37107
  }
37108
+ function stringEnum(value, allowed) {
37109
+ return typeof value === "string" && allowed.includes(value) ? value : void 0;
37110
+ }
35998
37111
  function isActiveStatus(status) {
35999
37112
  return status === "queued" || status === "running";
36000
37113
  }
@@ -36060,6 +37173,7 @@ var init_status_jobs = __esm({
36060
37173
  init_ai_video();
36061
37174
  init_music_status();
36062
37175
  init_build_asset_metadata();
37176
+ init_review_report();
36063
37177
  init_storyboard_parse();
36064
37178
  LOCAL_JOB_ORPHAN_AFTER_MS = 9e4;
36065
37179
  }
@@ -38941,10 +40055,18 @@ Examples:
38941
40055
  options.ratio = "16:9";
38942
40056
  }
38943
40057
  if (options.dryRun) {
40058
+ const costUsd = provider === "seedance" || provider === "fal" ? estimateSeedanceVideoCostUsd({
40059
+ durationSec: Number(options.duration) || 5,
40060
+ resolution: options.resolution,
40061
+ aspectRatio: options.ratio,
40062
+ fast: options.seedanceModel === "fast",
40063
+ hasVideoReference: Boolean(options.refVideos)
40064
+ }) : void 0;
38944
40065
  outputSuccess({
38945
40066
  command: "generate video",
38946
40067
  startedAt,
38947
40068
  dryRun: true,
40069
+ costUsd,
38948
40070
  data: {
38949
40071
  params: {
38950
40072
  prompt: prompt3,
@@ -44132,208 +45254,6 @@ var init_ai_review = __esm({
44132
45254
  }
44133
45255
  });
44134
45256
 
44135
- // src/commands/_shared/storyboard-edit.ts
44136
- function validateStoryboardMarkdown(markdown) {
44137
- const parsed = parseStoryboard(markdown);
44138
- const sections = splitBeatSections(markdown);
44139
- const issues = [];
44140
- if (parsed.beats.length === 0) {
44141
- issues.push({
44142
- severity: "error",
44143
- code: "NO_BEATS",
44144
- message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
44145
- });
44146
- }
44147
- const seen = /* @__PURE__ */ new Map();
44148
- for (const beat of parsed.beats) {
44149
- seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
44150
- }
44151
- for (const [id, count] of seen.entries()) {
44152
- if (count > 1) {
44153
- issues.push({
44154
- severity: "error",
44155
- code: "DUPLICATE_BEAT_ID",
44156
- beatId: id,
44157
- message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
44158
- });
44159
- }
44160
- }
44161
- for (const section of sections) {
44162
- const cueBlock = readLeadingCueBlock(section.body);
44163
- if (cueBlock?.error) {
44164
- issues.push({
44165
- severity: "error",
44166
- code: "MALFORMED_CUE_YAML",
44167
- beatId: section.id,
44168
- message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
44169
- });
44170
- }
44171
- }
44172
- for (const beat of parsed.beats) {
44173
- const cues = beat.cues ?? {};
44174
- for (const [key, value] of Object.entries(cues)) {
44175
- if (!ALLOWED_CUE_KEYS.has(key)) {
44176
- issues.push({
44177
- severity: "warning",
44178
- code: "UNKNOWN_CUE",
44179
- beatId: beat.id,
44180
- message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
44181
- });
44182
- continue;
44183
- }
44184
- if (key === "duration") {
44185
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
44186
- issues.push({
44187
- severity: "error",
44188
- code: "INVALID_DURATION",
44189
- beatId: beat.id,
44190
- message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
44191
- });
44192
- }
44193
- continue;
44194
- }
44195
- if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
44196
- issues.push({
44197
- severity: "error",
44198
- code: "INVALID_CUE_VALUE",
44199
- beatId: beat.id,
44200
- message: `Beat "${beat.id}" cue "${key}" must be a string.`
44201
- });
44202
- }
44203
- }
44204
- }
44205
- for (const beat of parsed.beats) {
44206
- if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
44207
- issues.push({
44208
- severity: "warning",
44209
- code: "BEAT_DURATION_TOO_LONG",
44210
- beatId: beat.id,
44211
- message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
44212
- });
44213
- }
44214
- }
44215
- return {
44216
- ok: !issues.some((i) => i.severity === "error"),
44217
- beats: parsed.beats,
44218
- issues
44219
- };
44220
- }
44221
- function getStoryboardBeat(markdown, beatId) {
44222
- return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
44223
- }
44224
- function setStoryboardCue(markdown, opts) {
44225
- const section = findBeatSection(markdown, opts.beatId);
44226
- if (!section) {
44227
- throw new Error(`Beat "${opts.beatId}" not found.`);
44228
- }
44229
- if (!ALLOWED_CUE_KEYS.has(opts.key)) {
44230
- throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
44231
- }
44232
- const cue = readLeadingCueBlock(section.body);
44233
- if (cue?.error) {
44234
- throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
44235
- }
44236
- const cues = cue?.value ? { ...cue.value } : {};
44237
- if (opts.unset) {
44238
- delete cues[opts.key];
44239
- } else {
44240
- cues[opts.key] = normalizeCueValue(opts.key, opts.value);
44241
- }
44242
- const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
44243
- const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
44244
- const nextBody = cueBlock + remainingBody.trimStart();
44245
- const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
44246
- return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
44247
- }
44248
- function moveStoryboardBeat(markdown, opts) {
44249
- if (opts.beatId === opts.afterBeatId) return markdown;
44250
- const sections = splitBeatSections(markdown);
44251
- const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
44252
- if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
44253
- const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
44254
- if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
44255
- const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
44256
- const chunks = sections.map((section) => section.raw);
44257
- const [moving] = chunks.splice(movingIndex, 1);
44258
- const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
44259
- chunks.splice(adjustedAfterIndex + 1, 0, moving);
44260
- return global2 + normalizeBeatChunks(chunks);
44261
- }
44262
- function normalizeBeatChunks(chunks) {
44263
- return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
44264
- }
44265
- function normalizeCueValue(key, value) {
44266
- if (key === "duration") {
44267
- const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
44268
- if (!Number.isFinite(n) || n <= 0) {
44269
- throw new Error("Cue duration must be a positive number.");
44270
- }
44271
- return n;
44272
- }
44273
- if (typeof value !== "string") return value;
44274
- return value.trim();
44275
- }
44276
- function splitBeatSections(markdown) {
44277
- const normalized = markdown.replace(/\r\n/g, "\n");
44278
- const headings = [];
44279
- HEADING_RE2.lastIndex = 0;
44280
- let match;
44281
- while ((match = HEADING_RE2.exec(normalized)) !== null) {
44282
- headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
44283
- }
44284
- return headings.map((heading, index) => {
44285
- const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
44286
- return {
44287
- id: deriveBeatId(heading.line),
44288
- heading: heading.line,
44289
- start: heading.start,
44290
- headingEnd: heading.end,
44291
- end,
44292
- raw: normalized.slice(heading.start, end),
44293
- body: normalized.slice(heading.end, end).trim()
44294
- };
44295
- });
44296
- }
44297
- function findBeatSection(markdown, beatId) {
44298
- return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
44299
- }
44300
- function readLeadingCueBlock(body) {
44301
- const match = body.match(LEADING_CUE_RE);
44302
- if (!match) return null;
44303
- try {
44304
- const parsed = (0, import_yaml5.parse)(match[2]);
44305
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
44306
- return { full: match[0], error: "cue block must parse to a YAML object" };
44307
- }
44308
- return { full: match[0], value: parsed };
44309
- } catch (error) {
44310
- return { full: match[0], error: error instanceof Error ? error.message : String(error) };
44311
- }
44312
- }
44313
- var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
44314
- var init_storyboard_edit = __esm({
44315
- "src/commands/_shared/storyboard-edit.ts"() {
44316
- "use strict";
44317
- import_yaml5 = __toESM(require_dist2(), 1);
44318
- init_storyboard_parse();
44319
- STORYBOARD_CUE_KEYS = [
44320
- "duration",
44321
- "narration",
44322
- "backdrop",
44323
- "video",
44324
- "motion",
44325
- "voice",
44326
- "music",
44327
- "asset"
44328
- ];
44329
- HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
44330
- LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
44331
- ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
44332
- STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
44333
- MAX_RECOMMENDED_BEAT_SEC = 15;
44334
- }
44335
- });
44336
-
44337
45257
  // src/commands/_shared/scene-inspect.ts
44338
45258
  import { existsSync as existsSync34 } from "node:fs";
44339
45259
  import { readdir as readdir4, readFile as readFile29, stat as stat5 } from "node:fs/promises";
@@ -44566,7 +45486,11 @@ async function inspectProject(opts) {
44566
45486
  );
44567
45487
  }
44568
45488
  function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
44569
- const normalizedIssues = normalizeReviewIssues(issues);
45489
+ const normalizedRetryWith = uniqueRetryWith(retryWith);
45490
+ const normalizedIssues = normalizeReviewIssues(issues, {
45491
+ projectDir,
45492
+ retryWith: normalizedRetryWith
45493
+ });
44570
45494
  const status = statusFromIssues(normalizedIssues);
44571
45495
  return {
44572
45496
  schemaVersion: "1",
@@ -44578,9 +45502,14 @@ function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
44578
45502
  score: scoreIssues(normalizedIssues),
44579
45503
  issues: normalizedIssues,
44580
45504
  summary: summarizeReviewIssues(normalizedIssues),
45505
+ nextActions: deriveNextReviewActions({
45506
+ issues: normalizedIssues,
45507
+ retryWith: normalizedRetryWith,
45508
+ projectDir
45509
+ }),
44581
45510
  sourceReports: projectSourceReports(checks),
44582
45511
  checks,
44583
- retryWith: uniqueRetryWith(retryWith)
45512
+ retryWith: normalizedRetryWith
44584
45513
  };
44585
45514
  }
44586
45515
  function storyboardPlaceholderIssues(storyboard) {
@@ -45122,6 +46051,8 @@ async function inspectRender(opts) {
45122
46051
  severity: "error",
45123
46052
  code: "RENDER_NOT_FOUND",
45124
46053
  message: opts.beatId ? `No rendered video was found for beat "${opts.beatId}". Pass --video or render the beat first.` : "No rendered video was found. Pass --video or render the project first.",
46054
+ beatId: opts.beatId,
46055
+ scene: opts.beatId,
45125
46056
  suggestedFix: opts.beatId ? "Run `vibe render <project> --beat <id> --json`." : "Run `vibe build --stage render --json` or `vibe render --json`."
45126
46057
  });
45127
46058
  retryWith.push(
@@ -45144,6 +46075,8 @@ async function inspectRender(opts) {
45144
46075
  code: "EMPTY_RENDER",
45145
46076
  message: "Rendered video file is empty.",
45146
46077
  file: displayPath(videoPath),
46078
+ beatId: opts.beatId,
46079
+ scene: opts.beatId,
45147
46080
  suggestedFix: opts.beatId ? "Render again with `vibe render --beat <id> --json`." : "Render again with `vibe render --json`."
45148
46081
  });
45149
46082
  retryWith.push(
@@ -45153,6 +46086,8 @@ async function inspectRender(opts) {
45153
46086
  const beatTimings = await beatTimingsFromBuildReport(projectDir, opts.beatId);
45154
46087
  const expectedDurationSec = beatTimings.length > 0 ? Number(beatTimings.reduce((sum, beat) => sum + beat.sceneDurationSec, 0).toFixed(3)) : await expectedDurationFromBuildReport(projectDir, opts.beatId);
45155
46088
  if (expectedDurationSec !== void 0) checks.expectedDurationSec = expectedDurationSec;
46089
+ const expectedAudio = await renderExpectsAudio(projectDir, opts.beatId);
46090
+ checks.expectedAudio = expectedAudio;
45156
46091
  if (!commandExists("ffprobe")) {
45157
46092
  issues.push({
45158
46093
  severity: "error",
@@ -45178,7 +46113,7 @@ async function inspectRender(opts) {
45178
46113
  file: displayPath(videoPath)
45179
46114
  });
45180
46115
  }
45181
- if (!audioStream) {
46116
+ if (!audioStream && expectedAudio) {
45182
46117
  issues.push({
45183
46118
  severity: "warning",
45184
46119
  code: "NO_AUDIO_STREAM",
@@ -45383,8 +46318,86 @@ async function inspectRender(opts) {
45383
46318
  );
45384
46319
  return maybeWriteRenderReport(projectDir, opts, result);
45385
46320
  }
46321
+ async function renderExpectsAudio(projectDir, beatId) {
46322
+ const root = resolve43(projectDir);
46323
+ const rootHtml = await readTextIfExists(join19(root, "index.html"));
46324
+ if (rootHtml && rootHtmlExpectsAudio(rootHtml, beatId)) return true;
46325
+ const buildReport = await readJsonIfExists(join19(root, "build-report.json"));
46326
+ if (buildReportExpectsAudio(buildReport, beatId)) return true;
46327
+ const storyboard = await readTextIfExists(join19(root, "STORYBOARD.md"));
46328
+ if (!storyboard) return false;
46329
+ try {
46330
+ const parsed = parseStoryboard(storyboard);
46331
+ const beats = beatId ? parsed.beats.filter((beat) => beat.id === beatId) : parsed.beats;
46332
+ return beats.some(
46333
+ (beat) => [
46334
+ `assets/narration-${beat.id}.mp3`,
46335
+ `assets/narration-${beat.id}.wav`,
46336
+ `assets/music-${beat.id}.mp3`,
46337
+ `assets/music-${beat.id}.wav`
46338
+ ].some((asset) => existsSync35(join19(root, asset)))
46339
+ );
46340
+ } catch {
46341
+ return false;
46342
+ }
46343
+ }
46344
+ function rootHtmlExpectsAudio(html, beatId) {
46345
+ const audioTags = html.match(/<audio\b[^>]*>/gi) ?? [];
46346
+ if (!beatId) return audioTags.length > 0;
46347
+ const escaped = escapeRegExp4(beatId);
46348
+ const beatAudioRe = new RegExp(
46349
+ `(id="(?:narration|music)-${escaped}"|src="[^"]*(?:narration|music)-${escaped}\\.(?:mp3|wav)")`,
46350
+ "i"
46351
+ );
46352
+ return audioTags.some((tag) => beatAudioRe.test(tag));
46353
+ }
46354
+ function buildReportExpectsAudio(report, beatId) {
46355
+ if (!report || typeof report !== "object") return false;
46356
+ const beats = Array.isArray(report.beats) ? report.beats ?? [] : [];
46357
+ const selected = beatId === void 0 ? beats : beats.filter((beat) => {
46358
+ if (!beat || typeof beat !== "object") return false;
46359
+ return beat.id === beatId;
46360
+ });
46361
+ return selected.some((beat) => beatReportExpectsAudio(beat));
46362
+ }
46363
+ function beatReportExpectsAudio(beat) {
46364
+ if (!beat || typeof beat !== "object") return false;
46365
+ const record = beat;
46366
+ if (typeof record.narrationPath === "string" && record.narrationPath.length > 0) return true;
46367
+ if (typeof record.musicPath === "string" && record.musicPath.length > 0) return true;
46368
+ if (parseOptionalNumber(record.narrationDurationSec) !== void 0) return true;
46369
+ const narration = record.narration;
46370
+ if (narration && typeof narration === "object") {
46371
+ const n = narration;
46372
+ if (typeof n.path === "string" && n.path.length > 0) return true;
46373
+ if (parseOptionalNumber(n.durationSec) !== void 0) return true;
46374
+ }
46375
+ const music = record.music;
46376
+ if (music && typeof music === "object") {
46377
+ const m = music;
46378
+ if (typeof m.path === "string" && m.path.length > 0) return true;
46379
+ if (parseOptionalNumber(m.durationSec) !== void 0) return true;
46380
+ }
46381
+ return false;
46382
+ }
46383
+ async function readJsonIfExists(path2) {
46384
+ const text = await readTextIfExists(path2);
46385
+ if (!text) return null;
46386
+ try {
46387
+ return JSON.parse(text);
46388
+ } catch {
46389
+ return null;
46390
+ }
46391
+ }
46392
+ function escapeRegExp4(value) {
46393
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46394
+ }
45386
46395
  function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith, score) {
45387
- const normalizedIssues = normalizeReviewIssues(issues);
46396
+ const normalizedRetryWith = uniqueRetryWith(retryWith);
46397
+ const normalizedIssues = normalizeReviewIssues(issues, {
46398
+ projectDir,
46399
+ retryWith: normalizedRetryWith
46400
+ });
45388
46401
  const status = statusFromIssues(normalizedIssues);
45389
46402
  return {
45390
46403
  schemaVersion: "1",
@@ -45397,9 +46410,14 @@ function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith
45397
46410
  score,
45398
46411
  issues: normalizedIssues,
45399
46412
  summary: summarizeReviewIssues(normalizedIssues),
46413
+ nextActions: deriveNextReviewActions({
46414
+ issues: normalizedIssues,
46415
+ retryWith: normalizedRetryWith,
46416
+ projectDir
46417
+ }),
45400
46418
  sourceReports: renderSourceReports(projectDir, videoPath, checks),
45401
46419
  checks,
45402
- retryWith: uniqueRetryWith(retryWith)
46420
+ retryWith: normalizedRetryWith
45403
46421
  };
45404
46422
  }
45405
46423
  async function maybeWriteRenderReport(projectDir, opts, result) {
@@ -59403,6 +60421,7 @@ var init_build_asset_reference = __esm({
59403
60421
  EXTENSIONS = {
59404
60422
  narration: [".mp3", ".wav", ".m4a"],
59405
60423
  backdrop: [".png", ".jpg", ".jpeg", ".webp"],
60424
+ character: [".png", ".jpg", ".jpeg", ".webp"],
59406
60425
  video: [".mp4", ".mov", ".webm"],
59407
60426
  music: [".mp3", ".wav", ".m4a"]
59408
60427
  };
@@ -59440,6 +60459,16 @@ function backdropCacheDescriptor(opts) {
59440
60459
  ext: "png"
59441
60460
  });
59442
60461
  }
60462
+ function characterCacheDescriptor(opts) {
60463
+ return cacheAssetDescriptor("character", {
60464
+ name: opts.name,
60465
+ cue: opts.cue,
60466
+ provider: opts.provider,
60467
+ quality: opts.quality,
60468
+ size: opts.size,
60469
+ ext: "png"
60470
+ });
60471
+ }
59443
60472
  function imageRatioForSize(size) {
59444
60473
  switch (size) {
59445
60474
  case "1024x1024":
@@ -59459,6 +60488,8 @@ function videoCacheDescriptor(opts) {
59459
60488
  provider: opts.provider,
59460
60489
  duration: normalizeVideoDuration(opts.duration),
59461
60490
  ratio: "16:9",
60491
+ // Omit when empty so existing (character-less) clips keep their cache key.
60492
+ characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
59462
60493
  ext: "mp4"
59463
60494
  });
59464
60495
  }
@@ -59619,13 +60650,13 @@ async function createBuildPlan(opts) {
59619
60650
  const backdropCue = backdropPrompt ?? backdropReference?.raw;
59620
60651
  const videoCue = videoPrompt ?? videoReference?.raw;
59621
60652
  const musicCue = musicPrompt ?? musicReference?.raw;
59622
- const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : 0;
60653
+ const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : resolved.narration.resolved === "openai" ? OPENAI_NARRATION_COST_USD : 0;
59623
60654
  const narrationCache = narrationText && !narrationReference ? narrationCacheDescriptor({
59624
60655
  beatId: beat.id,
59625
60656
  cue: narrationText,
59626
60657
  provider: resolved.narration.resolved,
59627
60658
  voice,
59628
- ext: resolved.narration.resolved === "elevenlabs" ? "mp3" : "wav"
60659
+ ext: resolved.narration.resolved === "kokoro" ? "wav" : "mp3"
59629
60660
  }) : null;
59630
60661
  const backdropCache = augmentedBackdropPrompt && !backdropReference ? backdropCacheDescriptor({
59631
60662
  beatId: beat.id,
@@ -59686,7 +60717,11 @@ async function createBuildPlan(opts) {
59686
60717
  reference: videoReference,
59687
60718
  projectDir,
59688
60719
  force: opts.force,
59689
- cost: VIDEO_COST_USD,
60720
+ cost: resolved.video.resolved === "seedance" || resolved.video.resolved === "fal" ? estimateSeedanceVideoCostUsd({
60721
+ durationSec: normalizeVideoDuration(beat.duration),
60722
+ resolution: "720p",
60723
+ aspectRatio: "16:9"
60724
+ }) : VIDEO_COST_USD,
59690
60725
  active: includeAssets
59691
60726
  }) : null;
59692
60727
  const music = musicCue && !opts.skipMusic ? assetPlan({
@@ -59797,6 +60832,21 @@ async function createBuildPlan(opts) {
59797
60832
  retryWith.push(skipCommand);
59798
60833
  }
59799
60834
  }
60835
+ if (validationOk && includeAssets && !opts.skipVideo) {
60836
+ const referenced = /* @__PURE__ */ new Set();
60837
+ for (const beat of sourceBeats) {
60838
+ for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
60839
+ }
60840
+ if (referenced.size > 0) {
60841
+ const toGenerate = resolveCharacters(parsed.frontmatter).filter(
60842
+ (c) => referenced.has(c.name) && c.prompt && (opts.force || !existsSync47(join23(projectDir, `assets/character-${c.name}.png`)))
60843
+ );
60844
+ if (toGenerate.length > 0) {
60845
+ estimatedCostUsd += toGenerate.length * backdropCostUsd(opts.imageQuality ?? "hd");
60846
+ missing.add("assets");
60847
+ }
60848
+ }
60849
+ }
59800
60850
  return finalizeBuildPlan({
59801
60851
  projectDir,
59802
60852
  config: config2,
@@ -60043,17 +61093,20 @@ function providerInput(opts) {
60043
61093
  async function resolveNarrationProvider(input, projectDir) {
60044
61094
  const requested = input.value.toLowerCase();
60045
61095
  const hasElevenLabs = Boolean(await getApiKeyFromConfig("elevenlabs", { cwd: projectDir }));
60046
- const resolved = requested === "elevenlabs" ? "elevenlabs" : requested === "kokoro" ? "kokoro" : hasElevenLabs ? "elevenlabs" : "kokoro";
61096
+ const hasOpenAi = Boolean(await getApiKeyFromConfig("openai", { cwd: projectDir }));
61097
+ const resolved = requested === "elevenlabs" ? "elevenlabs" : requested === "openai" ? "openai" : requested === "kokoro" ? "kokoro" : hasElevenLabs ? "elevenlabs" : hasOpenAi ? "openai" : "kokoro";
61098
+ const requiresKey = resolved !== "kokoro";
61099
+ const hasKey = resolved === "elevenlabs" ? hasElevenLabs : resolved === "openai" ? hasOpenAi : true;
60047
61100
  return {
60048
61101
  kind: "narration",
60049
61102
  requested: input.requested,
60050
61103
  resolved,
60051
61104
  source: input.source,
60052
- requiresKey: resolved === "elevenlabs",
60053
- configured: resolved !== "elevenlabs" || hasElevenLabs,
60054
- ...resolved === "elevenlabs" ? { configKey: "elevenlabs" } : {},
60055
- ...resolved === "elevenlabs" && !hasElevenLabs ? { missingKey: providerEnvVar("elevenlabs") } : {},
60056
- retryWith: resolved === "elevenlabs" && !hasElevenLabs ? ["vibe setup --full"] : []
61105
+ requiresKey,
61106
+ configured: hasKey,
61107
+ ...requiresKey ? { configKey: resolved } : {},
61108
+ ...requiresKey && !hasKey ? { missingKey: providerEnvVar(resolved) } : {},
61109
+ retryWith: requiresKey && !hasKey ? ["vibe setup --full"] : []
60057
61110
  };
60058
61111
  }
60059
61112
  async function keyedProviderResolution(opts, projectDir) {
@@ -60149,7 +61202,7 @@ function loadPlanEnv(projectDir) {
60149
61202
  dir = dirname21(dir);
60150
61203
  }
60151
61204
  }
60152
- var import_dotenv3, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
61205
+ var import_dotenv3, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, OPENAI_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
60153
61206
  var init_build_plan = __esm({
60154
61207
  "src/commands/_shared/build-plan.ts"() {
60155
61208
  "use strict";
@@ -60158,6 +61211,7 @@ var init_build_plan = __esm({
60158
61211
  init_schema();
60159
61212
  init_build_asset_reference();
60160
61213
  init_build_cache();
61214
+ init_dist2();
60161
61215
  init_build_asset_metadata();
60162
61216
  init_build_backdrop_prompt();
60163
61217
  init_composer_resolve();
@@ -60167,6 +61221,7 @@ var init_build_plan = __esm({
60167
61221
  VIDEO_COST_USD = 5;
60168
61222
  MUSIC_COST_USD = 0.5;
60169
61223
  ELEVENLABS_NARRATION_COST_USD = 0.05;
61224
+ OPENAI_NARRATION_COST_USD = 0.01;
60170
61225
  COMPOSE_COST_USD = 0.06;
60171
61226
  SUPPORTED_BUILD_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
60172
61227
  }
@@ -60984,7 +62039,7 @@ __export(scene_build_exports, {
60984
62039
  executeSceneBuild: () => executeSceneBuild,
60985
62040
  resolveSceneBuildMode: () => resolveSceneBuildMode
60986
62041
  });
60987
- import { existsSync as existsSync49 } from "node:fs";
62042
+ import { existsSync as existsSync49, readFileSync as readFileSync3 } from "node:fs";
60988
62043
  import { copyFile as copyFile5, mkdir as mkdir26, readFile as readFile46, writeFile as writeFile50 } from "node:fs/promises";
60989
62044
  import { dirname as dirname24, join as join25, resolve as resolve65 } from "node:path";
60990
62045
  async function executeSceneBuild(opts) {
@@ -61151,8 +62206,32 @@ async function executeSceneBuild(opts) {
61151
62206
  const voice = opts.voice ?? parsed.frontmatter?.voice;
61152
62207
  let beatOutcomes = collectExistingBeatOutcomes(parsed.beats, projectDir);
61153
62208
  let pendingJobs = [];
62209
+ let characterCostUsd = 0;
61154
62210
  if (shouldRunStage(selectedStage, "assets")) {
61155
62211
  onProgress({ type: "phase-start", phase: "primitives" });
62212
+ const characterPaths = /* @__PURE__ */ new Map();
62213
+ if (!(opts.skipVideo ?? false)) {
62214
+ const referenced = /* @__PURE__ */ new Set();
62215
+ for (const beat of activeBeats) {
62216
+ for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
62217
+ }
62218
+ if (referenced.size > 0) {
62219
+ const charResult = await buildCharacters(
62220
+ resolveCharacters(parsed.frontmatter).filter((c) => referenced.has(c.name)),
62221
+ {
62222
+ projectDir,
62223
+ imageProvider,
62224
+ imageSize: opts.imageSize ?? "1536x1024",
62225
+ imageQuality: opts.imageQuality ?? "hd",
62226
+ force: opts.force ?? false,
62227
+ onProgress
62228
+ }
62229
+ );
62230
+ for (const [name, path2] of charResult.paths) characterPaths.set(name, path2);
62231
+ characterCostUsd = charResult.costUsd;
62232
+ warnings.push(...charResult.failures);
62233
+ }
62234
+ }
61156
62235
  const primitiveResults = await mapWithConcurrency(
61157
62236
  activeBeats,
61158
62237
  BEAT_PRIMITIVES_CONCURRENCY,
@@ -61170,7 +62249,8 @@ async function executeSceneBuild(opts) {
61170
62249
  skipVideo: opts.skipVideo ?? false,
61171
62250
  skipMusic: opts.skipMusic ?? false,
61172
62251
  force: opts.force ?? false,
61173
- onProgress
62252
+ onProgress,
62253
+ characterPaths
61174
62254
  })
61175
62255
  );
61176
62256
  beatOutcomes = primitiveResults.map((result) => result.outcome);
@@ -61179,10 +62259,7 @@ async function executeSceneBuild(opts) {
61179
62259
  (beat) => beat.narrationStatus === "failed" || beat.backdropStatus === "failed" || beat.videoStatus === "failed" || beat.musicStatus === "failed"
61180
62260
  );
61181
62261
  stageReports.assets.status = assetFailed ? "failed" : pendingJobs.length > 0 ? "pending-jobs" : "done";
61182
- stageReports.assets.costUsd = estimateActualAssetCost(
61183
- beatOutcomes,
61184
- opts.imageQuality ?? "hd"
61185
- );
62262
+ stageReports.assets.costUsd = estimateActualAssetCost(beatOutcomes, opts.imageQuality ?? "hd") + characterCostUsd;
61186
62263
  stageReports.assets.retryWith = pendingJobs.map(
61187
62264
  (job) => `vibe status job ${job.id} --project ${projectDir} --json`
61188
62265
  );
@@ -61347,6 +62424,18 @@ async function executeSceneBuild(opts) {
61347
62424
  stageReports.compose.status = "skipped";
61348
62425
  }
61349
62426
  if (stageReports.compose.status === "done") {
62427
+ for (const beat of activeBeats) {
62428
+ const videoRel = `assets/video-${beat.id}.mp4`;
62429
+ const compositionAbs = join25(projectDir, "compositions", `scene-${beat.id}.html`);
62430
+ if (!existsSync49(join25(projectDir, videoRel)) || !existsSync49(compositionAbs)) continue;
62431
+ const html = readFileSync3(compositionAbs, "utf-8");
62432
+ if (!html.includes(videoRel)) {
62433
+ warnings.push(
62434
+ `Beat "${beat.id}" has a generated clip (${videoRel}) but its composition does not reference it, so the clip will not appear. Recompose the beat.`
62435
+ );
62436
+ retryWith.push(`vibe build ${projectDir} --beat ${beat.id} --stage compose --force --json`);
62437
+ }
62438
+ }
61350
62439
  const repair = await runBuildSceneRepair(projectDir, "compose", false);
61351
62440
  sceneRepair = mergeSceneRepairSummaries(sceneRepair, repair);
61352
62441
  applySceneRepairToStage(stageReports.compose, repair);
@@ -61767,6 +62856,120 @@ async function dispatchNarration(beat, ctx) {
61767
62856
  freshness: "fresh"
61768
62857
  };
61769
62858
  }
62859
+ function characterSheetPrompt(name, description) {
62860
+ return [
62861
+ `Character turnaround reference sheet of an original fictional character named ${name}: ${description}.`,
62862
+ "Show three full-body views side by side on one clean canvas \u2014 front view, side profile, and back view \u2014",
62863
+ "with identical outfit, hairstyle, hair color, and body proportions across all three views.",
62864
+ "Plain light-grey studio background, even neutral lighting, no props.",
62865
+ "Photorealistic digital character, professional concept-art character sheet layout."
62866
+ ].join(" ");
62867
+ }
62868
+ async function buildCharacters(characters, ctx) {
62869
+ const paths = /* @__PURE__ */ new Map();
62870
+ const failures = [];
62871
+ let costUsd = 0;
62872
+ const size = ctx.imageSize ?? "1536x1024";
62873
+ for (const character of characters) {
62874
+ if (character.imagePath) {
62875
+ if (existsSync49(join25(ctx.projectDir, character.imagePath))) {
62876
+ paths.set(character.name, character.imagePath);
62877
+ ctx.onProgress({ type: "character-cached", name: character.name, path: character.imagePath });
62878
+ } else {
62879
+ const error = `character "${character.name}" image not found: ${character.imagePath}`;
62880
+ failures.push(error);
62881
+ ctx.onProgress({ type: "character-failed", name: character.name, error });
62882
+ }
62883
+ continue;
62884
+ }
62885
+ if (!character.prompt) continue;
62886
+ const prompt3 = characterSheetPrompt(character.name, character.prompt);
62887
+ const rel = `assets/character-${character.name}.png`;
62888
+ const abs = join25(ctx.projectDir, rel);
62889
+ const metadataOptions = { quality: ctx.imageQuality, size };
62890
+ const cache = characterCacheDescriptor({
62891
+ name: character.name,
62892
+ cue: prompt3,
62893
+ provider: ctx.imageProvider,
62894
+ quality: ctx.imageQuality,
62895
+ size
62896
+ });
62897
+ if (existsSync49(abs) && !ctx.force) {
62898
+ const metadata = readAssetMetadata(ctx.projectDir, "character", character.name);
62899
+ if (!metadata || isFreshCanonicalAsset({
62900
+ projectDir: ctx.projectDir,
62901
+ kind: "character",
62902
+ beatId: character.name,
62903
+ cue: prompt3,
62904
+ provider: ctx.imageProvider,
62905
+ options: metadataOptions,
62906
+ cacheKey: cache.key
62907
+ })) {
62908
+ paths.set(character.name, rel);
62909
+ ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
62910
+ continue;
62911
+ }
62912
+ }
62913
+ const cacheAbs = join25(ctx.projectDir, cache.path);
62914
+ if (existsSync49(cacheAbs) && !ctx.force) {
62915
+ await mkdir26(dirname24(abs), { recursive: true });
62916
+ await copyFile5(cacheAbs, abs);
62917
+ await writeAssetMetadata({
62918
+ projectDir: ctx.projectDir,
62919
+ kind: "character",
62920
+ beatId: character.name,
62921
+ cue: prompt3,
62922
+ provider: ctx.imageProvider,
62923
+ options: metadataOptions,
62924
+ cacheKey: cache.key,
62925
+ canonicalPath: rel,
62926
+ cachePath: cache.path
62927
+ });
62928
+ paths.set(character.name, rel);
62929
+ ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
62930
+ continue;
62931
+ }
62932
+ const generated = await generateBackdropImage(
62933
+ prompt3,
62934
+ {
62935
+ projectDir: ctx.projectDir,
62936
+ imageProvider: ctx.imageProvider,
62937
+ imageSize: size,
62938
+ imageQuality: ctx.imageQuality
62939
+ },
62940
+ imageRatioForSize(size)
62941
+ );
62942
+ if (!generated.success) {
62943
+ failures.push(`character "${character.name}": ${generated.error}`);
62944
+ ctx.onProgress({ type: "character-failed", name: character.name, error: generated.error });
62945
+ continue;
62946
+ }
62947
+ await mkdir26(dirname24(abs), { recursive: true });
62948
+ await writeFile50(abs, generated.buffer);
62949
+ await mkdir26(dirname24(cacheAbs), { recursive: true });
62950
+ await writeFile50(cacheAbs, generated.buffer);
62951
+ await writeAssetMetadata({
62952
+ projectDir: ctx.projectDir,
62953
+ kind: "character",
62954
+ beatId: character.name,
62955
+ cue: prompt3,
62956
+ provider: ctx.imageProvider,
62957
+ options: metadataOptions,
62958
+ cacheKey: cache.key,
62959
+ canonicalPath: rel,
62960
+ cachePath: cache.path
62961
+ });
62962
+ costUsd += backdropCostUsd(ctx.imageQuality);
62963
+ paths.set(character.name, rel);
62964
+ ctx.onProgress({
62965
+ type: "character-generated",
62966
+ name: character.name,
62967
+ path: rel,
62968
+ provider: ctx.imageProvider
62969
+ });
62970
+ }
62971
+ return { paths, costUsd: Number(costUsd.toFixed(2)), failures };
62972
+ }
61770
62973
  async function dispatchBackdrop(beat, ctx) {
61771
62974
  const reference = assetReferenceForBeat(ctx.projectDir, "backdrop", beat);
61772
62975
  if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
@@ -61936,13 +63139,16 @@ async function dispatchVideo(beat, ctx) {
61936
63139
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
61937
63140
  const prompt3 = stringOrUndefined4(beat.cues?.video);
61938
63141
  if (!prompt3) return { status: "no-cue" };
63142
+ const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
63143
+ const characterRefsAbs = characterRefsRel.map((p) => join25(ctx.projectDir, p));
61939
63144
  const rel = `assets/video-${beat.id}.mp4`;
61940
63145
  const abs = join25(ctx.projectDir, rel);
61941
63146
  const cache = videoCacheDescriptor({
61942
63147
  beatId: beat.id,
61943
63148
  cue: prompt3,
61944
63149
  provider: ctx.videoProvider,
61945
- duration: normalizeVideoDuration(beat.duration)
63150
+ duration: normalizeVideoDuration(beat.duration),
63151
+ characters: characterRefsRel
61946
63152
  });
61947
63153
  const metadataPath = assetMetadataPath("video", beat.id);
61948
63154
  if (existsSync49(abs) && !ctx.force) {
@@ -62002,6 +63208,7 @@ async function dispatchVideo(beat, ctx) {
62002
63208
  ratio: "16:9",
62003
63209
  output: abs,
62004
63210
  wait: false,
63211
+ refImages: characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
62005
63212
  apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
62006
63213
  });
62007
63214
  if (!result.success || !result.taskId) {
@@ -62370,7 +63577,13 @@ function estimateActualAssetCost(outcomes, imageQuality) {
62370
63577
  for (const outcome of outcomes) {
62371
63578
  if (outcome.narrationStatus === "generated") cost += 0.05;
62372
63579
  if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
62373
- if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") cost += 5;
63580
+ if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
63581
+ cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
63582
+ durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
63583
+ resolution: "720p",
63584
+ aspectRatio: "16:9"
63585
+ }) : 5;
63586
+ }
62374
63587
  if (outcome.musicStatus === "generated" || outcome.musicStatus === "pending") cost += 0.5;
62375
63588
  }
62376
63589
  return Number(cost.toFixed(2));
@@ -67930,8 +69143,9 @@ function planBuildElicitation(args) {
67930
69143
  description: "Which TTS engine narrates the video?",
67931
69144
  enum: [...NARRATION_CHOICES],
67932
69145
  enumNames: [
67933
- "Kokoro \u2014 free, runs locally on this machine",
67934
- "ElevenLabs \u2014 cloud TTS, uses your ElevenLabs credits"
69146
+ "Kokoro \u2014 free, runs on this machine (first use downloads a ~90 MB voice model; slower)",
69147
+ "OpenAI \u2014 fast cloud voice, uses your OpenAI API key (~$0.02 per video)",
69148
+ "ElevenLabs \u2014 premium cloud voices, uses your ElevenLabs credits"
67935
69149
  ]
67936
69150
  };
67937
69151
  required.push("narration");
@@ -67987,7 +69201,7 @@ var NARRATION_CHOICES, BACKDROP_CHOICES;
67987
69201
  var init_elicit = __esm({
67988
69202
  "src/tools/_shared/elicit.ts"() {
67989
69203
  "use strict";
67990
- NARRATION_CHOICES = ["kokoro", "elevenlabs"];
69204
+ NARRATION_CHOICES = ["kokoro", "openai", "elevenlabs"];
67991
69205
  BACKDROP_CHOICES = ["skip", "openai"];
67992
69206
  }
67993
69207
  });
@@ -68266,6 +69480,9 @@ var init_scene2 = __esm({
68266
69480
  kicker: external_exports.string().optional().describe("Small label above the headline (used by 'explainer' and 'product-shot' presets)."),
68267
69481
  projectDir: external_exports.string().optional().describe(PROJECT_DIR_DESCRIPTION),
68268
69482
  insertInto: external_exports.string().optional().describe("Root composition file (relative to projectDir). Default 'index.html'."),
69483
+ syncStoryboard: external_exports.boolean().optional().describe(
69484
+ "Sync STORYBOARD.md when present, then rebuild root refs from it. Default true; set false for root-only insertion."
69485
+ ),
68269
69486
  imageProvider: external_exports.enum(["gemini", "openai"]).optional().describe("Image provider for visuals. Default 'gemini'."),
68270
69487
  voice: external_exports.string().optional().describe("ElevenLabs voice id or name."),
68271
69488
  skipAudio: external_exports.boolean().optional().describe("Skip TTS even if narration is provided."),
@@ -68278,7 +69495,7 @@ var init_scene2 = __esm({
68278
69495
  cost: "low",
68279
69496
  title: "Add Scene from Preset",
68280
69497
  annotations: { readOnly: false, openWorld: true },
68281
- description: "Add a single scene to an existing scene project. Optionally generates narration audio (ElevenLabs) and/or a backdrop image (Gemini/OpenAI), then emits compositions/scene-<id>.html with a paused GSAP timeline and splices a clip reference into the root index.html. Use skipAudio:true and skipImage:true for text-only scenes that need no API calls.",
69498
+ description: "Add a single scene to an existing scene project. Optionally generates narration audio (ElevenLabs) and/or a backdrop image (Gemini/OpenAI), then emits compositions/scene-<id>.html with a paused GSAP timeline. By default, syncs STORYBOARD.md and rebuilds root refs from it; set syncStoryboard:false for root-only insertion. Use skipAudio:true and skipImage:true for text-only scenes that need no API calls.",
68282
69499
  schema: sceneAddSchema,
68283
69500
  async execute(args, ctx) {
68284
69501
  const projectDir = args.projectDir ? resolve75(ctx.workingDirectory, args.projectDir) : ctx.workingDirectory;
@@ -68292,6 +69509,7 @@ var init_scene2 = __esm({
68292
69509
  kicker: args.kicker,
68293
69510
  projectDir,
68294
69511
  insertInto: args.insertInto,
69512
+ syncStoryboard: args.syncStoryboard,
68295
69513
  imageProvider: args.imageProvider,
68296
69514
  voice: args.voice,
68297
69515
  skipAudio: args.skipAudio,
@@ -68310,6 +69528,9 @@ var init_scene2 = __esm({
68310
69528
  ];
68311
69529
  if (result.audioPath) lines.push(` audio: ${result.audioPath}`);
68312
69530
  if (result.imagePath) lines.push(` image: ${result.imagePath}`);
69531
+ if (result.storyboardPath && result.storyboardSynced) {
69532
+ lines.push(` story: ${result.storyboardPath} (${result.storyboardAction})`);
69533
+ }
68313
69534
  return {
68314
69535
  success: true,
68315
69536
  data: {
@@ -68320,7 +69541,10 @@ var init_scene2 = __esm({
68320
69541
  scenePath: result.scenePath,
68321
69542
  rootPath: result.rootPath,
68322
69543
  audioPath: result.audioPath,
68323
- imagePath: result.imagePath
69544
+ imagePath: result.imagePath,
69545
+ storyboardPath: result.storyboardPath,
69546
+ storyboardSynced: result.storyboardSynced,
69547
+ storyboardAction: result.storyboardAction
68324
69548
  },
68325
69549
  humanLines: lines
68326
69550
  };
@@ -68465,7 +69689,9 @@ var init_scene2 = __esm({
68465
69689
  skipVideo: external_exports.boolean().optional().describe("Skip video generation for every beat (use existing MP4 assets if present)."),
68466
69690
  skipMusic: external_exports.boolean().optional().describe("Skip music generation for every beat (use existing audio assets if present)."),
68467
69691
  skipRender: external_exports.boolean().optional().describe("Stop after compose \u2014 produces compositions/*.html but no final MP4."),
68468
- ttsProvider: external_exports.enum(["auto", "elevenlabs", "kokoro"]).optional().describe("TTS provider override. Default 'auto'."),
69692
+ ttsProvider: external_exports.enum(["auto", "elevenlabs", "openai", "kokoro"]).optional().describe(
69693
+ "TTS provider override. Default 'auto' (elevenlabs key > openai key > local kokoro)."
69694
+ ),
68469
69695
  voice: external_exports.string().optional().describe("TTS voice id (provider-specific)."),
68470
69696
  imageProvider: external_exports.enum(["openai"]).optional().describe("Image provider for backdrops. Default 'openai' (gpt-image-2)."),
68471
69697
  videoProvider: external_exports.enum(["seedance", "grok", "kling", "runway", "veo"]).optional().describe("Video provider for per-beat video cues. Default seedance."),
@@ -70320,7 +71546,7 @@ var init_storyboard2 = __esm({
70320
71546
  skipBackdrop: external_exports.boolean().optional().describe("Don't include backdrop image generation in the plan."),
70321
71547
  skipVideo: external_exports.boolean().optional().describe("Don't include video generation in the plan."),
70322
71548
  skipMusic: external_exports.boolean().optional().describe("Don't include music generation in the plan."),
70323
- ttsProvider: external_exports.enum(["auto", "elevenlabs", "kokoro"]).optional().describe("TTS provider override."),
71549
+ ttsProvider: external_exports.enum(["auto", "elevenlabs", "openai", "kokoro"]).optional().describe("TTS provider override."),
70324
71550
  voice: external_exports.string().optional().describe("Voice id."),
70325
71551
  imageProvider: external_exports.string().optional().describe("Image provider override."),
70326
71552
  imageQuality: external_exports.enum(["standard", "hd"]).optional().describe("Image quality override."),
@@ -73436,7 +74662,7 @@ init_source();
73436
74662
  // package.json
73437
74663
  var package_default = {
73438
74664
  name: "@vibeframe/cli",
73439
- version: "0.112.1",
74665
+ version: "0.113.2",
73440
74666
  description: "VibeFrame CLI - agentic video workflow runtime",
73441
74667
  private: false,
73442
74668
  license: "MIT",
@@ -73501,7 +74727,7 @@ var package_default = {
73501
74727
  lint: "eslint --ext .ts,.tsx src/",
73502
74728
  test: "vitest run",
73503
74729
  "test:watch": "vitest",
73504
- pretest: "node build.js",
74730
+ pretest: "pnpm --dir ../core build && pnpm --dir ../ai-providers build && node build.js",
73505
74731
  prepublishOnly: "node build.js"
73506
74732
  },
73507
74733
  dependencies: {
@@ -82019,13 +83245,13 @@ function titleizeProjectName(name) {
82019
83245
  return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || "Video";
82020
83246
  }
82021
83247
  function describeTheme(brief, product) {
82022
- const cleaned = brief.replace(/\b\d+[- ]?second\b/gi, "").replace(new RegExp(`\\bfor\\s+${escapeRegExp3(product)}\\b`, "i"), "").replace(/\blaunch video\b/gi, "launch story").replace(/\s+/g, " ").trim();
83248
+ const cleaned = brief.replace(/\b\d+[- ]?second\b/gi, "").replace(new RegExp(`\\bfor\\s+${escapeRegExp5(product)}\\b`, "i"), "").replace(/\blaunch video\b/gi, "launch story").replace(/\s+/g, " ").trim();
82023
83249
  return cleaned || "focused product story";
82024
83250
  }
82025
83251
  function isMountainSunriseBrief(brief) {
82026
83252
  return /\b(mountain|ridge|sunrise|fog|peak)\b/i.test(brief);
82027
83253
  }
82028
- function escapeRegExp3(value) {
83254
+ function escapeRegExp5(value) {
82029
83255
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82030
83256
  }
82031
83257
 
@@ -82068,6 +83294,7 @@ function hostDefinitions(_projectDir = process.cwd()) {
82068
83294
  configKind: "codex-toml",
82069
83295
  notes: [
82070
83296
  "Codex reads AGENTS.md and can also load project-scoped .codex/config.toml after the project is trusted.",
83297
+ "Use Codex /goal as the outer loop for long-running VibeFrame projects; vibe supplies JSON reports, nextActions classified by cost/safety (prefer these), retryWith as a compatibility fallback, and repair commands.",
82071
83298
  "Keep provider/auth keys in VibeFrame config or environment, not in project-local Codex config."
82072
83299
  ]
82073
83300
  },
@@ -82080,6 +83307,7 @@ function hostDefinitions(_projectDir = process.cwd()) {
82080
83307
  configKind: "mcp-json",
82081
83308
  notes: [
82082
83309
  "Claude Code can drive vibe directly through shell plus AGENTS.md/CLAUDE.md.",
83310
+ "Use Claude Code /goal as the outer loop for long-running VibeFrame projects; vibe supplies JSON reports, nextActions classified by cost/safety (prefer these), retryWith as a compatibility fallback, and repair commands.",
82083
83311
  "The MCP entry is optional and gives Claude Code a typed tool surface."
82084
83312
  ]
82085
83313
  },
@@ -82154,7 +83382,15 @@ async function planHostSetup(host, projectDir, opts) {
82154
83382
  status: opts.write ? "would-merge" : "would-write",
82155
83383
  reason: opts.write ? "dry run" : "snippet only; pass --write to modify"
82156
83384
  });
82157
- return { host, snippet, command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0, configPath, files, warnings, nextSteps };
83385
+ return {
83386
+ host,
83387
+ snippet,
83388
+ command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0,
83389
+ configPath,
83390
+ files,
83391
+ warnings,
83392
+ nextSteps
83393
+ };
82158
83394
  }
82159
83395
  try {
82160
83396
  const action = host.configKind === "codex-toml" ? await mergeCodexToml(configPath, { force: opts.force === true }) : await mergeMcpJson(configPath, {
@@ -82166,7 +83402,15 @@ async function planHostSetup(host, projectDir, opts) {
82166
83402
  } catch (error) {
82167
83403
  warnings.push(error instanceof Error ? error.message : String(error));
82168
83404
  }
82169
- return { host, snippet, command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0, configPath, files, warnings, nextSteps };
83405
+ return {
83406
+ host,
83407
+ snippet,
83408
+ command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0,
83409
+ configPath,
83410
+ files,
83411
+ warnings,
83412
+ nextSteps
83413
+ };
82170
83414
  }
82171
83415
  async function inspectHost(host, projectDir) {
82172
83416
  const configPath = host.configPath(projectDir);
@@ -82210,7 +83454,9 @@ async function mergeMcpJson(configPath, opts) {
82210
83454
  try {
82211
83455
  root = raw.trim() ? JSON.parse(raw) : {};
82212
83456
  } catch (error) {
82213
- throw new Error(`Invalid JSON in ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
83457
+ throw new Error(
83458
+ `Invalid JSON in ${configPath}: ${error instanceof Error ? error.message : String(error)}`
83459
+ );
82214
83460
  }
82215
83461
  }
82216
83462
  const mcpServers = typeof root.mcpServers === "object" && root.mcpServers !== null && !Array.isArray(root.mcpServers) ? root.mcpServers : {};
@@ -82223,10 +83469,18 @@ async function mergeMcpJson(configPath, opts) {
82223
83469
  };
82224
83470
  await mkdir23(dirname19(configPath), { recursive: true });
82225
83471
  if (exists && opts.backup) {
82226
- await writeFile45(`${configPath}.bak-${timestamp()}`, await readFile39(configPath, "utf-8"), "utf-8");
83472
+ await writeFile45(
83473
+ `${configPath}.bak-${timestamp()}`,
83474
+ await readFile39(configPath, "utf-8"),
83475
+ "utf-8"
83476
+ );
82227
83477
  }
82228
83478
  await writeFile45(configPath, JSON.stringify(root, null, 2) + "\n", "utf-8");
82229
- return { path: configPath, status: "merged", reason: "anchored existing default vibeframe MCP server" };
83479
+ return {
83480
+ path: configPath,
83481
+ status: "merged",
83482
+ reason: "anchored existing default vibeframe MCP server"
83483
+ };
82230
83484
  }
82231
83485
  return {
82232
83486
  path: configPath,
@@ -82237,7 +83491,11 @@ async function mergeMcpJson(configPath, opts) {
82237
83491
  root.mcpServers = { ...mcpServers, vibeframe: opts.server };
82238
83492
  await mkdir23(dirname19(configPath), { recursive: true });
82239
83493
  if (exists && opts.backup) {
82240
- await writeFile45(`${configPath}.bak-${timestamp()}`, await readFile39(configPath, "utf-8"), "utf-8");
83494
+ await writeFile45(
83495
+ `${configPath}.bak-${timestamp()}`,
83496
+ await readFile39(configPath, "utf-8"),
83497
+ "utf-8"
83498
+ );
82241
83499
  }
82242
83500
  await writeFile45(configPath, JSON.stringify(root, null, 2) + "\n", "utf-8");
82243
83501
  return { path: configPath, status: exists ? "merged" : "wrote" };
@@ -82248,16 +83506,22 @@ async function mergeCodexToml(configPath, opts) {
82248
83506
  ${CODEX_MCP_BLOCK}${CODEX_MANAGED_END}`;
82249
83507
  if (!exists) {
82250
83508
  await mkdir23(dirname19(configPath), { recursive: true });
82251
- await writeFile45(configPath, `#:schema https://developers.openai.com/codex/config-schema.json
83509
+ await writeFile45(
83510
+ configPath,
83511
+ `#:schema https://developers.openai.com/codex/config-schema.json
82252
83512
 
82253
83513
  ${block}
82254
- `, "utf-8");
83514
+ `,
83515
+ "utf-8"
83516
+ );
82255
83517
  return { path: configPath, status: "wrote" };
82256
83518
  }
82257
83519
  const raw = await readFile39(configPath, "utf-8");
82258
83520
  if (raw.includes(CODEX_MANAGED_START)) {
82259
83521
  const updated = raw.replace(
82260
- new RegExp(`${escapeRegExp4(CODEX_MANAGED_START)}[\\s\\S]*?${escapeRegExp4(CODEX_MANAGED_END)}`),
83522
+ new RegExp(
83523
+ `${escapeRegExp6(CODEX_MANAGED_START)}[\\s\\S]*?${escapeRegExp6(CODEX_MANAGED_END)}`
83524
+ ),
82261
83525
  block
82262
83526
  );
82263
83527
  await writeFile45(configPath, ensureTrailingNewline(updated), "utf-8");
@@ -82291,7 +83555,8 @@ function hasVibeframeMcpServer(value) {
82291
83555
  function readVibeframeMcpServer(value) {
82292
83556
  if (!value || typeof value !== "object") return null;
82293
83557
  const root = value;
82294
- if (!root.mcpServers || typeof root.mcpServers !== "object" || Array.isArray(root.mcpServers)) return null;
83558
+ if (!root.mcpServers || typeof root.mcpServers !== "object" || Array.isArray(root.mcpServers))
83559
+ return null;
82295
83560
  const server = root.mcpServers.vibeframe;
82296
83561
  if (!server || typeof server !== "object" || Array.isArray(server)) return null;
82297
83562
  return server;
@@ -82302,7 +83567,9 @@ function isAnchoredClaudeDesktopServer(server) {
82302
83567
  return server.args[0] === "-lc" && typeof server.args[1] === "string" && server.args[1].includes("exec npx -y @vibeframe/mcp-server");
82303
83568
  }
82304
83569
  if (server.command === "cmd.exe" && Array.isArray(server.args)) {
82305
- return server.args.some((arg) => typeof arg === "string" && arg.includes("npx -y @vibeframe/mcp-server"));
83570
+ return server.args.some(
83571
+ (arg) => typeof arg === "string" && arg.includes("npx -y @vibeframe/mcp-server")
83572
+ );
82306
83573
  }
82307
83574
  return false;
82308
83575
  }
@@ -82319,13 +83586,15 @@ function hostNextSteps(host) {
82319
83586
  if (host.id === "codex") {
82320
83587
  return [
82321
83588
  "Trust the project in Codex so .codex/config.toml is loaded.",
82322
- "Run /mcp in Codex to confirm the vibeframe server is connected."
83589
+ "Run /mcp in Codex to confirm the vibeframe server is connected.",
83590
+ "Use /goal with the project AGENTS.md stop rules for long-running video builds."
82323
83591
  ];
82324
83592
  }
82325
83593
  if (host.id === "claude-code") {
82326
83594
  return [
82327
83595
  "Restart Claude Code or run /mcp to approve the project-scoped server.",
82328
- "Use AGENTS.md/CLAUDE.md for shell-driven vibe workflows."
83596
+ "Use AGENTS.md/CLAUDE.md for shell-driven vibe workflows.",
83597
+ "Use /goal with the project AGENTS.md stop rules for long-running video builds."
82329
83598
  ];
82330
83599
  }
82331
83600
  if (host.id === "claude-desktop") {
@@ -82339,7 +83608,11 @@ function claudeDesktopConfigPath() {
82339
83608
  return join21(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
82340
83609
  }
82341
83610
  if (process.platform === "win32") {
82342
- return join21(process.env.APPDATA ?? join21(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
83611
+ return join21(
83612
+ process.env.APPDATA ?? join21(home, "AppData", "Roaming"),
83613
+ "Claude",
83614
+ "claude_desktop_config.json"
83615
+ );
82343
83616
  }
82344
83617
  return join21(home, ".config", "Claude", "claude_desktop_config.json");
82345
83618
  }
@@ -82350,7 +83623,7 @@ function ensureTrailingNewline(value) {
82350
83623
  return value.endsWith("\n") ? value : `${value}
82351
83624
  `;
82352
83625
  }
82353
- function escapeRegExp4(value) {
83626
+ function escapeRegExp6(value) {
82354
83627
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82355
83628
  }
82356
83629
  function shellQuote(value) {
@@ -82542,6 +83815,45 @@ Default setup is snippet-only; use \`--write\` when you want VibeFrame to merge
82542
83815
  the generated config. Keep provider keys in \`.vibeframe/config.yaml\`, user
82543
83816
  config, or env files \u2014 not in app host config.
82544
83817
 
83818
+ ## Native host goal loop
83819
+
83820
+ For long-running work, use Codex Goal mode, Claude Code \`/goal\`, Cursor, or
83821
+ another host-native goal feature as the outer loop. VibeFrame should not own a
83822
+ competing project-level goal runner. It provides \`--json\` commands, dry runs,
83823
+ budget caps, \`build-report.json\`, \`review-report.json\`, \`nextActions\`,
83824
+ \`safeToAutoRun\`, \`requiresConfirmation\`, \`fixOwner\`, deterministic repair,
83825
+ and render inspection for the host agent to decide what to do next.
83826
+
83827
+ Copy-paste Codex:
83828
+
83829
+ \`\`\`text
83830
+ /goal Build this VibeFrame project into renders/final.mp4. Use --json for every
83831
+ vibe command, run --dry-run before paid operations, use --max-cost 5 for builds
83832
+ unless the user gives another budget, read build-report.json and
83833
+ review-report.json before deciding the next action, prefer nextActions before
83834
+ guessing, run only safeToAutoRun:true actions automatically, and ask before
83835
+ requiresConfirmation:true actions. Treat retryWith as the compatibility
83836
+ fallback, fixOwner:"vibe" as CLI repair work, and fixOwner:"host-agent" as
83837
+ STORYBOARD.md, DESIGN.md, or composition edits.
83838
+ Stop only when renders/final.mp4 exists, target duration and aspect ratio are
83839
+ met, inspect render --cheap has no errors, any AI review score is >= 90 when AI review is requested, and every
83840
+ remaining host-agent issue is fixed, accepted with rationale, or reported as
83841
+ blocked.
83842
+ \`\`\`
83843
+
83844
+ Copy-paste Claude Code:
83845
+
83846
+ \`\`\`text
83847
+ /goal Finish this VibeFrame render using Claude Code goal mode as the outer
83848
+ loop. Use vibe context/schema when unsure, --json everywhere, dry-run before
83849
+ paid operations, budget cap via --max-cost 5, nextActions before guessing,
83850
+ build-report.json/review-report.json as loop state, and safeToAutoRun,
83851
+ requiresConfirmation, and fixOwner to decide between vibe scene repair and
83852
+ host-agent edits. Stop only after final MP4,
83853
+ target duration/aspect ratio, clean render inspection, any AI review score >= 90 when AI review is requested, and
83854
+ no unresolved unacknowledged host-agent issues.
83855
+ \`\`\`
83856
+
82545
83857
  ### Composition rules (scene HTML)
82546
83858
 
82547
83859
  \`vibe init\` installs local composition rules into your project. The
@@ -83439,13 +84751,13 @@ init_output();
83439
84751
  import { resolve as resolve61 } from "node:path";
83440
84752
  var VALID_STAGES = ["assets", "compose", "sync", "render", "all"];
83441
84753
  var VALID_MODES = ["agent", "batch", "auto"];
83442
- var VALID_TTS_PROVIDERS = ["auto", "elevenlabs", "kokoro"];
84754
+ var VALID_TTS_PROVIDERS = ["auto", "elevenlabs", "openai", "kokoro"];
83443
84755
  var VALID_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
83444
84756
  var VALID_VIDEO_PROVIDERS = ["seedance", "grok", "kling", "runway", "veo"];
83445
84757
  var VALID_MUSIC_PROVIDERS = ["elevenlabs", "replicate"];
83446
84758
  var VALID_COMPOSERS = ["claude", "openai", "gemini"];
83447
84759
  var VALID_IMAGE_QUALITIES = ["standard", "hd"];
83448
- var planCommand = new Command("plan").description("Read STORYBOARD.md and show build plan, costs, missing cues, and provider needs").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Stage to plan: ${VALID_STAGES.join("|")}`, "all").option("--beat <id>", "Restrict the plan to one beat").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--skip-narration", "Don't include narration generation in the plan").option("--skip-backdrop", "Don't include backdrop image generation in the plan").option("--skip-video", "Don't include video generation in the plan").option("--skip-music", "Don't include music generation in the plan").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS.join("|")}`).option("--quality <q>", "Image quality: standard|hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--force", "Plan regeneration even when outputs already exist").option("--max-cost <usd>", "Fail if estimated cost exceeds this USD cap").action(async (projectDirArg, options) => {
84760
+ var planCommand = new Command("plan").description("Read STORYBOARD.md and show build plan, costs, missing cues, and provider needs").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Stage to plan: ${VALID_STAGES.join("|")}`, "all").option("--beat <id>", "Restrict the plan to one beat").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--skip-narration", "Don't include narration generation in the plan").option("--skip-backdrop", "Don't include backdrop image generation in the plan").option("--skip-video", "Don't include video generation in the plan").option("--skip-music", "Don't include music generation in the plan").option("--tts <provider>", "TTS provider: auto|elevenlabs|openai|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS.join("|")}`).option("--quality <q>", "Image quality: standard|hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--force", "Plan regeneration even when outputs already exist").option("--max-cost <usd>", "Fail if estimated cost exceeds this USD cap").action(async (projectDirArg, options) => {
83449
84761
  const startedAt = Date.now();
83450
84762
  const stage = String(options.stage ?? "all");
83451
84763
  if (!VALID_STAGES.includes(stage)) {
@@ -83630,13 +84942,13 @@ init_build_plan();
83630
84942
  init_storyboard_parse();
83631
84943
  init_output();
83632
84944
  import { resolve as resolve66, join as join26 } from "node:path";
83633
- import { readFileSync as readFileSync3, existsSync as existsSync50 } from "node:fs";
84945
+ import { readFileSync as readFileSync4, existsSync as existsSync50 } from "node:fs";
83634
84946
  var VALID_MODES2 = ["agent", "batch", "auto"];
83635
84947
  var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
83636
84948
  var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
83637
84949
  var VALID_VIDEO_PROVIDERS2 = ["seedance", "grok", "kling", "runway", "veo"];
83638
84950
  var VALID_MUSIC_PROVIDERS2 = ["elevenlabs", "replicate"];
83639
- var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-video", "Don't dispatch video generation even when beats declare video cues").option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS2.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").addHelpText(
84951
+ var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-video", "Don't dispatch video generation even when beats declare video cues").option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|openai|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS2.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").addHelpText(
83640
84952
  "after",
83641
84953
  `
83642
84954
  Examples:
@@ -83997,7 +85309,7 @@ function estimateBuildCost(params) {
83997
85309
  if (!existsSync50(storyboardPath2)) return null;
83998
85310
  let beats = 0;
83999
85311
  try {
84000
- const md = readFileSync3(storyboardPath2, "utf-8");
85312
+ const md = readFileSync4(storyboardPath2, "utf-8");
84001
85313
  beats = parseStoryboard(md).beats.length;
84002
85314
  } catch {
84003
85315
  return null;
@@ -84381,6 +85693,18 @@ function printProjectStatus(result) {
84381
85693
  console.log(` Build: ${result.build ? formatBuild(result.build) : source_default.dim("no build-report.json")}`);
84382
85694
  console.log(` Review: ${result.review ? formatReview(result.review) : source_default.dim("no review-report.json")}`);
84383
85695
  console.log(` Jobs: ${result.jobs.total} total, ${result.jobs.active} active, ${result.jobs.failed} failed`);
85696
+ if (result.nextActions.length > 0) {
85697
+ console.log();
85698
+ console.log(source_default.bold("Next actions"));
85699
+ for (const action of result.nextActions.slice(0, 5)) {
85700
+ console.log(formatNextAction(action));
85701
+ if (action.command) console.log(source_default.dim(` ${action.command}`));
85702
+ if (action.agentPrompt) console.log(source_default.dim(` ${action.agentPrompt}`));
85703
+ }
85704
+ if (result.nextActions.length > 5) {
85705
+ console.log(source_default.dim(` ... ${result.nextActions.length - 5} more`));
85706
+ }
85707
+ }
84384
85708
  if (result.jobs.latest.length > 0) {
84385
85709
  console.log();
84386
85710
  for (const job of result.jobs.latest.slice(0, 5)) {
@@ -84401,6 +85725,10 @@ function formatReview(review) {
84401
85725
  const status = review.status ?? "unknown";
84402
85726
  return `${formatStatus(status)} score ${review.score ?? "-"} (${review.issueCount} issue${review.issueCount === 1 ? "" : "s"})`;
84403
85727
  }
85728
+ function formatNextAction(action) {
85729
+ const badge = action.safeToAutoRun ? "auto" : action.requiresConfirmation ? "confirm" : action.kind;
85730
+ return ` ${source_default.dim(`[${badge}]`)} ${action.label}`;
85731
+ }
84404
85732
  function formatStatus(status) {
84405
85733
  if (status === "completed" || status === "pass") return source_default.green(status);
84406
85734
  if (status === "failed" || status === "fail") return source_default.red(status);
@@ -85407,6 +86735,8 @@ var contextCommand = new Command("context").description("Print CLI context/guide
85407
86735
  product: "vibeframe",
85408
86736
  sourceOfTruth: ["STORYBOARD.md", "DESIGN.md", "vibe.config.json"],
85409
86737
  preferredFlow: [
86738
+ "native Codex Goal mode or Claude Code /goal owns persistence, iteration, and stop decisions",
86739
+ "vibe context/schema for command discovery",
85410
86740
  "storyboard revise after init when the brief changes",
85411
86741
  "storyboard validate",
85412
86742
  "plan",
@@ -85421,6 +86751,19 @@ var contextCommand = new Command("context").description("Print CLI context/guide
85421
86751
  "inspect render --ai when needed",
85422
86752
  "status project when async jobs are involved"
85423
86753
  ],
86754
+ nativeGoalContract: {
86755
+ owner: "host-native goal mode (Codex /goal, Claude Code /goal, Cursor or other host equivalent)",
86756
+ vibeRole: "video-specific commands, JSON reports, cost gates, deterministic repair, render inspection, nextActions, and retryWith compatibility hints",
86757
+ canonicalLoop: "native host goal -> vibe context/schema -> plan dry-run -> build with budget -> status polling -> inspect project -> render -> inspect render -> repair/edit using nextActions/fixOwner -> repeat",
86758
+ stopRules: [
86759
+ "final MP4 exists at the requested output path",
86760
+ "duration and aspect ratio match the brief or explicitly accepted constraints",
86761
+ "inspect render --cheap reports no errors",
86762
+ "any AI review score meets the goal threshold, commonly >= 90, when AI review is requested",
86763
+ "all fixOwner:vibe issues have been repaired or are blocked with evidence",
86764
+ "all fixOwner:host-agent issues are fixed, intentionally accepted with a written reason, or reported as blocked"
86765
+ ]
86766
+ },
85424
86767
  mentalModel: {
85425
86768
  storyboard: "intent layer; edit or mutate beat cues here",
85426
86769
  scene: "generated artifact layer; lint/repair composition HTML here"
@@ -85454,7 +86797,8 @@ var contextCommand = new Command("context").description("Print CLI context/guide
85454
86797
  reviewReportContract: {
85455
86798
  kind: "review",
85456
86799
  modes: ["project", "render"],
85457
- fields: ["status", "score", "issues", "summary", "sourceReports", "retryWith"],
86800
+ fields: ["status", "score", "issues", "summary", "sourceReports", "nextActions", "retryWith"],
86801
+ recovery: "prefer nextActions (classified by costTier/safeToAutoRun/requiresConfirmation/fixOwner); retryWith is the compatibility fallback",
85458
86802
  issueFixOwner: {
85459
86803
  vibe: "deterministic CLI recovery",
85460
86804
  "host-agent": "storyboard/design/composition edits the host agent should make"
@@ -85462,7 +86806,7 @@ var contextCommand = new Command("context").description("Print CLI context/guide
85462
86806
  },
85463
86807
  machineStatusContract: {
85464
86808
  buildReport: "kind/status/currentStage/beatSummary/jobs/sceneRepair/stageReports/warnings/retryWith plus beat timing and nested per-beat asset metadata",
85465
- projectStatus: "kind/status/currentStage/beats/jobs.latest/build/review/warnings/retryWith",
86809
+ projectStatus: "kind/status/currentStage/beats/jobs.latest/build/review/warnings/nextActions/retryWith (nextActions is populated for every workflow state, not only when a review report exists)",
85466
86810
  reviewSummary: "review.mode/issueCount/errorCount/warningCount/infoCount/fixOwners/sourceReports/retryWith"
85467
86811
  },
85468
86812
  productSurfaceContract: {
@@ -85545,12 +86889,13 @@ render. Repair failures return \`code:"SCENE_REPAIR_FAILED"\` with
85545
86889
  render with \`currentStage:"assets"\` and \`code:"ASSET_REFERENCE_INVALID"\`,
85546
86890
  \`code:"MISSING_API_KEY"\`, or \`code:"ASSET_GENERATION_FAILED"\`.
85547
86891
  \`build-report.json\` includes \`sceneRepair\`, and \`status project\` carries
85548
- review issue counts, \`fixOwners\`, \`sourceReports\`, and
85549
- \`review.retryWith\`.
86892
+ review issue counts, \`fixOwners\`, \`sourceReports\`, top-level
86893
+ \`nextActions\` (populated for every workflow state), and \`retryWith\`.
86894
+ Prefer \`nextActions\` and treat \`retryWith\` as the compatibility fallback.
85550
86895
 
85551
86896
  \`review-report.json\` is written by \`inspect project\` and \`inspect render\`
85552
86897
  as \`kind:"review"\` with \`mode:"project"|"render"\`, \`summary\`,
85553
- \`sourceReports\`, \`retryWith\`, and issue-level
86898
+ \`sourceReports\`, \`nextActions\`, \`retryWith\`, and issue-level
85554
86899
  \`fixOwner:"vibe"|"host-agent"\`.
85555
86900
  \`inspect render --cheap\` checks duration drift, audio presence, black frames,
85556
86901
  long silence, and static-frame holds. Static or semantic beat issues are
@@ -85572,6 +86917,8 @@ Use 'vibe schema --list --surface public' for the small product surface.
85572
86917
  Use 'vibe schema <command> --json' to get parameter schemas.
85573
86918
  Use 'vibe doctor --json' to check configured API keys.
85574
86919
  Use '--dry-run --json' before any mutating/costly operation.
86920
+ Use native Codex Goal mode or Claude Code /goal as the outer loop; VibeFrame provides JSON reports, retryWith, fixOwner, cost gates, and render inspection for that host loop.
86921
+ Stop only when the final MP4 exists, target duration/aspect ratio are met, inspect render has no errors, any AI review score meets the goal threshold when AI review is requested, and every host-agent issue is fixed, accepted with rationale, or reported blocked.
85575
86922
 
85576
86923
  Cost tiers: Free (schema/context/doctor/detect/status/plan/storyboard validate/inspect project/render --cheap, deterministic edits) | Low (generate narration/sound-effect/music, audio transcribe, inspect media, optional AI review) | High (generate image/motion, edit image/reframe/grade/speed-ramp) | Very High (generate video, edit fill-gaps, remix highlights/auto-shorts, build with generated assets)
85577
86924