@aicopycoders/exodus 2026.9.2000-beta.2 → 2026.9.2000-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,7 +72,7 @@ export type ArtifactSubset = {
72
72
  export interface VideoRunNode {
73
73
  nodeId: string;
74
74
  kind: string;
75
- status: "idle" | "running" | "done" | "failed" | "skipped";
75
+ status: "idle" | "running" | "done" | "failed" | "skipped" | "out-of-scope";
76
76
  error?: string;
77
77
  warning?: string;
78
78
  outputs?: ArtifactSubset[];
@@ -293,7 +293,10 @@ export declare function waitFlow(runId: string, opts: {
293
293
  }, deps: VideoDeps): Promise<FlowResult>;
294
294
  export declare function statusFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
295
295
  export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
296
- export declare function approveFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
296
+ export declare function approveFlow(runId: string, opts: {
297
+ json: boolean;
298
+ approveStaleCut: boolean;
299
+ }, deps: VideoDeps): Promise<FlowResult>;
297
300
  export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
298
301
  export declare function retryFrameFlow(runId: string, nodeId: string, sceneIndex: number, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
299
302
  export type ClipRedoPlan = {
@@ -62,12 +62,14 @@ The whole loop, in order:
62
62
  Attaches your cut to the run and prints the page to approve it on.
63
63
 
64
64
  11. exodus video approve <runId>
65
- Or click Approve on the page from step 10.
65
+ Or click Approve on the page from step 10. If you redid a clip after
66
+ uploading, this stops and says which scenes changed. Upload a fresh cut,
67
+ or add --approve-stale-cut to deliver the cut you already uploaded.
66
68
 
67
69
  Usage:
68
70
  exodus video status <runId> [--json]
69
71
  exodus video storyboard <runId> [--json]
70
- exodus video approve <runId> [--json]
72
+ exodus video approve <runId> [--approve-stale-cut] [--json]
71
73
  exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
72
74
  exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--json]
73
75
  exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json]
@@ -88,6 +90,9 @@ Options:
88
90
  (retry-frame, retry-clip, revoice)
89
91
  --all Every finished clip that kept the video model's voice
90
92
  (revoice)
93
+ --approve-stale-cut Deliver the cut you uploaded even though a clip changed
94
+ after you uploaded it (approve). Without it, approving
95
+ stops and names the scenes that changed
91
96
  --set <who>=<id> Give a character an ElevenLabs voice (voices). Name the
92
97
  character by its ID or by the name your script uses.
93
98
  Repeat it once per character
@@ -956,6 +961,11 @@ function itemWord(item) {
956
961
  return "flagged";
957
962
  return ITEM_STATUS_WORD[item.status] ?? item.status;
958
963
  }
964
+ const CARRYING_ON_LINE = "Waiting for a worker to carry on. Everything finished so far is kept — the run picks up from there by itself.";
965
+ function carryingOn(run) {
966
+ return (run.status === "queued" &&
967
+ run.nodes.some((n) => n.status !== "idle" && n.status !== "out-of-scope"));
968
+ }
959
969
  export async function statusFlow(runId, json, deps) {
960
970
  const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
961
971
  if (!runRes.ok)
@@ -1007,6 +1017,7 @@ export async function statusFlow(runId, json, deps) {
1007
1017
  : `Ad run ${runId} — ${displayRunStatus(run.status)}`;
1008
1018
  const lines = [
1009
1019
  headline,
1020
+ ...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
1010
1021
  ...guidance,
1011
1022
  ...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
1012
1023
  ...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
@@ -1136,14 +1147,30 @@ function sceneCardLines(scene) {
1136
1147
  lines.push(` picture: ${scene.frame.status}`);
1137
1148
  return lines;
1138
1149
  }
1139
- export async function approveFlow(runId, json, deps) {
1140
- const res = await deps.post(APPROVE_PATH, { runId });
1141
- if (!res.ok)
1142
- return errorResult(res, json);
1143
- if (json)
1144
- return { code: 0, lines: [JSON.stringify({ ok: true, runId, data: res.data })] };
1145
- const voices = res.data
1146
- ?.voices;
1150
+ export async function approveFlow(runId, opts, deps) {
1151
+ const res = await deps.post(APPROVE_PATH, {
1152
+ runId,
1153
+ ...(opts.approveStaleCut ? { approveStaleCut: true } : {}),
1154
+ });
1155
+ if (!res.ok) {
1156
+ const refusal = errorResult(res, opts.json);
1157
+ const staleCut = !opts.json && videoApiError(res).includes("approve this cut anyway");
1158
+ return staleCut
1159
+ ? {
1160
+ ...refusal,
1161
+ lines: [
1162
+ ...refusal.lines,
1163
+ `To deliver it anyway: exodus video approve ${runId} --approve-stale-cut`,
1164
+ ],
1165
+ }
1166
+ : refusal;
1167
+ }
1168
+ const data = res.data;
1169
+ const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
1170
+ if (opts.json) {
1171
+ return { code: 0, lines: [JSON.stringify({ ok: true, runId, warnings, data: res.data })] };
1172
+ }
1173
+ const voices = data?.voices;
1147
1174
  const summary = voices
1148
1175
  ? voiceSummaryLines(Array.isArray(voices.cast) ? voices.cast : [], Array.isArray(voices.notices) ? voices.notices : [])
1149
1176
  : [];
@@ -1151,6 +1178,7 @@ export async function approveFlow(runId, json, deps) {
1151
1178
  code: 0,
1152
1179
  lines: [
1153
1180
  "Approved.",
1181
+ ...warnings,
1154
1182
  ...(summary.length > 0 ? ["", ...summary, ""] : []),
1155
1183
  `See what happens next: exodus video status ${runId}`,
1156
1184
  ],
@@ -1208,8 +1236,9 @@ export async function retryFrameFlow(runId, nodeId, sceneIndex, note, json, deps
1208
1236
  ],
1209
1237
  };
1210
1238
  }
1211
- const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip. Pull the pieces again, " +
1212
- "re-cut, and upload again.";
1239
+ const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip. If this redo replaces the clip, " +
1240
+ "exodus video approve stops until you pull the pieces again, re-cut and upload again, " +
1241
+ "or pass --approve-stale-cut to deliver the cut you uploaded as it is.";
1213
1242
  export function planClipRedo(run, items, target) {
1214
1243
  const stop = classifyRun(run);
1215
1244
  const found = findClipRow(run, items, target);
@@ -2028,8 +2057,9 @@ export async function run(flags, occurrences) {
2028
2057
  return printResult(await statusFlow(runId, json, defaultDeps));
2029
2058
  if (sub === "storyboard")
2030
2059
  return printResult(await storyboardFlow(runId, json, defaultDeps));
2031
- if (sub === "approve")
2032
- return printResult(await approveFlow(runId, json, defaultDeps));
2060
+ if (sub === "approve") {
2061
+ return printResult(await approveFlow(runId, { json, approveStaleCut: flags["approve-stale-cut"] === true }, defaultDeps));
2062
+ }
2033
2063
  if (sub === "flag") {
2034
2064
  const note = flagString(flags, "note");
2035
2065
  if (!note)
@@ -34,14 +34,60 @@ export interface WorkflowEdge {
34
34
  targetHandle: string;
35
35
  }
36
36
  export type WorkflowTriggerEvent = "winner-promoted";
37
+ export interface ImageRigLineOverride {
38
+ count?: number;
39
+ aspects?: string[];
40
+ adType?: string;
41
+ style?: string;
42
+ memeFormat?: string;
43
+ steering?: string;
44
+ copyPin?: string;
45
+ }
46
+ export interface ImageRigPlanOverride {
47
+ lines?: unknown[];
48
+ steering?: string;
49
+ defaultAspects?: string[];
50
+ defaultStyle?: string;
51
+ model?: string;
52
+ confirmLargeRun?: boolean;
53
+ }
54
+ export interface ImageRigNodeOverride {
55
+ plan?: ImageRigPlanOverride;
56
+ lines?: Record<string, ImageRigLineOverride>;
57
+ confirmLargeRun?: boolean;
58
+ model?: string;
59
+ }
60
+ export type ImageRigOverrides = Record<string, ImageRigNodeOverride>;
61
+ export interface VoiceRequest {
62
+ who: string;
63
+ choice: {
64
+ kind: "pin";
65
+ voiceId: string;
66
+ label?: string;
67
+ } | {
68
+ kind: "clear";
69
+ };
70
+ }
71
+ export type TriggerVoiceMap = Record<string, string | {
72
+ voiceId: string;
73
+ label?: string;
74
+ } | null>;
37
75
  export type WorkflowTrigger = {
38
76
  type: "event";
39
77
  event: WorkflowTriggerEvent;
40
78
  enabled: boolean;
79
+ imageRigOverrides?: ImageRigOverrides;
80
+ voices?: VoiceRequest[];
41
81
  } | {
42
82
  type: "cron";
43
83
  cron: string;
44
84
  enabled: boolean;
85
+ imageRigOverrides?: ImageRigOverrides;
86
+ voices?: VoiceRequest[];
87
+ };
88
+ type WithoutVoices<T> = T extends unknown ? Omit<T, "voices"> : never;
89
+ export type WorkflowContractTrigger = WithoutVoices<WorkflowTrigger> & {
90
+ voices?: TriggerVoiceMap;
45
91
  };
46
92
  export interface WorkflowContractJson {
47
93
  contract: "exodus-workflow";
@@ -51,7 +97,7 @@ export interface WorkflowContractJson {
51
97
  name: string;
52
98
  description?: string;
53
99
  slots?: WorkflowSlot[];
54
- triggers?: WorkflowTrigger[];
100
+ triggers?: WorkflowContractTrigger[];
55
101
  nodes: WorkflowNode[];
56
102
  edges: WorkflowEdge[];
57
103
  }
@@ -530,12 +576,13 @@ export declare function parseVersionFlag(flags: Record<string, string | boolean>
530
576
  export declare function versionsFlow(workflowRef: string, opts: {
531
577
  json: boolean;
532
578
  }, deps: WorkflowRunDeps, channel?: Channel): Promise<FlowResult>;
533
- export declare function triggerExpect(t: WorkflowTrigger): {
579
+ type AnyTrigger = WorkflowTrigger | WorkflowContractTrigger;
580
+ export declare function triggerExpect(t: AnyTrigger): {
534
581
  type: string;
535
582
  event?: string;
536
583
  cron?: string;
537
584
  };
538
- export declare function formatTriggers(triggers: WorkflowTrigger[]): string;
585
+ export declare function formatTriggers(triggers: AnyTrigger[]): string;
539
586
  export declare function triggersListFlow(workflowRef: string, opts: {
540
587
  json: boolean;
541
588
  }, deps: WorkflowRunDeps): Promise<FlowResult>;
@@ -546,6 +593,7 @@ export declare function triggersFireFlow(workflowRef: string, opts: {
546
593
  n?: number;
547
594
  text?: string;
548
595
  imageRigOverrides?: Record<string, unknown>;
596
+ voices?: VoiceMap;
549
597
  wait: boolean;
550
598
  json: boolean;
551
599
  onProgressLine?: (line: string) => void;
@@ -32,7 +32,7 @@ Usage:
32
32
  exodus workflow triggers <workflowId|name> [--json]
33
33
  exodus workflow triggers <workflowId|name> enable <n> [--json]
34
34
  exodus workflow triggers <workflowId|name> disable <n> [--json]
35
- exodus workflow triggers <workflowId|name> fire [<n>] [--text "..."] [--rig-overrides <json|@file>] [--wait] [--json]
35
+ exodus workflow triggers <workflowId|name> fire [<n>] [--text "..."] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--wait] [--json]
36
36
  exodus workflow inbox [--json]
37
37
  exodus workflow checkpoint <runId> [show] [--json]
38
38
  exodus workflow checkpoint <runId> edit <n> [--text "..." | --file <path> | (stdin)] [--json]
@@ -84,9 +84,9 @@ Flags:
84
84
  before anything runs, and the error names it. On
85
85
  "triggers fire" it REPLACES the schedule's own
86
86
  overrides for that one test fire.
87
- --voices <json> (run) Give the ad's characters their voices at the
88
- moment the run starts, instead of at the storyboard
89
- review. Keyed by the names the SCRIPT uses for its
87
+ --voices <json> (run, triggers fire) Give the ad's characters their
88
+ voices at the moment the run starts, instead of at the
89
+ storyboard review. Keyed by the names the SCRIPT uses for its
90
90
  speakers ("HOST 1") — the planner's character ids
91
91
  (C1, C2) do not exist yet when a run starts. Takes a
92
92
  JSON object, or @path to a .json file holding one:
@@ -103,6 +103,14 @@ Flags:
103
103
  <run>" at the storyboard review instead. Voice ids
104
104
  belong to the brand's own folder — never put them in a
105
105
  shared workflow or template.
106
+ A schedule can carry the same map in its YAML
107
+ ("voices:" beside the schedule), so a weekly ad arrives
108
+ already voiced. On "triggers fire" this flag REPLACES
109
+ the schedule's own voices for that one test fire. A
110
+ schedule whose voice has been deleted, or whose
111
+ ElevenLabs key is missing or refused, does not start at
112
+ all: it leaves a failed run saying which speaker and
113
+ what to fix, and spends nothing.
106
114
  --voice-treatment <v> (run) Say HOW this run makes its voices. Two kinds of
107
115
  answer. A name on its own picks a way of working that
108
116
  uses voices you already have:
@@ -279,7 +287,9 @@ Notes:
279
287
  panel in the app); anything you name that isn't there stops the launch and the
280
288
  message says which key was wrong. A trigger can carry the same payload in its
281
289
  YAML ("imageRigOverrides:" beside its schedule), so a Monday schedule and a
282
- Friday one can fire the same workflow at different sizes.
290
+ Friday one can fire the same workflow at different sizes. A trigger can carry
291
+ "voices:" the same way — the same map "workflow run --voices" takes — so a run
292
+ that starts on a schedule arrives at the storyboard already voiced.
283
293
  "workflow run --voices" hands a video run its voices at the start, so a brand
284
294
  that always uses the same hosts stops choosing them by hand on every run. The
285
295
  file is keyed by the names the script uses for its speakers, because that is
@@ -2450,6 +2460,7 @@ export async function triggersFireFlow(workflowRef, opts, deps) {
2450
2460
  ...(opts.imageRigOverrides
2451
2461
  ? { imageRigOverrides: opts.imageRigOverrides }
2452
2462
  : {}),
2463
+ ...(opts.voices ? { voices: opts.voices } : {}),
2453
2464
  });
2454
2465
  if (!res.ok)
2455
2466
  return triggerErrorResult(res, verb, opts.json);
@@ -3313,8 +3324,10 @@ export async function run(flags, occurrences) {
3313
3324
  }
3314
3325
  }
3315
3326
  let fireOverrides;
3327
+ let fireVoices;
3316
3328
  try {
3317
3329
  fireOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
3330
+ fireVoices = parseVoicesFlag(occurrences, defaultDeps.readFile);
3318
3331
  }
3319
3332
  catch (e) {
3320
3333
  console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
@@ -3324,6 +3337,7 @@ export async function run(flags, occurrences) {
3324
3337
  n,
3325
3338
  text: flagString(flags, "text"),
3326
3339
  imageRigOverrides: fireOverrides,
3340
+ voices: fireVoices,
3327
3341
  wait: flags["wait"] === true,
3328
3342
  json,
3329
3343
  onProgressLine: (line) => console.log(line),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.2",
3
+ "version": "2026.9.2000-beta.3",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",