@aicopycoders/exodus 2026.9.2000-beta.4 → 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.
@@ -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
@@ -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";
@@ -205,6 +216,9 @@ export declare function stepName(kind: string | undefined): string;
205
216
  export declare function isShowAd(run: Pick<VideoRun, "moduleOwned">): boolean;
206
217
  export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
207
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[];
208
222
  export interface PullDownload {
209
223
  file: string;
210
224
  url: string;
@@ -294,6 +308,7 @@ export declare function waitFlow(runId: string, opts: {
294
308
  maxPolls?: number;
295
309
  }, deps: VideoDeps): Promise<FlowResult>;
296
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>;
297
312
  export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
298
313
  export declare function approveFlow(runId: string, opts: {
299
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";
@@ -377,6 +388,30 @@ export function stopLines(stop, runId, runUrl) {
377
388
  }
378
389
  return [`Working: ${stageWord(stop.stage)}.`];
379
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
+ }
380
415
  export const CAST_LEDGER_BASE = 910000;
381
416
  const FALLBACK_EXT = {
382
417
  image: "png",
@@ -983,7 +1018,10 @@ export async function statusFlow(runId, json, deps) {
983
1018
  const items = itemsRes.data.items ?? [];
984
1019
  const hasFinal = hasAttachedCut(items);
985
1020
  const stop = resolveStop(classifyRun(run), hasFinal);
986
- 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));
987
1025
  const warnings = run.nodes
988
1026
  .filter((n) => n.warning)
989
1027
  .map((n) => ({ nodeId: n.nodeId, step: n.kind, warning: n.warning }));
@@ -991,7 +1029,20 @@ export async function statusFlow(runId, json, deps) {
991
1029
  return {
992
1030
  code: 0,
993
1031
  lines: [
994
- 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
+ }),
995
1046
  ],
996
1047
  };
997
1048
  }
@@ -1026,6 +1077,7 @@ export async function statusFlow(runId, json, deps) {
1026
1077
  ...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
1027
1078
  ...guidance,
1028
1079
  ...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
1080
+ ...planFailureLines(failedStoryboard, runId),
1029
1081
  ...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
1030
1082
  ];
1031
1083
  if (byScene.size === 0) {
@@ -1074,6 +1126,45 @@ export async function statusFlow(runId, json, deps) {
1074
1126
  : "Final cut: not uploaded yet.");
1075
1127
  return { code: 0, lines };
1076
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
+ }
1077
1168
  export async function storyboardFlow(runId, json, deps) {
1078
1169
  const res = await deps.get(`${STORYBOARD_PATH}?runId=${encodeURIComponent(runId)}`);
1079
1170
  if (!res.ok)
@@ -2066,8 +2157,12 @@ export async function run(flags, occurrences) {
2066
2157
  if (needsRunId.includes(sub) && !runId) {
2067
2158
  usage(`video ${sub} needs a run id: exodus video ${sub} <runId>`);
2068
2159
  }
2069
- 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
+ }
2070
2164
  return printResult(await statusFlow(runId, json, defaultDeps));
2165
+ }
2071
2166
  if (sub === "storyboard")
2072
2167
  return printResult(await storyboardFlow(runId, json, defaultDeps));
2073
2168
  if (sub === "approve") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.4",
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",