@aicopycoders/exodus 2026.9.2000-beta.11 → 2026.9.2000-beta.13

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.
@@ -138,7 +138,11 @@ under it:
138
138
  lines under `guidance`). It is also
139
139
  the one park where a finished clip can be redone, with `retry-clip`. A run
140
140
  only reaches this park if its video node sets `finalWatch: true` — otherwise
141
- it skips straight to finished.
141
+ it skips straight to finished. If a scene has no clip, the stop block names
142
+ it instead (`stop.missingScenes` in `--json`) and approval is refused until
143
+ it has one. A scene whose picture the video service refused needs
144
+ `retry-frame --note` before `retry-clip`; the clip redo alone resends the
145
+ refused picture.
142
146
  - paused: a builder checkpoint neither video verb resolves —
143
147
  `exodus workflow checkpoint <runId>` is the verb for that one.
144
148
  - failed: `status` shows how far it got, and a `--wait` that was running exits
@@ -115,6 +115,11 @@ export interface VideoRunNode {
115
115
  planFailure?: PlanFailureRecord;
116
116
  hasRejectedDraft?: boolean;
117
117
  outputs?: ArtifactSubset[];
118
+ missingScenes?: MissingScene[];
119
+ }
120
+ export interface MissingScene {
121
+ sceneIndex: number;
122
+ pictureRefused: boolean;
118
123
  }
119
124
  export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
120
125
  export interface PullCastLockMember {
@@ -162,6 +167,7 @@ export interface NodeItem {
162
167
  take: number;
163
168
  outcome: string;
164
169
  label: string;
170
+ reason?: string;
165
171
  displacedHistory?: {
166
172
  title: string;
167
173
  lines: string[];
@@ -221,6 +227,8 @@ export type RunStop = {
221
227
  showAd?: true;
222
228
  } | {
223
229
  at: "final-watch";
230
+ missingScenes?: MissingScene[];
231
+ framesNodeId?: string;
224
232
  } | {
225
233
  at: "paused";
226
234
  nodeId?: string;
@@ -237,10 +245,11 @@ export type RunStop = {
237
245
  };
238
246
  export type ResolvedStop = Exclude<RunStop, {
239
247
  at: "final-watch";
240
- }> | {
248
+ }> | (Extract<RunStop, {
241
249
  at: "final-watch";
250
+ }> & {
242
251
  cutAttached: boolean | null;
243
- };
252
+ });
244
253
  export declare function classifyRun(run: VideoRun): RunStop;
245
254
  export declare function hasAttachedCut(items: NodeItem[]): boolean;
246
255
  export declare function resolveStop(stop: RunStop, cutAttached: boolean | null): ResolvedStop;
@@ -223,6 +223,9 @@ export function renderQcTakeHistory(takes) {
223
223
  });
224
224
  return lines;
225
225
  }
226
+ function redoLine(lastRedo) {
227
+ return lastRedo.reason ? `${lastRedo.label}: ${lastRedo.reason}` : lastRedo.label;
228
+ }
226
229
  function printDisplacedHistory(lines, lastRedo) {
227
230
  const history = lastRedo.displacedHistory;
228
231
  if (!history)
@@ -346,8 +349,17 @@ export function classifyRun(run) {
346
349
  ...(isShowAd(run) ? { showAd: true } : {}),
347
350
  };
348
351
  }
349
- if (parkedAtFinalWatch(run))
350
- return { at: "final-watch" };
352
+ if (parkedAtFinalWatch(run)) {
353
+ const missingScenes = run.nodes
354
+ .filter((n) => n.kind === "video")
355
+ .flatMap((n) => n.missingScenes ?? []);
356
+ const framesNodeId = run.nodes.find((n) => n.kind === "scene-frames")?.nodeId;
357
+ return {
358
+ at: "final-watch",
359
+ ...(missingScenes.length > 0 ? { missingScenes } : {}),
360
+ ...(missingScenes.length > 0 && framesNodeId ? { framesNodeId } : {}),
361
+ };
362
+ }
351
363
  return { at: "paused", nodeId: run.pausedNodeId, reason: run.pauseReason };
352
364
  }
353
365
  const active = run.nodes.find((n) => n.status === "running");
@@ -359,7 +371,7 @@ export function hasAttachedCut(items) {
359
371
  export function resolveStop(stop, cutAttached) {
360
372
  if (stop.at !== "final-watch")
361
373
  return stop;
362
- return { at: "final-watch", cutAttached };
374
+ return { ...stop, cutAttached };
363
375
  }
364
376
  export async function resolveStopAtPark(stop, runId, deps) {
365
377
  if (stop.at !== "final-watch")
@@ -404,6 +416,30 @@ export function reviewUrl(dashboardUrl, run) {
404
416
  return `${dashboardUrl}/workflows/${run.workflowId}/runs/${run._id}`;
405
417
  return `${dashboardUrl}/runs/${run._id}`;
406
418
  }
419
+ function scenesPhrase(scenes) {
420
+ if (scenes.length === 1)
421
+ return `scene ${scenes[0]}`;
422
+ return `scenes ${scenes.slice(0, -1).join(", ")} and ${scenes[scenes.length - 1]}`;
423
+ }
424
+ function missingSceneLines(missing, framesNodeId, runId, runUrl) {
425
+ const scenes = missing.map((m) => m.sceneIndex);
426
+ const lines = [
427
+ `Parked: ${scenesPhrase(scenes)} ${scenes.length === 1 ? "has" : "have"} no clip yet, so the ad isn't complete.`,
428
+ ];
429
+ for (const { sceneIndex, pictureRefused } of missing) {
430
+ if (!pictureRefused) {
431
+ lines.push(`Redo clip ${sceneIndex}: exodus video retry-clip ${runId} --scene ${sceneIndex}`);
432
+ continue;
433
+ }
434
+ lines.push(`Scene ${sceneIndex}'s picture was refused by the video service as too close to an existing character, so redraw it before redoing the clip.`);
435
+ lines.push(framesNodeId
436
+ ? `Redraw frame ${sceneIndex}: exodus video retry-frame ${runId} --node ${framesNodeId} --scene ${sceneIndex} --note "<what to change>"`
437
+ : `Redraw frame ${sceneIndex}: on the run page, ${runUrl}`);
438
+ lines.push(`Then redo clip ${sceneIndex}: exodus video retry-clip ${runId} --scene ${sceneIndex}`);
439
+ }
440
+ lines.push("The ad can't be approved until every scene has a clip.", `Watch it here: ${runUrl}`);
441
+ return lines;
442
+ }
407
443
  export function stopLines(stop, runId, runUrl) {
408
444
  if (stop.at === "storyboard-gate") {
409
445
  const lines = [
@@ -420,6 +456,9 @@ export function stopLines(stop, runId, runUrl) {
420
456
  return lines;
421
457
  }
422
458
  if (stop.at === "final-watch") {
459
+ if (stop.missingScenes?.length) {
460
+ return missingSceneLines(stop.missingScenes, stop.framesNodeId, runId, runUrl);
461
+ }
423
462
  if (stop.cutAttached === null) {
424
463
  return [
425
464
  "Parked: every piece is made.",
@@ -1479,7 +1518,7 @@ export async function statusFlow(runId, json, deps) {
1479
1518
  const row = byScene.get(sceneIndex);
1480
1519
  lines.push(`${String(sceneIndex).padEnd(5)} ${itemWord(row.clip).padEnd(8)} ${itemWord(row.voiceover).padEnd(8)} ${itemWord(row.frame)}`);
1481
1520
  if (row.clip?.lastRedo) {
1482
- lines.push(` clip: ${row.clip.lastRedo.label}`);
1521
+ lines.push(` clip: ${redoLine(row.clip.lastRedo)}`);
1483
1522
  printDisplacedHistory(lines, row.clip.lastRedo);
1484
1523
  }
1485
1524
  if (row.clip?.lastRedo || row.clip?.flagged) {
@@ -1488,7 +1527,7 @@ export async function statusFlow(runId, json, deps) {
1488
1527
  }
1489
1528
  }
1490
1529
  if (row.voiceover?.lastRedo) {
1491
- lines.push(` voice: ${row.voiceover.lastRedo.label}`);
1530
+ lines.push(` voice: ${redoLine(row.voiceover.lastRedo)}`);
1492
1531
  printDisplacedHistory(lines, row.voiceover.lastRedo);
1493
1532
  }
1494
1533
  if (row.frame?.lastRedo)
@@ -1870,7 +1909,12 @@ export async function retryClipFlow(runId, target, { note, voiceFirst = false },
1870
1909
  "recorded in the character's own voice, then the clip is made to match it."
1871
1910
  : `Redoing scene ${plan.sceneIndex}'s clip on ${plan.nodeId}.`,
1872
1911
  `triggerRunId: ${triggerRunId ?? "-"}`,
1873
- "Every other scene stays as it is, and so do the pictures, the voices and the script.",
1912
+ ...(voiceFirst
1913
+ ? [
1914
+ "Every other scene stays as it is, and so do the pictures and the script.",
1915
+ "A speaking character with no voice yet gets one picked and saved for the whole run.",
1916
+ ]
1917
+ : ["Every other scene stays as it is, and so do the pictures, the voices and the script."]),
1874
1918
  ...(note
1875
1919
  ? ["Your note steers how this clip moves. It never changes the words that are spoken."]
1876
1920
  : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.11",
3
+ "version": "2026.9.2000-beta.13",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",