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

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.
@@ -152,7 +152,8 @@ run options:
152
152
  --mode auto|manual|hybrid Generation mode (default: auto)
153
153
  --render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
154
154
  --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
155
- --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
155
+ --model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
156
+ Kie.ai model (default: gpt-image-2.5)
156
157
  --realism off|realistic Realism enforcement (default: off)
157
158
  --quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
158
159
  --pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
@@ -244,6 +244,7 @@ internals into chat.
244
244
  - The rig's child image runs are still `creativeSuiteRuns` with engine `meme`,
245
245
  so finished memes appear in the creative-suite library alongside other engines
246
246
  and `exodus browse --agent meme` still covers them.
247
- - `run` accepts only the two models the rig renders (`gpt-image-2`,
248
- `nano-banana-pro`, or their Kie slugs); anything else is a 400 listing the
249
- accepted values. It used to accept any value and silently ignore it.
247
+ - `run` accepts only the models the rig renders (`gpt-image-2.5`, the house
248
+ default, plus `gpt-image-2.5-sunburst`, `gpt-image-2` and `nano-banana-pro`,
249
+ or their Kie slugs); anything else is a 400 listing the accepted values. It
250
+ used to accept any value and silently ignore it.
@@ -17,7 +17,8 @@ run options:
17
17
  --mode auto|manual|hybrid Generation mode (default: auto)
18
18
  --render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
19
19
  --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
20
- --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
20
+ --model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
21
+ Kie.ai model (default: gpt-image-2.5)
21
22
  --realism off|realistic Realism enforcement (default: off)
22
23
  --quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
23
24
  --pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
@@ -115,6 +115,11 @@ under it:
115
115
  `exodus workflow checkpoint <runId>` is the verb for that one.
116
116
  - failed: `status` shows how far it got, and a `--wait` that was running exits
117
117
  1 with the error. Tell the user; do not start another run on your own.
118
+ When the storyboard is the step that failed, `status` adds a "What happened
119
+ to the storyboard" block: which parts of the plan were accepted, which part
120
+ was turned down, and why (#1902). Relay it as written. On an admin key it may
121
+ also offer `status <runId> --rejected-draft`. That prints the draft the system
122
+ threw away. It is a record only: never build on it, edit it, or re-run it.
118
123
  - finished: pull what it made.
119
124
 
120
125
  **`--wait` stops at both video parks** (#1818). `workflow run --wait` ends at
@@ -184,7 +184,7 @@ async function runTemplate(text, flags, who) {
184
184
  renderMode: "images",
185
185
  inputAds: text,
186
186
  aspectRatio: aspect,
187
- model: "gpt-image-2",
187
+ model: "gpt-image-2.5",
188
188
  realismMode: realisticFlag ? "realistic" : "off",
189
189
  ...(steering ? { steering } : {}),
190
190
  ...(requestedImageCount !== undefined ? { requestedImageCount } : {}),
@@ -17,7 +17,8 @@ Run options:
17
17
  --mode auto|manual Generation mode (default: auto)
18
18
  --render-mode images|prompts Skip render and return prompts only (default: images)
19
19
  --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
20
- --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
20
+ --model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
21
+ Kie.ai model (default: gpt-image-2.5)
21
22
  --realism off|realistic Realism enforcement (default: off)
22
23
  --quantities <slug:N,slug:N> Manual-mode per-type counts (e.g. "testimonial:3,hero:2")
23
24
  --requested-count N Auto-mode total render target (optional)
@@ -103,9 +104,15 @@ async function runTemplate(flags) {
103
104
  process.exit(1);
104
105
  return;
105
106
  }
106
- const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2";
107
- if (model !== "gpt-image-2" && model !== "nano-banana-pro") {
108
- console.error(`Error: --model must be gpt-image-2 | nano-banana-pro (got "${model}").`);
107
+ const TEMPLATE_MODELS = [
108
+ "gpt-image-2.5",
109
+ "gpt-image-2.5-sunburst",
110
+ "gpt-image-2",
111
+ "nano-banana-pro",
112
+ ];
113
+ const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2.5";
114
+ if (!TEMPLATE_MODELS.includes(model)) {
115
+ console.error(`Error: --model must be one of ${TEMPLATE_MODELS.join(" | ")} (got "${model}").`);
109
116
  process.exit(1);
110
117
  return;
111
118
  }
@@ -69,12 +69,23 @@ export type ArtifactSubset = {
69
69
  } | {
70
70
  type: "text" | "primer" | "session" | "document";
71
71
  };
72
+ export interface PlanFailureRecord {
73
+ kind: string;
74
+ partsTotal?: number;
75
+ acceptedParts?: number;
76
+ acceptedScenes?: number;
77
+ failedPart?: number;
78
+ reasons?: string[];
79
+ lines: string[];
80
+ }
72
81
  export interface VideoRunNode {
73
82
  nodeId: string;
74
83
  kind: string;
75
84
  status: "idle" | "running" | "done" | "failed" | "skipped" | "out-of-scope";
76
85
  error?: string;
77
86
  warning?: string;
87
+ planFailure?: PlanFailureRecord;
88
+ hasRejectedDraft?: boolean;
78
89
  outputs?: ArtifactSubset[];
79
90
  }
80
91
  export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
@@ -173,6 +184,7 @@ export type RunStop = {
173
184
  at: "storyboard-gate";
174
185
  nodeId?: string;
175
186
  framesNodeId?: string;
187
+ showAd?: true;
176
188
  } | {
177
189
  at: "final-watch";
178
190
  } | {
@@ -201,8 +213,12 @@ export declare function resolveStop(stop: RunStop, cutAttached: boolean | null):
201
213
  export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
202
214
  export declare function stageWord(stage: string): string;
203
215
  export declare function stepName(kind: string | undefined): string;
216
+ export declare function isShowAd(run: Pick<VideoRun, "moduleOwned">): boolean;
204
217
  export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
205
218
  export declare function stopLines(stop: ResolvedStop, runId: string, runUrl: string): string[];
219
+ export declare function failedStoryboardNode(run: VideoRun): VideoRunNode | undefined;
220
+ export declare function errorEchoesReasons(error: string | undefined, record: PlanFailureRecord | undefined): boolean;
221
+ export declare function planFailureLines(node: VideoRunNode | undefined, runId: string): string[];
206
222
  export interface PullDownload {
207
223
  file: string;
208
224
  url: string;
@@ -292,6 +308,7 @@ export declare function waitFlow(runId: string, opts: {
292
308
  maxPolls?: number;
293
309
  }, deps: VideoDeps): Promise<FlowResult>;
294
310
  export declare function statusFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
311
+ export declare function rejectedDraftFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
295
312
  export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
296
313
  export declare function approveFlow(runId: string, opts: {
297
314
  json: boolean;
@@ -41,7 +41,13 @@ The whole loop, in order:
41
41
  Redo ONE still at the pixel gate. Neighbours stay. The gate holds.
42
42
 
43
43
  7. exodus video status <runId>
44
- Where the run is and how each scene's clip turned out.
44
+ Where the run is and how each scene's clip turned out. If the storyboard
45
+ failed, it also says which parts of it were accepted and why the next one
46
+ was turned down.
47
+
48
+ exodus video status <runId> --rejected-draft
49
+ Prints the storyboard draft that was turned down. A record of what
50
+ happened, thrown away by the system — never something to act on.
45
51
 
46
52
  8. exodus video pull <runId> --out ./ad
47
53
  Writes every piece to that folder plus a manifest.json index.
@@ -67,7 +73,7 @@ The whole loop, in order:
67
73
  or add --approve-stale-cut to deliver the cut you already uploaded.
68
74
 
69
75
  Usage:
70
- exodus video status <runId> [--json]
76
+ exodus video status <runId> [--rejected-draft] [--json]
71
77
  exodus video storyboard <runId> [--json]
72
78
  exodus video approve <runId> [--approve-stale-cut] [--json]
73
79
  exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
@@ -99,6 +105,8 @@ Options:
99
105
  --clear <who> Take a character's voice off again (voices). Repeatable
100
106
  --from <file.json> A file of characters and voice IDs (voices). --set and
101
107
  --clear win over the same name in the file
108
+ --rejected-draft Print the storyboard draft the system turned down
109
+ (status). It was thrown away — a record only
102
110
  --json Machine-readable output
103
111
  --help, -h Print this help
104
112
 
@@ -124,6 +132,9 @@ const SHOWS_PATH = "/api/v2/shows";
124
132
  const RUNS_PATH = "/api/v2/video/runs";
125
133
  const RUN_PATH = "/api/v2/workflow";
126
134
  const ITEMS_PATH = "/api/v2/workflow/items";
135
+ const REJECTED_DRAFT_PATH = "/api/v2/workflow/rejected-draft";
136
+ const REJECTED_DRAFT_NOT_ON_THIS_SERVER = "This Exodus server does not keep rejected drafts yet, so there is nothing to read. It arrives " +
137
+ "with the next server update.";
127
138
  const STORYBOARD_PATH = "/api/v2/video/storyboard";
128
139
  const FLAG_PATH = "/api/v2/video/storyboard/flag";
129
140
  const APPROVE_PATH = "/api/v2/workflow/approve";
@@ -250,6 +261,7 @@ export function classifyRun(run) {
250
261
  ...(pausedNode.kind === "scene-frames"
251
262
  ? { framesNodeId: pausedNode.nodeId }
252
263
  : {}),
264
+ ...(isShowAd(run) ? { showAd: true } : {}),
253
265
  };
254
266
  }
255
267
  if (parkedAtFinalWatch(run))
@@ -300,8 +312,11 @@ export function stepName(kind) {
300
312
  return "A step in this run";
301
313
  return STEP_NAMES[kind] ?? `The ${kind} step`;
302
314
  }
315
+ export function isShowAd(run) {
316
+ return run.moduleOwned === true;
317
+ }
303
318
  export function reviewUrl(dashboardUrl, run) {
304
- if (run.moduleOwned === true)
319
+ if (isShowAd(run))
305
320
  return `${dashboardUrl}/video?ad=${run._id}`;
306
321
  if (run.workflowId)
307
322
  return `${dashboardUrl}/workflows/${run.workflowId}/runs/${run._id}`;
@@ -313,8 +328,10 @@ export function stopLines(stop, runId, runUrl) {
313
328
  "Parked: the storyboard is waiting for your yes.",
314
329
  `Read it: exodus video storyboard ${runId}`,
315
330
  `Approve it: exodus video approve ${runId}`,
316
- `Send back: exodus video flag ${runId} --note "what's wrong"`,
317
331
  ];
332
+ if (stop.showAd) {
333
+ lines.push(`Send back: exodus video flag ${runId} --note "what's wrong"`);
334
+ }
318
335
  if (stop.framesNodeId) {
319
336
  lines.push(`Redo one frame: exodus video retry-frame ${runId} --node ${stop.framesNodeId} --scene <n>`);
320
337
  }
@@ -371,6 +388,30 @@ export function stopLines(stop, runId, runUrl) {
371
388
  }
372
389
  return [`Working: ${stageWord(stop.stage)}.`];
373
390
  }
391
+ export function failedStoryboardNode(run) {
392
+ return run.nodes.find((n) => n.kind === "storyboard" && n.status === "failed");
393
+ }
394
+ export function errorEchoesReasons(error, record) {
395
+ if (!error || record?.kind !== "rejected")
396
+ return false;
397
+ const reasons = record.reasons ?? [];
398
+ return reasons.length > 0 && reasons.every((reason) => error.includes(reason));
399
+ }
400
+ export function planFailureLines(node, runId) {
401
+ const record = node?.planFailure;
402
+ if (!record || !Array.isArray(record.lines) || !node)
403
+ return [];
404
+ const reasons = record.kind === "rejected" ? (record.reasons ?? []) : [];
405
+ const bodyCount = Math.max(0, record.lines.length - reasons.length);
406
+ const lines = [`What happened to the storyboard step (${node.nodeId}):`];
407
+ record.lines.forEach((line, i) => {
408
+ lines.push(i < bodyCount ? ` ${line}` : ` - ${line}`);
409
+ });
410
+ if (node?.hasRejectedDraft === true) {
411
+ lines.push(" A rejected draft is on file. The system threw it away, so it is a record of what happened and nothing to act on.", ` Read it: exodus video status ${runId} --rejected-draft`);
412
+ }
413
+ return lines;
414
+ }
374
415
  export const CAST_LEDGER_BASE = 910000;
375
416
  const FALLBACK_EXT = {
376
417
  image: "png",
@@ -977,7 +1018,10 @@ export async function statusFlow(runId, json, deps) {
977
1018
  const items = itemsRes.data.items ?? [];
978
1019
  const hasFinal = hasAttachedCut(items);
979
1020
  const stop = resolveStop(classifyRun(run), hasFinal);
980
- const guidance = stopLines(stop, runId, reviewUrl(deps.dashboardUrl, run));
1021
+ const failedStoryboard = failedStoryboardNode(run);
1022
+ const guidance = stopLines(stop.at === "failed" && errorEchoesReasons(stop.error, failedStoryboard?.planFailure)
1023
+ ? { ...stop, error: undefined }
1024
+ : stop, runId, reviewUrl(deps.dashboardUrl, run));
981
1025
  const warnings = run.nodes
982
1026
  .filter((n) => n.warning)
983
1027
  .map((n) => ({ nodeId: n.nodeId, step: n.kind, warning: n.warning }));
@@ -985,7 +1029,20 @@ export async function statusFlow(runId, json, deps) {
985
1029
  return {
986
1030
  code: 0,
987
1031
  lines: [
988
- JSON.stringify({ runId, status: run.status, stop, items, hasFinal, warnings, guidance }),
1032
+ JSON.stringify({
1033
+ runId,
1034
+ status: run.status,
1035
+ stop,
1036
+ items,
1037
+ hasFinal,
1038
+ warnings,
1039
+ guidance,
1040
+ ...(failedStoryboard?.planFailure ? { planFailure: failedStoryboard.planFailure } : {}),
1041
+ ...(failedStoryboard?.planFailure
1042
+ ? { storyboardNodeId: failedStoryboard.nodeId }
1043
+ : {}),
1044
+ ...(failedStoryboard?.hasRejectedDraft === true ? { hasRejectedDraft: true } : {}),
1045
+ }),
989
1046
  ],
990
1047
  };
991
1048
  }
@@ -1020,6 +1077,7 @@ export async function statusFlow(runId, json, deps) {
1020
1077
  ...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
1021
1078
  ...guidance,
1022
1079
  ...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
1080
+ ...planFailureLines(failedStoryboard, runId),
1023
1081
  ...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
1024
1082
  ];
1025
1083
  if (byScene.size === 0) {
@@ -1068,6 +1126,45 @@ export async function statusFlow(runId, json, deps) {
1068
1126
  : "Final cut: not uploaded yet.");
1069
1127
  return { code: 0, lines };
1070
1128
  }
1129
+ export async function rejectedDraftFlow(runId, json, deps) {
1130
+ const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
1131
+ if (!runRes.ok)
1132
+ return errorResult(runRes, json);
1133
+ const node = failedStoryboardNode(asVideoRun(runRes.data));
1134
+ if (!node) {
1135
+ const note = "The storyboard on this run didn't fail, so there is no rejected draft to read.";
1136
+ return {
1137
+ code: 0,
1138
+ lines: json ? [JSON.stringify({ runId, nodeId: null, draft: null })] : [note],
1139
+ };
1140
+ }
1141
+ const res = await deps.get(`${REJECTED_DRAFT_PATH}?runId=${encodeURIComponent(runId)}&nodeId=${encodeURIComponent(node.nodeId)}`);
1142
+ if (missingRouteLine(res, "exodus video status --rejected-draft")) {
1143
+ return {
1144
+ code: 1,
1145
+ lines: json
1146
+ ? [JSON.stringify({ ok: false, status: 404, error: REJECTED_DRAFT_NOT_ON_THIS_SERVER })]
1147
+ : [REJECTED_DRAFT_NOT_ON_THIS_SERVER],
1148
+ };
1149
+ }
1150
+ if (!res.ok)
1151
+ return errorResult(res, json);
1152
+ const draft = res.data.draft ?? null;
1153
+ if (json)
1154
+ return { code: 0, lines: [JSON.stringify({ runId, nodeId: node.nodeId, draft })] };
1155
+ if (!draft)
1156
+ return { code: 0, lines: ["No rejected draft was kept for this attempt."] };
1157
+ return {
1158
+ code: 0,
1159
+ lines: [
1160
+ `REJECTED DRAFT — ad run ${runId}, step ${node.nodeId}`,
1161
+ "The system turned this draft down and threw it away. It is kept only as a record of what happened.",
1162
+ "Nothing in it is part of your ad. There is nothing here to act on or edit.",
1163
+ "",
1164
+ ...draft.split("\n"),
1165
+ ],
1166
+ };
1167
+ }
1071
1168
  export async function storyboardFlow(runId, json, deps) {
1072
1169
  const res = await deps.get(`${STORYBOARD_PATH}?runId=${encodeURIComponent(runId)}`);
1073
1170
  if (!res.ok)
@@ -1119,7 +1216,14 @@ export async function storyboardFlow(runId, json, deps) {
1119
1216
  }
1120
1217
  }
1121
1218
  }
1122
- lines.push("", `approve with: exodus video approve ${runId}`, `send it back: exodus video flag ${runId} --note "what's wrong"`);
1219
+ const runRes = await deps
1220
+ .get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`)
1221
+ .catch(() => null);
1222
+ const showAd = runRes?.ok === true && isShowAd(asVideoRun(runRes.data));
1223
+ lines.push("", `approve with: exodus video approve ${runId}`);
1224
+ if (showAd) {
1225
+ lines.push(`send it back: exodus video flag ${runId} --note "what's wrong"`);
1226
+ }
1123
1227
  if (cards.framesNodeId) {
1124
1228
  lines.push(`redo one frame: exodus video retry-frame ${runId} --node ${cards.framesNodeId} --scene <n>`);
1125
1229
  }
@@ -2053,8 +2157,12 @@ export async function run(flags, occurrences) {
2053
2157
  if (needsRunId.includes(sub) && !runId) {
2054
2158
  usage(`video ${sub} needs a run id: exodus video ${sub} <runId>`);
2055
2159
  }
2056
- if (sub === "status")
2160
+ if (sub === "status") {
2161
+ if (flags["rejected-draft"] !== undefined && flags["rejected-draft"] !== false) {
2162
+ return printResult(await rejectedDraftFlow(runId, json, defaultDeps));
2163
+ }
2057
2164
  return printResult(await statusFlow(runId, json, defaultDeps));
2165
+ }
2058
2166
  if (sub === "storyboard")
2059
2167
  return printResult(await storyboardFlow(runId, json, defaultDeps));
2060
2168
  if (sub === "approve") {
@@ -10,7 +10,7 @@ import { runDisplayName, runHeadingWord, runTypeWord } from "../lib/runNames.js"
10
10
  import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
11
11
  import { missingRouteLine } from "../lib/route-support.js";
12
12
  import { getChannel } from "../lib/channel.js";
13
- import { asVideoRun, classifyRun, resolveStopAtPark, reviewUrl, stopLines, } from "./video.js";
13
+ import { asVideoRun, classifyRun, resolveStopAtPark, reviewUrl, scenePrefix, stopLines, } from "./video.js";
14
14
  export const helpText = `
15
15
  exodus workflow — List, describe, run, inspect, import, and export saved workflows
16
16
 
@@ -1382,6 +1382,43 @@ function planArtifactFile(artifact) {
1382
1382
  url,
1383
1383
  };
1384
1384
  }
1385
+ function artifactFileSuffixes(artifacts) {
1386
+ const sceneBearing = artifacts.some((a) => typeof a.sceneIndex === "number" || a.final === true);
1387
+ if (!sceneBearing) {
1388
+ return artifacts.map((_, i) => ({
1389
+ suffix: i === 0 ? "" : `-${i + 1}`,
1390
+ renamed: false,
1391
+ }));
1392
+ }
1393
+ const taken = new Set();
1394
+ let fullCount = 0;
1395
+ return artifacts.map((a, i) => {
1396
+ let suffix;
1397
+ if (typeof a.sceneIndex === "number") {
1398
+ suffix = `-${scenePrefix(a.sceneIndex)}`;
1399
+ }
1400
+ else if (a.final === true) {
1401
+ suffix = "-final";
1402
+ }
1403
+ else if (a.type === "audio") {
1404
+ fullCount += 1;
1405
+ suffix = fullCount === 1 ? "-full" : `-full-${fullCount}`;
1406
+ }
1407
+ else {
1408
+ suffix = i === 0 ? "" : `-${i + 1}`;
1409
+ }
1410
+ if (taken.has(suffix)) {
1411
+ let fallback = `${suffix}-${i + 1}`;
1412
+ while (taken.has(fallback))
1413
+ fallback = `${fallback}-${i + 1}`;
1414
+ suffix = fallback;
1415
+ taken.add(suffix);
1416
+ return { suffix, renamed: true };
1417
+ }
1418
+ taken.add(suffix);
1419
+ return { suffix, renamed: false };
1420
+ });
1421
+ }
1385
1422
  export async function saveDeliveries(run, dir, deps) {
1386
1423
  const paths = [];
1387
1424
  if (!run.isTerminal) {
@@ -1416,9 +1453,10 @@ export async function saveDeliveries(run, dir, deps) {
1416
1453
  lines.push(` skipped ${delivery.label} (${delivery.key}) — ${why}`);
1417
1454
  continue;
1418
1455
  }
1456
+ const suffixes = artifactFileSuffixes(delivery.artifacts);
1419
1457
  for (let i = 0; i < delivery.artifacts.length; i++) {
1420
1458
  const plan = planArtifactFile(delivery.artifacts[i]);
1421
- const suffix = i === 0 ? "" : `-${i + 1}`;
1459
+ const { suffix, renamed } = suffixes[i];
1422
1460
  if (plan.kind === "none") {
1423
1461
  lines.push(` skipped ${delivery.label} (${delivery.key})${suffix} — ${plan.reason}`);
1424
1462
  continue;
@@ -1429,7 +1467,7 @@ export async function saveDeliveries(run, dir, deps) {
1429
1467
  deps.writeFile(file, plan.body);
1430
1468
  else
1431
1469
  await downloadToFile(plan.url, file);
1432
- lines.push(` wrote ${file}`);
1470
+ lines.push(` wrote ${file}${renamed ? " — renamed to avoid a collision" : ""}`);
1433
1471
  paths.push(file);
1434
1472
  }
1435
1473
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.3",
3
+ "version": "2026.9.2000-beta.5",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",