@aicopycoders/exodus 2026.6.2700-beta.2 → 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.
@@ -0,0 +1,125 @@
1
+ # Creative Strategy Workspace
2
+
3
+ > **New here?** Your onboarding home is the Exodus dashboard (Settings → Claude Code) — it walks you through install and keys. If anything's broken at runtime, run `npx @aicopycoders/exodus doctor` from this directory — it diagnoses local and dashboard-side issues in one shot.
4
+
5
+ This is a creative-strategy workspace operated by the **exodus** CLI. It is brand-agnostic infrastructure serving **all of the account's brands**: each brand has its own subfolder here (marked by a `.exodus-brand.json` file) holding that brand's `state/` and `output/`. Which brand you're working on resolves **folder first, pointer second**: commands run from inside a brand's subfolder target that brand automatically; otherwise the `npx @aicopycoders/exodus brand use <slug>` pointer applies. (Older single-brand installs keep `state/` and `output/` at this root — same rules, one brand.)
6
+
7
+ **How to engage the system:** every skill here is namespaced `exodus-*`, and a front-door `exodus` hub skill routes generic asks. Members say "exodus" to engage ("exodus, write me some ads") or invoke a specific skill with `/exodus-<name>` (e.g. `/exodus-write`). This namespacing is deliberate: it lets these skills coexist with other skill packs (like the workshop pack) installed in the same folder, and it keeps the bare word "Genesis" unambiguous — on its own, "Genesis" always means the member's own Genesis API key/recipes, never Exodus. CLI commands are unchanged (`npx @aicopycoders/exodus genesis`, `npx @aicopycoders/exodus brand use`, etc.).
8
+
9
+ The strategist persona, the Luke Mills / Genesis frameworks, the operating rules and creative principles, and the operating discipline all live in **one skill** plus its references:
10
+
11
+ - `.claude/skills/exodus-strategist/SKILL.md` — the single always-on creative-strategy skill: persona, operating rules, creative principles, red flags, active-brand resolution, the quality bars, and a **Framework Index** that routes to the depth below
12
+ - `references/` — the framework depth, consulted from the skill when needed: `creative-strategy.md` (the encyclopedia + Genesis bot catalog), `segments.md`, `copy-blocks.md` (P3C2), `cast-video.md` (CAST), `iterations.md` (CASHED), `editing-rules.md`, `scrawls.md`, `hook-quality-checklist.md`, `awareness-framework.md`
13
+
14
+ If the persona isn't firing for you, check that the skill loaded. It is not optional reading.
15
+
16
+ ---
17
+
18
+ ## Two systems in this folder: Exodus (automatic) + Genesis (manual)
19
+
20
+ This folder usually holds **two related products that share the same Genesis engine** — keep them straight. Think of it as a car: same engine underneath, two ways to drive it.
21
+
22
+ - **Exodus — the automatic transmission.** This workspace: the `exodus-*` skills and the `npx @aicopycoders/exodus` CLI. Pre-built, done-for-you pipelines (write, image, idea bank, brand onboarding) that drive the Genesis bots *for* you against your brand. You engage it by saying **"exodus"** ("exodus, write me some ads"). Its keys are `EXODUS_API_KEY` / `EXODUS_API_URL` in `.env`; Genesis access is internal — there's nothing to configure or debug about Genesis for Exodus to work.
23
+
24
+ - **Genesis (standalone) — the manual transmission.** A separate skill at `.claude/skills/genesis-bots` that calls the *same* Genesis copywriting / research / strategy bots **directly** with the member's own `gen_` key — you pick a bot and prompt it yourself. It reads `GENESIS_API_KEY` plus a provider key (`ANTHROPIC_API_KEY` or `OPENROUTER_API_KEY`) from `.env`. This is the member's own workflow, **not** owned by the `exodus-*` skills. (It may not be installed in every folder — check for the `genesis-bots` skill.)
25
+
26
+ **Routing rule:** "exodus …" → the Exodus pipelines below. Bare **"genesis"**, "run the bots", or a named bot slug → the `genesis-bots` skill (the member's direct workflow — never route it into Exodus). If someone clearly just wants ads and doesn't know which system they have, ask one question — "drive the Genesis bots directly, or run an Exodus pipeline?" — rather than guessing.
27
+
28
+ **Provider key:** the manual Genesis bots need a provider key in `.env`. `npx @aicopycoders/exodus keys pull` syncs your dashboard provider + image keys down for you (the install runs it; safe to re-run anytime keys change). Exodus's own pipelines don't need it locally — they run server-side.
29
+
30
+ ---
31
+
32
+ ## Multi-Brand Operating Notes
33
+
34
+ Any user can create and own unlimited brands — `npx @aicopycoders/exodus brand create "<name>"` (or the dashboard); it is NOT admin-only, and brand names are scoped per user (two users can each have a "Matt Beard"). One account key then switches across every brand the user owns (admins: every brand). The only exception is a brand someone ELSE invited the user into — they don't own it, so their key stays pinned to just that one. Never tell a user they can't make brands or are "locked to one brand" just because their key shows `role: member`. The active brand resolves **folder > pointer > key default**: being inside a brand's subfolder wins, then the `brand use` pointer, then the key's default brand.
35
+
36
+ - **Identify the active brand:** `npx @aicopycoders/exodus brand current` (also says whether it came from the folder or the pointer)
37
+ - **List accessible brands:** `npx @aicopycoders/exodus brand list`
38
+ - **Create a new brand:** `npx @aicopycoders/exodus brand create "<name>"` — creates it server-side, switches you in, and sets up its local subfolder
39
+ - **Switch brand:** `npx @aicopycoders/exodus brand use <slug>` — or just work from inside that brand's subfolder
40
+ - **New brand created on the dashboard?** `npx @aicopycoders/exodus@latest init` creates its subfolder and pulls its profile
41
+ - **Brand voice / ICP / offer:** the active brand's `state/brand-profile.md` (inside its subfolder; at this root on older single-brand installs) — read this at session start and after any switch. If it doesn't match the active brand, the exodus-strategist skill will prompt to refresh.
42
+
43
+ Don't carry voice or proven angles across brands — each brand is its own creative universe.
44
+
45
+ ---
46
+
47
+ ## Available Pipelines
48
+
49
+ ### Exodus pipelines (automatic)
50
+
51
+ These are the **Exodus** pipelines — the automatic system that drives the Genesis bots for you. Use them to generate and analyze creative. Each has its own skill directory in `.claude/skills/`. Invoke them by saying "exodus" plus what you want, or directly with `/exodus-<name>`.
52
+
53
+ | Pipeline | Skill | When to Use |
54
+ |----------|-----------|-------------|
55
+ | **Write** | `exodus-write` | **Start here for copywriting.** Say what you want ("exodus, write me some ads"); it routes new-brand → foundation → brief → genesis → Doc and sequences the whole flow. |
56
+ | **Genesis** | `exodus-genesis` | Brief → ad copy in two voices (Mario + Infeed). Default 1 pass = 2 variants; scale with `--passes` (pass 2 adds the Top-Ads-biased primer). Also writes from a pasted reel (`--reel`). |
57
+ | **Idea** | `exodus-idea` | The Idea Bank — capture reels/brain-dumps/swipes into formed ideas, curate them, then write the keepers through Genesis. Use when collecting and curating ideas rather than writing one ad on the spot. |
58
+ | **Image** | `exodus-image` | **Start here for static images.** Say what you want; it reads the request and routes to the right engine — `exodus-creative` (renders from copy) or `exodus-template` (33 ad-type formats). |
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
+ | **Template** | `exodus-template` | Finished ad copy → many static ads across 33 structured formats (testimonial, hero, UGC…). Otherwise start at **Image**. |
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
+ | **Browse** | `exodus-browse` | View history and retrieve past outputs |
63
+ | **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) |
64
+
65
+ For the operator-facing quick reference on how to invoke these in Claude Code, see `PIPELINES.md` in the workspace root.
66
+
67
+ ### Genesis bots (manual)
68
+
69
+ The same folder usually also carries **`genesis-bots`** — the *manual* way to drive the same Genesis bots. You pick a bot and prompt it yourself with the member's own `gen_` key, chaining bots by hand (buyer profile → creative brief → ad hooks → ads). It is **not** an `exodus-*` skill and is **never** invoked by saying "exodus".
70
+
71
+ | Skill | When to Use |
72
+ |-------|-------------|
73
+ | `genesis-bots` | The user says bare **"genesis"**, "run the bots", "openclaw", or names a bot slug, and wants to call a Genesis copywriting / research / strategy / image-prompt bot directly. Needs `GENESIS_API_KEY` plus a provider key (`ANTHROPIC_API_KEY` or `OPENROUTER_API_KEY`) in `.env` — see **Dashboard** below. May not be installed in every folder — check for the skill directory before assuming it's there. |
74
+
75
+ Route a bare-"genesis" ask to this skill and an "exodus …" ask to the pipelines above — never cross the two. If someone clearly just wants ads and doesn't know which system they have, ask one question ("drive the Genesis bots directly, or run an Exodus pipeline?") rather than guessing.
76
+
77
+ ---
78
+
79
+ ## Awareness Level Routing
80
+
81
+ Every ad targets an audience at a specific awareness level. Getting this right is one of the highest-leverage decisions in creative strategy.
82
+
83
+ > `--awareness` sets the audience awareness level for a Genesis run (unaware / problem-aware / solution-aware / product-aware). Pick the level the idea is actually written for.
84
+
85
+ | Level | Audience State | Ad Approach |
86
+ |-------|---------------|-------------|
87
+ | **Unaware** | Doesn't know they have a problem | Shock, pattern interrupt, reveal |
88
+ | **Problem-Aware** | Knows the problem, not the solution | Educate, validate, agitate |
89
+ | **Solution-Aware** | Knows solutions exist, not ours | Compare, differentiate, position |
90
+ | **Product-Aware** | Knows the brand, hasn't bought yet | Offer, urgency, social proof |
91
+
92
+ Routing detail and signal patterns live in `.claude/skills/exodus-strategist/SKILL.md` and `references/creative-strategy.md`.
93
+
94
+ ---
95
+
96
+ ## Dashboard
97
+
98
+ This workspace connects to a live dashboard for:
99
+ - Settings management (defaults, account connections)
100
+ - **Pipeline keys** — your provider LLM key (Anthropic or OpenRouter) plus Kie.ai, entered once in Settings → Pipeline Keys
101
+ - Google Drive credentials (where outputs are saved)
102
+ - Run history and output retrieval
103
+ - Ad account connections (e.g. the Genesis top-ads-biased track)
104
+ - Brand admin (switching, profile management)
105
+
106
+ The dashboard URL and your `EXODUS_API_KEY` live in `.env`.
107
+
108
+ **Keys & setup.** Exodus's own pipelines run server-side and need nothing beyond `EXODUS_API_KEY` — there's no provider key to configure for them. The **manual `genesis-bots` path** (and other local tools) *do* need a provider key in `.env`. Enter your keys once in the dashboard, then pull them down:
109
+
110
+ - `npx @aicopycoders/exodus keys pull` — writes your dashboard provider + Kie.ai keys into `.env` (the install runs this for you; safe to re-run anytime a key changes). It only upserts the keys and preserves the rest of the file.
111
+ - `npx @aicopycoders/exodus keys status` — shows which keys are set in the dashboard vs locally. Values are never printed.
112
+
113
+ ---
114
+
115
+ ## Reviewing Run Output
116
+
117
+ When you need to review a run's creative output, always read it via `npx @aicopycoders/exodus read-doc <runId>`. This walks every tab of the Google Doc and gives you the full output as markdown.
118
+
119
+ Do **not** use the Drive MCP as your first choice — it only sees tab 1 and will misreport the run as empty. If `npx @aicopycoders/exodus read-doc` exits with code 2 (`unavailable`), fall back to the Drive MCP using the URL printed by the command, and warn the user in your summary that you only saw tab 1.
120
+
121
+ ---
122
+
123
+ ## Tool Feedback
124
+
125
+ This workspace is also a testbed for the CLI and pipelines. When you run a pipeline and hit a gap — a missing input, missing output, missing filter, awkward flow — log it per the **Tool Feedback Protocol** in `.claude/skills/exodus-strategist/SKILL.md`. Your feedback drives the next round of pipeline improvements.
@@ -0,0 +1,202 @@
1
+ # Pipelines Cheat Sheet
2
+
3
+ In Claude Code you don't click buttons. You say **"exodus"** plus what you want — "exodus, write me some ads" — and the `exodus` hub skill routes you to the right pipeline. You can also invoke a specific skill directly with a slash command: `/exodus-write`, `/exodus-image`, etc.
4
+
5
+ > This doc is the CLI/skill invocation reference for the active brand workspace. For the dashboard's clickable pipeline list, see the dashboard's Creative Suite.
6
+
7
+ ## All skills at a glance
8
+
9
+ | Skill | What it does |
10
+ |-------|--------------|
11
+ | exodus-write | The front door — routes brand → foundation → brief → Genesis → Doc. Start here. |
12
+ | exodus-genesis | Brief → Ad Copy (two voices; 1 pass = 2 variants, scalable via --passes) |
13
+ | exodus-image | The front door for static images — routes to exodus-creative or exodus-template. Start here for "make me images / statics". |
14
+ | exodus-creative | Creative-Suite Engines (native / copy-derived / ref-match) — when you've already picked an engine |
15
+ | exodus-template | Ad-Type Format Variations across 33 structured formats |
16
+ | exodus-meme | Meme Ad Generator — recommend formats, one batched server-side run |
17
+ | exodus-browse | View History and Surface the Right Run |
18
+ | exodus-drive | Google Drive, Docs, Sheets via the Dashboard's OAuth |
19
+
20
+ ---
21
+
22
+ ## exodus-write (start here)
23
+
24
+ **What it does:** The guided front door for copywriting. You don't need to know which command to run — say "exodus" plus what you want and the exodus-write flow figures out where you are, helps shape the brief, sets up your foundation if needed, runs Genesis, and hands back the Google Doc.
25
+
26
+ ```operator-guide
27
+ No command — say "exodus" plus what you want (or /exodus-write). It sequences
28
+ the existing commands (foundation, genesis, browse, read-doc) for you.
29
+
30
+ Say things like:
31
+ "exodus, I want to write some ads: <your idea>"
32
+ "exodus, turn this winning ad into copy: <paste>"
33
+ "exodus, set up a new brand and write my first ad"
34
+ "exodus, make a variant of that last run"
35
+
36
+ Routing:
37
+ new brand / no foundation → onboarding (foundation, optional primer prompt)
38
+ brief or idea in hand → straight to the Genesis writing pass
39
+ rerun / tweak → browse past runs + the editing menu
40
+ Works without a primer (defaults); sharper once your foundation has winning examples.
41
+ ```
42
+
43
+ ---
44
+
45
+ ## exodus-genesis
46
+
47
+ **What it does:** Take a brief through Luke's full Genesis writing process — two voices (Mario × Brand + Infeed × Brand). Default is 1 pass per bot = 2 variants; add `--passes` to widen coverage (pass 2 brings in the Top-Ads-biased primer). Also accepts per-run seeds extracted from a source ad.
48
+
49
+ ```operator-guide
50
+ Inputs:
51
+ --brief <file|"text"> (required) — path to a text file OR an inline brief string
52
+ Optional:
53
+ --seeds <file|"text"> — per-run creative seeds (file: one per line or JSON array; inline: one string)
54
+ --awareness — unaware | problem-aware (default) | solution-aware | product-aware
55
+ --passes <n> — writing passes per bot (1–5, default 1). 1 pass = 2 variants
56
+ (1 Mario×Brand + 1 Infeed×Brand); pass 2 adds the Top-Ads-biased primer
57
+ --variants <n> — advanced: raw total count (1–10) that overrides --passes
58
+ --ad-account <id> — Meta ad account for the top-ads-biased track (defaults to first active account)
59
+ --no-wait — return immediately with the run ID
60
+ Returns: Google Doc URL printed to stdout when the run completes
61
+ Examples:
62
+ exodus genesis run --brief brief.txt --seeds seeds.txt
63
+ exodus genesis run --brief "Grounding sheets help joint pain" --awareness problem-aware --passes 2
64
+ ```
65
+
66
+ ---
67
+
68
+ ## exodus-image (start here for statics)
69
+
70
+ **What it does:** The front door for static image ads. Just say "exodus" plus what
71
+ you want — "exodus, make me some statics from this ad", "exodus, render AD-001" —
72
+ and Claude reads the request and runs the right engine: **exodus-creative**
73
+ (renders straight from copy, or matches a reference image) or **exodus-template**
74
+ (one piece of copy spread across 33 ad-type formats). You don't pick the engine;
75
+ the request decides it. Use the engines below directly only when you already know
76
+ exactly which one you want.
77
+
78
+ **Say something like:** "exodus, make me some image ads from this copy" · "exodus,
79
+ render this ad as statics" · "exodus, spread AD-001 across ad formats" · "exodus,
80
+ make ads that look like this library image".
81
+
82
+ ---
83
+
84
+ ## exodus-creative
85
+
86
+ **What it does:** Generate static creative images through the creative-suite engines — native (render image ads straight from ad copy), copy-derived (renders derived from copy variations), and ref-match (match the look of one or more reference images you've uploaded to the library).
87
+
88
+ ```operator-guide
89
+ Subcommands:
90
+ exodus creative native --ad "<text>" [options] Render image ads from ad copy
91
+ exodus creative copy-derived --ad "<text>" [options] Renders derived from copy variations
92
+ exodus creative ref-match --refs <id,id,...> [options] Match the look of reference image(s)
93
+ exodus creative status --id <runId> Poll a run
94
+
95
+ Shared options (kickoffs):
96
+ --variations N Renders to generate (default: engine-specific)
97
+ --aspect 1:1|4:5|9:16 Image aspect ratio (default: 1:1)
98
+ --name "<label>" Custom run name (otherwise auto-generated)
99
+ --ad-group <id> Attach to an existing ad-group
100
+
101
+ ref-match additional options:
102
+ --refs <id,id,...> REQUIRED. Comma-separated creativeSuiteImages ids
103
+ (the images must already be in the dashboard library)
104
+ --subject "<text>" Optional subject hint
105
+ --objects <id,id,...> Optional object-overlay image ids
106
+
107
+ Returns: runId + dashboard URL (/creative-suite/runs/<runId>).
108
+ Status: `exodus creative status --id <runId>` → engine, status, completed/total, isTerminal.
109
+ Auth: Bearer (kickoff via Convex HTTP; status via dashboard route).
110
+ ```
111
+
112
+ ---
113
+
114
+ ## exodus-template
115
+
116
+ **What it does:** Turn finished ad copy into many static ad images spread across structured ad-type formats (testimonial, hero, UGC, logo, infographic, and 28 more). Runs Fernando's 5-stage Template pipeline — give it one brief or a numbered list of ads and it generates format variations, then renders them.
117
+
118
+ ```operator-guide
119
+ Subcommands:
120
+ exodus template run --input "<text>" [options] Kick off a run
121
+ exodus template resume --id <runId> Finalize an orphaned run (#56)
122
+ exodus template ad-types Print the 33 AD_TYPES
123
+ exodus template reptile-triggers Print the 13 reptile triggers
124
+
125
+ run options:
126
+ --input "<text>" REQUIRED. One brief, or a numbered list of finished ads
127
+ ("1. <ad copy> 2. <ad copy> ..."). Min 3 chars.
128
+ --mode auto|manual|hybrid Generation mode (default: auto)
129
+ --render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
130
+ --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
131
+ --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
132
+ --realism off|realistic Realism enforcement (default: off)
133
+ --quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
134
+ --pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
135
+ --requested-count N auto-mode total render target (optional)
136
+
137
+ Returns:
138
+ runId + dashboard URL. POST-only — there is NO CLI status endpoint yet.
139
+ Live progress renders in the dashboard at /creative-suite/template/sessions/<runId>.
140
+ ```
141
+
142
+ ---
143
+
144
+ ## exodus-meme
145
+
146
+ **What it does:** Generate meme-style ad creative for the active brand — recommends meme formats for a brief, then renders the whole batch server-side in one run (AI image memes via Kie.ai, classic Imgflip-template memes on the member's Imgflip login).
147
+
148
+ ```operator-guide
149
+ Subcommands:
150
+ exodus meme recommend --brief "<text>" [--avatar "<text>"]
151
+ exodus meme run --brief "<text>" --formats '<json>' [--avatar "<text>"] [--name "<label>"]
152
+ exodus meme regenerate --brief "<text>" --layer 1|2|3 <format fields> [--hint "<text>"] [--id <runId>]
153
+
154
+ Typical flow:
155
+ 1. recommend → { recommendations: [...] } — 15 picks (5 per layer) with
156
+ name, layer, reasoning, ad_angle. Layer 1 = classic Imgflip
157
+ templates; layers 2/3 = AI-image formats.
158
+ 2. run → pass the picked recommendation objects straight into
159
+ --formats (JSON array, 1–50). One enqueue; the batch renders
160
+ server-side via Trigger.dev — closing the session won't kill it.
161
+ 3. poll → exodus status --id <runId> --type creative
162
+ (terminal: complete | partial-error | error)
163
+ 4. regenerate → re-render a single miss (layer 1: --template-id
164
+ --template-name --boxes; layer 2/3: --format [--hint]).
165
+
166
+ Keys (strict BYOK, preflighted server-side): classic memes need the member's
167
+ Imgflip login; AI memes need their Kie.ai key; captions always need an LLM key.
168
+ Returns: runId + dashboard URL /creative-suite/runs/<runId> (results land in the library).
169
+ ```
170
+
171
+ ---
172
+
173
+ ## exodus-browse
174
+
175
+ **What it does:** View past pipeline runs and pull outputs from previous sessions.
176
+
177
+ ```operator-guide
178
+ Inputs:
179
+ none required — defaults to recent runs across all pipelines
180
+ Flags:
181
+ --limit (default: 20) — max runs to list
182
+ --agent — filter by agent: genesis | creative | template | meme
183
+ Returns: terminal table of recent runs with timestamps, agents, statuses, and Doc URLs
184
+ ```
185
+
186
+ ---
187
+
188
+ ## exodus-drive
189
+
190
+ **What it does:** Read and write Google Drive / Docs / Sheets via the dashboard's existing OAuth, using `npx @aicopycoders/exodus drive` subcommands.
191
+
192
+ ```operator-guide
193
+ Subcommands (use the one that matches intent):
194
+ get-doc <docId> Read a Google Doc by ID
195
+ get-sheet <sheetId> Read a spreadsheet range
196
+ list-files Search/list Drive files
197
+ create-doc <title> Create a new Google Doc
198
+ batch-update <docId> Apply Docs API requests (insertText, etc.)
199
+ For pipeline-run docs, use:
200
+ npx @aicopycoders/exodus read-doc <runId> — walks every tab, renders to markdown
201
+ Returns: structured JSON forwarded from Google's APIs (or markdown for read-doc)
202
+ ```
@@ -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;
@@ -3,6 +3,7 @@ export interface InitResult {
3
3
  existing: boolean;
4
4
  envCreated: boolean;
5
5
  skills: string[];
6
+ docs: string[];
6
7
  }
7
8
  export declare function scaffoldInit(root: string): InitResult;
8
9
  export declare function syncBrands(root: string): Promise<{
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { writeEnvScaffold, ensureGitignore, writeSkills, writeReferences, ensureBaseDirs, } from "../lib/scaffold.js";
3
+ import { writeEnvScaffold, ensureGitignore, writeSkills, writeReferences, writeDocs, ensureBaseDirs, } from "../lib/scaffold.js";
4
4
  import { loadWorkspaceEnv } from "../lib/load-env.js";
5
5
  import { ensureBrandDir } from "../lib/layout.js";
6
6
  export const helpText = `
@@ -22,7 +22,8 @@ export function scaffoldInit(root) {
22
22
  ensureGitignore(root);
23
23
  const skills = writeSkills(root);
24
24
  writeReferences(root);
25
- return { existing, envCreated, skills };
25
+ const docs = writeDocs(root);
26
+ return { existing, envCreated, skills, docs };
26
27
  }
27
28
  export async function syncBrands(root) {
28
29
  loadWorkspaceEnv(root);
@@ -48,6 +49,9 @@ export async function run(flags) {
48
49
  const r = scaffoldInit(root);
49
50
  console.log(`${r.existing ? "Refreshed" : "Initialized"} Exodus workspace at ${root}`);
50
51
  console.log(` Installed ${r.skills.length} skills into .claude/skills/`);
52
+ if (r.docs.length) {
53
+ console.log(` Installed workspace docs: ${r.docs.join(", ")}`);
54
+ }
51
55
  const brands = await syncBrands(root);
52
56
  if ("synced" in brands && brands.synced.length) {
53
57
  console.log(` Synced ${brands.synced.length} brand folder(s): ${brands.synced.join(", ")}`);
@@ -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);
@@ -1,3 +1,4 @@
1
1
  export declare function assetsRoot(override?: string): string;
2
2
  export declare function skillsDir(override?: string): string;
3
3
  export declare function referencesDir(override?: string): string;
4
+ export declare function docsDir(override?: string): string;
@@ -21,3 +21,6 @@ export function skillsDir(override) {
21
21
  export function referencesDir(override) {
22
22
  return path.join(assetsRoot(override), "references");
23
23
  }
24
+ export function docsDir(override) {
25
+ return path.join(assetsRoot(override), "docs");
26
+ }
@@ -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);
@@ -5,4 +5,5 @@ export declare function writeEnvScaffold(root: string): {
5
5
  export declare function ensureGitignore(root: string): void;
6
6
  export declare function writeSkills(root: string, srcOverride?: string): string[];
7
7
  export declare function writeReferences(root: string, srcOverride?: string): void;
8
+ export declare function writeDocs(root: string, srcOverride?: string): string[];
8
9
  export declare function ensureBaseDirs(root: string): void;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { skillsDir, referencesDir } from "./assets.js";
3
+ import { skillsDir, referencesDir, docsDir } from "./assets.js";
4
4
  export const ENV_SCAFFOLD = `# Exodus config — paste your dashboard .env block below this line.
5
5
  # Get it from your Exodus dashboard -> Settings -> Claude Code -> "Copy .env block".
6
6
  # ONE key covers every brand you own; switch brands with brand subfolders or
@@ -51,6 +51,25 @@ export function writeReferences(root, srcOverride) {
51
51
  fs.rmSync(dest, { recursive: true, force: true });
52
52
  fs.cpSync(src, dest, { recursive: true });
53
53
  }
54
+ export function writeDocs(root, srcOverride) {
55
+ let src;
56
+ try {
57
+ src = docsDir(srcOverride);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ if (!fs.existsSync(src))
63
+ return [];
64
+ const names = [];
65
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
66
+ if (!entry.isFile() || !entry.name.endsWith(".md"))
67
+ continue;
68
+ fs.copyFileSync(path.join(src, entry.name), path.join(root, entry.name));
69
+ names.push(entry.name);
70
+ }
71
+ return names.sort();
72
+ }
54
73
  export function ensureBaseDirs(root) {
55
74
  const out = path.join(root, "output");
56
75
  fs.mkdirSync(out, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.6.2700-beta.2",
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",