@aicopycoders/exodus 2026.7.200-beta.13 → 2026.7.200-beta.14
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.
- package/assets/docs/CLAUDE.md +1 -1
- package/assets/skills/exodus/SKILL.md +1 -1
- package/assets/skills/exodus-workflow/SKILL.md +221 -31
- package/dist/commands/browse.js +2 -1
- package/dist/commands/creative.js +2 -2
- package/dist/commands/genesis.js +4 -4
- package/dist/commands/primer.js +2 -1
- package/dist/commands/status.js +4 -4
- package/dist/commands/swipe.js +4 -4
- package/dist/commands/template.js +2 -2
- package/dist/commands/winners.js +3 -3
- package/dist/commands/workflow.d.ts +83 -17
- package/dist/commands/workflow.js +892 -246
- package/dist/lib/format.d.ts +2 -0
- package/dist/lib/format.js +21 -5
- package/dist/lib/intel-format.js +2 -1
- package/dist/lib/poll.js +7 -4
- package/dist/lib/runStatus.d.ts +22 -0
- package/dist/lib/runStatus.js +139 -0
- package/package.json +1 -1
package/assets/docs/CLAUDE.md
CHANGED
|
@@ -59,7 +59,7 @@ These are the **Exodus** pipelines — the automatic system that drives the Gene
|
|
|
59
59
|
| **Creative** | `exodus-creative` | A specific creative-suite engine (native / copy-derived renders from copy, or ref-match to a reference image) when you've already picked one. Otherwise start at **Image**. |
|
|
60
60
|
| **Template** | `exodus-template` | Finished ad copy → many static ads across 50 structured formats (testimonial, hero, UGC…). Otherwise start at **Image**. |
|
|
61
61
|
| **Meme** | `exodus-meme` | Meme ads — recommends formats for a brief, then one batched run renders AI-image and classic Imgflip-template memes server-side. |
|
|
62
|
-
| **Workflow** | `exodus-workflow` | Run and build saved multi-node workflow automations from the CLI, resolve
|
|
62
|
+
| **Workflow** | `exodus-workflow` | Run and build saved multi-node workflow automations from the CLI, resolve checkpoint/repair parks from the inbox, continue bot sessions, fire triggers, and read banks or promote winners. |
|
|
63
63
|
| **Browse** | `exodus-browse` | View history and retrieve past outputs |
|
|
64
64
|
| **Drive** | `exodus-drive` | Read/write Google Docs, Sheets, and Drive files via `npx @aicopycoders/exodus drive` (uses the dashboard's Google OAuth — no local CLI) |
|
|
65
65
|
| **Winners** | `exodus-winners` | Mine your own Meta ad account for winning ads (via the Meta Ads MCP) and import them into Exodus as generative fuel — interview, visual confirmation, `npx @aicopycoders/exodus winners import` |
|
|
@@ -19,7 +19,7 @@ This skill ROUTES. Read what the user wants, name the destination skill, and han
|
|
|
19
19
|
| A specific image engine they already chose | `exodus-creative` (native / copy-derived / ref-match) or `exodus-template` (33 ad-type formats) |
|
|
20
20
|
| Meme ads ("exodus, meme this offer", "make me meme ads") | `exodus-meme` |
|
|
21
21
|
| Run / chain / build / edit a saved workflow, enable/fire its triggers, or roll back a version ("run my workflow", "what does this workflow need", "build a workflow that…", "list my workflows", "enable/fire the trigger", "roll back my workflow", "workflow versions") | `exodus-workflow` |
|
|
22
|
-
| Check the workflow inbox or review/resolve a parked run —
|
|
22
|
+
| Check the workflow inbox or review/resolve a parked run — checkpoint or repair ("check my workflow inbox", "approve that checkpoint", "approve the run", "repair that run") | `exodus-workflow` |
|
|
23
23
|
| Continue a bot chat session ("continue that session", "chat with the bot session", "list my sessions") | `exodus-workflow` |
|
|
24
24
|
| Read copy banks or promote a winner ("show my hook bank", "list my banks", "promote this winner") | `exodus-workflow` |
|
|
25
25
|
| Onboard a new brand / build the primer | `exodus-primer` (with winning ads) or `exodus-foundation` (no ads yet) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: exodus-workflow
|
|
3
|
-
description: Run, chain, build, edit, and automate saved Exodus Workflows — the multi-node automations that wire Genesis bots, primers, briefs, transforms, and image nodes together on the dashboard canvas — from the CLI. Use it to list a brand's workflows, describe what a workflow needs before running it (inputs, per-workspace primer prerequisites, outputs), run one and collect its outputs, chain one workflow's output into another's input, browse the bot catalog, author or update a workflow contract via templates → edit → validate → import, resolve runs that park (
|
|
3
|
+
description: Run, chain, build, edit, and automate saved Exodus Workflows — the multi-node automations that wire Genesis bots, primers, briefs, transforms, and image nodes together on the dashboard canvas — from the CLI. Use it to list a brand's workflows, describe what a workflow needs before running it (inputs, per-workspace primer prerequisites, outputs), run one and collect its outputs, chain one workflow's output into another's input, browse the bot catalog, author or update a workflow contract via templates → edit → validate → import, resolve runs that park (approve a step paused at a checkpoint, check the workflow inbox, repair a stalled run, answer a nested workflow's slots), continue a bot session in chat, enable/disable/fire triggers, read the brand's copy banks or promote a winner, and list versions or roll one back. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("exodus, run my launch workflow", "exodus, what does this workflow need", "exodus, build a workflow that turns a swipe into hooks then ads", "list my exodus workflows", "exodus, edit that workflow", "exodus, approve that checkpoint", "exodus, check my workflow inbox", "exodus, resolve that parked run", "exodus, continue that session", "exodus, enable/fire the trigger", "exodus, read my hooks bank", "exodus, promote this winner", "exodus, list the versions / roll it back"), named this skill or /exodus-workflow, ran an `npx @aicopycoders/exodus workflow` / `session` / `bank` command, or the `exodus` hub skill routed here. Never claim generic "workflow"/"automation"/"inbox"/"promote a winner"/"continue the session" requests ("automate this", "build me a workflow", "check my inbox") without Exodus context — in shared folders those may belong to the user's other tools; if the user did not say exodus, this skill is not for them. The bare word "Genesis" without "exodus" refers to the member's own Genesis API key and personal bot recipes, NOT to Exodus.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
```operator-guide
|
|
@@ -8,8 +8,8 @@ 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 ...] [--terminal <nodeId> ...] [--wait] [--json] Run it
|
|
12
|
-
exodus workflow status [--id <runId>] [--json]
|
|
11
|
+
exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--terminal <nodeId> ...] [--wait] [--out <dir>] [--json] Run it
|
|
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
|
|
|
15
15
|
Authoring:
|
|
@@ -21,7 +21,8 @@ Authoring:
|
|
|
21
21
|
|
|
22
22
|
Parked runs:
|
|
23
23
|
exodus workflow inbox [--json] Every run parked waiting on you
|
|
24
|
-
exodus workflow
|
|
24
|
+
exodus workflow checkpoint <runId> [show | edit <n> | approve [--reject <n,..>] [--wait] | retry [--wait] | cancel [--reason "..."]] [--json] Resolve a checkpoint park
|
|
25
|
+
exodus workflow gate <runId> RETIRED in 2.0 — prints a pointer at "checkpoint" and exits 1
|
|
25
26
|
exodus workflow repair <runId> retry|skip|kill [--wait] [--json] Resolve a repair park
|
|
26
27
|
exodus workflow answer <runId> [--slot key=value ...] [--json] Answer a nested workflow's slots (no --slot → list them)
|
|
27
28
|
|
|
@@ -40,10 +41,25 @@ Banks + versions:
|
|
|
40
41
|
Flag notes that add agent-level guidance (everything else is in --help):
|
|
41
42
|
--input key=@path Load an input's value from a file (path resolved from cwd);
|
|
42
43
|
key=@@text keeps a leading "@" as a literal character.
|
|
44
|
+
--input key=<path> (run) A FILE input — one `describe` reports with source
|
|
45
|
+
"asset" — takes a local file path instead of text: the CLI
|
|
46
|
+
uploads the file and sends the stored asset in its place
|
|
47
|
+
(--input hero=./photos/hero.png). An asset id from an
|
|
48
|
+
earlier upload is relayed as-is. Accepted: image PNG/JPEG/
|
|
49
|
+
WebP/GIF ≤15MB, video MP4/MOV/WebM ≤200MB, audio MP3/M4A/
|
|
50
|
+
WAV/OGG ≤50MB, document PDF/TXT/MD/DOC/DOCX ≤25MB.
|
|
43
51
|
--terminal <nodeId> (run) Repeatable. Scope the run to the upstream closure of
|
|
44
52
|
these end node(s); omit to run the whole graph.
|
|
53
|
+
--out <dir> (run --wait / status --id) Save the finished run's delivered
|
|
54
|
+
outputs into this directory and print the paths: text as
|
|
55
|
+
.md, storyboards/frame sets as .json, images/video/audio
|
|
56
|
+
downloaded. Unfulfilled slots are reported, not written.
|
|
45
57
|
--version <n> (export) A real 1-based id from `workflow versions`; a
|
|
46
58
|
version export carries NO triggers/description.
|
|
59
|
+
--reject <n,..> (checkpoint approve) Only when the paused step ran once
|
|
60
|
+
per item (a Splitter fan-out). Drops those item numbers —
|
|
61
|
+
the "Item N of M" headings in `checkpoint show` — and
|
|
62
|
+
approves the rest. Rejecting every item is refused.
|
|
47
63
|
|
|
48
64
|
Every workflow/session/bank verb takes --json, and that output IS the machine API —
|
|
49
65
|
there is no separate agent command set.
|
|
@@ -53,7 +69,7 @@ Auth: Bearer (EXODUS_API_KEY). The canvas lives on the dashboard at /workflows.
|
|
|
53
69
|
|
|
54
70
|
# Workflow — Run, Resolve, Build, and Automate Saved Workflows
|
|
55
71
|
|
|
56
|
-
An Exodus **Workflow** is a saved graph the member builds on the dashboard canvas (`/workflows`): brief and primer sources feed Genesis bots, bots feed transforms, other bots,
|
|
72
|
+
An Exodus **Workflow** is a saved graph the member builds on the dashboard canvas (`/workflows`): brief and primer sources feed Genesis bots, bots feed transforms, formatters, other bots, or an image node, deposits land copy in banks, and an Output node collects the deliverables. This skill is the CLI operator for those graphs — a **peer door** to the same artifact the web canvas edits. Anything you can do to a run in the browser (approve a checkpoint, repair a stall, continue a session, promote a winner), you can do here.
|
|
57
73
|
|
|
58
74
|
**The `--json` contract (read this once).** Every `workflow`, `session`, and `bank` verb takes `--json`, and that structured output **is** the machine API — there is no separate agent command set and no MCP server. Read for humans without it; parse `--json` when you need to chain or branch on the result.
|
|
59
75
|
|
|
@@ -67,9 +83,9 @@ The end-to-end narrative, every step naming its verb. You rarely walk all of it
|
|
|
67
83
|
2. **Validate** — `workflow validate <file>` until it comes back clean (it's `import --dry-run` under its own door).
|
|
68
84
|
3. **Import** — `workflow import <file>` to create, or `import <file> --update <id>` to edit in place.
|
|
69
85
|
4. **Describe** — `workflow describe` to confirm inputs, prerequisites (✓/✗ primers), and outputs before spending a run.
|
|
70
|
-
5. **Run** — `workflow run … --wait` (streams progress, prints outputs) or omit `--wait` and poll `workflow status --id <runId>`.
|
|
86
|
+
5. **Run** — `workflow run … --wait` (streams progress, prints outputs) or omit `--wait` and poll `workflow status --id <runId>`. Inputs come from `--input key=value` flags, from `--fill <name>` (a saved fill — a named, reusable set of launch answers saved from the app's Run dialog; flags win per key when you pass both), or a mix. Launches that don't satisfy the workflow's entry contract are rejected at the door with the gap named — `describe` shows each input's type, required/optional, and accepted values.
|
|
71
87
|
6. **Notice the park** — a run can stop and wait on you. There are no webhooks in v1; you notice a park one of three ways: `run --wait` prints a pause notice naming the verb to use (the command keeps waiting — resolve from another shell, or Ctrl-C and pick it up via `inbox`), `workflow inbox` lists it, or `workflow status` shows `awaiting-review`.
|
|
72
|
-
7. **Resolve** — `workflow
|
|
88
|
+
7. **Resolve** — `workflow checkpoint <runId> …` (a step flagged "pause for approval"), `workflow repair <runId> …` (a stalled collector), or `workflow answer <runId> …` (a nested workflow's slots).
|
|
73
89
|
8. **Harvest** — read deliverables with `workflow status --id <runId> --json`; read banked deposits with `bank show <key>`; keep thinking with a bot via `session chat`.
|
|
74
90
|
9. **Promote** — `bank promote <key>` lands the winner **and** fires the Winner Flywheel; then check `workflow inbox` for any background run it kicked off.
|
|
75
91
|
|
|
@@ -93,7 +109,7 @@ npx @aicopycoders/exodus workflow describe "Launch Flow"
|
|
|
93
109
|
|
|
94
110
|
Accepts a workflow id **or** a name. It reports three things:
|
|
95
111
|
|
|
96
|
-
- **Inputs** — each free input the run needs: field name, source (`text`, `swipe-ad`, `swipe-bundle`, `organic-url`, `ad-url`), whether it's required, and a description. These are the `--input key=value` pairs you'll pass.
|
|
112
|
+
- **Inputs** — each free input the run needs: field name, source (`text`, `swipe-ad`, `swipe-bundle`, `organic-url`, `ad-url`, `asset`), whether it's required, and a description. These are the `--input key=value` pairs you'll pass. A source of **`asset`** is a **file** input — the workflow wants an actual image, video, audio clip, or document, and describe names which family it takes (e.g. `hero — asset (image), required`).
|
|
97
113
|
- **Prerequisites** — the per-workspace primer markers the graph's primer nodes depend on, each shown **✓ stored** or **✗ missing** for *this brand*. **If any prerequisite is ✗, warn the user before running** — the run will not produce good output (or will fail) without it. Route them to the `exodus-primer` / `exodus-foundation` skill to fill the gap first, rather than burning a run.
|
|
98
114
|
- **Outputs** — the deliverables the Output node collects (text or image, with labels), so you know what to expect back.
|
|
99
115
|
|
|
@@ -101,6 +117,18 @@ Accepts a workflow id **or** a name. It reports three things:
|
|
|
101
117
|
|
|
102
118
|
Fill every required input. For a free-text brief you can compose a **bespoke brief from in-session research** — the winning angle you just worked out, the swipe you analyzed — and pass it inline (`--input brief="..."`) or, if it's long, write it to a project file and pass it with `@file` (see Chain, below). For URL/swipe sources, pass the value the source expects.
|
|
103
119
|
|
|
120
|
+
For an **`asset` (file) input, pass the path to a local file** and the CLI does the uploading for you — mint, push the bytes, register, then start the run with the stored asset:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx @aicopycoders/exodus workflow run "Product Shots" --input hero=./photos/hero.png --wait
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
104
132
|
### Step 4 — Run and collect
|
|
105
133
|
|
|
106
134
|
```bash
|
|
@@ -115,38 +143,81 @@ npx @aicopycoders/exodus workflow status --id <runId>
|
|
|
115
143
|
|
|
116
144
|
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.
|
|
117
145
|
|
|
146
|
+
**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.
|
|
147
|
+
|
|
148
|
+
**Save the deliverables to disk with `--out <dir>`** (works on `run --wait` and on `status --id`):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx @aicopycoders/exodus workflow run "Launch Flow" --input brief="new offer" --wait --out ./deliverables
|
|
152
|
+
npx @aicopycoders/exodus workflow status --id <runId> --out ./deliverables
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Every delivered slot becomes a file named `<workflow>-<runId tail>-<slot key>.<ext>` — text as `.md`, storyboards and frame sets as `.json`, images/video/audio downloaded from their URLs — and each written path is printed. Unfulfilled slots are reported, never written. The directory is created if it doesn't exist.
|
|
156
|
+
|
|
118
157
|
Report the outputs plus a short take. Don't call a run "done" off the kickoff line alone — a run reaches `completed`, `partial`, `failed`, or **parks** (`awaiting-review`). A `partial` means some nodes were skipped or failed; a park means it's waiting on you (see the next section). Read `status` before declaring victory.
|
|
119
158
|
|
|
120
159
|
## 2. Runs that park
|
|
121
160
|
|
|
122
161
|
A run doesn't always finish on its own — it can **park** and wait for a human decision. There are three actionable park kinds:
|
|
123
162
|
|
|
124
|
-
- **
|
|
163
|
+
- **checkpoint** — any step whose author flipped the "pause for approval" switch has finished, and the run is holding until you approve what it wrote.
|
|
125
164
|
- **repair** — a require-all collector stalled on a dead input and needs a decision.
|
|
126
165
|
- **slots** — a nested sub-workflow is waiting on inputs (slot answers).
|
|
127
166
|
|
|
128
|
-
**No webhooks in v1 — nothing parks silently, but nothing pings you either.** You find parks three ways: `run --wait` prints a pause notice naming the verb to use (a park is not terminal — the command keeps polling so it can resume after you resolve; resolve from another shell, or Ctrl-C and pick it up via `inbox`); `workflow inbox` lists every parked run badged by kind (`
|
|
167
|
+
**No webhooks in v1 — nothing parks silently, but nothing pings you either.** You find parks three ways: `run --wait` prints a pause notice naming the verb to use (a park is not terminal — the command keeps polling so it can resume after you resolve; resolve from another shell, or Ctrl-C and pick it up via `inbox`); `workflow inbox` lists every parked run badged by kind (`checkpoint`/`repair`/`slots`; the badge `gate` shows up only on a pre-2.0 run parked at the retired Gate node — those can't be resumed, cancel and run the workflow again) and how it started (`bg`, `trig:<event>`); or `workflow status` shows `awaiting-review`. **`inbox` is the one place nothing hides** — including background runs a trigger fired or a promote's flywheel kicked off.
|
|
129
168
|
|
|
130
169
|
```bash
|
|
131
170
|
npx @aicopycoders/exodus workflow inbox
|
|
132
171
|
```
|
|
133
172
|
|
|
134
|
-
###
|
|
173
|
+
### The `gate` verbs retired in 2.0
|
|
174
|
+
|
|
175
|
+
`exodus workflow gate …` no longer does anything: the Gate node it drove was
|
|
176
|
+
removed. Running it prints a pointer at `workflow checkpoint` and exits 1.
|
|
177
|
+
|
|
178
|
+
A run that parked at a Gate box **before** 2.0 is frozen there — it can't be
|
|
179
|
+
resumed, because the step it's waiting on no longer exists. Cancel it and run the
|
|
180
|
+
workflow again:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> cancel --reason "legacy gate park"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Checkpoint parks — show / edit / approve / retry / cancel
|
|
187
|
+
|
|
188
|
+
A **checkpoint** is the "pause for approval" switch on an ordinary step (any node kind can carry it). The step runs, finishes, and then the whole run stops there until a human signs off — it waits **indefinitely**, so a `run --wait` left unattended simply keeps polling; nothing times out and nothing is lost.
|
|
189
|
+
|
|
190
|
+
There are **no candidates to pick** — there's one step's output to read and either bless, fix, or redo. Outputs are numbered **1-based**:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> # show the paused step + its output
|
|
194
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> edit 1 --text "tighter opener" # rewrite an output in place
|
|
195
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> approve --wait # sign off, the run continues
|
|
196
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> retry --wait # redo the step, wait for its fresh output
|
|
197
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> cancel --reason "wrong direction" # stop the run here
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- **`edit`** takes `--text`, `--file <path>`, or piped stdin. It does **not** resume the run — edit first, then `approve`.
|
|
201
|
+
- **`retry`** discards what the step produced and runs that one step again. The run parks right back at the *same* checkpoint with the fresh output — that re-park **is** the success. So `retry --wait` finishes as soon as the new output is ready, prints it, and hands you back the same four choices; it does **not** wait for the run to complete (it never would). Review the fresh output and `approve` when you're happy.
|
|
202
|
+
- **`cancel`** stops the run for good, recording your reason — the same audit trail the web uses.
|
|
203
|
+
|
|
204
|
+
#### When the step ran once per item (a Splitter fan-out)
|
|
135
205
|
|
|
136
|
-
|
|
206
|
+
If a Splitter (below) sits upstream, the flagged step ran **once per item**, and `show` groups its output under **`Item 1 of 7`**-style headings. The run still parks **once**, after every lane has finished — you review the whole batch in one sitting.
|
|
207
|
+
|
|
208
|
+
The numbering does **not** restart per item: rows are numbered straight through, so `edit 4` still means row 4 wherever it sits. What's new is that you can drop whole items:
|
|
137
209
|
|
|
138
210
|
```bash
|
|
139
|
-
npx @aicopycoders/exodus workflow
|
|
140
|
-
npx @aicopycoders/exodus workflow
|
|
141
|
-
npx @aicopycoders/exodus workflow
|
|
142
|
-
npx @aicopycoders/exodus workflow gate <runId> push "make them shorter" # steer the upstream session
|
|
143
|
-
npx @aicopycoders/exodus workflow gate <runId> approve --wait # resume the run
|
|
144
|
-
npx @aicopycoders/exodus workflow gate <runId> reject --reason "off-brand" # cancel it
|
|
211
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> # grouped by item
|
|
212
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> approve --reject 2 --wait # keep all but item 2
|
|
213
|
+
npx @aicopycoders/exodus workflow checkpoint <runId> approve --reject 2,5 --wait # drop items 2 and 5
|
|
145
214
|
```
|
|
146
215
|
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
216
|
+
- `--reject` takes the **item numbers you see** (1-based, comma-separated) — not row numbers, not the 0-based indexes in `--json`.
|
|
217
|
+
- **Rejecting is filtering, not failing.** The dropped items disappear from the batch and the run carries on with the survivors; nothing is marked failed and no warning is raised. Say it that way to the user: "kept 5 of 7."
|
|
218
|
+
- **Approve with no `--reject` keeps everything** — the ordinary approve, unchanged.
|
|
219
|
+
- **Rejecting every item is refused.** If nothing is worth keeping, `cancel` the run — that's the honest action, and the CLI relays the server's message telling you so.
|
|
220
|
+
- `edit <n>` works inside a fan exactly as it does anywhere else; edit the rows you want to fix, then approve (with or without `--reject`).
|
|
150
221
|
|
|
151
222
|
### Repair parks — retry / skip / kill
|
|
152
223
|
|
|
@@ -192,9 +263,9 @@ npx @aicopycoders/exodus workflow triggers "Winner Flywheel" fire 1 --text "fake
|
|
|
192
263
|
|
|
193
264
|
- **`enable`/`disable` flip exactly one switch** — never rewrite a trigger by re-importing the whole contract to toggle it. The CLI reads the live trigger list and sends a **fingerprint** of the trigger's fields as a guard, so if the workflow changed under you the flip **fails loud** rather than hitting the wrong trigger. If it fails, re-list and retry.
|
|
194
265
|
- **Add or remove triggers by editing the YAML export** (they live in the head export) and re-importing — enable/disable never adds or removes, only toggles.
|
|
195
|
-
- **`fire`** starts a **real** background run on the workflow owner's keys. An event trigger's `fire` needs `--text` (the input the event would carry); a cron trigger's `fire` rejects `--text`. Its
|
|
266
|
+
- **`fire`** starts a **real** background run on the workflow owner's keys. An event trigger's `fire` needs `--text` (the input the event would carry); a cron trigger's `fire` rejects `--text`. Its checkpoints park into the **inbox** like any other run.
|
|
196
267
|
|
|
197
|
-
**The ruled test flow:** author the workflow with the trigger **disabled** → `fire <n> --text "fake winner copy"` to dry-fire it and watch the run (
|
|
268
|
+
**The ruled test flow:** author the workflow with the trigger **disabled** → `fire <n> --text "fake winner copy"` to dry-fire it and watch the run (checkpoints land in the inbox) → `enable <n>` once you're happy with what it produces.
|
|
198
269
|
|
|
199
270
|
## 5. Banks + the flywheel
|
|
200
271
|
|
|
@@ -241,12 +312,16 @@ Cold-start is a **template, not a blank file** — there is no `workflow new`. L
|
|
|
241
312
|
```bash
|
|
242
313
|
npx @aicopycoders/exodus workflow bots # full catalog, grouped by category
|
|
243
314
|
npx @aicopycoders/exodus workflow bots --slug new-hook-bot # one bot's full port + param spec
|
|
244
|
-
npx @aicopycoders/exodus workflow schema # LIVE node kinds, ports, config rules, faces,
|
|
315
|
+
npx @aicopycoders/exodus workflow schema # LIVE node kinds, ports, config rules, faces, wiring rules
|
|
245
316
|
npx @aicopycoders/exodus workflow schema --kind transform # one node kind
|
|
317
|
+
npx @aicopycoders/exodus workflow schema --kind formatter # the Formatter's two modes, bounds, and notes
|
|
318
|
+
npx @aicopycoders/exodus workflow schema --kind splitter # the Splitter's three modes, the item cap, and the lane rules
|
|
246
319
|
npx @aicopycoders/exodus workflow schema --face splitter # one transform face's ports + spec
|
|
247
320
|
```
|
|
248
321
|
|
|
249
|
-
Per bot you get its **input ports** (id, what it `accepts` — `text` / `primer` / `image`, whether it's required, and for primer ports which `primerKinds` gate it), its **params**, and its output type. `workflow schema` prints the **live** graph grammar from the backend you're deployed against — node kinds, port ids, config rules, transform faces,
|
|
322
|
+
Per bot you get its **input ports** (id, what it `accepts` — `text` / `primer` / `image`, whether it's required, and for primer ports which `primerKinds` gate it), its **params**, and its output type. `workflow schema` prints the **live** graph grammar from the backend you're deployed against — node kinds, port ids, config rules, transform faces, collector policy, wiring rules — so what you author matches what will validate. This is always current; pull it fresh rather than trusting memory.
|
|
323
|
+
|
|
324
|
+
**Faces now carry a `retired` flag.** `schema` (and `schema --face <face>`) reports it per face. Five faces are retired — `parser` (Parser/Extractor), `edit-shorten` (Shorten), the old `formatter` reshaper, and (newest) `splitter` (Splitter (Text)) and `decomposer`. **Retired means "no new ones," not "broken":** they still validate, still run, and every existing graph that names one keeps working unchanged, so never rewrite a member's graph to get rid of one. Just don't author a fresh one — use the `formatter` node kind instead of `parser`/`formatter`, and the `splitter` **node kind** instead of the `splitter`/`decomposer` faces (both below). The ad-hoc edit-pass buttons on finished run results, Shorten included, are a different surface and are unaffected.
|
|
250
325
|
|
|
251
326
|
### Step 2 — Start from a template
|
|
252
327
|
|
|
@@ -265,7 +340,7 @@ The contract is `nodes` (each with `id`, `kind`, a `config`, and optional `posit
|
|
|
265
340
|
npx @aicopycoders/exodus workflow validate my.yaml
|
|
266
341
|
```
|
|
267
342
|
|
|
268
|
-
`validate` is the front door; `import --dry-run` is the same check under a different door. It **needs network + login** — there is no offline validation, ever. It returns compiler-style issues, each naming the **node id**, the **port id** where relevant, and a concrete **remedy**. Codes include `dangling-edge`, `unknown-port`, `type-mismatch`, `missing-required-input`, `duplicate-input`, `cycle`, `unknown-kind`, and `
|
|
343
|
+
`validate` is the front door; `import --dry-run` is the same check under a different door. It **needs network + login** — there is no offline validation, ever. It returns compiler-style issues, each naming the **node id**, the **port id** where relevant, and a concrete **remedy**. Codes include `dangling-edge`, `unknown-port`, `type-mismatch`, `missing-required-input`, `duplicate-input`, `cycle`, `unknown-kind`, `bad-config`, and the three Splitter lane rules `nested-split`, `lane-ineligible`, and `fan-overlap`. Fix, re-run `validate`, repeat until clean.
|
|
269
344
|
|
|
270
345
|
### Step 4 — Import (create or update)
|
|
271
346
|
|
|
@@ -278,16 +353,131 @@ Then **tell the user to review it on the canvas at `/workflows`** — the CLI wr
|
|
|
278
353
|
|
|
279
354
|
**The 409 rule.** `--update` sends the export's `updatedAt` as an optimistic-concurrency guard. If the workflow moved on the dashboard since your export, the import 409s. Re-export the current workflow, reapply your edits onto the fresh contract, and import again — never force past a 409, or you'll clobber their change.
|
|
280
355
|
|
|
356
|
+
### The `formatter` node — guarantee the shape of what comes out
|
|
357
|
+
|
|
358
|
+
Reach for a `formatter` node whenever the next step needs a **known shape**: a rendered layout, or named fields split onto their own wires. Two modes on one kind, chosen by `config.mode`. Its ports are **derived from its config** in both modes, so edit the config and the handles change — re-check your edges after any edit.
|
|
359
|
+
|
|
360
|
+
**`mode: template`** — deterministic, no model call. Write a layout with `{{variable}}` slots; **each distinct slot becomes a required `text` input port named for the variable**, and the single output port is `formatted`. Same inputs → identical output, every time. A slot that gets nothing renders empty and warns; it never fails the run.
|
|
361
|
+
|
|
362
|
+
```yaml
|
|
363
|
+
- id: assemble
|
|
364
|
+
kind: formatter
|
|
365
|
+
config:
|
|
366
|
+
mode: template
|
|
367
|
+
template: |
|
|
368
|
+
HOOK: {{hook}}
|
|
369
|
+
|
|
370
|
+
{{body}}
|
|
371
|
+
|
|
372
|
+
CTA: {{cta}}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Ports: inputs `hook`, `body`, `cta` (all required, one wire each); output `formatted`.
|
|
376
|
+
|
|
377
|
+
**`mode: extract`** — the AI reads loose text, the node **validates the answer against your declaration**. Declare 1–12 fields; **each field key becomes its own output port**, and the one input port is `source` (required, accepts many wires — their texts are joined). A `list` field emits **one artifact per element** on its port, so downstream nodes get hooks one at a time, not one blob.
|
|
378
|
+
|
|
379
|
+
```yaml
|
|
380
|
+
# Winner ad text in → hooks, offer, and a mechanism line out, each on its own wire.
|
|
381
|
+
- id: mine-winner
|
|
382
|
+
kind: formatter
|
|
383
|
+
config:
|
|
384
|
+
mode: extract
|
|
385
|
+
temperature: 0.2 # 0..1, default 0.2
|
|
386
|
+
maxRetries: 2 # 0..3, default 2 → up to 3 model attempts
|
|
387
|
+
fields:
|
|
388
|
+
- { key: hooks, label: Hooks, type: list, required: true }
|
|
389
|
+
- { key: offer, label: Offer, type: text, required: true }
|
|
390
|
+
- { key: mechanism, label: Mechanism, type: text, required: false }
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Ports: input `source`; outputs `hooks`, `offer`, `mechanism`. Wire `sourceHandle: hooks` into a hook-consuming bot.
|
|
394
|
+
|
|
395
|
+
**The contract that makes it worth using: extract mode never emits a wrong shape.** If the reply isn't a JSON object with exactly the declared keys, correctly typed, and non-empty where you marked `required`, the node re-asks the model with the exact violations attached. `maxRetries` counts those re-asks *after* the first attempt (default `2` = up to 3 attempts). When the budget is spent the node **fails the run** with the last answer and its errors on the failure note — it never passes nonconforming output downstream. Tell the user that plainly when an extract node fails: the model couldn't produce the declared shape, so the node refused to guess.
|
|
396
|
+
|
|
397
|
+
**Field rules** (each row needs all three of `key`/`type`/`required`): `key` is letters, digits, spaces, underscores, hyphens, ≤40 chars, unique in the list; `type` is `text`, `number`, or `list`; `required` must be an explicit `true`/`false`. `label` is optional and defaults to the key.
|
|
398
|
+
|
|
399
|
+
### The `splitter` node — one thing in, N parallel lanes out
|
|
400
|
+
|
|
401
|
+
A **Splitter** turns one thing into many. Feed it a list (or text a model can cut into pieces) and it emits N **items** — and here is the part that matters: **every box after it runs once per item.** Five items means five copies of the same downstream chain running side by side, like five assembly lines. Each piece of work knows its own number, so the run view and the CLI can say "Item 3 of 7".
|
|
402
|
+
|
|
403
|
+
It has the same ports in every mode: one required input `source` (it accepts several wires), one output `items`. Three modes, picked by `config.mode`:
|
|
404
|
+
|
|
405
|
+
- **`structural`** — no model call. Whatever artifacts arrive on `source` **are** the items. This is the natural partner of a Formatter `list` field, which already emits one artifact per element. (A single artifact whose text is a JSON list of strings gets exploded too.)
|
|
406
|
+
- **`rule`** — no model call. Joins the incoming text and cuts it on a `delimiter`: `newline`, `blank-line`, `numbered` (1. 2. 3. list markers), or `custom` plus a `customPattern` (a **literal** separator, not a regular expression).
|
|
407
|
+
- **`semantic`** — the only mode that calls a model. Your `instruction` says what counts as one item ("separate this into ad concepts"); the reply must be a clean list, and a bad one is re-asked before the node gives up.
|
|
408
|
+
|
|
409
|
+
```yaml
|
|
410
|
+
- id: split-modules
|
|
411
|
+
kind: splitter
|
|
412
|
+
config:
|
|
413
|
+
mode: semantic
|
|
414
|
+
instruction: "Break this winning ad into its reusable modules — hook, mechanism, proof, offer, CTA. One module per item."
|
|
415
|
+
maxItems: 25
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**`maxItems` is a hard cap and it is required in every mode** (default 25, raise it to at most 100 deliberately — you cannot remove it). Going over the cap **fails the node loudly** with the real count ("Splitter produced 61 items; cap is 25"). It never quietly trims the list: silent data loss is worse than a visible failure. Producing **zero** items fails too, for the same reason.
|
|
419
|
+
|
|
420
|
+
**Reviewing a fan item by item.** If a step inside the fan has the "pause for approval" switch on, the run parks once, after all the lanes finish, and you approve or reject **each item**. Rejecting is filtering, not failing — see "When the step ran once per item" above.
|
|
421
|
+
|
|
422
|
+
**Three rules the validator enforces — all three scoped to a Splitter's OPEN fan** (the chain running once per item, which ends at the Collector that closes it):
|
|
423
|
+
|
|
424
|
+
- **No split inside an open fan** (`nested-split`) — a Splitter can't sit inside another Splitter's open fan. An artifact carries exactly one item number, so items-of-items has nowhere to live. The way through is **sequential**: close the first fan with a Collector, then split the combined result again. That is fully legal.
|
|
425
|
+
- **One open fan per node** (`fan-overlap`) — no node may sit inside two different Splitters' open fans at once. Their item numbering is unrelated, so the lanes can't merge mid-fan. Two fans that have **each closed through their own Collector** may merge downstream freely.
|
|
426
|
+
- **Lane eligibility** (`lane-ineligible`) — every node inside the open fan must be a `bot`, `formatter`, `transform`, or `output` node. Video nodes, Call nodes, and the video member-gate kinds are blocked, because running them once per item would corrupt state they keep per node. The `collector` kind is **exempt** (it ends the lane rather than running in it), and **after** a Collector any kind at all is allowed again — video, Call, all of it.
|
|
427
|
+
|
|
428
|
+
**One lane failing doesn't sink the batch.** If a lane exhausts its retries, that item drops out and the others carry on; the step ends `done` with a warning naming the casualties. Only *every* item failing fails the step. (A Collector downstream can be told to be stricter than that — see the next section.)
|
|
429
|
+
|
|
430
|
+
**The old cards are gone, the old graphs are fine.** The `splitter` (Splitter (Text)) and `decomposer` **transform faces** are retired — they put N snippets on one wire and ran downstream **once**, which is a different thing entirely. Existing workflows that use them keep running exactly as before, and there is no migration; just don't author new ones. The Winner Flywheel starter has been rebuilt on the Splitter node, so its Output step now files each module separately.
|
|
431
|
+
|
|
432
|
+
**Blocks Run** (`bad-config`, so `validate` catches it): no/unknown `mode`; template mode with an empty template, over 20000 characters, or more than 12 distinct slots; extract mode with fewer than 1 or more than 12 fields, a bad/duplicate key, a bad type, a non-boolean `required`, a `temperature` outside 0..1, or a `maxRetries` outside 0..3. A template slot with no wire into it is `missing-required-input` — same block, different code.
|
|
433
|
+
|
|
434
|
+
### The `collector` node — close the lanes
|
|
435
|
+
|
|
436
|
+
A **Collector** is the Splitter's other half. Where a Splitter opens N lanes, a Collector waits for all of them and folds them back into **one** result. That result carries no item number, which is the whole point: **after a Collector the run is ordinary again.** Anything may follow it, you may split again, and two fans that each closed through their own Collector may merge.
|
|
437
|
+
|
|
438
|
+
Its ports are fixed in every mode: one required input `items` (a whole fan lands on that single port), one output `combined`. Three modes, picked by `config.mode`:
|
|
439
|
+
|
|
440
|
+
- **`assemble`** — no model call. Stitches the surviving items into one block, in order, joined by `separator` (`newline`, `blank-line`, or `custom` plus a literal `customSeparator`; newline if you say nothing). An optional `itemTemplate` wraps each item, with the slots `{{item.value}}`, `{{item.index}}` (1-based), and `{{item.total}}`.
|
|
441
|
+
- **`list`** — no model call. Emits one artifact whose text is JSON: the items with their numbers, a count, and what dropped out. Use it when the next step wants structure, not prose. It is the only mode that puts the dropped detail *in* the artifact.
|
|
442
|
+
- **`synthesize`** — the only mode that calls a model. Your `instruction` says what the merge should produce; the model writes one new piece out of every item. The model itself is sealed (you get `temperature` and `maxRetries`, not a model pick).
|
|
443
|
+
|
|
444
|
+
```yaml
|
|
445
|
+
- id: fold-modules
|
|
446
|
+
kind: collector
|
|
447
|
+
config:
|
|
448
|
+
mode: assemble
|
|
449
|
+
separator: blank-line
|
|
450
|
+
itemTemplate: |
|
|
451
|
+
### Module {{item.index}} of {{item.total}}
|
|
452
|
+
{{item.value}}
|
|
453
|
+
onBranchFailure: survivors
|
|
454
|
+
minItems: 2
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**The lane policies ride every mode.** `onBranchFailure: survivors` (the default) carries on with whatever made it through and warns you who didn't; `strict` stops the run right there if any lane broke. `minItems` (default 1, up to 100) is a floor that applies under **both** policies — fewer survivors than that and the node fails, so a fan where everything died can never quietly hand you an empty result. `ordering` is `index` (the Splitter's original order, the default) or `arrival` (whichever lane finished first).
|
|
458
|
+
|
|
459
|
+
**A rejection is never a failure.** An item the user rejected at a checkpoint was filtered on purpose, so it never trips `strict` — it only counts against `minItems` by not being there. The node's warning spells the difference out, and it's the sentence to quote back to a user asking why the run kept going: *"Collected 7 of 10 items — items 2 and 5 rejected at your checkpoint; item 8 failed."*
|
|
460
|
+
|
|
461
|
+
**Every wire into a Collector must come from inside ONE Splitter's open fan** (`collector-unpaired`, blocks Run) — the Splitter itself or a node running in its lanes. A Collector with nothing split upstream has no fan to close, and a Brief wired straight into one is merging unrelated branches, which is a Formatter's job in template mode, not a Collector's.
|
|
462
|
+
|
|
463
|
+
**The old card is gone, the old graphs are fine.** The `collector` **transform face** is retired — it compiled whatever text arrived on merged wires and ran once, which is a different job from closing a lane fan. Existing workflows using the face keep running unchanged and there is no migration; author the `collector` **node** instead. Note the vocabulary split while you're at it: the node reads `onBranchFailure`, and the legacy `onFailedInput` key is rejected on it outright.
|
|
464
|
+
|
|
465
|
+
**Blocks Run** (`bad-config`): no/unknown `mode`; assemble mode with an illegal `separator`, an empty or over-200-character `customSeparator` on `custom`, or an `itemTemplate` over 4000 characters; synthesize mode with an empty or over-2000-character `instruction`, a `temperature` outside 0..1, or a `maxRetries` outside 0..3; any mode with an illegal `onBranchFailure`/`ordering`, a `minItems` outside 1..100, or a stray `onFailedInput`.
|
|
466
|
+
|
|
281
467
|
### Authoring gotchas
|
|
282
468
|
|
|
283
469
|
A hand-picked list of the traps that bite first. `workflow schema` is authoritative on any conflict with what's written here:
|
|
284
470
|
|
|
285
471
|
- **Edge handles are port ids** — `sourceHandle`/`targetHandle` name a **port**, not a node id or a label. An edge only connects if the target port `accepts` the source port's type.
|
|
286
472
|
- **Required inputs hard-block** — a run refuses to start until every required input is supplied; there's no "run anyway".
|
|
287
|
-
- **Transform faces are sealed** — each face has fixed, per-face **output port ids** you must wire from by name; `workflow schema --face <face>` prints them
|
|
288
|
-
-
|
|
473
|
+
- **Transform faces are sealed** — each face has fixed, per-face **output port ids** you must wire from by name; `workflow schema --face <face>` prints them, along with whether the face is `retired`.
|
|
474
|
+
- **Don't author a retired face** — `parser`, `edit-shorten`, the old `formatter` face, `splitter` / `decomposer`, and now `collector` are no longer offered; author a `formatter` node (parser/formatter), a `splitter` node (splitter/decomposer), or a `collector` node instead. Existing graphs that use them keep running, so leave those alone.
|
|
475
|
+
- **The fan rules are scoped to the OPEN fan** — inside a Splitter's open fan (the chain up to the Collector that closes it) you may not place another Splitter (`nested-split`), every node must be a `bot`, `formatter`, `transform`, or `output` (`lane-ineligible`), and no node may sit in two Splitters' open fans at once (`fan-overlap`). Close the fan with a Collector and all three restrictions lift: split again, run any kind, merge two collected fans.
|
|
476
|
+
- **A Collector must be paired** — every wire into a `collector` node has to come from inside one Splitter's open fan, or it's `collector-unpaired` and the run is blocked. To merge branches that were never split, use a Formatter in template mode.
|
|
477
|
+
- **A Formatter's handles come from its own config** — template slots name its inputs, field keys name its outputs. Change the config and old edges go `unknown-port`.
|
|
478
|
+
- **`session` outputs have nowhere to wire** — a session-typed output exists so you can re-open the chat from the run page; no node kind accepts a session wire.
|
|
289
479
|
- **Deposit shapes are strict** — a deposit's config must match the target bank's expected shape exactly, or import rejects it.
|
|
290
|
-
- **Empty defaults are deliberately invalid** — `rig`, `call`, `transform`, and `
|
|
480
|
+
- **Empty defaults are deliberately invalid** — `rig`, `call`, `transform`, `bot`, and `formatter` nodes ship with empty config placeholders that fail validation on purpose; you MUST fill them, not leave them.
|
|
291
481
|
- **Quote YAML that looks numeric or boolean** — `"true"`, `"3.0"`, `"01"` need quotes or the parser coerces them to the wrong type.
|
|
292
482
|
- **A 409 on `--update` means re-export first** — reapply your edits onto the fresh contract; don't force.
|
|
293
483
|
|
|
@@ -325,13 +515,13 @@ The same contract is on the v2 API, so a script can skip the CLI:
|
|
|
325
515
|
- **Export:** `GET /api/v2/workflows/export?id=<workflowId>&format=yaml` returns the canonical YAML (`text/yaml`); drop `format` (or `format=json`) for the legacy JSON body.
|
|
326
516
|
- **Import:** `POST /api/v2/workflows/import`. A JSON object body carries the contract *plus* the control fields inline (`dryRun`, `targetWorkflowId`, `expectedUpdatedAt`). A raw **YAML** body is the contract text alone — pass the control fields as **query params** instead (`?dryRun=true&targetWorkflowId=<id>&expectedUpdatedAt=<ts>`).
|
|
327
517
|
|
|
328
|
-
The
|
|
518
|
+
The inbox/checkpoint/session/bank verbs have v2 routes too, but the CLI is the maintained path — reach for HTTP only when you're already scripting, and prefer the CLI's `--json` output over hand-rolling routes. Both are Bearer-auth'd (`EXODUS_API_KEY`) like every v2 route.
|
|
329
519
|
|
|
330
520
|
## Failure handling
|
|
331
521
|
|
|
332
522
|
- **`describe` shows a ✗ prerequisite** — don't run yet. Fill the primer via `exodus-primer` / `exodus-foundation`, then re-check `describe`.
|
|
333
523
|
- **Run comes back `partial` or `failed`** — read `status --id <runId>` (drop `--json` for a readable view) to see which node failed and why **before re-running**. A partial still has usable outputs; harvest them first.
|
|
334
|
-
- **A verb refuses because the run isn't in that state** — the
|
|
524
|
+
- **A verb refuses because the run isn't in that state** — the checkpoint/repair/answer verbs preflight the run and name its **actual** state (e.g. "parked for repair" when you tried a checkpoint verb). Read the state it reports and switch to the matching verb, or check `inbox` for what's actually pending.
|
|
335
525
|
- **Trigger fingerprint mismatch** — the workflow changed since you listed its triggers. Re-list (`workflow triggers <wf>`) and retry the enable/disable/fire against the fresh numbering.
|
|
336
526
|
- **`validate` / `import` errors** — fix the exact node/port each issue names using its remedy; don't guess. `validate` needs network + login — there is no offline check.
|
|
337
527
|
- **`import --update` 409** — re-export, reapply, re-import. This is a conflict, not a bug.
|
package/dist/commands/browse.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiGet } from "../lib/client.js";
|
|
2
2
|
import { formatBrowse, formatError } from "../lib/format.js";
|
|
3
|
+
import { isActiveRunStatus } from "../lib/runStatus.js";
|
|
3
4
|
export const helpText = `
|
|
4
5
|
exodus browse — List recent pipeline runs (hooks, ads, image concepts, etc.)
|
|
5
6
|
|
|
@@ -119,7 +120,7 @@ export async function run(flags) {
|
|
|
119
120
|
? all
|
|
120
121
|
: all.filter((g) => {
|
|
121
122
|
const status = g["status"];
|
|
122
|
-
if (status
|
|
123
|
+
if (!status || !isActiveRunStatus(status))
|
|
123
124
|
return true;
|
|
124
125
|
const created = normalizeCreatedAt(g);
|
|
125
126
|
return created > 0 && now - created < STUCK_THRESHOLD_MS;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiGet, apiGetDashboard, apiPost, getDashboardUrl, } from "../lib/client.js";
|
|
2
|
-
import { formatError } from "../lib/format.js";
|
|
2
|
+
import { displayRunStatus, formatError } from "../lib/format.js";
|
|
3
3
|
export const helpText = `
|
|
4
4
|
exodus creative — Creative-suite engines (native, copy-derived, ref-match)
|
|
5
5
|
|
|
@@ -165,7 +165,7 @@ async function runStatus(flags) {
|
|
|
165
165
|
console.log(`name: ${d.name}`);
|
|
166
166
|
if (d.engine)
|
|
167
167
|
console.log(`engine: ${d.engine}`);
|
|
168
|
-
console.log(`status: ${d.status
|
|
168
|
+
console.log(`status: ${displayRunStatus(d.status, "—")}${d.isTerminal ? " (terminal)" : ""}`);
|
|
169
169
|
if (d.completedImages !== undefined || d.totalImages !== undefined) {
|
|
170
170
|
console.log(`progress: ${d.completedImages ?? 0} / ${d.totalImages ?? "?"} completed${d.failedImages ? `, ${d.failedImages} failed` : ""}`);
|
|
171
171
|
}
|
package/dist/commands/genesis.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
5
5
|
import { pollUntilDone } from "../lib/poll.js";
|
|
6
|
-
import { formatGenesisRun, formatError } from "../lib/format.js";
|
|
6
|
+
import { displayRunStatus, formatGenesisRun, formatError, tickerRunStatus } from "../lib/format.js";
|
|
7
7
|
import { formatCcCommand } from "../lib/cc-command.js";
|
|
8
8
|
import { resolveActiveBrand } from "../lib/layout.js";
|
|
9
9
|
import { captureReelAndWrite } from "../lib/reel-write.js";
|
|
@@ -441,7 +441,7 @@ async function waitForGenesisRun(runId) {
|
|
|
441
441
|
const status = data["status"];
|
|
442
442
|
const currentStep = data["currentStep"];
|
|
443
443
|
if (status) {
|
|
444
|
-
process.stdout.write(`\r ${status}${currentStep ? ` — ${currentStep}` : ""} `);
|
|
444
|
+
process.stdout.write(`\r ${tickerRunStatus(status)}${currentStep ? ` — ${currentStep}` : ""} `);
|
|
445
445
|
}
|
|
446
446
|
},
|
|
447
447
|
});
|
|
@@ -515,7 +515,7 @@ async function runHooks(flags) {
|
|
|
515
515
|
process.exit(1);
|
|
516
516
|
}
|
|
517
517
|
if (res.data.status !== "awaiting_hook_selection") {
|
|
518
|
-
console.log(`Run ${runId} is not awaiting hook selection (status: ${res.data.status
|
|
518
|
+
console.log(`Run ${runId} is not awaiting hook selection (status: ${displayRunStatus(res.data.status)}).`);
|
|
519
519
|
return;
|
|
520
520
|
}
|
|
521
521
|
const pool = Array.isArray(res.data.hookPool) ? res.data.hookPool : [];
|
|
@@ -677,7 +677,7 @@ async function pollScrape(runId, noWait, timeoutMs) {
|
|
|
677
677
|
? ` captured=${captured ?? 0} qualified=${qualified ?? 0}`
|
|
678
678
|
: "";
|
|
679
679
|
if (status)
|
|
680
|
-
process.stdout.write(`\r status: ${status}${counts} `);
|
|
680
|
+
process.stdout.write(`\r status: ${tickerRunStatus(status)}${counts} `);
|
|
681
681
|
},
|
|
682
682
|
});
|
|
683
683
|
console.log();
|
package/dist/commands/primer.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
|
|
4
4
|
import { promptChoice, promptMultiline, openInEditor } from "../lib/prompts.js";
|
|
5
|
+
import { normalizeRunStatus } from "../lib/runStatus.js";
|
|
5
6
|
import { refreshBrandProfileMd } from "./brand.js";
|
|
6
7
|
export const helpText = `
|
|
7
8
|
exodus primer — Build a brand's primer (the single foundation step)
|
|
@@ -152,7 +153,7 @@ async function buildPrimer(submission) {
|
|
|
152
153
|
process.stdout.write("·");
|
|
153
154
|
continue;
|
|
154
155
|
}
|
|
155
|
-
if (data.status === "failed") {
|
|
156
|
+
if (normalizeRunStatus(data.status) === "failed") {
|
|
156
157
|
console.log("");
|
|
157
158
|
fail(`primer build failed: ${data.error ?? "unknown error"}`);
|
|
158
159
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiGet } from "../lib/client.js";
|
|
2
|
-
import { formatGeneration, formatGenesisRun, formatError } from "../lib/format.js";
|
|
2
|
+
import { formatGeneration, formatGenesisRun, formatError, displayRunStatus } from "../lib/format.js";
|
|
3
3
|
import { hydrateScoutIdeasCount } from "../lib/scout-hydrate.js";
|
|
4
4
|
import { formatIntelResult as formatIntelResultRich } from "../lib/intel-format.js";
|
|
5
5
|
export const helpText = `
|
|
@@ -31,7 +31,7 @@ export function formatIntelResult(data) {
|
|
|
31
31
|
export function formatPulseResult(data) {
|
|
32
32
|
const lines = [];
|
|
33
33
|
lines.push("## Pulse Status");
|
|
34
|
-
lines.push(`**Status:** ${data["status"]
|
|
34
|
+
lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
|
|
35
35
|
if (data["accountId"])
|
|
36
36
|
lines.push(`**Account ID:** ${data["accountId"]}`);
|
|
37
37
|
if (data["commentsAnalyzed"] !== undefined)
|
|
@@ -61,7 +61,7 @@ export function formatPulseResult(data) {
|
|
|
61
61
|
export function formatScoutResult(data) {
|
|
62
62
|
const lines = [];
|
|
63
63
|
lines.push("## Scout Status");
|
|
64
|
-
lines.push(`**Status:** ${data["status"]
|
|
64
|
+
lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
|
|
65
65
|
const pipelineSlug = data["pipelineSlug"];
|
|
66
66
|
if (pipelineSlug)
|
|
67
67
|
lines.push(`**Pipeline:** ${pipelineSlug}`);
|
|
@@ -123,7 +123,7 @@ export function formatScoutResult(data) {
|
|
|
123
123
|
export function formatCreativeSuiteStatus(title, data) {
|
|
124
124
|
const lines = [];
|
|
125
125
|
lines.push(`## ${title}`);
|
|
126
|
-
lines.push(`**Status:** ${data["status"]
|
|
126
|
+
lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
|
|
127
127
|
const show = (label, key) => {
|
|
128
128
|
const v = data[key];
|
|
129
129
|
if (v !== undefined && v !== null && v !== "")
|
package/dist/commands/swipe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { apiGet, apiPost } from "../lib/client.js";
|
|
3
|
-
import { formatError } from "../lib/format.js";
|
|
3
|
+
import { displayRunStatus, formatError } from "../lib/format.js";
|
|
4
4
|
export const helpText = `
|
|
5
5
|
exodus swipe — Swipe pipeline lifecycle + competitor watchlist
|
|
6
6
|
|
|
@@ -121,7 +121,7 @@ async function runRunPipeline(flags) {
|
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
console.log(`Run started: ${res.data.runId}`);
|
|
124
|
-
console.log(` status: ${res.data.status
|
|
124
|
+
console.log(` status: ${displayRunStatus(res.data.status, "Running")}`);
|
|
125
125
|
if (res.data.triggerRunId)
|
|
126
126
|
console.log(` triggerRunId: ${res.data.triggerRunId}`);
|
|
127
127
|
console.log(`\nPoll: exodus swipe status ${res.data.runId}`);
|
|
@@ -224,13 +224,13 @@ async function runHistory(flags) {
|
|
|
224
224
|
for (const r of runs) {
|
|
225
225
|
const when = r.startedAt ? new Date(r.startedAt).toLocaleString() : "?";
|
|
226
226
|
const doc = r.docUrl ? ` doc=${r.docUrl}` : r.error ? ` error=${r.error}` : "";
|
|
227
|
-
console.log(` ${r.runId} ${r.status} ads=${r.adsSelectedCount} by=${r.triggeredBy} ${when}${doc}`);
|
|
227
|
+
console.log(` ${r.runId} ${displayRunStatus(r.status, "—")} ads=${r.adsSelectedCount} by=${r.triggeredBy} ${when}${doc}`);
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
function printRunStatus(r) {
|
|
231
231
|
console.log(`Run ${r.runId ?? "?"}`);
|
|
232
232
|
console.log(` brand: ${r.targetBrandSlug ?? "?"}`);
|
|
233
|
-
console.log(` status: ${r.status
|
|
233
|
+
console.log(` status: ${displayRunStatus(r.status, "?")}`);
|
|
234
234
|
console.log(` by: ${r.triggeredBy ?? "?"}`);
|
|
235
235
|
console.log(` ads: ${r.adsSelectedCount ?? 0}`);
|
|
236
236
|
if (r.startedAt)
|