@aicopycoders/exodus 2026.10.1701-beta.1 → 2026.10.1900-beta.1

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.
@@ -101,7 +101,7 @@ never how it is made. Every answer to them says `"view": "customer"`, and the
101
101
  CLI words it the way the run page does. On their own brands they can:
102
102
 
103
103
  - `video start --script <file> --conceit <key> --style <slug>` with
104
- `--direction`, `--frame`, `--pacing`, `--music`, `--one-shot`, `--review-storyboard`, `--stop-at-script` and `--wait`. Any other start
104
+ `--direction`, `--frame`, `--pacing`, `--music`, `--one-shot`, `--review-storyboard`, `--stop-at-script`, `--rewrite-to-fit` and `--wait`. Any other start
105
105
  flag is refused with one sentence naming it. The answer is the run id and the
106
106
  link to the video's page, plus what the server did to the script: laid a
107
107
  plain transcript out, or wrote a script from a description (see "The script
@@ -158,7 +158,7 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
158
158
  exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
159
159
  same, with the video model speaking voices you WROTE
160
160
  exodus video status <runId> where it is, per scene
161
- exodus video start --script <file> --conceit <kind> --style <look> [--direction "…"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--one-shot] [--director] [--stop-at-script] [--wait]
161
+ exodus video start --script <file> --conceit <kind> --style <look> [--direction "…"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--one-shot] [--director] [--stop-at-script] [--rewrite-to-fit] [--wait]
162
162
  start from a script and the kinds the server offers.
163
163
  The file can be a plain transcript, a draft or a
164
164
  description: the server tidies it (see "The script
@@ -397,6 +397,16 @@ What the CLI prints, and what you tell the user:
397
397
  one-person kind of ad): the notes come first, then the refusal, and `--json`
398
398
  carries the tidied script under `script`. Fix that script and send it. It is
399
399
  already in the format, so it is not tidied or paid for again.
400
+ - **The script doesn't fit the kind of ad** (too many speakers, or a rule the
401
+ kind of ad has): the refusal says `--rewrite-to-fit` can fix it, and
402
+ `--json` carries `rewriteOffered: true`. Ask the user before you add the
403
+ flag: the server then writes a new script for that kind of ad from theirs
404
+ (the dashboard's "Rewrite to fit"), which can change every line and the
405
+ names, starts the run on it, and prints it (`rewritten` in `--json`). Show
406
+ the user that script. To let them read it first, add `--stop-at-script` too.
407
+ If the rewrite can't be made, nothing starts and the error says why
408
+ (`SCRIPT_NOT_REWRITTEN`). The other way out is a kind of ad made for the
409
+ script's cast.
400
410
 
401
411
  **`--stop-at-script`** prints the script the run would start on and starts
402
412
  nothing (exit 0). A question or a description too thin to write from answers
@@ -57,6 +57,7 @@ export interface CustomerStart {
57
57
  dashboardUrl: string;
58
58
  tidied?: ScriptTidied;
59
59
  written?: string;
60
+ rewritten?: string;
60
61
  }
61
62
  export interface CustomerStoryboard {
62
63
  view: "customer";
@@ -457,6 +457,7 @@ export interface ScriptStartOptions {
457
457
  director?: true;
458
458
  shots?: RunShots;
459
459
  stopAtScript?: true;
460
+ rewriteToFit?: true;
460
461
  wait: boolean;
461
462
  json: boolean;
462
463
  onProgressLine?: (line: string) => void;
@@ -168,6 +168,13 @@ Options:
168
168
  --stop-at-script Print the script the run would start on, and start
169
169
  nothing (start, with --conceit). To read a written or
170
170
  laid-out script first; send it back with --script
171
+ --rewrite-to-fit If the script doesn't fit the kind of ad (more speakers
172
+ than it takes, or a rule it has), write a new script for
173
+ it from yours, print it, and start on it (start, with
174
+ --conceit). That can change every line and the names.
175
+ A script that fits starts as it is. With
176
+ --stop-at-script, it prints the new script and starts
177
+ nothing
171
178
  --conceit <kind> Kind of ad (start)
172
179
  --style <look> Look (start)
173
180
  The server lists the kinds and looks it offers. An unknown word prints them.
@@ -207,7 +214,7 @@ Options:
207
214
  Who can use what. Admins get every command here. Beta testers with video
208
215
  switched on get these, on their own brands: start (--script, --conceit,
209
216
  --style, --direction, --frame, --pacing, --music, --one-shot,
210
- --review-storyboard, --stop-at-script),
217
+ --review-storyboard, --stop-at-script, --rewrite-to-fit),
211
218
  status, storyboard, approve (the storyboard), voices, retry-frame --scene <n> (redo one scene's
212
219
  picture while the storyboard waits), pull (the finished video, each scene's
213
220
  clip and voice, and the music) and upload. These stay admin-only:
@@ -1457,6 +1464,20 @@ export function planVideoStart(flags, occurrences) {
1457
1464
  line: "--stop-at-script works with --conceit, not --show. A Show's script is read as you wrote it.",
1458
1465
  };
1459
1466
  }
1467
+ const refitFlags = occurrences.filter((o) => o.flag === "rewrite-to-fit");
1468
+ if (refitFlags.some((o) => o.value !== undefined && !o.value.startsWith("--"))) {
1469
+ return {
1470
+ kind: "usage",
1471
+ line: "--rewrite-to-fit takes no value. Leave it off to start the script as you wrote it.",
1472
+ };
1473
+ }
1474
+ const rewriteToFit = refitFlags.length > 0;
1475
+ if (showId && rewriteToFit) {
1476
+ return {
1477
+ kind: "usage",
1478
+ line: "--rewrite-to-fit works with --conceit, not --show. A Show's script is read as you wrote it.",
1479
+ };
1480
+ }
1460
1481
  if (showId && director) {
1461
1482
  return {
1462
1483
  kind: "usage",
@@ -1531,6 +1552,8 @@ export function planVideoStart(flags, occurrences) {
1531
1552
  opts.director = true;
1532
1553
  if (stopAtScript)
1533
1554
  opts.stopAtScript = true;
1555
+ if (rewriteToFit)
1556
+ opts.rewriteToFit = true;
1534
1557
  if (frameFlag.value || pacingFlag.value) {
1535
1558
  opts.shots = {
1536
1559
  ...(frameFlag.value ? { frame: frameFlag.value } : {}),
@@ -1644,6 +1667,20 @@ function scriptWrittenHead(written, started) {
1644
1667
  function writtenOf(value) {
1645
1668
  return typeof value === "string" && value.trim() !== "" ? value : null;
1646
1669
  }
1670
+ function scriptRewrittenHead(rewritten, started) {
1671
+ if (rewritten === null)
1672
+ return [];
1673
+ return [
1674
+ started
1675
+ ? "Your script didn't fit this kind of ad, so we wrote a new one from it and started the run on it:"
1676
+ : "Your script didn't fit this kind of ad, so we wrote a new one from it:",
1677
+ "",
1678
+ ...rewritten.split("\n").map((line) => (line ? ` ${line}` : "")),
1679
+ "",
1680
+ ];
1681
+ }
1682
+ const REWRITE_OFFER = "To have a new script written from yours for this kind of ad, run the same command with --rewrite-to-fit. " +
1683
+ "That can change every line and the names.";
1647
1684
  function scriptNeedsCallerResult(res, json) {
1648
1685
  const data = res.data;
1649
1686
  if (!isRecord(data) || !isRecord(data.error))
@@ -1667,7 +1704,9 @@ function scriptStartRefused(res, json) {
1667
1704
  const data = isRecord(res.data) ? res.data : {};
1668
1705
  const tidied = data.tidied === undefined ? null : scriptTidiedOf(data.tidied);
1669
1706
  const written = writtenOf(data.written);
1670
- if (!tidied && written === null)
1707
+ const rewritten = writtenOf(data.rewritten);
1708
+ const offered = data.rewriteOffered === true;
1709
+ if (!tidied && written === null && rewritten === null && !offered)
1671
1710
  return errorResult(res, json);
1672
1711
  const script = typeof data.script === "string" ? data.script : null;
1673
1712
  const error = videoApiError(res);
@@ -1681,16 +1720,25 @@ function scriptStartRefused(res, json) {
1681
1720
  error,
1682
1721
  ...(tidied ? { tidied } : {}),
1683
1722
  ...(written !== null ? { written } : {}),
1723
+ ...(rewritten !== null ? { rewritten } : {}),
1684
1724
  ...(script !== null ? { script } : {}),
1725
+ ...(offered ? { rewriteOffered: true } : {}),
1685
1726
  }),
1686
1727
  ]
1687
- : [...scriptTidiedHead(tidied), ...scriptWrittenHead(written, false), error],
1728
+ : [
1729
+ ...scriptTidiedHead(tidied),
1730
+ ...scriptWrittenHead(written, false),
1731
+ ...scriptRewrittenHead(rewritten, false),
1732
+ error,
1733
+ ...(offered ? [REWRITE_OFFER] : []),
1734
+ ],
1688
1735
  };
1689
1736
  }
1690
1737
  function stoppedAtScriptResult(data, json) {
1691
1738
  const script = typeof data.script === "string" ? data.script : "";
1692
1739
  const tidied = data.tidied === undefined ? null : scriptTidiedOf(data.tidied);
1693
1740
  const written = writtenOf(data.written);
1741
+ const rewritten = writtenOf(data.rewritten);
1694
1742
  if (json) {
1695
1743
  return {
1696
1744
  code: 0,
@@ -1700,6 +1748,7 @@ function stoppedAtScriptResult(data, json) {
1700
1748
  script,
1701
1749
  ...(tidied ? { tidied } : {}),
1702
1750
  ...(written !== null ? { written } : {}),
1751
+ ...(rewritten !== null ? { rewritten } : {}),
1703
1752
  }),
1704
1753
  ],
1705
1754
  };
@@ -1708,9 +1757,11 @@ function stoppedAtScriptResult(data, json) {
1708
1757
  code: 0,
1709
1758
  lines: [
1710
1759
  ...scriptTidiedHead(tidied),
1711
- written !== null
1712
- ? "Your file described the ad, so we wrote this script from it. Nothing has started."
1713
- : "This is the script the run would start on. Nothing has started.",
1760
+ rewritten !== null
1761
+ ? "Your script didn't fit this kind of ad, so we wrote a new one from it. Nothing has started."
1762
+ : written !== null
1763
+ ? "Your file described the ad, so we wrote this script from it. Nothing has started."
1764
+ : "This is the script the run would start on. Nothing has started.",
1714
1765
  "",
1715
1766
  ...script.split("\n").map((line) => (line ? ` ${line}` : "")),
1716
1767
  "",
@@ -1792,6 +1843,7 @@ export async function startScriptFlow(opts, deps) {
1792
1843
  ...(opts.director ? { director: true } : {}),
1793
1844
  ...(opts.shots ? { shots: opts.shots } : {}),
1794
1845
  ...(opts.stopAtScript ? { stopAtScript: true } : {}),
1846
+ ...(opts.rewriteToFit ? { rewriteToFit: true } : {}),
1795
1847
  });
1796
1848
  if (!res.ok)
1797
1849
  return scriptNeedsCallerResult(res, opts.json) ?? scriptStartRefused(res, opts.json);
@@ -1811,13 +1863,19 @@ export async function startScriptFlow(opts, deps) {
1811
1863
  const video = startedVideoChoice(started.videoChoice);
1812
1864
  const tidied = started.tidied === undefined ? null : scriptTidiedOf(started.tidied);
1813
1865
  const written = writtenOf(started.written);
1866
+ const rewritten = writtenOf(started.rewritten);
1814
1867
  const facts = {
1815
1868
  card,
1816
1869
  ...(video ? { videoChoice: video } : {}),
1817
1870
  ...(tidied ? { tidied } : {}),
1818
1871
  ...(written !== null ? { written } : {}),
1872
+ ...(rewritten !== null ? { rewritten } : {}),
1819
1873
  };
1820
- const head = [...scriptTidiedHead(tidied), ...scriptWrittenHead(written, true)];
1874
+ const head = [
1875
+ ...scriptTidiedHead(tidied),
1876
+ ...scriptWrittenHead(written, true),
1877
+ ...scriptRewrittenHead(rewritten, true),
1878
+ ];
1821
1879
  if (!opts.wait) {
1822
1880
  return {
1823
1881
  code: 0,
@@ -1844,16 +1902,22 @@ async function customerStartedResult(started, opts, deps) {
1844
1902
  const { runId, dashboardUrl } = started;
1845
1903
  const tidied = started.tidied === undefined ? null : scriptTidiedOf(started.tidied);
1846
1904
  const written = writtenOf(started.written);
1905
+ const rewritten = writtenOf(started.rewritten);
1847
1906
  const head = [
1848
1907
  ...scriptTidiedHead(tidied),
1849
1908
  ...scriptWrittenHead(written, true),
1909
+ ...scriptRewrittenHead(rewritten, true),
1850
1910
  `Started video ${runId}`,
1851
1911
  `Watch it: ${dashboardUrl}`,
1852
1912
  ];
1853
1913
  if (opts.wait) {
1854
1914
  const waited = await startedThenWait(runId, head, { ...opts, url: dashboardUrl }, deps);
1855
- const facts = { ...(tidied ? { tidied } : {}), ...(written !== null ? { written } : {}) };
1856
- return opts.json && (tidied || written !== null) ? waitJsonWith(waited, facts) : waited;
1915
+ const facts = {
1916
+ ...(tidied ? { tidied } : {}),
1917
+ ...(written !== null ? { written } : {}),
1918
+ ...(rewritten !== null ? { rewritten } : {}),
1919
+ };
1920
+ return opts.json && (tidied || written !== null || rewritten !== null) ? waitJsonWith(waited, facts) : waited;
1857
1921
  }
1858
1922
  if (opts.json)
1859
1923
  return { code: 0, lines: [JSON.stringify(started)] };
@@ -2045,7 +2109,7 @@ export async function statusFlow(runId, json, deps) {
2045
2109
  if (item.itemKind !== "clip" && item.itemKind !== "voiceover" && item.itemKind !== "frame") {
2046
2110
  continue;
2047
2111
  }
2048
- if (item.sceneIndex < 0)
2112
+ if (!isPullSceneIndex(item.sceneIndex))
2049
2113
  continue;
2050
2114
  if (item.itemKind === "frame" && !sceneFrameNodeIds.has(item.nodeId))
2051
2115
  continue;
@@ -14,7 +14,7 @@ export declare function isActiveRunStatus(raw: string | null | undefined): boole
14
14
  export declare const WORKFLOW_RUN_STATUS_VALUES: readonly ["queued", "running", "awaiting-approval", "succeeded", "succeeded-with-warnings", "failed", "cancelled"];
15
15
  export declare const LEGACY_WORKFLOW_RUN_STATUS_VALUES: readonly ["awaiting-review", "completed", "partial", "canceled"];
16
16
  export declare function storedWorkflowStatusForms(status: RunStatus): string[];
17
- export declare function workflowRunPresentation(rawStatus: string | null | undefined, pauseReason?: string | null): {
17
+ export declare function workflowRunPresentation(rawStatus: string | null | undefined, pauseReason?: string | null, workerStopped?: boolean): {
18
18
  status: RunStatus;
19
19
  label: string;
20
20
  tone: RunStatusTone;
@@ -115,7 +115,7 @@ export function storedWorkflowStatusForms(status) {
115
115
  const twin = LEGACY_WORKFLOW_TWIN[status];
116
116
  return twin ? [status, twin] : [status];
117
117
  }
118
- export function workflowRunPresentation(rawStatus, pauseReason) {
118
+ export function workflowRunPresentation(rawStatus, pauseReason, workerStopped) {
119
119
  let status = normalizeRunStatus(rawStatus);
120
120
  let detail;
121
121
  if (status === "awaiting-approval") {
@@ -127,7 +127,7 @@ export function workflowRunPresentation(rawStatus, pauseReason) {
127
127
  detail = "needs inputs";
128
128
  }
129
129
  else if (pauseReason === "repair") {
130
- detail = "needs repair";
130
+ detail = workerStopped ? "worker stopped" : "needs repair";
131
131
  }
132
132
  }
133
133
  return {
@@ -8,6 +8,7 @@ export interface RunVerdictDelivery {
8
8
  export interface RunVerdictInput {
9
9
  status?: string | null;
10
10
  pauseReason?: string | null;
11
+ error?: string | null;
11
12
  counts?: {
12
13
  done?: number;
13
14
  failed?: number;
@@ -1,4 +1,4 @@
1
- import { RUN_STATUS_LABELS, workflowRunPresentation } from "./runStatus.js";
1
+ import { RUN_STATUS_LABELS, isWorkerStoppedPark, workflowRunPresentation, } from "./runStatus.js";
2
2
  export function summarizeDeliveries(deliveries) {
3
3
  if (deliveries === undefined || deliveries === null)
4
4
  return undefined;
@@ -8,7 +8,7 @@ export function summarizeDeliveries(deliveries) {
8
8
  };
9
9
  }
10
10
  export function runVerdict(run) {
11
- const presented = workflowRunPresentation(run.status, run.pauseReason);
11
+ const presented = workflowRunPresentation(run.status, run.pauseReason, isWorkerStoppedPark(run));
12
12
  const delivery = summarizeDeliveries(run.deliveries) ?? run.deliverySummary ?? undefined;
13
13
  if ((presented.status === "succeeded" || presented.status === "succeeded-with-warnings") &&
14
14
  (run.counts?.failed ?? 0) > 0 &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.10.1701-beta.1",
3
+ "version": "2026.10.1900-beta.1",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",