@aicopycoders/exodus 2026.9.2000-beta.8 → 2026.9.2000

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.
@@ -83,6 +83,7 @@ These are not pipelines. They are Luke's methods written as skills, so the think
83
83
  | `find-competitors` | Who is actually strong in a niche, on Meta first and Amazon second. Ranked competitor sheet and a watch list. Optional scripts for Atria and the Meta Ad Library via Apify (each needs its own key; they bill per result and say the plan back before spending). |
84
84
  | `patterns-and-gaps` | What repeats and matters across own ads, competitor ads, organic content, and research, read layer by layer, and the three kinds of gap. |
85
85
  | `anatomy-of-ads` | The parts list of an ad (Messaging × Creative). Write a brief, dissect any ad into the same parts, tag a batch so patterns can be counted. Also the vocabulary the other skills use. |
86
+ | `swipe` | Swipe a winning ad for the active brand at one of three closeness levels (1 same scene, 2 new scene same beats, 3 same bones new drama). Gathers the ad, product, and level, then calls the `swipe-narrative-agent` Genesis bot (needs `GENESIS_API_KEY` plus a provider key). |
86
87
  | `copy-instincts` | See, Attract, Make. Run on a draft that already exists (hook, ad, mechanism paragraph) to take it from good to great. |
87
88
  | `bot-builder` | Build a custom bot the way Luke and Mario build the Genesis bots (SEAK), and put it where it runs: an Exodus workflow Prompt node, a Genesis bot, or a skill. |
88
89
  | `remember-that` | Bank one reaction mid-session ("remember that", "never do X") into `STANDARDS.md`, and push approved winning lines to the matching Exodus bank. |
@@ -52,7 +52,7 @@ Cheap, one query. It tells you whether this brand already has a primer.
52
52
  - **✗ Not ready** — no primer yet. Proceed to Step 1.
53
53
  - **✓ Ready** — a primer is already saved. **Don't rebuild blind.** Surface it and offer the lighter moves first:
54
54
  - `npx @aicopycoders/exodus primer show` — print the saved primer so the user can see what's there.
55
- - Rebuild only if the user explicitly wants to replace it (they've got better winning ads, the product changed). A rebuild *replaces* the existing primer.
55
+ - Rebuild only if the user explicitly wants a newer one (they've got better winning ads, the product changed). A rebuild adds a new primer and makes it your default; the old one stays in your list.
56
56
 
57
57
  This follows the same "check first, don't re-ask" discipline as the `exodus-foundation` skill. Use the status result to decide what to do — don't open by asking the user where they want to start.
58
58
 
@@ -67,11 +67,16 @@ WHO IT'S FOR:
67
67
  OFFER / PRICE (optional):
68
68
  PROOF / NUMBERS (optional):
69
69
 
70
- WINNING ADS (paste 10, full copy; fewer is OK — tell me if you have none):
71
- --- Ad 1: …
72
- --- Ad 2: …
70
+ WINNING ADS (paste 10, full copy; fewer is OK — tell me if you have none).
71
+ Paste each ad on the lines under its "--- Ad N:" line, and leave that line as it is:
72
+ --- Ad 1:
73
+ (ad 1, full copy)
74
+ --- Ad 2:
75
+ (ad 2, full copy)
73
76
  ```
74
77
 
78
+ Leave nothing after "Ad 1:" — no name or title. The builder names every ad itself, and text on that line would have to be guessed as either a name or the ad's first line.
79
+
75
80
  State plainly that the product facts matter as much as the ads — the winning ads frequently don't name the product, and you will **not** invent it. If the user says they have *no* winning ads, stop here and jump to the **No-Ads Branch** below; don't push them to manufacture examples.
76
81
 
77
82
  ### Step 2 — Assemble the submission
@@ -86,7 +91,7 @@ npx @aicopycoders/exodus primer --file state/primer-submission.md --yes
86
91
 
87
92
  The build reads every ad and writes the full module taxonomy. **The `--yes` is load-bearing in Claude Code:** without it, the final "accept & save" prompt silently no-ops in the non-TTY shell and the primer is never saved (the symptom that forced a `printf 'a\n' |` hack before this flag existed). With `--yes`, the command builds and saves in one shot and prints the same `✓ saved` / `✓ ready` output.
88
93
 
89
- If a primer already exists, `--yes` rebuilds in place (replaces it) rather than prompting — which is what you want when the user has explicitly asked to rebuild.
94
+ If a primer already exists, `--yes` builds and saves without prompting — which is what you want when the user has explicitly asked to rebuild. It adds a new primer and makes it your default; the old one stays in your list.
90
95
 
91
96
  ### Step 4 — Verify
92
97
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: exodus-video
3
- description: Make a video ad with Exodus from the terminal. The main path is script-first: run a saved video workflow with `npx @aicopycoders/exodus workflow run`. You read and approve the storyboard, pull every finished piece (per-scene clips, the narration track and per-scene voice, keyframes, cast identity stills, word timings, the music bed) into a folder with `exodus video pull`, and upload a finished cut for approval. Exodus makes the pieces; the cut is made outside it, in whatever editor the user wants — this skill hands over the pieces and the facts about them, it does not prescribe an editor. Use it whenever the user has invoked Exodus and wants anything to do with a video ad, an ad run's clips or storyboard, "pull the pieces", "stitch it together", "make the cut", "upload my cut", or checking on a video run ("exodus, make a video ad from this script", "exodus, is my video run done", "exodus, pull the clips for run X and cut them", "exodus, what's wrong with scene 3", "exodus video status"). Also use it when the user ran an `npx @aicopycoders/exodus video` command or the `exodus` hub skill routed here. Video is admin-only: "video isn't enabled for this key" means either the dashboard user lacks the admin role or the run id is wrong, so check the id and then say so; do not retry. Never claim generic video-editing asks ("edit this mp4", "add captions to my reel") without Exodus context; in shared folders those belong to the user's other tools. Running or authoring workflows generally is `exodus-workflow`; static image ads are `exodus-image`; copy is `exodus-write`.
3
+ description: Make a video ad with Exodus from the terminal. The main path is script-first: run a saved video workflow with `npx @aicopycoders/exodus workflow run`. You read and approve the storyboard, pull every finished piece (per-scene clips, the narration track and per-scene voice, keyframes, cast identity stills, word timings, the music bed) into a folder with `exodus video pull`, and upload a finished cut for approval. Exodus makes the pieces; the cut is made outside it, in whatever editor the user wants — this skill hands over the pieces and the facts about them, it does not prescribe an editor. Use it whenever the user has invoked Exodus and wants anything to do with a video ad, an ad run's clips or storyboard, "pull the pieces", "stitch it together", "make the cut", "upload my cut", or checking on a video run ("exodus, make a video ad from this script", "exodus, is my video run done", "exodus, pull the clips for run X and cut them", "exodus, what's wrong with scene 3", "exodus video status"). Also use it when the user ran an `npx @aicopycoders/exodus video` command or the `exodus` hub skill routed here. Admins get every video command; beta testers with video switched on get start, status, storyboard, approve, voices, retry-frame, pull and upload on their own brands (see "Beta testers"). "video isn't enabled for this key" means the run id is wrong, the command is admin-only, or video isn't switched on for that dashboard user, so check the id and then say so; do not retry. Never claim generic video-editing asks ("edit this mp4", "add captions to my reel") without Exodus context; in shared folders those belong to the user's other tools. Running or authoring workflows generally is `exodus-workflow`; static image ads are `exodus-image`; copy is `exodus-write`.
4
4
  ---
5
5
 
6
6
  # Video: make the pieces, cut the ad, hand it back
@@ -21,26 +21,82 @@ Your job in this skill is to drive that loop, hand over the pieces with the
21
21
  facts about them, and make whatever cut the user asked for. **How to edit is the
22
22
  user's call, not this skill's** — see "Cutting the ad".
23
23
 
24
+ This file ships on the member's machine. It holds commands, the manifest's
25
+ shape, and recipes for the cut. It does not hold prompt text, house primers,
26
+ judge wording, or model instructions.
27
+
24
28
  ## How a run starts
25
29
 
26
30
  The ad is a saved workflow in the brand, started script-first with `exodus
27
31
  workflow run`. From there the same nodes render, `video status` reads the run,
28
32
  `video pull` writes the folder, and `video upload` and `approve` finish it.
29
33
 
34
+ A Show is no longer required. `exodus video start` starts from a script, a
35
+ conceit, and a style. The same parks follow, except that the app approves the
36
+ storyboard itself when its checks pass (see "What costs money"). The full line is under
37
+ "Starting a run". An optional `--direction` note is whole-ad rules, and that
38
+ note cannot contain a colon. "The script file" shows the right form and the
39
+ wrong one.
40
+
41
+ The Rig box on that workflow is where the run's Conceit and Style come from.
42
+ Conceit is the family plus Wrapper, Narrator Person, and Delivery. Style is the
43
+ look. Narrated Story is the voice-over family and Acted Story is the dialogue
44
+ family (`familyTree.ts`). Those family names stay the code's names.
45
+
30
46
  ## Before anything
31
47
 
32
48
  - `npx @aicopycoders/exodus video --help` and `workflow --help` are the
33
- authoritative flag lists. The `video` verb is admin-only and hidden from the
34
- top-level `--help`; that is expected, not a broken install.
35
- - **Video is admin-only**, and says so two different ways.
36
- - "video isn't enabled for this key", from a `video` verb, has two causes the
37
- CLI cannot tell apart: the user is not an admin on this brand, or the run id
38
- does not exist. Check the id first; if it is right, they need the admin role.
49
+ authoritative flag lists. The `video` verb is hidden from the top-level
50
+ `--help` because most members can't use it; that is expected, not a broken
51
+ install.
52
+ - **Who can use video.** Admins get everything in this file. A beta tester
53
+ (video switched on for their dashboard user) gets a smaller set on their own
54
+ brands, listed under "Beta testers" below. Two refusals:
55
+ - "video isn't enabled for this key", from a `video` verb, has three causes
56
+ the CLI cannot tell apart: the run id does not exist on this brand, the
57
+ command is admin-only, or video isn't switched on for this user. Check the
58
+ id first, then check the command against "Beta testers".
39
59
  - "Video workflows aren't available on your account. Ask an admin if you need
40
- video.", from `workflow run`, is unambiguous — `startRun` refuses any graph
41
- containing a video node for a non-admin (`convex/workflows.ts:6471`).
60
+ video.", from `workflow run`, is unambiguous. `startRun` refuses any graph
61
+ containing a video node for a non-admin, beta testers included. A beta
62
+ tester starts with `exodus video start` instead.
42
63
 
43
64
  No flag or retry fixes either one.
65
+
66
+ ## Beta testers
67
+
68
+ A beta tester's key gets the same video the dashboard's Video page makes, and
69
+ never how it is made. Every answer to them says `"view": "customer"`, and the
70
+ CLI words it the way the run page does. On their own brands they can:
71
+
72
+ - `video start --script <file> --conceit <key> --style <slug>` with
73
+ `--direction`, `--music`, `--review-storyboard` and `--wait`. Any other start
74
+ flag is refused with one sentence naming it. The answer is the run id and the
75
+ link to the video's page.
76
+ - `video status <runId>`: where the video is, in plain words (waiting in line,
77
+ planning, drawing, storyboard ready for review, filming, ready with its link,
78
+ or stopped and why). `--json` prints the server's answer as it came.
79
+ - `video storyboard <runId>`: each scene's line and picture, while the
80
+ storyboard waits for review.
81
+ - `video approve <runId>`: approves the storyboard. Nothing else.
82
+ - `video voices <runId>`: who speaks, their current voice, and the voices on
83
+ the tester's own ElevenLabs account. `--set <character>=<voiceId>` picks one,
84
+ until the storyboard is approved.
85
+ - `video retry-frame <runId> --scene <n>`: redraws one scene's picture while
86
+ the storyboard waits. No `--node`; the server finds the step.
87
+ - `video pull <runId> --out <dir>`: the finished video (`video.<ext>`), each
88
+ scene's clip (`scene-NN.<ext>`) and voice (`scene-NN.voice.<ext>`), and the
89
+ music (`music.<ext>`), with a `manifest.json` holding only `runId`,
90
+ `pulledAt`, `dashboardUrl`, `scenes` (`index`, `clip`, `voice`), `music` and
91
+ `video`. A file that did not download is `null` there. Nothing else in "The
92
+ handover" below applies to them.
93
+ - `video upload <runId> --file <cut>`: attaches their cut and prints the page
94
+ to watch it on.
95
+
96
+ Admin-only, answered "video isn't enabled for this key" (or, for retry-clip and
97
+ revoice, a sentence saying they are for admins): `director`,
98
+ `status --rejected-draft`, `retry-clip`, `revoice`, and the start flags
99
+ `--video-model`, `--voice`, `--voice-path` and `--director`.
44
100
  - Other failures: `npx @aicopycoders/exodus doctor` first, then follow what it prints.
45
101
  - `ffprobe` on the PATH lets `upload` read the cut's length by itself. Without
46
102
  it, `upload` falls back to parsing an MP4's own header, and for anything else
@@ -69,6 +125,20 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
69
125
  exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
70
126
  same, with the video model speaking voices you WROTE
71
127
  exodus video status <runId> where it is, per scene
128
+ exodus video start --script <file> --conceit <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--review-storyboard] [--director] [--wait]
129
+ start from a script, a conceit and a look.
130
+ No music bed unless you pass --music.
131
+ The app approves its own storyboard when its
132
+ checks pass and goes straight on to the clips,
133
+ which is the part that costs. It stops at the
134
+ gate only when a check finds a problem, and says
135
+ why. --review-storyboard always stops there.
136
+ Each conceit has its own video model and voice;
137
+ --video-model / --voice override them for one run.
138
+ --director turns the trial director on (admins only)
139
+ exodus video director <runId> on|off|follow the trial director for one run (admins only);
140
+ follow = the brand's own setting. status shows
141
+ what it is doing and its last three notes
72
142
  ```
73
143
 
74
144
  **From the storyboard on.**
@@ -79,9 +149,10 @@ exodus video voices <runId> who speaks and with wh
79
149
  exodus video voices <runId> --set <character>=<voiceId> give a character a voice
80
150
  exodus video approve <runId> keep going
81
151
  exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
152
+ (a beta tester leaves --node off)
82
153
  exodus video status <runId> where it is, per scene
83
154
  exodus video pull <runId> --out ./ad-<runId> every piece + manifest.json
84
- exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"]
155
+ exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"] [--voice-first]
85
156
  redo one clip at the final watch
86
157
  exodus video revoice <runId> --all | --scene <n> redo only the voice; no new video
87
158
  … make the cut (your choice of tool)
@@ -98,7 +169,15 @@ already printed the same stop block, and `workflow inbox` lists a parked video
98
169
  run, but neither shows the per-scene detail, and the checkpoint verbs cannot
99
170
  resolve a video park — see "What the CLI can and cannot do".
100
171
 
101
- **Where a run can stop.** Five stops, each with the exact next command printed
172
+ **Where a run can stop.** The two video parks come in order when the run has
173
+ both. The storyboard gate is first. You read the cards there and you set the
174
+ voices there. Approving that gate releases the voices and the clips. That is the
175
+ expensive part. The final watch is next, after the pieces exist, and only when
176
+ the video node sets `finalWatch: true`. That park is for the cut. You pull, you
177
+ cut, and you upload. Approving the upload makes it the ad. A node with
178
+ `finalWatch` off skips the second park and the run finishes.
179
+
180
+ Five stops, each with the exact next command printed
102
181
  under it:
103
182
 
104
183
  - storyboard gate: needs `approve`. When the stop block prints a `Redo one
@@ -110,7 +189,11 @@ under it:
110
189
  lines under `guidance`). It is also
111
190
  the one park where a finished clip can be redone, with `retry-clip`. A run
112
191
  only reaches this park if its video node sets `finalWatch: true` — otherwise
113
- it skips straight to finished.
192
+ it skips straight to finished. If a scene has no clip, the stop block names
193
+ it instead (`stop.missingScenes` in `--json`) and approval is refused until
194
+ it has one. A scene whose picture the video service refused needs
195
+ `retry-frame --note` before `retry-clip`; the clip redo alone resends the
196
+ refused picture.
114
197
  - paused: a builder checkpoint neither video verb resolves —
115
198
  `exodus workflow checkpoint <runId>` is the verb for that one.
116
199
  - failed: `status` shows how far it got, and a `--wait` that was running exits
@@ -137,18 +220,31 @@ minutes with "Still running after 60 minutes" and exit 0; that is not done, so
137
220
  check `status` before treating it as finished. Do not poll `status` in a tight
138
221
  loop; a full run is several minutes.
139
222
 
140
- **What costs money.** Starting the run writes the storyboard and then draws the
141
- reference, cast and scene pictures, and only then pauses. Each picture is a few
142
- cents and each scene draws a few to choose from: about 25 cents on a small test,
143
- and it can approach a dollar on a longer ad. Approving releases the expensive
144
- part: the voices and the clips. So a run that has not paused yet is already
145
- spending a little, and a run still drawing pictures is healthy. `exodus workflow
146
- status` and `exodus video status` both print "This run will pause for your
147
- approval once the frames are ready" on a run that is headed for the gate. Do not
148
- cancel it because the pause has not arrived. `workflow list`, `workflow describe`, `video status`,
149
- `storyboard` and `pull` are free reads; run them as often as you like. Never
150
- start a second run to "retry" without the user asking; tell them what failed
151
- instead.
223
+ **What costs money.** Every run writes the storyboard first and then draws the
224
+ reference, cast and scene pictures. Each picture is a few cents and each scene
225
+ draws a few to choose from: about 25 cents on a small test, and it can approach
226
+ a dollar on a longer ad. The expensive part comes after the storyboard: the
227
+ voices and the clips. What happens at that point depends on how the run
228
+ started.
229
+
230
+ - A paste-a-script run (`exodus video start --conceit …`) checks its own
231
+ storyboard once the pictures are drawn. When every check passes, the app
232
+ approves the storyboard itself and goes straight on to the voices and the
233
+ clips, with no stop. It stops for the user only when a check finds a problem,
234
+ and the stop block says why in plain words. Started with
235
+ `--review-storyboard`, it always stops at the storyboard instead. `status`
236
+ prints "the app checks the storyboard and approves it itself if the checks
237
+ pass" on a run headed that way.
238
+ - A Show run (`video start --show`) and a saved workflow run (`workflow run`)
239
+ still pause at the storyboard for approval. Approving is what releases the
240
+ voices and the clips. `status` prints "This run will pause for your approval
241
+ once the frames are ready" on a run headed for that pause.
242
+
243
+ Either way, a run still drawing pictures is healthy and already spending a
244
+ little. Do not cancel it because nothing has happened yet. `workflow list`,
245
+ `workflow describe`, `video status`, `storyboard` and `pull` are free reads; run
246
+ them as often as you like. Never start a second run to "retry" without the user
247
+ asking; tell them what failed instead.
152
248
 
153
249
  **`--auto-approve` does not cover either video gate.** It releases Checkpoint
154
250
  boxes only (`convex/workflows.ts:7266` keys on `pauseReason === "checkpoint"`),
@@ -160,8 +256,8 @@ on its own.
160
256
  ## What the CLI can and cannot do
161
257
 
162
258
  Verified against the CLI and the server, not assumed. Everything here is
163
- admin-gated on top. Five things worth knowing before you promise a user
164
- anything:
259
+ about the admin tools; a beta tester has only what "Beta testers" lists. Five
260
+ things worth knowing before you promise a user anything:
165
261
 
166
262
  - **A workflow only stops for your cut if its video node asks to.** `finalWatch`
167
263
  defaults to `false` (`convex/lib/workflow/catalog.ts:780`), and the run only
@@ -211,11 +307,42 @@ anything:
211
307
 
212
308
  ## The script file
213
309
 
214
- Plain text. A `CAST:` block naming the speakers, one line per turn, then a
215
- `CTA:` block. The planner sizes every line at
216
- 2.5 words per second (`WORD_LADDER_WPS`), so a turn near 14 words plans a clip
217
- near 6 seconds. If a line does get truncated in its clip, the manifest says so
218
- (`speech-cutoff`).
310
+ Plain text. An optional `DIRECTION:` block may open the file, before `CAST:` or
311
+ the first spoken line. Then a `CAST:` block naming the speakers, one line per
312
+ turn, then a `CTA:` block.
313
+
314
+ `DIRECTION:` carries whole-ad rules and no spoken words. The header is its own
315
+ line. The lines under it are the rules. The block ends at a blank line, at
316
+ `CAST:`, at `CTA:`, or at the first line that names a speaker. A second
317
+ `DIRECTION:` block is refused. So is a `DIRECTION:` after the first spoken line.
318
+ An empty block is refused too. The error code is `malformed-direction`.
319
+
320
+ Do not put a colon inside direction prose. The reader splits a line at its first
321
+ colon to find a speaker, so a colon in that prose is read as a name.
322
+
323
+ Right.
324
+
325
+ ```
326
+ DIRECTION:
327
+ Nobody looks at the camera. The product stays on the table until the last line.
328
+ ```
329
+
330
+ Wrong.
331
+
332
+ ```
333
+ DIRECTION:
334
+ Rule: nobody looks at the camera.
335
+ ```
336
+
337
+ The wrong line is not direction. `Rule` is read as the speaker, and the
338
+ block above it is empty, so the file is refused.
339
+
340
+ The planner sizes every line at 2.5 words per second (`WORD_LADDER_WPS`), so a
341
+ turn near 14 words plans a clip near 6 seconds. If a line does get truncated in
342
+ its clip, the manifest says so (`speech-cutoff`). That finding means the clip
343
+ cut the words off. Redo the clip. Do not speed the speech or cut it short to
344
+ hide the finding. The product rule is under "What the product actually
345
+ requires".
219
346
 
220
347
  ```
221
348
  CAST:
@@ -229,6 +356,15 @@ CTA:
229
356
  THE SOFTGEL: Tap below and give your eyes ClearBlink.
230
357
  ```
231
358
 
359
+ The dash in a `CAST:` line is the parser's own separator. The example above
360
+ already shows that dash. A cast line may separate the name from the description
361
+ with an em dash, an en dash, or a hyphen with a space on each side. Keep that
362
+ dash on cast lines. A reader who strips it makes the line fail.
363
+
364
+ Do not use a long dash in words a character says, or in words shown on screen.
365
+ Synthesised speech reads a long dash oddly, and so does burned-in text. A spoken
366
+ line stays `NAME: what they say`.
367
+
232
368
  **How many speakers you may have depends on the rig.** Three by default. A rig
233
369
  that carries saved format rules sets its own number: an ensemble rig seats more,
234
370
  a talking-head rig seats one. Over the number, the launch refuses before
@@ -261,9 +397,13 @@ unattended.
261
397
  By default a workflow run's characters have no voice of their own. A character
262
398
  without one gets the run's default voice (the workflow's narrator voice) when the
263
399
  run has one; with no default either, the clip keeps the voice the video model
264
- invents. `exodus video voices` shows which of the three applies and is where it
265
- gets fixed, and it only works **before** the storyboard is approved — once clips start
266
- it refuses.
400
+ invents. `exodus video voices` shows which of the three applies, and it is where you
401
+ fix the mapping. Fix it before you approve the storyboard gate. Approving that
402
+ gate locks the mapping, and `exodus video voices` refuses afterwards. On a
403
+ finished clip whose voice was never applied, the later command is `exodus video
404
+ revoice`, and that pass bills again. A clip that already went through the voice
405
+ pass is refused there. Use `retry-clip` for that one. See "Redoing only the
406
+ voice on finished clips".
267
407
 
268
408
  ```
269
409
  exodus video voices <runId>
@@ -337,7 +477,7 @@ nobody knows the speaker names yet. When the storyboard arrives, the voices are
337
477
  already set and `exodus video voices <runId>` shows them.
338
478
 
339
479
  **Choosing how the voices are made.** `--voice-treatment` on `workflow run` says
340
- HOW a run makes its voices, and it is the only door. Two
480
+ HOW a run makes its voices, and it is the only way. Two
341
481
  kinds of answer.
342
482
 
343
483
  A name on its own picks a way of working that plays a voice you already have,
@@ -410,7 +550,8 @@ person made at a keyboard for one run, and nobody made it for the sub-workflow.
410
550
  **Written voices cannot be changed at the review.** They are chosen when the run
411
551
  starts. `exodus video voices <runId>` on a described run names the treatment and
412
552
  prints each written voice, and says plainly that `--voice-treatment` at launch is
413
- what changes them. Do not set a voice path in node config — it is not a door.
553
+ what changes them. Do not set a voice path in node config. Node config does
554
+ not set the treatment.
414
555
 
415
556
  **Where it shows up afterwards.** The launch receipt and `exodus workflow status`
416
557
  both print one line naming the treatment and the speakers who got a written
@@ -488,6 +629,20 @@ manifest.json the index below
488
629
  runId, pulledAt, dashboardUrl the run and the page that opens it:
489
630
  /workflows/<workflowId>/runs/<runId>
490
631
  storyboard, reference, music filenames, or null when not delivered
632
+ musicBed on | off | unknown — whether the RUN has a
633
+ music bed, which is not the same question
634
+ as the `music` filename above. "unknown" is
635
+ a run made before the run carried the
636
+ answer; it is not "off"
637
+ musicHeardScenes[] scene numbers whose clip came back with
638
+ music the video model baked in. A run with
639
+ the bed off can still list scenes here, and
640
+ those are the ones to listen to
641
+ pendingRedo[] scene numbers the app is redoing right
642
+ now. Their clip and words are left out,
643
+ because the file on the run is the take
644
+ being replaced: run `exodus video wait`,
645
+ then pull again
491
646
  narration { file, timing }, or null on a per-scene run
492
647
  cast[] one per identity still
493
648
  characterId, name from the run's cast lock; null for an
@@ -520,6 +675,9 @@ scenes[] one per scene, in order
520
675
  on record when the voice pass replaced the
521
676
  clip with a new file; null when the clip IS
522
677
  that file, or there is no clip
678
+ voiceMode "voice-first" when the clip was redone voice
679
+ first (its line recorded, then the clip made
680
+ to match it); null otherwise
523
681
  provenance which saved rulebook this run followed:
524
682
  {format: {via, nodeId, rigId, rigName,
525
683
  rulesFrom, specVersion}}. Absent when the
@@ -542,15 +700,19 @@ depends on the scene; see "Which scenes carry their voice where" below.
542
700
  terms. `script[]` is the ad's spoken words in order — `{lineId, speakerId, text}`,
543
701
  with `speakerId: null` meaning narrator VO. Each scene carries `sceneIndex`,
544
702
  `durationSec`, `dialogue[]`, `voText`, `notes`, and `lineIds` (the script lines
545
- that scene renders). A scene with `kind: "cutaway"` carries `cueLineId` instead:
546
- it is a picture-only takeover of the frame while the spoken track underneath
547
- keeps running, so it adds no audio and takes no place in the running order. The
703
+ that scene renders). A scene with `kind: "cutaway"` carries `cueLineId` and a
704
+ `cutawayType` instead. Most cutaways are a picture-only takeover of the frame
705
+ while the spoken track underneath keeps running, so they add no audio and take
706
+ no place in the running order. A `cutawayType: "reaction"` cutaway is the room
707
+ reacting out loud to its cue line: it is its own short beat after that line,
708
+ heard with its own sound while the speaker is silent. The
548
709
  envelope also carries `overlays[]` (each with a `cueLineId` and a `placement`)
549
710
  and `audio` (`roomTone`, `music`, `sfxCues[]`) — the planner's notes on what
550
711
  should sit on top. The image and motion prompts (`framePrompt`, `videoPrompt`,
551
712
  `referencePrompt`) reach an admin key in full. A caller who cannot use video on
552
- this brand gets them stripped before the file arrives — they are style-pack IP
553
- (`redactVideoIp` in `convex/workflows.ts`).
713
+ this brand gets them stripped before the file arrives. They are private Style
714
+ fragment IP (`redactVideoIp` in `convex/workflows.ts`). Style pack is the
715
+ retired name.
554
716
 
555
717
  **The narration track** (`narration.mp3` + `narration.json`). Present when the
556
718
  run recorded continuously: every narrated scene's `voText` joined into one
@@ -577,9 +739,9 @@ speech inside the clip — under the default voice treatment the video model's
577
739
  invented voice is then replaced, in place, with the cast member's pinned
578
740
  ElevenLabs voice (that is `revoiced: true`). Other treatments leave the render
579
741
  owning its own audio, e.g. lip-sync retargeting a VO track onto the video. The
580
- treatment is frozen on the run when it starts, and there is exactly one door
742
+ treatment is frozen on the run when it starts, and there is exactly one flag
581
743
  that sets it: `--voice-treatment` on `workflow run` (see **Choosing how the
582
- voices are made**). Node config is never a door — nothing on the canvas sets a
744
+ voices are made**). Node config never sets it. Nothing on the canvas sets a
583
745
  voice treatment, and a run that was given none renders the default. Read
584
746
  `wordsFrom` and `revoiced` per scene rather than assuming.
585
747
 
@@ -641,6 +803,11 @@ blocking the run.
641
803
  A `speech-cutoff` on the CTA scene is the one to worry about; the ad's last
642
804
  words are missing. `status` shows the checker's own wording behind each
643
805
  finding; the manifest carries only the verdict.
806
+ - A `pass` verdict is not a review. The checks are often optimistic. Watch the
807
+ frames and listen to the audio before you call the run good. Two checks fail
808
+ the wrong way. A cast-count check can reject a silent
809
+ listener who belongs in the shot. A clip check can flag a clip that is fine.
810
+ Read the finding, then watch the scene.
644
811
  - `clipStatus: "failed"` or `"missing"` with a `keyframe`: `durationSec` is null
645
812
  (it comes from the clip), but the scene can still hold its place as a still —
646
813
  for the length of its voice track, or the storyboard's planned duration when
@@ -658,6 +825,7 @@ is no second run, no new storyboard and no re-render of anything else.
658
825
  exodus video retry-clip <runId> --scene 3
659
826
  exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
660
827
  exodus video retry-clip <runId> --scene 3 --node video-2
828
+ exodus video retry-clip <runId> --scene 3 --voice-first
661
829
  ```
662
830
 
663
831
  `--node` is only needed when a run has clips for that scene on two video steps;
@@ -680,7 +848,17 @@ revoiced and trimmed the same way, with fresh findings and fresh word timings.
680
848
  scene's motion prompt only (`withRedoDirection`,
681
849
  `scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
682
850
  the approved storyboard. Asking for different words in a note will not change
683
- them. If the words are wrong, that is a new run.
851
+ them. If the script's words are wrong, that is a new run.
852
+
853
+ **`--voice-first` fixes a clip that said its line wrong** (#2299). It is for a
854
+ clip whose findings include `speech-mismatch`, `speech-repeat`,
855
+ `speech-after-line`, `speech-extra-words` or `speech-long-pause`. The scene's line is recorded in each
856
+ speaker's pinned ElevenLabs voice, and MiniMax (`minimax-h3/reference-to-video`)
857
+ makes the clip to match the recording. Only that clip changes; the word check
858
+ and the other checks still run on it. It needs an ElevenLabs key and a voice for
859
+ every speaker in the scene, and it is refused for a scene with no spoken line.
860
+ The clip then reads `clip: redone voice first` in `status`, carries
861
+ `voiceMode: "voice-first"` in the manifest, and is listed in the `pull` summary.
684
862
 
685
863
  **A redo whose take fails the checks keeps the original clip.** On a finished
686
864
  clip the row stays `done` with the ORIGINAL take and only its findings are
@@ -767,7 +945,10 @@ they want; if they have not said and are not around, say which you used and why.
767
945
  - **HyperFrames** — HTML/CSS compositions rendered to video. Suits an ad that
768
946
  needs captions, title cards, overlays or motion graphics laid over the clips.
769
947
  It is a separate skill pack; check that a `hyperframes` skill is installed
770
- before offering it.
948
+ before offering it. The cut is made outside Exodus. `exodus video pull` is the
949
+ handover. The folder it writes is what the next tool reads. When a HyperFrames
950
+ skill pack is installed, that pack is the route for an HTML and CSS
951
+ composition. Do not look for a stitch inside Exodus.
771
952
  - **An NLE over MCP** — DaVinci Resolve publishes an MCP server, so a timeline
772
953
  can be built in a real editor and the user can carry on by hand afterwards.
773
954
  Suits a user who wants to keep editing after you stop. Check what MCP servers
@@ -789,6 +970,13 @@ Read these out of `storyboard.json` before you build anything, in any tool:
789
970
  running order. To find the moment, locate the A-roll scene whose `lineIds`
790
971
  contain the cue line, then read the start time of the first word of that line
791
972
  out of that scene's `words.json`.
973
+ - **Reactions.** A cutaway with `cutawayType: "reaction"` is the exception. Do
974
+ not lay it over the speech. Put it into the running order as its own beat
975
+ right after its cue line ends, play its own sound (the audience laughing),
976
+ keep it to about 1.5 s, then carry on with the speaker. Nobody talks during
977
+ it. When more lines follow the cue line in the same scene, cut that scene
978
+ where the cue line's last word ends in its `words.json` and put the reaction
979
+ between the two halves.
792
980
  - **Overlays and SFX.** `overlays[]` and `audio.sfxCues[]` are cued the same
793
981
  way, by `cueLineId`. Nothing renders them for you.
794
982
  - **Captions.** Nothing is burned into any clip. Captions belong in post; the
@@ -805,31 +993,52 @@ the pieces:
805
993
  holds its last frame for the difference. The narration is never sped up,
806
994
  slowed down or cut short, so every scene plays at the speed it was recorded,
807
995
  and the word times are handed on as they were measured.
996
+ - **It closes the dead air at the joins.** A clip that plays its own dialogue
997
+ keeps about 0.15 s after its last word and about 0.1 s before its first (0.25 s
998
+ when a different speaker answers), read from its `words.json`, so one clip
999
+ runs into the next like one conversation. It only ever shortens a clip's
1000
+ silence and never cuts into a word. The ad's first clip also opens about 0.1 s
1001
+ before its first word, so a hum a clip makes before anyone speaks is not the
1002
+ first thing heard. The ad's last tail, a narrated scene, a still and a
1003
+ reaction beat keep their length.
1004
+ - **It fades the audience reaction in and out.** A reaction beat fades in
1005
+ across any quiet start before its laugh (a generated laugh clip can hum
1006
+ there). When a line follows, the picture moves on early and the laugh's own
1007
+ remaining sound plays on under that line, quieter and fading out: up to
1008
+ 0.75 s, and the beat keeps at least 1 s of picture (a 1.5 s clip shows 1 s
1009
+ and carries its last 0.5 s). A beat that ends the ad, or has under 0.4 s to
1010
+ carry, fades out over its last 0.4 s instead.
808
1011
  - **It ignores the continuous master.** It reads only `scene-NN.voice` files;
809
1012
  it never opens `narration.mp3` or `manifest.narration`. A tool that lays the
810
1013
  master down whole and cuts the pictures to it keeps the read intact.
811
- - Otherwise: A-roll in manifest order as the spine, each cutaway laid over the
812
- spine at its cued word (falling back to a proportional estimate when the scene
813
- has no word times, and saying which method it used), a narrated scene's voice
1014
+ - Otherwise: A-roll in manifest order as the spine, each reaction put in as its
1015
+ own beat of at most 1.5 s right after its cue line with its own sound, each
1016
+ other cutaway laid over the spine at its cued word (falling back to a
1017
+ proportional estimate when the scene has no word times, and saying which
1018
+ method it used), a narrated scene's voice
814
1019
  replacing whatever the clip recorded, a dialogue scene keeping its performed
815
1020
  line, a clip-less scene becoming a still, everything normalized to 1080×1920
816
1021
  at 24 fps and −16 LUFS per segment, a generated room-tone bed under the whole
817
1022
  ad, and the music bed underneath at low level. Output is H.264 + AAC in an MP4
818
1023
  with `-movflags +faststart`.
819
1024
  - Flags: `--out <file>`, `--skip 2,5`, `--no-music`. It prints what went in,
820
- what was left out and why, and the upload command.
1025
+ what was left out and why, and the upload command. When a clip hums or
1026
+ clicks where nobody speaks at an edge the cut plays, it lists each one under
1027
+ "Sound at the edges" with its time in the cut. That is a warning to listen
1028
+ there; the cut is made anyway.
821
1029
  - Beside the MP4 it writes `cut.words.json` (every spoken word of the finished
822
1030
  ad with its start and end) and `cut.srt` (the same words grouped into subtitle
823
1031
  lines), named from the `--out` basename. Those times are on the finished ad's
824
1032
  timeline, each scene's own word times offset by where that scene starts in the
825
- cut and otherwise unaltered. Scenes with no word times leave
1033
+ cut, less any lead-in trimmed at the join, and otherwise unaltered. Scenes with no word times leave
826
1034
  gaps and are named in the report; when no scene delivered any, neither file is
827
1035
  written. These sidecars are the script's own output — another tool will not
828
1036
  produce them.
829
1037
 
830
1038
  Before uploading anything, from any tool, check what you made:
831
1039
  `ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 cut.mp4`
832
- should be close to the sum of the scene durations you kept. A cut that is a few
1040
+ should be close to the sum of the scene durations you kept (the bundled
1041
+ script's cut runs somewhat shorter, by the silence it trimmed at the joins). A cut that is a few
833
1042
  seconds long when five clips went in means a filter dropped inputs; do not
834
1043
  upload it.
835
1044
 
@@ -864,6 +1073,11 @@ lines, re-recording narration, or cutting words out of the CTA will ship,
864
1073
  silently. Cut the pictures to the words, not the words to the pictures. If the
865
1074
  words are wrong, that is a new run, not an edit.
866
1075
 
1076
+ Never stretch speech, speed it up, slow it down, or cut it short so it fits a
1077
+ clip. That is a product rule, and it holds for every tool that makes the cut,
1078
+ including the bundled script. If the audio does not fit, redo the clip with
1079
+ `exodus video retry-clip`. Do not repair the fit in the editor.
1080
+
867
1081
  ## Reporting to the user
868
1082
 
869
1083
  Lead with where the run is and what it needs from them. Then, when you pulled
@@ -0,0 +1,27 @@
1
+ // #2988: the types of the one export the Trigger worker calls. The script is
2
+ // plain JS so the CLI can run it with `node` and no build.
3
+
4
+ export class FirstCutError extends Error {
5
+ constructor(code: 1 | 2, message: string);
6
+ readonly code: 1 | 2;
7
+ }
8
+
9
+ export interface FirstCutOptions {
10
+ /** A folder written by `exodus video pull`. */
11
+ dir: string;
12
+ /** Defaults to <dir>/cut.mp4. */
13
+ out?: string;
14
+ skip?: Set<number>;
15
+ /** The music bed, when the pull has one. Defaults to on, as the CLI does. */
16
+ music?: boolean;
17
+ }
18
+
19
+ export interface FirstCut {
20
+ out: string;
21
+ durationSec: number;
22
+ /** The music file the cut used, or null. */
23
+ music: string | null;
24
+ timeline: { spine: Array<{ sceneIndex: number }>; left: string[] };
25
+ }
26
+
27
+ export function firstCut(options: FirstCutOptions): FirstCut;