@aicopycoders/exodus 2026.8.1200 → 2026.8.1400

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,7 +8,7 @@ Core — discover & run:
8
8
  exodus workflow list [--json] List the brand's saved workflows
9
9
  exodus workflow describe <workflowId|name> [--json] Inputs, prerequisites, outputs
10
10
  exodus workflow bots [--category <cat>] [--slug <slug>] [--json] Bot catalog / one bot's spec
11
- exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--terminal <nodeId> ...] [--auto-approve] [--wait] [--out <dir>] [--json] Run it
11
+ exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--auto-approve] [--wait] [--out <dir>] [--json] Run it
12
12
  exodus workflow status [--id <runId>] [--out <dir>] [--json] Poll a run / read its outputs
13
13
  exodus workflow export <workflowId|name> [--version <n>] [--out <file>] [--json] Dump the contract (YAML)
14
14
 
@@ -50,8 +50,6 @@ Flag notes that add agent-level guidance (everything else is in --help):
50
50
  earlier upload is relayed as-is. Accepted: image PNG/JPEG/
51
51
  WebP/GIF ≤15MB, video MP4/MOV/WebM ≤200MB, audio MP3/M4A/
52
52
  WAV/OGG ≤50MB, document PDF/TXT/MD/DOC/DOCX ≤25MB.
53
- --terminal <nodeId> (run) Repeatable. Scope the run to the upstream closure of
54
- these end node(s); omit to run the whole graph.
55
53
  --auto-approve (run) Send this ONE run off unattended: every Checkpoint it
56
54
  reaches is approved for you, exactly as it stands, and the
57
55
  run records that nobody looked. Without it a Checkpoint
@@ -133,7 +131,7 @@ npx @aicopycoders/exodus workflow run "Product Shots" --input hero=./photos/hero
133
131
 
134
132
  A bare path is the argument (a leading `@` is accepted and ignored — a file field never text-expands). Accepted files: **image** PNG/JPEG/WebP/GIF ≤15MB · **video** MP4/MOV/WebM ≤200MB · **audio** MP3/M4A/WAV/OGG ≤50MB · **document** PDF/TXT/MD/DOC/DOCX ≤25MB. **Every file you passed is checked first — all of them — before a single byte moves**, so one bad path can't leave the others half-uploaded; you get every problem in one message and fix them in one edit. If an upload itself dies partway, the message names the asset ids that DID land so you can pass those instead of re-uploading them.
135
133
 
136
- There is no file picker and no prompt: a required file input with no `--input` fails immediately, naming the flag to pass — **unless you scoped the run with `--terminal`**, since the file's node may not even be in that slice; there the server decides. If you already uploaded the file on an earlier run, pass the asset id instead of the path and it's relayed as-is.
134
+ There is no file picker and no prompt: a required file input with no `--input` fails immediately, naming the flag to pass. If you already uploaded the file on an earlier run, pass the asset id instead of the path and it's relayed as-is.
137
135
 
138
136
  One edge case worth knowing: the CLI reads the workflow's input list before it uploads anything. If that read fails (the server is down or flaking) and any value you passed looks like a file path, the run stops right there rather than sending your path as literal text. Retry, or pass an already-uploaded asset id.
139
137
 
@@ -149,7 +147,7 @@ npx @aicopycoders/exodus workflow run "Launch Flow" --input brief="new cortisol
149
147
  npx @aicopycoders/exodus workflow status --id <runId>
150
148
  ```
151
149
 
152
- To run only part of a graph, scope it with `--terminal <nodeId>` (repeatable): only nodes feeding the picked end node(s) execute; the rest are recorded out-of-scope.
150
+ There is no way to run only part of a graph: a run executes the whole workflow or it doesn't start.
153
151
 
154
152
  **Read the `Deliveries:` block first.** Each Output node on the canvas is a named, typed promise — "Final ad copy · text", "Hero image · asset" — and a finished run answers every one of them: `delivered` with the artifacts underneath, or `unfulfilled` with the reason the upstream node gave. That block is the honest scorecard; the `Outputs:` block below it is the same artifacts as one flat pile, kept for chaining. If a run says `completed` but a slot says `unfulfilled`, the workflow did NOT deliver what it promised — say so.
155
153
 
@@ -1,6 +1,7 @@
1
1
  import { type ApiResponse } from "../lib/client.js";
2
2
  import { type PollOptions, type PollResult } from "../lib/poll.js";
3
3
  import { LEGACY_WORKFLOW_RUN_STATUS_VALUES, type RunStatus } from "../lib/runStatus.js";
4
+ import { type RunDeliverySummary } from "../lib/runVerdict.js";
4
5
  import { type Channel } from "../lib/channel.js";
5
6
  export declare const helpText: string;
6
7
  export type WorkflowNodeKind = "brief" | "brief-form" | "asset" | "bot" | "primer" | "image" | "image-rig" | "rig" | "storyboard" | "reference" | "scene-frames" | "video" | "voiceover" | "output" | "call" | "show-set" | "show-cast" | "show-voices" | "product-truth" | "transform" | "formatter" | "splitter" | "collector" | "checkpoint" | "prompt";
@@ -140,6 +141,7 @@ export interface WorkflowCatalog {
140
141
  nodeKinds: string[];
141
142
  briefSources: string[];
142
143
  primerKinds: string[];
144
+ primerAwarenessLanes: string[];
143
145
  imageModels: string[];
144
146
  aspectRatios: string[];
145
147
  imageQuantityModes: string[];
@@ -358,6 +360,7 @@ export interface WorkflowRun {
358
360
  nodes: WorkflowRunNode[];
359
361
  outputs?: WorkflowRunOutput[];
360
362
  deliveries?: WorkflowRunDelivery[];
363
+ deliverySummary?: RunDeliverySummary;
361
364
  sessions?: WorkflowRunSession[];
362
365
  pauseReason?: WorkflowPauseReason;
363
366
  pausedNodeId?: string;
@@ -400,7 +403,6 @@ export interface FlowResult {
400
403
  }
401
404
  interface RunFlowOptions {
402
405
  inputs: Record<string, string>;
403
- terminalNodeIds?: string[];
404
406
  fill?: string;
405
407
  autoApprove?: boolean;
406
408
  imageRigOverrides?: Record<string, unknown>;
@@ -421,7 +423,7 @@ export declare const ASSET_UPLOAD_POLICY: Record<WorkflowMediaType, {
421
423
  export declare const NO_DELIVERIES_WARNING: string;
422
424
  export declare function noDeliveriesWarning(described: unknown): string | undefined;
423
425
  export declare function serverWarningsToPrint(serverWarnings: unknown, alreadyPrinted: string[]): string[];
424
- export declare function parseTerminalFlags(args: string[]): string[];
426
+ export declare function rejectTerminalFlag(args: string[]): void;
425
427
  export declare function parseAutoApproveFlag(args: string[]): boolean;
426
428
  export declare function parseRigOverridesFlag(args: string[], readFile?: (path: string) => string): Record<string, unknown> | undefined;
427
429
  export declare function parseFillFlag(args: string[]): string | undefined;
@@ -4,7 +4,8 @@ import nodePath from "node:path";
4
4
  import { apiGet, apiGetText, apiPost, apiPostDashboard, getDashboardUrl, } from "../lib/client.js";
5
5
  import { formatApiError } from "../lib/format.js";
6
6
  import { pollUntilDone } from "../lib/poll.js";
7
- import { normalizeRunStatus, runStatusLabel, storedWorkflowStatusForms, workflowRunPresentation, RUN_STATUS_LABELS, TERMINAL_RUN_STATUSES, } from "../lib/runStatus.js";
7
+ import { normalizeRunStatus, storedWorkflowStatusForms, TERMINAL_RUN_STATUSES, } from "../lib/runStatus.js";
8
+ import { runVerdict } from "../lib/runVerdict.js";
8
9
  import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
9
10
  import { missingRouteLine } from "../lib/route-support.js";
10
11
  import { getChannel } from "../lib/channel.js";
@@ -18,7 +19,7 @@ Usage:
18
19
  exodus workflow templates [list] [--json]
19
20
  exodus workflow templates export <key> [--out <file>] [--json]
20
21
  exodus workflow schema [--kind <kind>] [--face <face>] [--json]
21
- exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--terminal <nodeId> ...] [--rig-overrides <json|@file>] [--auto-approve] [--wait] [--out <dir>] [--json]
22
+ exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--auto-approve] [--wait] [--out <dir>] [--json]
22
23
  exodus workflow status [--id <runId>] [--out <dir>] [--json]
23
24
  exodus workflow versions <workflowId|name> [--json]
24
25
  exodus workflow export <workflowId|name> [--version <n>] [--out <file>] [--json]
@@ -66,10 +67,6 @@ Flags:
66
67
  lock. Names are exact (case-sensitive); an unknown one
67
68
  is rejected before the run starts. Fills are created and
68
69
  named in the dashboard's run dialog.
69
- --terminal <nodeId> (run) Repeatable. Scope the run to the upstream closure
70
- of these end node(s) — only nodes feeding a picked
71
- terminal execute; the rest are recorded out-of-scope.
72
- Omit to run the whole graph.
73
70
  --rig-overrides <json> (run, triggers fire) Change what an Image Rig box fires
74
71
  for THIS ONE run, without editing the saved workflow —
75
72
  a different number of images, a different meme format,
@@ -159,7 +156,6 @@ Examples:
159
156
  exodus workflow run "Product Shots" --input hero=https://example.com/hero.png
160
157
  exodus workflow run "Launch Flow" --fill "Weekly promo" --wait
161
158
  exodus workflow run "Launch Flow" --fill "Weekly promo" --input brief="new offer"
162
- exodus workflow run "Launch Flow" --terminal bot-3 --terminal image-2
163
159
  exodus workflow run "Launch Flow" --auto-approve --wait
164
160
  exodus workflow run "Product Shots" --rig-overrides '{"rig_1":{"lines":{"line_1":{"count":3}}}}'
165
161
  exodus workflow run "Product Shots" --rig-overrides @rig.json --wait
@@ -429,17 +425,6 @@ function gateParkedNodeId(status, pauseReason, pausedNodeId) {
429
425
  }
430
426
  return undefined;
431
427
  }
432
- function runVerdict(run) {
433
- const presented = workflowRunPresentation(run.status, run.pauseReason);
434
- if ((presented.status === "succeeded" || presented.status === "succeeded-with-warnings") &&
435
- (run.counts?.failed ?? 0) > 0 &&
436
- run.deliveries !== undefined &&
437
- run.deliveries.length > 0 &&
438
- !run.deliveries.some((d) => d.status === "delivered")) {
439
- return `${RUN_STATUS_LABELS.failed} — nothing delivered`;
440
- }
441
- return presented.detail ? `${presented.label} — ${presented.detail}` : presented.label;
442
- }
443
428
  const RETIRED_GATE_VERB_POINTER = "The Gate node retired in 2.0 — runs now pause at a Checkpoint box on the canvas. " +
444
429
  "Use: exodus workflow checkpoint <runId> [approve|edit|retry|cancel]";
445
430
  export function formatPauseNotice(pauseReason, runId, dashboardUrl) {
@@ -732,7 +717,7 @@ export function serverWarningsToPrint(serverWarnings, alreadyPrinted) {
732
717
  }
733
718
  return out;
734
719
  }
735
- async function prepareRunInputs(workflowId, raw, deps, note, scoped) {
720
+ async function prepareRunInputs(workflowId, raw, deps, note) {
736
721
  const described = await deps.get(`${DESCRIBE_PATH}?id=${encodeURIComponent(workflowId)}`);
737
722
  if (!described.ok) {
738
723
  const fileish = Object.keys(raw).filter((key) => looksLikeFileArgument(raw[key], deps));
@@ -761,16 +746,14 @@ async function prepareRunInputs(workflowId, raw, deps, note, scoped) {
761
746
  if (descriptor?.type === "multi-select")
762
747
  multiFields.add(descriptor.fieldName);
763
748
  }
764
- if (!scoped) {
765
- const missing = [...assetFields.values()].filter((d) => d.required && (raw[d.fieldName] ?? "").trim() === "");
766
- if (missing.length > 0) {
767
- const names = missing.map((d) => d.fieldName).join(", ");
768
- const first = missing[0];
769
- throw new Error(`Missing required file input(s): ${names}. Pass each one as a local file ` +
770
- `or a URL — e.g. --input ${first.fieldName}=./path/to/file or ` +
771
- `--input ${first.fieldName}=https://…` +
772
- (first.assetType ? ` (${acceptedLabel(first.assetType)})` : ""));
773
- }
749
+ const missing = [...assetFields.values()].filter((d) => d.required && (raw[d.fieldName] ?? "").trim() === "");
750
+ if (missing.length > 0) {
751
+ const names = missing.map((d) => d.fieldName).join(", ");
752
+ const first = missing[0];
753
+ throw new Error(`Missing required file input(s): ${names}. Pass each one as a local file ` +
754
+ `or a URL — e.g. --input ${first.fieldName}=./path/to/file or ` +
755
+ `--input ${first.fieldName}=https://…` +
756
+ (first.assetType ? ` (${acceptedLabel(first.assetType)})` : ""));
774
757
  }
775
758
  const prepared = {};
776
759
  const planned = [];
@@ -829,29 +812,13 @@ async function prepareRunInputs(workflowId, raw, deps, note, scoped) {
829
812
  }
830
813
  return { inputs: prepared, warnings };
831
814
  }
832
- export function parseTerminalFlags(args) {
833
- const ids = [];
834
- for (let i = 0; i < args.length; i++) {
835
- const arg = args[i];
836
- let raw;
837
- if (arg === "--terminal") {
838
- raw = args[i + 1];
839
- i++;
840
- }
841
- else if (arg.startsWith("--terminal=")) {
842
- raw = arg.slice("--terminal=".length);
843
- }
844
- else {
845
- continue;
815
+ export function rejectTerminalFlag(args) {
816
+ for (const arg of args) {
817
+ if (arg === "--terminal" || arg.startsWith("--terminal=")) {
818
+ throw new Error("--terminal is no longer supported: a run now executes the whole " +
819
+ "workflow or it doesn't start. Re-run without --terminal.");
846
820
  }
847
- if (raw === undefined)
848
- throw new Error("--terminal requires a node id");
849
- const id = raw.trim();
850
- if (!id)
851
- throw new Error("--terminal requires a node id");
852
- ids.push(id);
853
821
  }
854
- return ids;
855
822
  }
856
823
  export function parseAutoApproveFlag(args) {
857
824
  for (const arg of args) {
@@ -1362,6 +1329,9 @@ function formatBotVocabulary(catalog) {
1362
1329
  lines.push(` node kinds: ${v.nodeKinds.join(", ")}`);
1363
1330
  lines.push(` brief sources: ${v.briefSources.join(", ")}`);
1364
1331
  lines.push(` primer kinds: ${v.primerKinds.join(", ")}`);
1332
+ if (v.primerAwarenessLanes?.length) {
1333
+ lines.push(` body primer awareness lanes (config.primerAwareness, body kind only): ${v.primerAwarenessLanes.join(", ")}`);
1334
+ }
1365
1335
  lines.push(` image models: ${v.imageModels.join(", ")}`);
1366
1336
  lines.push(` aspect ratios: ${v.aspectRatios.join(", ")}`);
1367
1337
  lines.push(` custom bot: set a bot node's slug to "${catalog.customBot.slug}" and ` +
@@ -1717,7 +1687,6 @@ export async function runFlow(workflowRef, opts, deps) {
1717
1687
  return resolveIdErrorResult(e, opts.json ?? false);
1718
1688
  }
1719
1689
  const preface = [];
1720
- const scoped = (opts.terminalNodeIds?.length ?? 0) > 0;
1721
1690
  let prepared;
1722
1691
  try {
1723
1692
  prepared = await prepareRunInputs(workflowId, opts.inputs, deps, (line) => {
@@ -1727,7 +1696,7 @@ export async function runFlow(workflowRef, opts, deps) {
1727
1696
  opts.onProgressLine(line);
1728
1697
  else
1729
1698
  preface.push(line);
1730
- }, scoped);
1699
+ });
1731
1700
  }
1732
1701
  catch (e) {
1733
1702
  const message = e instanceof Error ? e.message : String(e);
@@ -1749,9 +1718,6 @@ export async function runFlow(workflowRef, opts, deps) {
1749
1718
  workflowId,
1750
1719
  ...(Object.keys(inputs).length > 0 ? { inputs } : {}),
1751
1720
  ...(opts.fill && opts.fill.trim() !== "" ? { fill: opts.fill.trim() } : {}),
1752
- ...(opts.terminalNodeIds && opts.terminalNodeIds.length > 0
1753
- ? { terminalNodeIds: opts.terminalNodeIds }
1754
- : {}),
1755
1721
  ...(opts.autoApprove === true ? { autoApprove: true } : {}),
1756
1722
  ...(opts.imageRigOverrides ? { imageRigOverrides: opts.imageRigOverrides } : {}),
1757
1723
  launchedVia: "cli",
@@ -2138,7 +2104,15 @@ export async function triggersSetEnabledFlow(workflowRef, n, enabled, opts, deps
2138
2104
  return triggerErrorResult(res, verb, opts.json);
2139
2105
  if (opts.json)
2140
2106
  return { code: 0, lines: [JSON.stringify(res.data)] };
2141
- return { code: 0, lines: [`Trigger ${n} ${enabled ? "enabled" : "disabled"}.`] };
2107
+ const lines = [`Trigger ${n} ${enabled ? "enabled" : "disabled"}.`];
2108
+ const warnings = res.data?.warnings;
2109
+ if (Array.isArray(warnings)) {
2110
+ for (const w of warnings) {
2111
+ if (typeof w === "string")
2112
+ lines.push(` ⚠ ${w}`);
2113
+ }
2114
+ }
2115
+ return { code: 0, lines };
2142
2116
  }
2143
2117
  export async function triggersFireFlow(workflowRef, opts, deps) {
2144
2118
  const verb = "workflow triggers fire";
@@ -2378,7 +2352,7 @@ function asWorkflowRun(data) {
2378
2352
  }
2379
2353
  function describePark(run) {
2380
2354
  if (normalizeRunStatus(run.status) !== "awaiting-approval") {
2381
- return `status: ${runStatusLabel(run.status)}`;
2355
+ return `status: ${runVerdict(run)}`;
2382
2356
  }
2383
2357
  switch (run.pauseReason) {
2384
2358
  case "taste":
@@ -2872,13 +2846,12 @@ export async function run(flags) {
2872
2846
  process.exit(1);
2873
2847
  }
2874
2848
  let inputs;
2875
- let terminalNodeIds;
2876
2849
  let fill;
2877
2850
  let autoApprove;
2878
2851
  let imageRigOverrides;
2879
2852
  try {
2880
2853
  inputs = parseRawInputFlags(process.argv.slice(3));
2881
- terminalNodeIds = parseTerminalFlags(process.argv.slice(3));
2854
+ rejectTerminalFlag(process.argv.slice(3));
2882
2855
  fill = parseFillFlag(process.argv.slice(3));
2883
2856
  autoApprove = parseAutoApproveFlag(process.argv.slice(3));
2884
2857
  imageRigOverrides = parseRigOverridesFlag(process.argv.slice(3), defaultDeps.readFile);
@@ -2889,7 +2862,6 @@ export async function run(flags) {
2889
2862
  }
2890
2863
  return printResult(await runFlow(workflowRef, {
2891
2864
  inputs,
2892
- terminalNodeIds,
2893
2865
  fill,
2894
2866
  autoApprove,
2895
2867
  imageRigOverrides,
@@ -0,0 +1,22 @@
1
+ export interface RunDeliverySummary {
2
+ total: number;
3
+ delivered: number;
4
+ }
5
+ export interface RunVerdictDelivery {
6
+ status: string;
7
+ }
8
+ export interface RunVerdictInput {
9
+ status?: string | null;
10
+ pauseReason?: string | null;
11
+ counts?: {
12
+ done?: number;
13
+ failed?: number;
14
+ skipped?: number;
15
+ total?: number;
16
+ outOfScope?: number;
17
+ } | null;
18
+ deliveries?: readonly RunVerdictDelivery[];
19
+ deliverySummary?: RunDeliverySummary | null;
20
+ }
21
+ export declare function summarizeDeliveries(deliveries?: readonly RunVerdictDelivery[] | null): RunDeliverySummary | undefined;
22
+ export declare function runVerdict(run: RunVerdictInput): string;
@@ -0,0 +1,21 @@
1
+ import { RUN_STATUS_LABELS, workflowRunPresentation } from "./runStatus.js";
2
+ export function summarizeDeliveries(deliveries) {
3
+ if (deliveries === undefined || deliveries === null)
4
+ return undefined;
5
+ return {
6
+ total: deliveries.length,
7
+ delivered: deliveries.filter((d) => d.status === "delivered").length,
8
+ };
9
+ }
10
+ export function runVerdict(run) {
11
+ const presented = workflowRunPresentation(run.status, run.pauseReason);
12
+ const delivery = summarizeDeliveries(run.deliveries) ?? run.deliverySummary ?? undefined;
13
+ if ((presented.status === "succeeded" || presented.status === "succeeded-with-warnings") &&
14
+ (run.counts?.failed ?? 0) > 0 &&
15
+ delivery !== undefined &&
16
+ delivery.total > 0 &&
17
+ delivery.delivered === 0) {
18
+ return `${RUN_STATUS_LABELS.failed} — nothing delivered`;
19
+ }
20
+ return presented.detail ? `${presented.label} — ${presented.detail}` : presented.label;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.8.1200",
3
+ "version": "2026.8.1400",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",