@aicopycoders/exodus 2026.9.2000-beta.20 → 2026.9.2000-beta.21

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.
@@ -32,7 +32,8 @@ workflow run`. From there the same nodes render, `video status` reads the run,
32
32
  `video pull` writes the folder, and `video upload` and `approve` finish it.
33
33
 
34
34
  A Show is no longer required. `exodus video start` starts from a script, a
35
- conceit, and a style, and the same parks follow. The full line is under
35
+ conceit, and a style. The same parks follow, except that the app approves the
36
+ storyboard itself when its checks pass (see "What costs money"). The full line is under
36
37
  "Starting a run". An optional `--direction` note is whole-ad rules, and that
37
38
  note cannot contain a colon. "The script file" shows the right form and the
38
39
  wrong one.
@@ -84,9 +85,14 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
84
85
  exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
85
86
  same, with the video model speaking voices you WROTE
86
87
  exodus video status <runId> where it is, per scene
87
- exodus video start --script <file> --conceit <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--wait]
88
+ exodus video start --script <file> --conceit <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--review-storyboard] [--wait]
88
89
  start from a script, a conceit and a look.
89
90
  No music bed unless you pass --music.
91
+ The app approves its own storyboard when its
92
+ checks pass and goes straight on to the clips,
93
+ which is the part that costs. It stops at the
94
+ gate only when a check finds a problem, and says
95
+ why. --review-storyboard always stops there.
90
96
  Each conceit has its own video model and voice;
91
97
  --video-model / --voice override them for one run
92
98
  ```
@@ -169,18 +175,31 @@ minutes with "Still running after 60 minutes" and exit 0; that is not done, so
169
175
  check `status` before treating it as finished. Do not poll `status` in a tight
170
176
  loop; a full run is several minutes.
171
177
 
172
- **What costs money.** Starting the run writes the storyboard and then draws the
173
- reference, cast and scene pictures, and only then pauses. Each picture is a few
174
- cents and each scene draws a few to choose from: about 25 cents on a small test,
175
- and it can approach a dollar on a longer ad. Approving releases the expensive
176
- part: the voices and the clips. So a run that has not paused yet is already
177
- spending a little, and a run still drawing pictures is healthy. `exodus workflow
178
- status` and `exodus video status` both print "This run will pause for your
179
- approval once the frames are ready" on a run that is headed for the gate. Do not
180
- cancel it because the pause has not arrived. `workflow list`, `workflow describe`, `video status`,
181
- `storyboard` and `pull` are free reads; run them as often as you like. Never
182
- start a second run to "retry" without the user asking; tell them what failed
183
- instead.
178
+ **What costs money.** Every run writes the storyboard first and then draws the
179
+ reference, cast and scene pictures. Each picture is a few cents and each scene
180
+ draws a few to choose from: about 25 cents on a small test, and it can approach
181
+ a dollar on a longer ad. The expensive part comes after the storyboard: the
182
+ voices and the clips. What happens at that point depends on how the run
183
+ started.
184
+
185
+ - A paste-a-script run (`exodus video start --conceit …`) checks its own
186
+ storyboard once the pictures are drawn. When every check passes, the app
187
+ approves the storyboard itself and goes straight on to the voices and the
188
+ clips, with no stop. It stops for the user only when a check finds a problem,
189
+ and the stop block says why in plain words. Started with
190
+ `--review-storyboard`, it always stops at the storyboard instead. `status`
191
+ prints "the app checks the storyboard and approves it itself if the checks
192
+ pass" on a run headed that way.
193
+ - A Show run (`video start --show`) and a saved workflow run (`workflow run`)
194
+ still pause at the storyboard for approval. Approving is what releases the
195
+ voices and the clips. `status` prints "This run will pause for your approval
196
+ once the frames are ready" on a run headed for that pause.
197
+
198
+ Either way, a run still drawing pictures is healthy and already spending a
199
+ little. Do not cancel it because nothing has happened yet. `workflow list`,
200
+ `workflow describe`, `video status`, `storyboard` and `pull` are free reads; run
201
+ them as often as you like. Never start a second run to "retry" without the user
202
+ asking; tell them what failed instead.
184
203
 
185
204
  **`--auto-approve` does not cover either video gate.** It releases Checkpoint
186
205
  boxes only (`convex/workflows.ts:7266` keys on `pauseReason === "checkpoint"`),
@@ -574,6 +593,11 @@ musicHeardScenes[] scene numbers whose clip came back with
574
593
  music the video model baked in. A run with
575
594
  the bed off can still list scenes here, and
576
595
  those are the ones to listen to
596
+ pendingRedo[] scene numbers the app is redoing right
597
+ now. Their clip and words are left out,
598
+ because the file on the run is the take
599
+ being replaced: run `exodus video wait`,
600
+ then pull again
577
601
  narration { file, timing }, or null on a per-scene run
578
602
  cast[] one per identity still
579
603
  characterId, name from the run's cast lock; null for an
@@ -118,6 +118,7 @@ export interface VideoRunNode {
118
118
  hasRejectedDraft?: boolean;
119
119
  outputs?: ArtifactSubset[];
120
120
  missingScenes?: MissingScene[];
121
+ autoRedoScenes?: number[];
121
122
  }
122
123
  export interface MissingScene {
123
124
  sceneIndex: number;
@@ -151,6 +152,11 @@ export interface VideoRun {
151
152
  provenance?: RunProvenance;
152
153
  musicBed?: boolean;
153
154
  videoChoice?: RunVideoChoice;
155
+ storyboardAutoApproval?: {
156
+ outcome: "approved" | "held";
157
+ at: number;
158
+ reason?: string;
159
+ };
154
160
  workflowId?: string;
155
161
  moduleOwned?: boolean;
156
162
  }
@@ -222,11 +228,13 @@ export declare function videoApiError(res: ApiResponse<unknown>): string;
222
228
  export type RunStop = {
223
229
  at: "running";
224
230
  stage: string;
231
+ autoRedoScenes?: number[];
225
232
  } | {
226
233
  at: "storyboard-gate";
227
234
  nodeId?: string;
228
235
  framesNodeId?: string;
229
236
  showAd?: true;
237
+ heldReason?: string;
230
238
  } | {
231
239
  at: "final-watch";
232
240
  missingScenes?: MissingScene[];
@@ -254,6 +262,7 @@ export type ResolvedStop = Exclude<RunStop, {
254
262
  });
255
263
  export declare function classifyRun(run: VideoRun): RunStop;
256
264
  export declare function hasAttachedCut(items: NodeItem[]): boolean;
265
+ export declare function scenesBeingRedone(run: VideoRun, items: NodeItem[]): number[];
257
266
  export declare function resolveStop(stop: RunStop, cutAttached: boolean | null): ResolvedStop;
258
267
  export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
259
268
  export declare function stageWord(stage: string): string;
@@ -325,6 +334,7 @@ export interface VideoManifest {
325
334
  music: string | null;
326
335
  musicBed: MusicBedState;
327
336
  musicHeardScenes: number[];
337
+ pendingRedo: number[];
328
338
  cast: ManifestCastRef[];
329
339
  narration: {
330
340
  file: string;
@@ -371,6 +381,7 @@ export interface ScriptStartOptions {
371
381
  videoModel?: string;
372
382
  voiceMode?: VoiceMode;
373
383
  music?: boolean;
384
+ reviewStoryboard?: true;
374
385
  wait: boolean;
375
386
  json: boolean;
376
387
  }
@@ -103,7 +103,7 @@ Usage:
103
103
  exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
104
104
  exodus video pull <runId> --out <dir> [--json]
105
105
  exodus video upload <runId> --file <cut.mp4> [--duration <sec>] [--json]
106
- exodus video start --script <file> --conceit <${CONCEIT_KEYS.join("|")}> --style <${STYLE_SLUGS.join("|")}> [--direction "<note>"] [--voice-path <path>] [--video-model <id>] [--voice <native|voice-first>] [--music] [--wait] [--json]
106
+ exodus video start --script <file> --conceit <${CONCEIT_KEYS.join("|")}> --style <${STYLE_SLUGS.join("|")}> [--direction "<note>"] [--voice-path <path>] [--video-model <id>] [--voice <native|voice-first>] [--music] [--review-storyboard] [--wait] [--json]
107
107
 
108
108
  Options:
109
109
  --out <dir> Folder to write the pulled pieces into (pull)
@@ -144,7 +144,11 @@ Options:
144
144
  --music Put a music bed under the ad (start). An ad has no music
145
145
  unless you ask for it here
146
146
  --no-music Leave the music bed off (start), which it already is
147
- --wait Wait until the storyboard needs a yes (start)
147
+ --review-storyboard Stop at the storyboard so you can approve it yourself
148
+ (start, with --conceit). By default the app approves its
149
+ own storyboard when its checks pass
150
+ --wait Wait until the run needs you (start): at the
151
+ storyboard, or once every piece is made
148
152
  --json Machine-readable output
149
153
  --help, -h Print this help
150
154
 
@@ -324,6 +328,14 @@ function parkedAtFinalWatch(run) {
324
328
  return (!parkedByBuilderCheckpoint(run) &&
325
329
  run.nodes.some((n) => n.kind === "video" && n.status === "done"));
326
330
  }
331
+ function heldReason(run) {
332
+ const verdict = run.storyboardAutoApproval;
333
+ if (verdict?.outcome !== "held")
334
+ return undefined;
335
+ const reason = verdict.reason?.trim() ||
336
+ "The app didn't approve this storyboard by itself because its checks found a problem.";
337
+ return `${reason} (checked ${new Date(verdict.at).toISOString()})`;
338
+ }
327
339
  export function classifyRun(run) {
328
340
  if (run.status === "failed")
329
341
  return { at: "failed", error: run.error };
@@ -343,6 +355,7 @@ export function classifyRun(run) {
343
355
  }
344
356
  const pausedNode = run.nodes.find((n) => n.nodeId === run.pausedNodeId);
345
357
  if (pausedNode && parkedAtStoryboardGate(run, pausedNode)) {
358
+ const held = heldReason(run);
346
359
  return {
347
360
  at: "storyboard-gate",
348
361
  nodeId: pausedNode.nodeId,
@@ -350,9 +363,15 @@ export function classifyRun(run) {
350
363
  ? { framesNodeId: pausedNode.nodeId }
351
364
  : {}),
352
365
  ...(isShowAd(run) ? { showAd: true } : {}),
366
+ ...(held ? { heldReason: held } : {}),
353
367
  };
354
368
  }
355
369
  if (parkedAtFinalWatch(run)) {
370
+ const autoRedoScenes = run.nodes
371
+ .filter((n) => n.kind === "video")
372
+ .flatMap((n) => n.autoRedoScenes ?? []);
373
+ if (autoRedoScenes.length > 0)
374
+ return { at: "running", stage: "video", autoRedoScenes };
356
375
  const missingScenes = run.nodes
357
376
  .filter((n) => n.kind === "video")
358
377
  .flatMap((n) => n.missingScenes ?? []);
@@ -371,6 +390,18 @@ export function classifyRun(run) {
371
390
  export function hasAttachedCut(items) {
372
391
  return items.some((i) => i.itemKind === "final" && i.status === "done");
373
392
  }
393
+ export function scenesBeingRedone(run, items) {
394
+ const scenes = new Set(run.nodes.filter((n) => n.kind === "video").flatMap((n) => n.autoRedoScenes ?? []));
395
+ for (const i of items) {
396
+ if (i.itemKind === "clip" &&
397
+ i.status === "running" &&
398
+ (i.attempt ?? 1) > 1 &&
399
+ i.staleClaim !== true) {
400
+ scenes.add(i.sceneIndex);
401
+ }
402
+ }
403
+ return [...scenes].sort((a, b) => a - b);
404
+ }
374
405
  export function resolveStop(stop, cutAttached) {
375
406
  if (stop.at !== "final-watch")
376
407
  return stop;
@@ -444,6 +475,7 @@ function missingSceneLines(missing, framesNodeId, runId, runUrl) {
444
475
  export function stopLines(stop, runId, runUrl) {
445
476
  if (stop.at === "storyboard-gate") {
446
477
  const lines = [
478
+ ...(stop.heldReason ? [stop.heldReason] : []),
447
479
  "Parked: the storyboard is waiting for your yes.",
448
480
  `Read it: exodus video storyboard ${runId}`,
449
481
  `Approve it: exodus video approve ${runId}`,
@@ -515,8 +547,15 @@ export function stopLines(stop, runId, runUrl) {
515
547
  `Open it: ${runUrl}`,
516
548
  ];
517
549
  }
550
+ if (stop.autoRedoScenes?.length)
551
+ return [autoRedoLine(stop.autoRedoScenes)];
518
552
  return [`Working: ${stageWord(stop.stage)}.`];
519
553
  }
554
+ function autoRedoLine(scenes) {
555
+ const one = scenes.length === 1;
556
+ return (`Working: the app is redoing ${scenesPhrase(scenes)} on ${one ? "its" : "their"} own because ` +
557
+ `${one ? "it" : "they"} repeated words. The ad isn't ready to watch until ${one ? "it lands" : "they land"}.`);
558
+ }
520
559
  export function failedStoryboardNode(run) {
521
560
  return run.nodes.find((n) => n.kind === "storyboard" && n.status === "failed");
522
561
  }
@@ -852,6 +891,9 @@ export function planPull(run, items, opts) {
852
891
  continue;
853
892
  keyframeByScene.set(sceneIndex, keyframeDownload(sceneIndex, artifact.imageUrl));
854
893
  }
894
+ const pendingRedo = scenesBeingRedone(run, items);
895
+ for (const sceneIndex of pendingRedo)
896
+ clipByScene.delete(sceneIndex);
855
897
  const sceneIndexes = [
856
898
  ...new Set([
857
899
  ...clipByScene.keys(),
@@ -933,6 +975,7 @@ export function planPull(run, items, opts) {
933
975
  musicHeardScenes: scenes
934
976
  .filter((scene) => scene.findings.some((f) => f.code === MUSIC_HEARD_CODE))
935
977
  .map((scene) => scene.sceneIndex),
978
+ pendingRedo,
936
979
  cast,
937
980
  narration: narrationDownload
938
981
  ? { file: narrationDownload.file, timing: "narration.json" }
@@ -1188,6 +1231,13 @@ export function planVideoStart(flags, occurrences) {
1188
1231
  return { kind: "usage", line: voiceFlag.line };
1189
1232
  const videoModel = videoModelFlag.value;
1190
1233
  const voiceMode = voiceFlag.value;
1234
+ const reviewStoryboard = occurrences.some((o) => o.flag === "review-storyboard");
1235
+ if (showId && reviewStoryboard) {
1236
+ return {
1237
+ kind: "usage",
1238
+ line: "--review-storyboard works with --conceit, not --show. A Show's ad always stops at the storyboard for you.",
1239
+ };
1240
+ }
1191
1241
  if (showId && (videoModel || voiceMode)) {
1192
1242
  return {
1193
1243
  kind: "usage",
@@ -1250,6 +1300,8 @@ export function planVideoStart(flags, occurrences) {
1250
1300
  opts.voiceMode = voiceMode;
1251
1301
  if (musicChoice.music !== undefined)
1252
1302
  opts.music = musicChoice.music;
1303
+ if (reviewStoryboard)
1304
+ opts.reviewStoryboard = true;
1253
1305
  return { kind: "script", opts };
1254
1306
  }
1255
1307
  function isRecord(value) {
@@ -1337,6 +1389,7 @@ export async function startScriptFlow(opts, deps) {
1337
1389
  ...(opts.videoModel ? { videoModel: opts.videoModel } : {}),
1338
1390
  ...(opts.voiceMode ? { voiceMode: opts.voiceMode } : {}),
1339
1391
  ...(opts.music === true ? { music: true } : {}),
1392
+ ...(opts.reviewStoryboard ? { reviewStoryboard: true } : {}),
1340
1393
  });
1341
1394
  if (!res.ok)
1342
1395
  return errorResult(res, opts.json);
@@ -1356,7 +1409,12 @@ export async function startScriptFlow(opts, deps) {
1356
1409
  : [
1357
1410
  ...scriptStartedLines(runId, url, card, video),
1358
1411
  "",
1359
- "Wait for the storyboard here instead: exodus video start … --wait",
1412
+ ...(opts.reviewStoryboard
1413
+ ? ["Wait for the storyboard here instead: exodus video start … --wait"]
1414
+ : [
1415
+ "The app approves the storyboard itself if its checks pass. It only stops for you if they find a problem.",
1416
+ "Wait here until it needs you: exodus video start … --wait",
1417
+ ]),
1360
1418
  `Or check in whenever: exodus video status ${runId}`,
1361
1419
  ],
1362
1420
  };
@@ -1375,7 +1433,8 @@ export async function waitFlow(runId, opts, deps) {
1375
1433
  const interval = opts.intervalMs ?? POLL_INTERVAL_MS;
1376
1434
  const maxPolls = opts.maxPolls ?? MAX_POLLS;
1377
1435
  const lines = [];
1378
- let lastStage = null;
1436
+ let lastProgress = null;
1437
+ let redoSeen = false;
1379
1438
  for (let poll = 0; poll < maxPolls; poll++) {
1380
1439
  if (poll > 0)
1381
1440
  await deps.sleep(interval);
@@ -1387,14 +1446,35 @@ export async function waitFlow(runId, opts, deps) {
1387
1446
  const run = asVideoRun(res.data);
1388
1447
  const stop = classifyRun(run);
1389
1448
  if (stop.at === "running") {
1390
- if (stop.stage !== lastStage) {
1391
- lastStage = stop.stage;
1449
+ const progress = stop.autoRedoScenes?.length
1450
+ ? autoRedoLine(stop.autoRedoScenes)
1451
+ : `Working: ${stageWord(stop.stage)}…`;
1452
+ if (progress !== lastProgress) {
1453
+ lastProgress = progress;
1392
1454
  if (!opts.json)
1393
- lines.push(`Working: ${stageWord(stop.stage)}…`);
1455
+ lines.push(progress);
1394
1456
  }
1395
1457
  continue;
1396
1458
  }
1397
- const resolved = await resolveStopAtPark(stop, runId, deps);
1459
+ let items = null;
1460
+ if (stop.at === "final-watch") {
1461
+ const itemsRes = await deps.get(`${ITEMS_PATH}?runId=${encodeURIComponent(runId)}`);
1462
+ items = itemsRes.ok ? (itemsRes.data.items ?? []) : null;
1463
+ if (items === null && redoSeen)
1464
+ continue;
1465
+ const redoing = items ? scenesBeingRedone(run, items) : [];
1466
+ redoSeen = redoing.length > 0;
1467
+ if (redoSeen) {
1468
+ const progress = `Working: redoing scene${redoing.length === 1 ? "" : "s"} ${redoing.join(", ")}…`;
1469
+ if (progress !== lastProgress) {
1470
+ lastProgress = progress;
1471
+ if (!opts.json)
1472
+ lines.push(progress);
1473
+ }
1474
+ continue;
1475
+ }
1476
+ }
1477
+ const resolved = resolveStop(stop, items ? hasAttachedCut(items) : null);
1398
1478
  if (opts.json) {
1399
1479
  return {
1400
1480
  code: stop.at === "failed" ? 1 : 0,
@@ -1805,7 +1885,10 @@ export function planClipRedo(run, items, target) {
1805
1885
  const { row, uploadedCut } = found;
1806
1886
  const redoable = row.status === "failed" ||
1807
1887
  row.staleClaim === true ||
1808
- (row.status === "done" && (row.flagged === true || stop.at === "final-watch"));
1888
+ (row.status === "done" &&
1889
+ (row.flagged === true ||
1890
+ stop.at === "final-watch" ||
1891
+ (stop.at === "running" && stop.autoRedoScenes !== undefined)));
1809
1892
  if (!redoable) {
1810
1893
  if (row.status === "done") {
1811
1894
  return {
@@ -2299,6 +2382,9 @@ export async function pullFlow(runId, dir, json, deps) {
2299
2382
  `Every piece is indexed in ${path.join(dir, "manifest.json")} — scene numbers there are the run's own.`,
2300
2383
  ...musicLines(plan.manifest),
2301
2384
  ];
2385
+ for (const sceneIndex of plan.manifest.pendingRedo) {
2386
+ lines.push("", `Scene ${sceneIndex} is being redone right now; its file will change. Run \`exodus video wait ${runId}\`, then pull again.`);
2387
+ }
2302
2388
  const voiceFirst = plan.manifest.scenes.filter((s) => s.voiceMode === "voice-first");
2303
2389
  if (voiceFirst.length > 0) {
2304
2390
  lines.push("", `Redone voice first (the line recorded, then the clip made to match it): ` +
@@ -460,6 +460,7 @@ export interface WorkflowRun {
460
460
  pauseAhead?: {
461
461
  nodeId: string;
462
462
  after: "frames" | "storyboard";
463
+ selfApproves?: true;
463
464
  };
464
465
  }
465
466
  export declare function pauseAheadLine(ahead: NonNullable<WorkflowRun["pauseAhead"]>): string;
@@ -378,6 +378,9 @@ export function formatRulesLine(provenance) {
378
378
  return `Format rules: ${rigName}${version}${where}.${borrowed}`;
379
379
  }
380
380
  export function pauseAheadLine(ahead) {
381
+ if (ahead.after === "frames" && ahead.selfApproves) {
382
+ return "When the frames are ready, the app checks the storyboard and approves it itself if the checks pass, then goes on to the voices and clips. It only stops for your approval if a check finds a problem. The reference, cast and scene pictures are made first (a few cents each).";
383
+ }
381
384
  return ahead.after === "frames"
382
385
  ? "This run will pause for your approval once the frames are ready. The reference, cast and scene pictures are made first (a few cents each). Nothing bigger is spent until you approve."
383
386
  : "This run will pause for your approval once the storyboard is written. Nothing is spent on pictures, voices or clips until you approve.";
@@ -2088,6 +2091,7 @@ function videoParkStop(raw) {
2088
2091
  async function waitForRun(runId, opts, deps) {
2089
2092
  const seen = new Map();
2090
2093
  let pausedNotified = false;
2094
+ let autoRedoNotified;
2091
2095
  const landOnPark = opts.landOnPark;
2092
2096
  const isLandingPark = (raw) => (landOnPark !== undefined && raw["pauseReason"] === landOnPark.pauseReason) ||
2093
2097
  videoParkStop(raw) !== undefined;
@@ -2105,7 +2109,15 @@ async function waitForRun(runId, opts, deps) {
2105
2109
  return;
2106
2110
  const parked = isParkedSnapshot(raw);
2107
2111
  const isLanding = isLandingPark(raw);
2108
- if (parked && !pausedNotified && !isLanding) {
2112
+ const autoRedo = parked ? classifyRun(asVideoRun(raw)) : undefined;
2113
+ if (autoRedo?.at === "running" && autoRedo.autoRedoScenes) {
2114
+ const line = ` ${stopLines(autoRedo, runId, "")[0]}`;
2115
+ if (line !== autoRedoNotified) {
2116
+ autoRedoNotified = line;
2117
+ opts.onProgressLine(line);
2118
+ }
2119
+ }
2120
+ else if (parked && !pausedNotified && !isLanding) {
2109
2121
  pausedNotified = true;
2110
2122
  const dashboardUrl = deps.dashboardUrl ?? getDashboardUrl();
2111
2123
  const pauseReason = raw["pauseReason"];
@@ -2113,7 +2125,9 @@ async function waitForRun(runId, opts, deps) {
2113
2125
  opts.onProgressLine(line);
2114
2126
  }
2115
2127
  }
2116
- const parkedNodeId = gateParkedNodeId(raw["status"], raw["pauseReason"], raw["pausedNodeId"]);
2128
+ const parkedNodeId = autoRedo?.at === "running"
2129
+ ? undefined
2130
+ : gateParkedNodeId(raw["status"], raw["pauseReason"], raw["pausedNodeId"]);
2117
2131
  const nodes = Array.isArray(raw["nodes"]) ? raw["nodes"] : [];
2118
2132
  for (const node of nodes) {
2119
2133
  const isParked = parkedNodeId !== undefined && node.nodeId === parkedNodeId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.20",
3
+ "version": "2026.9.2000-beta.21",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",