@aicopycoders/exodus 2026.6.2700-beta.3 → 2026.6.2900-beta.4

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: exodus-genesis
3
- description: Run the standalone Exodus Genesis pipeline — Luke's full writing process across two voices (MarioBot + Infeed VSL) on the brand primer. The default is one pass per bot = 2 variants (1 Mario × Brand + 1 Infeed × Brand); the user can run more passes to widen coverage and bring in the Top-Ads-Biased primer. Includes the per-variant editing menu (Natural Language / Shorten / Cut / Simplify / Make Better). `--reel` also writes ads on the spot from a pasted Instagram/TikTok reel — it transcribes the reel into an idea and writes it through this same writer (just another input, nothing custom); to COLLECT reels/ideas into a bank to curate before writing, that's the `exodus-idea` skill. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("run the exodus genesis pipeline", "exodus, run another pass", "exodus, make me ads from this reel"), named this skill or /exodus-genesis, ran an `npx exodus` command, or the `exodus` hub skill routed here. Never claim generic requests ("run the Genesis pipeline", "write ads from this reel") — 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 — never hijack their direct Genesis workflows into this pipeline.
3
+ description: Run the standalone Exodus Genesis pipeline — Luke's full writing process. Hooks come from the dedicated new-hook-bot (a single ~10-hook pool); the human hook gate then lets the user review and pick those hooks RIGHT IN CLAUDE CODE (the primary venue; dashboard is ad-hoc) — each pick becomes one ad — or auto-pick and write straight through. Body copy is written across two voices (MarioBot + Infeed VSL) on the brand primer; in auto mode the default is one pass per bot = 2 variants and more passes widen coverage / bring in the Top-Ads-Biased primer (passes don't apply in manual mode). Includes the per-variant editing menu (Natural Language / Shorten / Cut / Simplify / Make Better). `--reel` also writes ads on the spot from a pasted Instagram/TikTok reel — it transcribes the reel into an idea and writes it through this same writer (just another input, nothing custom); to COLLECT reels/ideas into a bank to curate before writing, that's the `exodus-idea` skill. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("run the exodus genesis pipeline", "exodus, run another pass", "exodus, make me ads from this reel"), named this skill or /exodus-genesis, ran an `npx exodus` command, or the `exodus` hub skill routed here. Never claim generic requests ("run the Genesis pipeline", "write ads from this reel") — 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 — never hijack their direct Genesis workflows into this pipeline.
4
4
  ---
5
5
 
6
6
  ```operator-guide
@@ -19,14 +19,21 @@ Optional:
19
19
  Returns:
20
20
  Google Doc with one tab per variant (Headlines + Hook + Body Copy)
21
21
  Per-variant editing menu surfaced on the dashboard run page
22
+
23
+ Hook gate (manual mode — review hooks in Claude Code):
24
+ on pause the run prints a numbered hook pool, then:
25
+ genesis continue --id <runId> --hooks 1,3,5 write the picked hooks (one ad each)
26
+ genesis regenerate --id <runId> [--steering "…"] re-roll the pool (reject + steer)
27
+ genesis hooks --id <runId> re-print the pool (fresh-session resume)
22
28
  ```
23
29
 
24
30
  # Genesis — Brief + Seeds → Mario + Infeed Variants → Editing Menu
25
31
 
26
32
  Luke's full Ad Writing Process end-to-end:
27
33
 
28
- - Two parallel hook voices (MarioBot + InfeedVSL) producing ~40 hooks total
29
- - Body-copy variants in two voices, **scaled by passes**:
34
+ - Hook generation from the dedicated **new-hook-bot** (single source, ~10 strategically
35
+ ordered hooks) — the pool the user reviews at the hook gate
36
+ - Body-copy variants in two voices, **scaled by passes** (auto mode):
30
37
  - **Mario × Brand primer** — established brand voice
31
38
  - **Infeed × Brand primer** — InfeedVSL voice on the same brand reference
32
39
  - **Top-Ads-Biased** — primed with the brand's actual top performers (CTR × spend),
@@ -41,9 +48,11 @@ How Much Coverage* below).
41
48
 
42
49
  > If the user hasn't named the pipeline specifically and just wants Exodus to "write some ads,"
43
50
  > start from the `exodus-write` skill — it walks brand → foundation → brief → here, and it owns the passes menu.
44
- > This skill owns the run once the brief, awareness, and pass count are settled. If the user
45
- > invoked Genesis directly and didn't say how many, surface the same quick passes menu (default
46
- > 1 pass = 2 variants, recommended) before firing — don't silently assume a big run.
51
+ > This skill owns the run once the brief, awareness, and (in auto mode) pass count are settled.
52
+ > **In manual mode the passes question disappears** — the ad count equals the number of hooks the
53
+ > user picks, so don't ask "how many passes?" Passes only apply to **auto** mode (default 1 pass =
54
+ > 2 variants, recommended); if the user is in auto and didn't say how many, surface the quick passes
55
+ > menu before firing — don't silently assume a big run.
47
56
 
48
57
  ## Workflow
49
58
 
@@ -95,37 +104,77 @@ Save to `/tmp/genesis-seeds.txt` (one per line, `#` comments allowed), or pass `
95
104
  # Resolve hook mode FIRST (see "Hook selection mode" below) — these commands
96
105
  # assume a saved default exists, else add --stop-at-hooks or --auto-hooks or they error.
97
106
 
98
- # Default — 1 pass per bot = 2 variants (1 Mario × Brand + 1 Infeed × Brand):
99
- npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level>
100
-
101
- # More coverage — 2 passes = 4 variants (adds the Top-Ads-Biased pass):
102
- npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --passes 2
103
-
104
- # With seeds:
105
- npx @aicopycoders/exodus genesis run --brief "<brief>" --seeds /tmp/genesis-seeds.txt --awareness <level> --passes 2
106
-
107
- # Manual hook selection — pause after hook generation:
107
+ # Manual (default) — pause so the user picks hooks IN CLAUDE CODE; one ad per pick.
108
+ # Passes do NOT apply in manual mode (ad count = hooks chosen):
108
109
  npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --stop-at-hooks
109
110
 
110
- # Force auto-select (overrides a saved "manual" preference):
111
+ # Auto — auto-pick the top N hooks (N = passes default) and write straight through:
111
112
  npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --auto-hooks
113
+
114
+ # Auto, more coverage — 2 passes = 4 variants (adds the Top-Ads-Biased pass):
115
+ npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --auto-hooks --passes 2
116
+
117
+ # With seeds:
118
+ npx @aicopycoders/exodus genesis run --brief "<brief>" --seeds /tmp/genesis-seeds.txt --awareness <level> --stop-at-hooks
112
119
  ```
113
120
 
114
- `--brief` accepts inline text or a file path. `--passes <n>` (1–5) is the friendly knob; `--variants <n>` (1–10) is an advanced raw-total override that wins if both are given.
121
+ `--brief` accepts inline text or a file path. `--passes <n>` (1–5) is the friendly knob (auto mode only); `--variants <n>` (1–10) is an advanced raw-total override that wins if both are given.
115
122
 
116
123
  **Hook selection mode — REQUIRED gate before EVERY run (do not skip):**
117
- The pipeline can pause after hook generation so the user picks which hooks become body copy — the human quality gate. You MUST resolve the mode **before** firing a run. A `genesis run` with **no** `--stop-at-hooks`/`--auto-hooks` **and** no saved preference now **hard-errors** (`Hook-selection preference not set`) **by design** — so the gate can't be silently skipped. If you see that error, you skipped this step; do it now.
124
+ The pipeline can pause after hook generation so the user picks which hooks become body copy — the human quality gate, and **reviewing those hooks right here in Claude Code is the primary venue** (the dashboard is an ad-hoc escape hatch). You MUST resolve the mode **before** firing a run. A `genesis run` with **no** `--stop-at-hooks`/`--auto-hooks` **and** no saved preference now **hard-errors** (`Hook-selection preference not set`) **by design** — so the gate can't be silently skipped. If you see that error, you skipped this step; do it now.
118
125
 
119
126
  Resolve it in this order, every time, BEFORE you run:
120
127
  1. **Check the saved default:** run `npx @aicopycoders/exodus genesis hook-pref` (no argument). It prints `manual`, `auto`, or `unset` on the first line.
121
- 2. **If it prints `manual` or `auto`:** just fire the run — the saved default applies, no flag needed.
122
- 3. **If it prints `unset`:** STOP and ask the user once, plainly — *"Want to choose the hooks yourself before I write the copy, or have me auto-pick and write straight through?"* Then fire the run with the matching flag (`--stop-at-hooks` = they choose, `--auto-hooks` = auto), and after it starts, offer to save it as their default so you never ask again: `npx @aicopycoders/exodus genesis hook-pref <manual|auto>`. **Do NOT fire the run before they answer.**
128
+ 2. **If it prints `manual` or `auto`:** just fire the run — the saved default applies, no flag needed. (`manual` = review the hooks here in Claude Code.)
129
+ 3. **If it prints `unset`:** STOP and ask the user once — **mode only, two options, in-Claude-Code first:**
130
+ > ① **Show me the hooks here — I'll pick** (review in Claude Code) · ② **Just write the ads — auto-pick the hooks**
131
+ > *(Prefer the dashboard? Say so and I'll give you the link.)*
132
+
133
+ Don't ask "how many passes?" in this question — that only matters in auto mode. Fire the run with the matching flag (`--stop-at-hooks` = ①, `--auto-hooks` = ②). **Do NOT fire the run before they answer.** Then, **after** the run kicks off (a separate step), ask **once** whether to save it as the default so you never ask again: `npx @aicopycoders/exodus genesis hook-pref <manual|auto>`. (Only `manual`/`auto` are savable — never save "dashboard".)
123
134
 
124
135
  Flags:
125
- - **`--stop-at-hooks`** — pause after hook generation. The run lands in `awaiting_hook_selection` and the CLI prints a dashboard URL; the user picks hooks on the run-detail page and the pipeline continues automatically to body copy.
126
- - **`--auto-hooks`** — auto-pick the strongest hooks and write straight through (also overrides a saved "manual" default for this one run).
136
+ - **`--stop-at-hooks`** — manual: pause after hook generation. The run lands in `awaiting_hook_selection` and **the CLI prints the hook pool as a numbered list** (see *Manual hook selection* below). Picking resumes the pipeline to body copy; rejecting re-rolls the pool (`regenerate`).
137
+ - **`--auto-hooks`** — auto: pick the top N hooks in bot order and write straight through (also overrides a saved "manual" default for this one run). Stay silent — just deliver the Doc.
138
+
139
+ ### 3b. Manual hook selection — review and pick IN CLAUDE CODE
140
+
141
+ When a manual run pauses, the `run` command prints the generated pool as a flat numbered list. Surface it to the user **verbatim — hook text only, numbered.** Do **not** add ratings, tags, voice labels, or your own opinion about which hooks are best; the user picks.
142
+
143
+ ```
144
+ ⏸ Paused for hook selection (10 hooks).
145
+
146
+ 1. <hook text>
147
+ 2. <hook text>
148
+ …
149
+ ```
150
+
151
+ Then the user does one of two things — **pick** or **re-roll**:
152
+
153
+ **A. Pick the hooks to write:**
154
+ 1. Ask the user which hooks to write — **by number ("1, 3, 5") or in natural language** ("the first one and the curiosity one about cortisol"). Resolve any natural-language reference to the actual hook numbers yourself.
155
+ 2. Each pick becomes one ad. If the user picks **more than 10**, warn and confirm before continuing — the pipeline caps at 10 and would drop the rest; don't let that happen silently.
156
+ 3. Write the chosen hooks:
157
+ ```bash
158
+ npx @aicopycoders/exodus genesis continue --id <runId> --hooks 1,3,5
159
+ ```
160
+ This resumes the run (one ad per hook) and polls to the Doc.
161
+
162
+ **B. Reject the pool and re-roll (regenerate):** when the user says "these all suck, try again" — optionally with direction ("…and lead with the cost, not the shame"):
163
+ ```bash
164
+ npx @aicopycoders/exodus genesis regenerate --id <runId> --steering "lead with the cost, not the shame"
165
+ ```
166
+ - Drop `--steering` entirely for a plain "just try again" — it re-rolls on the brand steering alone.
167
+ - The whole pool is **replaced** with a fresh set, the run re-pauses, and the new numbered list prints. Surface it the same way (verbatim, hook text only) and loop back to A or B. **Unlimited rounds** — keep re-rolling until the user picks or walks away.
168
+ - Steering is **additive across rounds**: round 2's "punchier" stacks on round 1's "lead with the cost" — pass only the *new* direction each time; the backend accumulates.
169
+ - If the user references a specific hook ("**7** is the closest but too aggressive"), **resolve that number to the real hook text yourself** and fold it into the steering you pass — e.g. `--steering "build on 'Your cortisol is wrecking your sleep' but soften the aggression"`. The CLI sends steering as plain text; the bot never sees your numbering.
170
+
171
+ **Fresh session / lost the list?** Re-fetch and reprint the pool any time with:
172
+ ```bash
173
+ npx @aicopycoders/exodus genesis hooks --id <runId>
174
+ ```
175
+ A paused run resumes cleanly from a cold session this way — fetch the pool, then pick (`continue`) or re-roll (`regenerate`).
127
176
 
128
- > "Genesis run started at problem-aware — 1 pass per bot (2 variants: Mario × Brand + Infeed × Brand). I'll surface the Doc when it lands."
177
+ > "Genesis paused with 10 hooks — here they are. Tell me which to write (numbers or describe them), or say the word and I'll re-roll the whole set."
129
178
 
130
179
  Status check (only if user asks): `npx @aicopycoders/exodus status --id <runId> --type genesis` — `--type genesis` required.
131
180
 
@@ -161,7 +210,9 @@ What still needs *your* judgment after the run: **verify any statistics, studies
161
210
 
162
211
  ## Common Failure Modes
163
212
 
164
- - **In-feed-vsl-bot fails** — InfeedVSL hook generation degrades to Mario-only; the Infeed variant still runs if recovered. Note in report.
213
+ - **new-hook-bot fails** — hooks now come from a single source (no two-lane fallback), so a hook-gen failure fails the run cleanly with no pool. Re-run; if it persists, it's a Tool Feedback item.
214
+ - **Empty hook bank** — if the brand's hook primer is empty the pool is generated from the brief alone (weaker hooks). Surface it so the user knows to fill the foundation.
215
+ - **In-feed-vsl-bot fails** — affects **body copy** only (Phase 2): the Infeed variant degrades to Mario-only; the Infeed variant still runs if recovered. Note in report.
165
216
  - **Top-ads primer empty (2+ passes)** — the 2nd pass's two variants fall back to the brand primer. Pipeline completes; flag it.
166
217
  - **Pipeline timeout** — rare, and only realistic on a large pass count. Check run-detail for partial variants; trigger an editing pass from the UI to recover usable output.
167
218
  - **`status` 400 "Invalid generation ID format"** — missing `--type genesis`.
@@ -108,31 +108,37 @@ Awareness picks the primer; never mix buckets in one run. Detail in `references/
108
108
 
109
109
  ---
110
110
 
111
- ## Stage C — Pick the pass count, then run Genesis
111
+ ## Stage C — Resolve the hook gate, then run Genesis
112
112
 
113
- Genesis writes in two voices — **MarioBot** and the **Infeed VSL bot**. The default is **one pass per bot = 2 variants** (1 Mario × Brand + 1 Infeed × Brand): the fastest read on both voices against the same brief. More passes widen coverage and bring in the Top-Ads-Biased primer.
113
+ **First, resolve the hook-selection gate — it decides the whole shape of the run** (the `exodus-genesis` skill's "Hook selection mode" step). Run `npx @aicopycoders/exodus genesis hook-pref`:
114
114
 
115
- How many passes is the user's call, not yours — so **menu it** (one AskUserQuestion call) rather than assuming. Lead with the recommended default:
115
+ - **`manual`** — the user reviews and picks the hooks **right here in Claude Code** (the primary venue). The run pauses, the CLI prints a numbered hook pool, the user picks, and each pick becomes one ad. **Passes do not apply — ad count = hooks chosen — so skip the passes menu entirely.** Fire with `--stop-at-hooks`.
116
+ - **`auto`** — auto-pick the top hooks and write straight through. Passes apply here (see below). Fire with `--auto-hooks`.
117
+ - **`unset`** — STOP and ask **mode only, two options, in-Claude-Code first**: *① Show me the hooks here — I'll pick · ② Just write the ads — auto-pick the hooks. (Prefer the dashboard? I'll give you the link.)* Then fire with the matching flag, and **after** the run kicks off, offer **once** to save it as the default (`genesis hook-pref <manual|auto>` — never "dashboard"). A run with no flag and no saved default hard-errors by design — don't skip this.
118
+
119
+ **Passes (auto mode only).** Genesis writes body copy in two voices — **MarioBot** and the **Infeed VSL bot**. The default is **one pass per bot = 2 variants** (1 Mario × Brand + 1 Infeed × Brand). How many passes is the user's call — **menu it** (one AskUserQuestion call) rather than assuming, but only when the user chose auto:
116
120
 
117
121
  - **1 pass — 2 variants (recommended)** — one Mario, one Infeed, on the brand primer. Fast.
118
122
  - **2 passes — 4 variants** — adds a Mario + Infeed pair on the brand's top-performers primer.
119
123
  - **3+ passes — 6+ variants** — a wider sprint; "Other" takes a custom number.
120
124
 
121
- Skip the menu only if the user already said how many ("just give me two", "run a big batch"). Awareness you do *not* menu — that's your strategist call from Stage B; state it and move.
122
-
123
- Hand off to the `exodus-genesis` skill — it owns the run (double-pass hooks, the built-in 15-point QA, the editing menu). Always background it; even the default run is ~12–15 min, past the 10-min foreground cap.
125
+ Skip the passes menu if the user already said how many, or whenever the mode is manual. Awareness you do *not* menu — that's your strategist call from Stage B; state it and move.
124
126
 
125
- **Before firing, resolve the hook-selection gate** (the `exodus-genesis` skill's "Hook selection mode" step): run `npx @aicopycoders/exodus genesis hook-pref` — if it prints `unset`, ask the user *"choose the hooks yourself, or auto-pick and write straight through?"* and pass `--stop-at-hooks` or `--auto-hooks` accordingly (offer to save the choice). A run with no flag and no saved default hard-errors by design — don't skip this.
127
+ Hand off to the `exodus-genesis` skill — it owns the run (the built-in 15-point QA, the in-Claude-Code hook review, the editing menu). Always background it; even the default run is ~12–15 min, past the 10-min foreground cap.
126
128
 
127
129
  ```bash
128
- # Default — 1 pass per bot = 2 variants:
129
- npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level>
130
- # More coverage — 2 passes = 4 variants:
131
- npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --passes 2
130
+ # Manual — review & pick hooks in Claude Code (one ad per pick; no passes):
131
+ npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --stop-at-hooks
132
+ # Auto — auto-pick and write straight through (default 1 pass = 2 variants):
133
+ npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --auto-hooks
134
+ # Auto, more coverage — 2 passes = 4 variants:
135
+ npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --auto-hooks --passes 2
132
136
  # with seeds:
133
- npx @aicopycoders/exodus genesis run --brief "<brief>" --seeds /tmp/genesis-seeds.txt --awareness <level> --passes 2
137
+ npx @aicopycoders/exodus genesis run --brief "<brief>" --seeds /tmp/genesis-seeds.txt --awareness <level> --stop-at-hooks
134
138
  ```
135
139
 
140
+ When a manual run pauses, drive the in-Claude-Code pick loop from the `exodus-genesis` skill (§3b): print the numbered pool verbatim, let the user pick by number or natural language, then `genesis continue --id <runId> --hooks 1,3,5`.
141
+
136
142
  > "Genesis run started at problem-aware — 1 pass per bot (2 variants: Mario × Brand + Infeed × Brand). I'll surface the Doc when it lands."
137
143
 
138
144
  Quality is enforced inside the pipeline (Cleanup + QA — see the `exodus-genesis` skill): markdown stripped, bot commentary removed, body copy 700–1500 words, no hook repetition, clean 14pt Google Doc. You don't hand-clean output.
@@ -20,6 +20,18 @@ export interface GenesisBody {
20
20
  }
21
21
  export declare function buildBriefBody(brief: string, opts: GenesisOpts): GenesisBody;
22
22
  export declare function buildPasteBody(text: string, opts: GenesisOpts): GenesisBody;
23
+ export declare const VARIANT_CAP = 10;
24
+ export declare function parseHookSelection(raw: string): number[];
25
+ export declare function buildContinueBody(runId: string, selection: number[]): {
26
+ runId: string;
27
+ selectedHookIndices: number[];
28
+ };
29
+ export declare function exceedsVariantCap(count: number): boolean;
30
+ export declare function buildRegenerateBody(runId: string, steering?: string): {
31
+ runId: string;
32
+ steering?: string;
33
+ };
34
+ export declare function formatHookPool(hooks: string[]): string;
23
35
  export interface SwipeRow {
24
36
  _id?: string;
25
37
  brandName?: string;
@@ -37,6 +37,7 @@ Source (choose one):
37
37
  Options:
38
38
  --steering "<text>" With --swipe-url: what to model from the ad (angle,
39
39
  tone, structure) — folded into the brief as direction.
40
+ With regenerate: re-roll guidance for the hook pool.
40
41
  --seeds <file|"text"> Per-run creative seeds (brief mode). File: one per
41
42
  line OR a JSON array. Inline: a single seed.
42
43
  --awareness <level> unaware | problem-aware (default) | solution-aware | product-aware
@@ -121,8 +122,45 @@ export function buildPasteBody(text, opts) {
121
122
  body.variantCount = opts.variantCount;
122
123
  if (opts.adAccountId)
123
124
  body.adAccountId = opts.adAccountId;
125
+ if (typeof opts.stopAtHooks === "boolean")
126
+ body.stopAtHooks = opts.stopAtHooks;
124
127
  return body;
125
128
  }
129
+ export const VARIANT_CAP = 10;
130
+ export function parseHookSelection(raw) {
131
+ const tokens = raw
132
+ .split(/[\s,]+/)
133
+ .map((t) => t.trim())
134
+ .filter((t) => t.length > 0);
135
+ if (tokens.length === 0) {
136
+ throw new Error("No hooks selected. Give the numbers to write, e.g. --hooks 1,3,5");
137
+ }
138
+ const nums = tokens.map((t) => {
139
+ if (!/^\d+$/.test(t)) {
140
+ throw new Error(`"${t}" is not a hook number. Pick from the printed list, e.g. --hooks 1,3,5`);
141
+ }
142
+ const n = parseInt(t, 10);
143
+ if (n < 1) {
144
+ throw new Error(`Hook numbers start at 1 (got "${t}").`);
145
+ }
146
+ return n;
147
+ });
148
+ return nums;
149
+ }
150
+ export function buildContinueBody(runId, selection) {
151
+ const indices = Array.from(new Set(selection.map((n) => n - 1))).sort((a, b) => a - b);
152
+ return { runId, selectedHookIndices: indices };
153
+ }
154
+ export function exceedsVariantCap(count) {
155
+ return count > VARIANT_CAP;
156
+ }
157
+ export function buildRegenerateBody(runId, steering) {
158
+ const trimmed = typeof steering === "string" ? steering.trim() : "";
159
+ return trimmed ? { runId, steering: trimmed } : { runId };
160
+ }
161
+ export function formatHookPool(hooks) {
162
+ return hooks.map((h, i) => ` ${i + 1}. ${h}`).join("\n");
163
+ }
126
164
  export function resolveSwipeText(swipe) {
127
165
  const transcript = typeof swipe.transcript === "string" ? swipe.transcript.trim() : "";
128
166
  if (transcript)
@@ -229,6 +267,12 @@ export async function run(flags) {
229
267
  return runScrape(flags, cc);
230
268
  if (sub === "hook-pref")
231
269
  return runHookPref();
270
+ if (sub === "continue")
271
+ return runContinue(flags, cc);
272
+ if (sub === "regenerate")
273
+ return runRegenerate(flags, cc);
274
+ if (sub === "hooks")
275
+ return runHooks(flags);
232
276
  const action = resolveGenesisAction(flags);
233
277
  const noWait = flags["wait"] === false || flags["no-wait"] === true;
234
278
  switch (action.kind) {
@@ -301,7 +345,7 @@ async function runFromBank(ideaId, opts, rt) {
301
345
  }
302
346
  async function runReel(urls, opts, rt) {
303
347
  console.log(`Transcribing ${urls.length === 1 ? "the reel" : `${urls.length} reels`} → idea → Genesis…`);
304
- const result = await captureReelAndWrite(urls, { awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount }, { cc: rt.cc });
348
+ const result = await captureReelAndWrite(urls, { awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount, stopAtHooks: opts.stopAtHooks }, { cc: rt.cc });
305
349
  for (const f of result.bankedFailed) {
306
350
  console.error(` ✗ couldn't pull an idea from ${f} (private, region-locked, or no transcript?)`);
307
351
  }
@@ -322,7 +366,7 @@ async function runReel(urls, opts, rt) {
322
366
  }
323
367
  async function runSwipeUrl(urls, opts, steering, rt) {
324
368
  console.log(`Swiping ${urls.length === 1 ? "the ad" : `${urls.length} ads`} → idea → Genesis…`);
325
- const result = await captureSwipeAndWrite(urls, { awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount, steering }, { cc: rt.cc });
369
+ const result = await captureSwipeAndWrite(urls, { awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount, steering, stopAtHooks: opts.stopAtHooks }, { cc: rt.cc });
326
370
  for (const f of result.bankedFailed) {
327
371
  console.error(` ✗ couldn't pull an idea from ${f} (inactive, image-only, or not in the Ad Library index?)`);
328
372
  }
@@ -433,10 +477,18 @@ async function waitForGenesisRun(runId) {
433
477
  });
434
478
  console.log();
435
479
  if (result.data["status"] === "awaiting_hook_selection") {
480
+ const pool = Array.isArray(result.data["hookPool"])
481
+ ? result.data["hookPool"]
482
+ : [];
436
483
  const dashboardUrl = getDashboardUrl();
437
- console.log(`\n⏸ Paused for hook selection.`);
438
- console.log(`Pick your hooks here: ${dashboardUrl}/runs/${runId}`);
439
- console.log(`(The pipeline resumes automatically once you choose.)`);
484
+ console.log(`\n⏸ Paused for hook selection (${pool.length} hook${pool.length === 1 ? "" : "s"}).`);
485
+ if (pool.length > 0) {
486
+ console.log("");
487
+ console.log(formatHookPool(pool));
488
+ console.log("");
489
+ }
490
+ console.log(`Write the ones you want: exodus genesis continue --id ${runId} --hooks 1,3,5`);
491
+ console.log(`(Prefer the dashboard? ${dashboardUrl}/runs/${runId})`);
440
492
  return;
441
493
  }
442
494
  console.log(formatGenesisRun(result.data));
@@ -481,6 +533,80 @@ async function runHookPref() {
481
533
  console.log("Future runs will select hooks automatically without pausing.");
482
534
  }
483
535
  }
536
+ async function runHooks(flags) {
537
+ const runId = typeof flags["id"] === "string" ? flags["id"].trim() : "";
538
+ if (!runId) {
539
+ console.error("Usage: exodus genesis hooks --id <runId>");
540
+ process.exit(1);
541
+ }
542
+ const res = await apiGet(`/api/v2/genesis?id=${runId}`);
543
+ if (!res.ok) {
544
+ console.log(formatError(res));
545
+ process.exit(1);
546
+ }
547
+ if (res.data.status !== "awaiting_hook_selection") {
548
+ console.log(`Run ${runId} is not awaiting hook selection (status: ${res.data.status ?? "unknown"}).`);
549
+ return;
550
+ }
551
+ const pool = Array.isArray(res.data.hookPool) ? res.data.hookPool : [];
552
+ if (pool.length === 0) {
553
+ console.log("No hooks available for this run.");
554
+ return;
555
+ }
556
+ console.log(`Hooks for run ${runId} (${pool.length}):`);
557
+ console.log("");
558
+ console.log(formatHookPool(pool));
559
+ console.log("");
560
+ console.log(`Write the ones you want: exodus genesis continue --id ${runId} --hooks 1,3,5`);
561
+ }
562
+ async function runContinue(flags, cc) {
563
+ const runId = typeof flags["id"] === "string" ? flags["id"].trim() : "";
564
+ if (!runId) {
565
+ console.error("Usage: exodus genesis continue --id <runId> --hooks 1,3,5");
566
+ process.exit(1);
567
+ }
568
+ const raw = typeof flags["hooks"] === "string" ? flags["hooks"] : "";
569
+ let selection;
570
+ try {
571
+ selection = parseHookSelection(raw);
572
+ }
573
+ catch (e) {
574
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
575
+ process.exit(1);
576
+ return;
577
+ }
578
+ if (exceedsVariantCap(selection.length)) {
579
+ console.error(`Warning: you picked ${selection.length} hooks but the pipeline writes at most ${VARIANT_CAP}. ` +
580
+ `The ${VARIANT_CAP} lowest-numbered picks will be written and the rest dropped. ` +
581
+ `Re-run with ${VARIANT_CAP} or fewer to control exactly which.`);
582
+ }
583
+ const body = buildContinueBody(runId, selection);
584
+ const res = await apiPost("/api/v2/genesis/continue", body, { ccCommand: cc });
585
+ if (!res.ok) {
586
+ console.log(formatError(res));
587
+ process.exit(1);
588
+ }
589
+ console.log(`Resuming run ${runId} with ${body.selectedHookIndices.length} hook${body.selectedHookIndices.length === 1 ? "" : "s"}…`);
590
+ await waitForGenesisRun(runId);
591
+ }
592
+ async function runRegenerate(flags, cc) {
593
+ const runId = typeof flags["id"] === "string" ? flags["id"].trim() : "";
594
+ if (!runId) {
595
+ console.error("Usage: exodus genesis regenerate --id <runId> [--steering \"…\"]");
596
+ process.exit(1);
597
+ }
598
+ const steering = typeof flags["steering"] === "string" ? flags["steering"] : undefined;
599
+ const body = buildRegenerateBody(runId, steering);
600
+ const res = await apiPost("/api/v2/genesis/regenerate", body, { ccCommand: cc });
601
+ if (!res.ok) {
602
+ console.log(formatError(res));
603
+ process.exit(1);
604
+ }
605
+ console.log(body.steering
606
+ ? `Regenerating hooks for run ${runId} with steering…`
607
+ : `Regenerating hooks for run ${runId}…`);
608
+ await waitForGenesisRun(runId);
609
+ }
484
610
  function requireActiveBrand() {
485
611
  const slug = resolveActiveBrand().slug;
486
612
  if (!slug) {
@@ -28,6 +28,7 @@ export type IdeaAction = {
28
28
  keys: string[];
29
29
  awarenessLevel: string;
30
30
  variantCount?: number;
31
+ stopAtHooks?: boolean;
31
32
  } | {
32
33
  kind: "rm";
33
34
  key: string;
@@ -41,6 +42,7 @@ export type IdeaAction = {
41
42
  write: boolean;
42
43
  awarenessLevel: string;
43
44
  variantCount?: number;
45
+ stopAtHooks?: boolean;
44
46
  } | {
45
47
  kind: "swipe";
46
48
  limit?: number;
@@ -17,9 +17,13 @@ Usage:
17
17
  exodus idea list [--source ..] [--since YYYY-MM-DD] [--status raw|writing|written|archived] [--limit n]
18
18
  exodus idea note <KEY> "<notes>" Attach persistent steering to an idea
19
19
  exodus idea edit <KEY> "<new concept>" Edit an idea's description
20
- exodus idea write <KEYS> [--awareness <level>] [--passes n] Fire-and-forget; one run per key (default 1 pass = 2 variants)
20
+ exodus idea write <KEYS> [--awareness <level>] [--passes n] [--stop-at-hooks|--auto-hooks] Fire-and-forget; one run per key (default 1 pass = 2 variants)
21
21
  exodus idea rm <KEY> [--hard] Archive (or hard-delete with --hard)
22
22
 
23
+ Hook gate (write paths): --stop-at-hooks pauses each run for in-Claude-Code hook
24
+ review; --auto-hooks auto-picks and writes straight through. Omit both to use your
25
+ saved default (exodus genesis hook-pref). Applies to write and organic --write.
26
+
23
27
  Examples:
24
28
  exodus idea gambit "joint pain at 40, sleep angle, grounding vs pills"
25
29
  exodus idea organic "https://www.instagram.com/reel/abc https://www.tiktok.com/@x/video/123"
@@ -56,6 +60,22 @@ function resolveVariantCount(flags) {
56
60
  }
57
61
  return passes * 2;
58
62
  }
63
+ function resolveStopAtHooks(flags) {
64
+ if (flags["stop-at-hooks"] === true)
65
+ return true;
66
+ if (flags["auto-hooks"] === true)
67
+ return false;
68
+ return undefined;
69
+ }
70
+ function printHookPauseHint(dispatched) {
71
+ if (dispatched.length === 0)
72
+ return;
73
+ console.log("");
74
+ console.log("Hooks are gated (manual) — each run paused for hook selection. Review and write:");
75
+ for (const d of dispatched) {
76
+ console.log(` exodus genesis hooks --id ${d.runId} then exodus genesis continue --id ${d.runId} --hooks 1,3,5`);
77
+ }
78
+ }
59
79
  export function resolveIdeaAction(positionals, flags) {
60
80
  const sub = positionals[0];
61
81
  if (sub === "add") {
@@ -116,6 +136,7 @@ export function resolveIdeaAction(positionals, flags) {
116
136
  keys,
117
137
  awarenessLevel: str(flags["awareness"]) ?? "problem-aware",
118
138
  variantCount: resolveVariantCount(flags),
139
+ stopAtHooks: resolveStopAtHooks(flags),
119
140
  };
120
141
  }
121
142
  if (sub === "rm") {
@@ -140,6 +161,7 @@ export function resolveIdeaAction(positionals, flags) {
140
161
  write: flags["write"] === true,
141
162
  awarenessLevel: str(flags["awareness"]) ?? "problem-aware",
142
163
  variantCount: resolveVariantCount(flags),
164
+ stopAtHooks: resolveStopAtHooks(flags),
143
165
  };
144
166
  }
145
167
  if (sub === "swipe") {
@@ -218,7 +240,7 @@ export async function run(flags) {
218
240
  case "organic": {
219
241
  if (action.write) {
220
242
  console.log(`Transcribing ${action.urls.length} reel(s) → ideas → Genesis…`);
221
- const result = await captureReelAndWrite(action.urls, { awarenessLevel: action.awarenessLevel, variantCount: action.variantCount }, { cc });
243
+ const result = await captureReelAndWrite(action.urls, { awarenessLevel: action.awarenessLevel, variantCount: action.variantCount, stopAtHooks: action.stopAtHooks }, { cc });
222
244
  for (const f of result.bankedFailed) {
223
245
  console.error(` ✗ couldn't pull an idea from ${f} (private, region-locked, or no transcript?)`);
224
246
  }
@@ -228,6 +250,8 @@ export async function run(flags) {
228
250
  }
229
251
  for (const d of result.dispatched)
230
252
  console.log(` ✓ banked ${d.key} → Genesis run ${d.runId}`);
253
+ if (action.stopAtHooks === true)
254
+ printHookPauseHint(result.dispatched);
231
255
  console.log("");
232
256
  console.log("Track them: exodus idea list (status flips to 'written' with a doc link)");
233
257
  return;
@@ -304,7 +328,7 @@ export async function run(flags) {
304
328
  return;
305
329
  }
306
330
  case "write": {
307
- const res = await apiPost("/api/v2/idea-bank/dispatch", { keys: action.keys, awarenessLevel: action.awarenessLevel, variantCount: action.variantCount }, { ccCommand: cc });
331
+ const res = await apiPost("/api/v2/idea-bank/dispatch", { keys: action.keys, awarenessLevel: action.awarenessLevel, variantCount: action.variantCount, stopAtHooks: action.stopAtHooks }, { ccCommand: cc });
308
332
  if (!res.ok) {
309
333
  console.log(formatError(res));
310
334
  process.exit(1);
@@ -316,6 +340,8 @@ export async function run(flags) {
316
340
  console.log(` ${d.key} → run ${d.runId}`);
317
341
  for (const s of skipped)
318
342
  console.log(` skipped ${s.key}: ${s.reason}`);
343
+ if (action.stopAtHooks === true)
344
+ printHookPauseHint(dispatched);
319
345
  console.log("");
320
346
  console.log("Track them: exodus idea list (status flips to 'written' with a doc link)");
321
347
  return;
@@ -10,6 +10,7 @@ export declare function captureSwipeAndWrite(urls: string[], opts: {
10
10
  awarenessLevel: string;
11
11
  variantCount?: number;
12
12
  steering?: string;
13
+ stopAtHooks?: boolean;
13
14
  }, rt?: {
14
15
  cc?: string;
15
16
  }): Promise<SwipeWriteResult>;
@@ -52,7 +52,7 @@ export async function captureSwipeAndWrite(urls, opts, rt = {}) {
52
52
  console.error(` (couldn't attach steering to ${key}: ${formatError(noted)})`);
53
53
  }
54
54
  }
55
- const dispatch = await apiPost("/api/v2/idea-bank/dispatch", { keys, awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount }, { ccCommand: rt.cc });
55
+ const dispatch = await apiPost("/api/v2/idea-bank/dispatch", { keys, awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount, stopAtHooks: opts.stopAtHooks }, { ccCommand: rt.cc });
56
56
  if (!dispatch.ok) {
57
57
  console.log(formatError(dispatch));
58
58
  process.exit(1);
@@ -9,6 +9,7 @@ export interface ReelWriteResult {
9
9
  export declare function captureReelAndWrite(urls: string[], opts: {
10
10
  awarenessLevel: string;
11
11
  variantCount?: number;
12
+ stopAtHooks?: boolean;
12
13
  }, rt?: {
13
14
  cc?: string;
14
15
  }): Promise<ReelWriteResult>;
@@ -44,7 +44,7 @@ export async function captureReelAndWrite(urls, opts, rt = {}) {
44
44
  if (found.size === 0)
45
45
  return { dispatched: [], bankedFailed };
46
46
  const keys = [...found.values()];
47
- const dispatch = await apiPost("/api/v2/idea-bank/dispatch", { keys, awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount }, { ccCommand: rt.cc });
47
+ const dispatch = await apiPost("/api/v2/idea-bank/dispatch", { keys, awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount, stopAtHooks: opts.stopAtHooks }, { ccCommand: rt.cc });
48
48
  if (!dispatch.ok) {
49
49
  console.log(formatError(dispatch));
50
50
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.6.2700-beta.3",
3
+ "version": "2026.6.2900-beta.4",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",