@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.
- package/assets/skills/exodus-genesis/SKILL.md +76 -25
- package/assets/skills/exodus-write/SKILL.md +18 -12
- package/dist/commands/genesis.d.ts +12 -0
- package/dist/commands/genesis.js +131 -5
- package/dist/commands/idea.d.ts +2 -0
- package/dist/commands/idea.js +29 -3
- package/dist/lib/ad-library-write.d.ts +1 -0
- package/dist/lib/ad-library-write.js +1 -1
- package/dist/lib/reel-write.d.ts +1 -0
- package/dist/lib/reel-write.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
-
|
|
29
|
-
|
|
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.
|
|
45
|
-
>
|
|
46
|
-
>
|
|
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
|
-
#
|
|
99
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
126
|
-
- **`--auto-hooks`** — auto
|
|
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
|
|
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
|
-
- **
|
|
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 —
|
|
111
|
+
## Stage C — Resolve the hook gate, then run Genesis
|
|
112
112
|
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
129
|
-
npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level>
|
|
130
|
-
#
|
|
131
|
-
npx @aicopycoders/exodus genesis run --brief "<brief>" --awareness <level> --
|
|
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> --
|
|
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;
|
package/dist/commands/genesis.js
CHANGED
|
@@ -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
|
-
|
|
439
|
-
|
|
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) {
|
package/dist/commands/idea.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/idea.js
CHANGED
|
@@ -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;
|
|
@@ -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);
|
package/dist/lib/reel-write.d.ts
CHANGED
package/dist/lib/reel-write.js
CHANGED
|
@@ -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);
|