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

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.
@@ -8,10 +8,31 @@ export interface ClipWord {
8
8
  s: number;
9
9
  e: number;
10
10
  }
11
+ export type ClipQcTakeNeighbours = {
12
+ state: "not-applicable";
13
+ } | {
14
+ state: "not-attached";
15
+ } | {
16
+ state: "none-accepted";
17
+ } | {
18
+ state: "attached";
19
+ scenes: number[];
20
+ };
21
+ export interface ClipQcTake {
22
+ kind: "take" | "soft-retry";
23
+ failCodes: string[];
24
+ warnCodes: string[];
25
+ neighbours: ClipQcTakeNeighbours;
26
+ judgeWording?: {
27
+ code: string;
28
+ wording: string;
29
+ }[];
30
+ }
11
31
  export interface ClipQc {
12
32
  verdict: "pass" | "fail";
13
33
  attempts: number;
14
34
  neighbours?: number[];
35
+ takes?: ClipQcTake[];
15
36
  }
16
37
  export interface ClipFinding {
17
38
  check: string;
@@ -21,6 +42,9 @@ export interface ClipFinding {
21
42
  judgeDetail?: string;
22
43
  judgeSeverity?: "fail" | "warn";
23
44
  }
45
+ export declare function qcWithoutJudgeWording(qc: ClipQc): ClipQc;
46
+ export declare const NO_TAKE_HISTORY_LINE = "no history recorded for this clip";
47
+ export declare function renderQcTakeHistory(takes: ClipQcTake[] | undefined): string[];
24
48
  export type ArtifactSubset = {
25
49
  type: "storyboard";
26
50
  storyboard?: unknown;
@@ -277,7 +301,7 @@ export interface VideoManifest {
277
301
  failed: PullFailure[];
278
302
  provenance?: RunProvenance;
279
303
  }
280
- export declare const CAST_LEDGER_BASE = 910000;
304
+ export declare const CAST_LEDGER_BASE: number;
281
305
  export interface PullPlan {
282
306
  downloads: PullDownload[];
283
307
  texts: PullTextFile[];
@@ -6,6 +6,7 @@ import { displayRunStatus, formatApiError } from "../lib/format.js";
6
6
  import { missingRouteLine } from "../lib/route-support.js";
7
7
  import { hasBinary } from "../lib/preflight.js";
8
8
  import { ASSET_UPLOAD_POLICY, pauseAheadLine, } from "./workflow.js";
9
+ import { HELPER_LEDGER_BASE } from "../lib/helperLedgerBase.js";
9
10
  export const helpText = `
10
11
  exodus video — make a video ad from a saved workflow, pull every piece, upload your cut
11
12
 
@@ -149,6 +150,40 @@ const ASSETS_PATH = "/api/v2/workflows/assets";
149
150
  function withoutJudgeFields({ judgeDetail: _d, judgeSeverity: _s, ...rest }) {
150
151
  return rest;
151
152
  }
153
+ export function qcWithoutJudgeWording(qc) {
154
+ if (!qc.takes)
155
+ return qc;
156
+ return {
157
+ ...qc,
158
+ takes: qc.takes.map(({ judgeWording: _w, ...rest }) => rest),
159
+ };
160
+ }
161
+ export const NO_TAKE_HISTORY_LINE = "no history recorded for this clip";
162
+ const TAKE_NEIGHBOUR_SENTENCE = {
163
+ "not-applicable": () => "never compared",
164
+ "not-attached": () => "no neighbours attached",
165
+ "none-accepted": () => "no accepted neighbours yet",
166
+ attached: ({ scenes }) => `compared against scene${scenes.length === 1 ? "" : "s"} ${scenes.join(", ")}`,
167
+ };
168
+ export function renderQcTakeHistory(takes) {
169
+ if (!takes || takes.length === 0)
170
+ return [NO_TAKE_HISTORY_LINE];
171
+ const lines = [];
172
+ takes.forEach((take, index) => {
173
+ const say = TAKE_NEIGHBOUR_SENTENCE[take.neighbours.state];
174
+ const parts = [
175
+ take.failCodes.length ? `failed ${take.failCodes.join(", ")}` : "passed",
176
+ ...(take.warnCodes.length ? [`warned ${take.warnCodes.join(", ")}`] : []),
177
+ say(take.neighbours),
178
+ ];
179
+ const bonus = take.kind === "soft-retry" ? " (bonus take)" : "";
180
+ lines.push(`take ${index + 1}${bonus}: ${parts.join("; ")}`);
181
+ for (const { code, wording } of take.judgeWording ?? []) {
182
+ lines.push(` checker said (${code}): ${wording}`);
183
+ }
184
+ });
185
+ return lines;
186
+ }
152
187
  export function asVideoRun(data) {
153
188
  const run = (data ?? {});
154
189
  return { ...run, nodes: Array.isArray(run.nodes) ? run.nodes : [] };
@@ -363,6 +398,13 @@ export function stopLines(stop, runId, runUrl) {
363
398
  }
364
399
  if (stop.at === "failed") {
365
400
  if (stop.repair) {
401
+ if (!stop.step) {
402
+ return [
403
+ "This run stopped at a step that can't be picked back up from here.",
404
+ "Start a fresh run to get this ad made.",
405
+ `Open the run: ${runUrl}`,
406
+ ];
407
+ }
366
408
  return [
367
409
  `${stepName(stop.step)} failed${stop.error ? `: ${stop.error}` : "."}`,
368
410
  `Try that step again: exodus workflow repair ${runId} retry`,
@@ -412,7 +454,10 @@ export function planFailureLines(node, runId) {
412
454
  }
413
455
  return lines;
414
456
  }
415
- export const CAST_LEDGER_BASE = 910000;
457
+ export const CAST_LEDGER_BASE = HELPER_LEDGER_BASE + 10000;
458
+ function isPullSceneIndex(sceneIndex) {
459
+ return sceneIndex >= 0 && sceneIndex < HELPER_LEDGER_BASE;
460
+ }
416
461
  const FALLBACK_EXT = {
417
462
  image: "png",
418
463
  video: "mp4",
@@ -429,6 +474,10 @@ function extFor(url, family) {
429
474
  export function scenePrefix(sceneIndex) {
430
475
  return `scene-${String(sceneIndex).padStart(2, "0")}`;
431
476
  }
477
+ function clipQcOf(item) {
478
+ const artifact = item?.artifact;
479
+ return artifact && artifact.type === "video" ? artifact.qc?.takes : undefined;
480
+ }
432
481
  function outputsOfNodeKind(run, kind) {
433
482
  return run.nodes.filter((n) => n.kind === kind).flatMap((n) => n.outputs ?? []);
434
483
  }
@@ -611,6 +660,8 @@ export function planPull(run, items, opts) {
611
660
  for (const frame of artifact.frames ?? []) {
612
661
  if (!frame.imageUrl)
613
662
  continue;
663
+ if (!isPullSceneIndex(frame.sceneIndex))
664
+ continue;
614
665
  keyframeByScene.set(frame.sceneIndex, keyframeDownload(frame.sceneIndex, frame.imageUrl));
615
666
  }
616
667
  }
@@ -628,7 +679,7 @@ export function planPull(run, items, opts) {
628
679
  };
629
680
  continue;
630
681
  }
631
- if (typeof artifact.sceneIndex !== "number" || artifact.sceneIndex < 0)
682
+ if (typeof artifact.sceneIndex !== "number" || !isPullSceneIndex(artifact.sceneIndex))
632
683
  continue;
633
684
  voiceByScene.set(artifact.sceneIndex, {
634
685
  download: {
@@ -669,6 +720,8 @@ export function planPull(run, items, opts) {
669
720
  }
670
721
  if (artifact.type !== "video" || typeof artifact.sceneIndex !== "number")
671
722
  continue;
723
+ if (!isPullSceneIndex(artifact.sceneIndex))
724
+ continue;
672
725
  const clip = clipFromArtifact(artifact.sceneIndex, artifact);
673
726
  if (!clip)
674
727
  continue;
@@ -678,6 +731,8 @@ export function planPull(run, items, opts) {
678
731
  const frameItemByScene = new Map();
679
732
  const sceneFrameNodeIds = new Set(run.nodes.filter((n) => n.kind === "scene-frames").map((n) => n.nodeId));
680
733
  for (const item of items) {
734
+ if (!isPullSceneIndex(item.sceneIndex))
735
+ continue;
681
736
  if (item.itemKind === "clip")
682
737
  clipItemByScene.set(item.sceneIndex, item);
683
738
  if (item.itemKind === "frame" && sceneFrameNodeIds.has(item.nodeId)) {
@@ -709,7 +764,7 @@ export function planPull(run, items, opts) {
709
764
  ...frameItemByScene.keys(),
710
765
  ]),
711
766
  ]
712
- .filter((sceneIndex) => sceneIndex >= 0 && sceneIndex < CAST_LEDGER_BASE)
767
+ .filter((sceneIndex) => isPullSceneIndex(sceneIndex))
713
768
  .sort((a, b) => a - b);
714
769
  const scenes = sceneIndexes.map((sceneIndex) => {
715
770
  const clip = clipByScene.get(sceneIndex);
@@ -744,7 +799,7 @@ export function planPull(run, items, opts) {
744
799
  : "this-file",
745
800
  voice: voice?.download.file ?? null,
746
801
  keyframe: keyframeByScene.get(sceneIndex)?.file ?? null,
747
- qc: clip?.qc ?? null,
802
+ qc: clip?.qc ? qcWithoutJudgeWording(clip.qc) : null,
748
803
  revoiced: clip ? clip.revoiced : null,
749
804
  speechTrimmed: clip ? clip.speechTrimmed : null,
750
805
  rawStorageId: clip ? clip.rawStorageId : null,
@@ -1090,6 +1145,11 @@ export async function statusFlow(runId, json, deps) {
1090
1145
  lines.push(`${String(sceneIndex).padEnd(5)} ${itemWord(row.clip).padEnd(8)} ${itemWord(row.voiceover).padEnd(8)} ${itemWord(row.frame)}`);
1091
1146
  if (row.clip?.lastRedo)
1092
1147
  lines.push(` clip: ${row.clip.lastRedo.label}`);
1148
+ if (row.clip?.lastRedo || row.clip?.flagged) {
1149
+ for (const line of renderQcTakeHistory(clipQcOf(row.clip))) {
1150
+ lines.push(` ${line}`);
1151
+ }
1152
+ }
1093
1153
  if (row.voiceover?.lastRedo)
1094
1154
  lines.push(` voice: ${row.voiceover.lastRedo.label}`);
1095
1155
  if (row.frame?.lastRedo)
@@ -0,0 +1 @@
1
+ export declare const HELPER_LEDGER_BASE = 900000;
@@ -0,0 +1 @@
1
+ export const HELPER_LEDGER_BASE = 900000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.5",
3
+ "version": "2026.9.2000-beta.6",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",