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

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;
@@ -132,6 +156,10 @@ export interface NodeItem {
132
156
  take: number;
133
157
  outcome: string;
134
158
  label: string;
159
+ displacedHistory?: {
160
+ title: string;
161
+ lines: string[];
162
+ };
135
163
  };
136
164
  artifact?: ArtifactSubset;
137
165
  }
@@ -277,7 +305,7 @@ export interface VideoManifest {
277
305
  failed: PullFailure[];
278
306
  provenance?: RunProvenance;
279
307
  }
280
- export declare const CAST_LEDGER_BASE = 910000;
308
+ export declare const CAST_LEDGER_BASE: number;
281
309
  export interface PullPlan {
282
310
  downloads: PullDownload[];
283
311
  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, SET_OPTION_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,48 @@ 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
+ }
187
+ function printDisplacedHistory(lines, lastRedo) {
188
+ const history = lastRedo.displacedHistory;
189
+ if (!history)
190
+ return;
191
+ lines.push(` ${history.title}`);
192
+ for (const line of history.lines)
193
+ lines.push(` ${line}`);
194
+ }
152
195
  export function asVideoRun(data) {
153
196
  const run = (data ?? {});
154
197
  return { ...run, nodes: Array.isArray(run.nodes) ? run.nodes : [] };
@@ -363,6 +406,13 @@ export function stopLines(stop, runId, runUrl) {
363
406
  }
364
407
  if (stop.at === "failed") {
365
408
  if (stop.repair) {
409
+ if (!stop.step) {
410
+ return [
411
+ "This run stopped at a step that can't be picked back up from here.",
412
+ "Start a fresh run to get this ad made.",
413
+ `Open the run: ${runUrl}`,
414
+ ];
415
+ }
366
416
  return [
367
417
  `${stepName(stop.step)} failed${stop.error ? `: ${stop.error}` : "."}`,
368
418
  `Try that step again: exodus workflow repair ${runId} retry`,
@@ -412,7 +462,10 @@ export function planFailureLines(node, runId) {
412
462
  }
413
463
  return lines;
414
464
  }
415
- export const CAST_LEDGER_BASE = 910000;
465
+ export const CAST_LEDGER_BASE = HELPER_LEDGER_BASE + 10000;
466
+ function isPullSceneIndex(sceneIndex) {
467
+ return sceneIndex >= 0 && sceneIndex < HELPER_LEDGER_BASE;
468
+ }
416
469
  const FALLBACK_EXT = {
417
470
  image: "png",
418
471
  video: "mp4",
@@ -429,6 +482,10 @@ function extFor(url, family) {
429
482
  export function scenePrefix(sceneIndex) {
430
483
  return `scene-${String(sceneIndex).padStart(2, "0")}`;
431
484
  }
485
+ function clipQcOf(item) {
486
+ const artifact = item?.artifact;
487
+ return artifact && artifact.type === "video" ? artifact.qc?.takes : undefined;
488
+ }
432
489
  function outputsOfNodeKind(run, kind) {
433
490
  return run.nodes.filter((n) => n.kind === kind).flatMap((n) => n.outputs ?? []);
434
491
  }
@@ -458,7 +515,9 @@ function keyframeDownload(sceneIndex, imageUrl) {
458
515
  function reservedCastFrames(items) {
459
516
  const byIndex = new Map();
460
517
  for (const item of items) {
461
- if (item.itemKind === "frame" && item.sceneIndex >= CAST_LEDGER_BASE) {
518
+ if (item.itemKind === "frame" &&
519
+ item.sceneIndex >= CAST_LEDGER_BASE &&
520
+ item.sceneIndex < SET_OPTION_LEDGER_BASE) {
462
521
  byIndex.set(item.sceneIndex, item);
463
522
  }
464
523
  }
@@ -611,6 +670,8 @@ export function planPull(run, items, opts) {
611
670
  for (const frame of artifact.frames ?? []) {
612
671
  if (!frame.imageUrl)
613
672
  continue;
673
+ if (!isPullSceneIndex(frame.sceneIndex))
674
+ continue;
614
675
  keyframeByScene.set(frame.sceneIndex, keyframeDownload(frame.sceneIndex, frame.imageUrl));
615
676
  }
616
677
  }
@@ -628,7 +689,7 @@ export function planPull(run, items, opts) {
628
689
  };
629
690
  continue;
630
691
  }
631
- if (typeof artifact.sceneIndex !== "number" || artifact.sceneIndex < 0)
692
+ if (typeof artifact.sceneIndex !== "number" || !isPullSceneIndex(artifact.sceneIndex))
632
693
  continue;
633
694
  voiceByScene.set(artifact.sceneIndex, {
634
695
  download: {
@@ -669,6 +730,8 @@ export function planPull(run, items, opts) {
669
730
  }
670
731
  if (artifact.type !== "video" || typeof artifact.sceneIndex !== "number")
671
732
  continue;
733
+ if (!isPullSceneIndex(artifact.sceneIndex))
734
+ continue;
672
735
  const clip = clipFromArtifact(artifact.sceneIndex, artifact);
673
736
  if (!clip)
674
737
  continue;
@@ -678,6 +741,8 @@ export function planPull(run, items, opts) {
678
741
  const frameItemByScene = new Map();
679
742
  const sceneFrameNodeIds = new Set(run.nodes.filter((n) => n.kind === "scene-frames").map((n) => n.nodeId));
680
743
  for (const item of items) {
744
+ if (!isPullSceneIndex(item.sceneIndex))
745
+ continue;
681
746
  if (item.itemKind === "clip")
682
747
  clipItemByScene.set(item.sceneIndex, item);
683
748
  if (item.itemKind === "frame" && sceneFrameNodeIds.has(item.nodeId)) {
@@ -709,7 +774,7 @@ export function planPull(run, items, opts) {
709
774
  ...frameItemByScene.keys(),
710
775
  ]),
711
776
  ]
712
- .filter((sceneIndex) => sceneIndex >= 0 && sceneIndex < CAST_LEDGER_BASE)
777
+ .filter((sceneIndex) => isPullSceneIndex(sceneIndex))
713
778
  .sort((a, b) => a - b);
714
779
  const scenes = sceneIndexes.map((sceneIndex) => {
715
780
  const clip = clipByScene.get(sceneIndex);
@@ -744,7 +809,7 @@ export function planPull(run, items, opts) {
744
809
  : "this-file",
745
810
  voice: voice?.download.file ?? null,
746
811
  keyframe: keyframeByScene.get(sceneIndex)?.file ?? null,
747
- qc: clip?.qc ?? null,
812
+ qc: clip?.qc ? qcWithoutJudgeWording(clip.qc) : null,
748
813
  revoiced: clip ? clip.revoiced : null,
749
814
  speechTrimmed: clip ? clip.speechTrimmed : null,
750
815
  rawStorageId: clip ? clip.rawStorageId : null,
@@ -1088,10 +1153,19 @@ export async function statusFlow(runId, json, deps) {
1088
1153
  for (const sceneIndex of [...byScene.keys()].sort((a, b) => a - b)) {
1089
1154
  const row = byScene.get(sceneIndex);
1090
1155
  lines.push(`${String(sceneIndex).padEnd(5)} ${itemWord(row.clip).padEnd(8)} ${itemWord(row.voiceover).padEnd(8)} ${itemWord(row.frame)}`);
1091
- if (row.clip?.lastRedo)
1156
+ if (row.clip?.lastRedo) {
1092
1157
  lines.push(` clip: ${row.clip.lastRedo.label}`);
1093
- if (row.voiceover?.lastRedo)
1158
+ printDisplacedHistory(lines, row.clip.lastRedo);
1159
+ }
1160
+ if (row.clip?.lastRedo || row.clip?.flagged) {
1161
+ for (const line of renderQcTakeHistory(clipQcOf(row.clip))) {
1162
+ lines.push(` ${line}`);
1163
+ }
1164
+ }
1165
+ if (row.voiceover?.lastRedo) {
1094
1166
  lines.push(` voice: ${row.voiceover.lastRedo.label}`);
1167
+ printDisplacedHistory(lines, row.voiceover.lastRedo);
1168
+ }
1095
1169
  if (row.frame?.lastRedo)
1096
1170
  lines.push(` picture: ${row.frame.lastRedo.label}`);
1097
1171
  for (const finding of row.clip?.findings ?? []) {
@@ -0,0 +1,3 @@
1
+ export declare const HELPER_LEDGER_BASE = 900000;
2
+ export declare const SET_OPTION_LEDGER_BASE = 920000;
3
+ export declare const CAST_OPTION_LEDGER_BASE = 960000;
@@ -0,0 +1,3 @@
1
+ export const HELPER_LEDGER_BASE = 900000;
2
+ export const SET_OPTION_LEDGER_BASE = 920000;
3
+ export const CAST_OPTION_LEDGER_BASE = 960000;
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.7",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",