@aicopycoders/exodus 2026.6.2902 → 2026.7.200-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/docs/CLAUDE.md +3 -1
- package/assets/docs/PIPELINES.md +24 -0
- package/assets/references/cast-video.md +9 -0
- package/assets/skills/exodus/SKILL.md +3 -1
- package/assets/skills/exodus-brand/SKILL.md +2 -2
- package/assets/skills/exodus-genesis/SKILL.md +9 -0
- package/assets/skills/exodus-winners/SKILL.md +230 -0
- package/assets/skills/exodus-workflow/SKILL.md +173 -0
- package/dist/bin/exodus.js +4 -1
- package/dist/commands/brand.js +161 -1
- package/dist/commands/creative.js +5 -22
- package/dist/commands/doctor.js +22 -16
- package/dist/commands/genesis.js +19 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/keys.js +1 -0
- package/dist/commands/meme.js +5 -4
- package/dist/commands/migrate.js +7 -6
- package/dist/commands/primer.js +4 -2
- package/dist/commands/segment.d.ts +57 -0
- package/dist/commands/segment.js +274 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +20 -0
- package/dist/commands/winners.d.ts +10 -0
- package/dist/commands/winners.js +401 -0
- package/dist/commands/workflow.d.ts +298 -0
- package/dist/commands/workflow.js +763 -0
- package/dist/lib/channel.d.ts +5 -0
- package/dist/lib/channel.js +15 -0
- package/dist/lib/image-sniff.d.ts +6 -0
- package/dist/lib/image-sniff.js +18 -0
- package/dist/lib/keys-sync.js +4 -1
- package/dist/lib/scaffold.d.ts +5 -4
- package/dist/lib/scaffold.js +27 -6
- package/dist/lib/template-constants.d.ts +1 -1
- package/dist/lib/template-constants.js +102 -0
- package/package.json +1 -1
package/assets/docs/CLAUDE.md
CHANGED
|
@@ -31,13 +31,14 @@ This folder usually holds **two related products that share the same Genesis eng
|
|
|
31
31
|
|
|
32
32
|
## Multi-Brand Operating Notes
|
|
33
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
|
|
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 an "Evergreen Fitness"). 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
35
|
|
|
36
36
|
- **Identify the active brand:** `npx @aicopycoders/exodus brand current` (also says whether it came from the folder or the pointer)
|
|
37
37
|
- **List accessible brands:** `npx @aicopycoders/exodus brand list`
|
|
38
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
39
|
- **Switch brand:** `npx @aicopycoders/exodus brand use <slug>` — or just work from inside that brand's subfolder
|
|
40
40
|
- **New brand created on the dashboard?** `npx @aicopycoders/exodus@latest init` creates its subfolder and pulls its profile
|
|
41
|
+
- **Update the CLI + skills:** `npx @aicopycoders/exodus@latest update` — the only command that refreshes `.claude/skills/` and the workspace docs (`doctor` reports health but never rewrites them)
|
|
41
42
|
- **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
|
|
|
43
44
|
Don't carry voice or proven angles across brands — each brand is its own creative universe.
|
|
@@ -60,6 +61,7 @@ These are the **Exodus** pipelines — the automatic system that drives the Gene
|
|
|
60
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. |
|
|
61
62
|
| **Browse** | `exodus-browse` | View history and retrieve past outputs |
|
|
62
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
|
+
| **Winners** | `exodus-winners` | Mine your own Meta ad account for winning ads (via the Meta Ads MCP) and import them into Exodus as generative fuel — interview, visual confirmation, `npx @aicopycoders/exodus winners import` |
|
|
63
65
|
|
|
64
66
|
For the operator-facing quick reference on how to invoke these in Claude Code, see `PIPELINES.md` in the workspace root.
|
|
65
67
|
|
package/assets/docs/PIPELINES.md
CHANGED
|
@@ -16,6 +16,7 @@ In Claude Code you don't click buttons. You say **"exodus"** plus what you want
|
|
|
16
16
|
| exodus-meme | Meme Ad Generator — recommend formats, one batched server-side run |
|
|
17
17
|
| exodus-browse | View History and Surface the Right Run |
|
|
18
18
|
| exodus-drive | Google Drive, Docs, Sheets via the Dashboard's OAuth |
|
|
19
|
+
| exodus-winners | Mine your own Meta ad account for winners (needs the Meta Ads MCP) and import them into Exodus |
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -200,3 +201,26 @@ For pipeline-run docs, use:
|
|
|
200
201
|
npx @aicopycoders/exodus read-doc <runId> — walks every tab, renders to markdown
|
|
201
202
|
Returns: structured JSON forwarded from Google's APIs (or markdown for read-doc)
|
|
202
203
|
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## exodus-winners
|
|
208
|
+
|
|
209
|
+
**What it does:** Mines your own Meta ad account for winning ads and imports them into Exodus as generative fuel. Your agent session needs the official Meta Ads MCP connected (`https://mcp.facebook.com/ads` — OAuth with your own Facebook login); the skill interviews you (no numeric questions), finds the ads carrying ~80% of your lifetime results, confirms each pick visually, then pushes a winner package through the CLI. Imported winners surface in generation selection with their verdict.
|
|
210
|
+
|
|
211
|
+
```operator-guide
|
|
212
|
+
Subcommands:
|
|
213
|
+
exodus winners import <file.json | -> Push a winner package (- reads stdin)
|
|
214
|
+
exodus winners status <importId> Re-poll an import later
|
|
215
|
+
exodus winners list Winners Exodus already holds
|
|
216
|
+
|
|
217
|
+
Import flags:
|
|
218
|
+
--dry-run Would-create vs would-update per winner; zero writes
|
|
219
|
+
--no-wait Return the importId immediately instead of polling
|
|
220
|
+
--json Machine-readable output
|
|
221
|
+
|
|
222
|
+
Requires: Meta Ads MCP in the session (mining) + your Scrape Creators key in
|
|
223
|
+
Settings → Keys (the own-page match scrape bills your account).
|
|
224
|
+
Say things like: "exodus, import my winning ads" · "exodus, mine my ad
|
|
225
|
+
account for winners" · "exodus, which winners does Exodus already hold?"
|
|
226
|
+
```
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# CAST — a generative grammar for video ad formats
|
|
2
2
|
|
|
3
|
+
> **SUPERSEDED (2026-07, map #528):** the 4-axis CAST grammar below is superseded by the
|
|
4
|
+
> 5-knob **CASTS** system — **Conceit · Actor · Style · Terrain · Setup** — whose canonical
|
|
5
|
+
> terms live in the repo-root **`CONTEXT.md`** glossary. CASTS adds the **Setup** knob (frame
|
|
6
|
+
> arrangement / opening / pacing / overlays) and restructures **Actor** as Role × Look; the
|
|
7
|
+
> product's shelf registry and Rig entity (#538) are built on those terms. This file's
|
|
8
|
+
> thinking still carries forward — conceit = the engine room, novelty vs diversity knobs,
|
|
9
|
+
> and the conceit-primer idea (now the registry's per-value `promptGuidance`) — but for
|
|
10
|
+
> terminology and knob shapes, defer to `CONTEXT.md`.
|
|
11
|
+
|
|
3
12
|
> Part of the creative-strategy framework set; compact index lives in `exodus-strategist`.
|
|
4
13
|
> A faceted system for **decoding any video ad** into its parts and **generating infinite new
|
|
5
14
|
> ones** — while staying human-readable. This is a *thinking* framework: the base Exodus CLI
|
|
@@ -18,11 +18,13 @@ This skill ROUTES. Read what the user wants, name the destination skill, and han
|
|
|
18
18
|
| Static image ads ("make images", "statics", "render this ad") | `exodus-image` (the image front door — it menus engines and confirms before firing) |
|
|
19
19
|
| A specific image engine they already chose | `exodus-creative` (native / copy-derived / ref-match) or `exodus-template` (33 ad-type formats) |
|
|
20
20
|
| Meme ads ("exodus, meme this offer", "make me meme ads") | `exodus-meme` |
|
|
21
|
+
| Run / chain / build / edit a saved workflow ("run my workflow", "what does this workflow need", "build a workflow that…", "list my workflows") | `exodus-workflow` |
|
|
21
22
|
| Onboard a new brand / build the primer | `exodus-primer` (with winning ads) or `exodus-foundation` (no ads yet) |
|
|
22
23
|
| List/switch/troubleshoot brands | `exodus-brand` |
|
|
23
24
|
| See past runs / find an output | `exodus-browse` |
|
|
24
25
|
| Open or create Google Docs/Sheets | `exodus-drive` |
|
|
25
|
-
|
|
|
26
|
+
| Import their OWN winning ads from their Meta ad account ("import my winners", "mine my ad account for winners", "exodus winners") | `exodus-winners` |
|
|
27
|
+
| Update the CLI + skills ("update exodus", "run an exodus update") | run `npx @aicopycoders/exodus@latest update` — the ONLY command that refreshes skills; `doctor` checks health but never rewrites them |
|
|
26
28
|
|
|
27
29
|
If a custom pipeline skill is installed in this folder (any additional `exodus-*` directory under `.claude/skills/`), it routes the same way — match the user's words to that skill's description.
|
|
28
30
|
|
|
@@ -18,7 +18,7 @@ Returns:
|
|
|
18
18
|
|
|
19
19
|
## Strategic Context
|
|
20
20
|
|
|
21
|
-
One API key, many brands. **Any user can create and own unlimited brands** — via `npx @aicopycoders/exodus brand create "<name>"` or the dashboard. This is NOT admin-only, and brand names are scoped per user (two different users can each have
|
|
21
|
+
One API key, many brands. **Any user can create and own unlimited brands** — via `npx @aicopycoders/exodus brand create "<name>"` or the dashboard. This is NOT admin-only, and brand names are scoped per user (two different users can each have an "Evergreen Fitness"). The key works for every brand the user OWNS (admin keys see everything); the CLI attaches `X-Active-Brand: <slug>` on every request. The ONLY "pinned to one brand" case is a brand someone ELSE invited the user into: they don't own it, so the header is ignored for it and it's the single brand their key sees. Never tell a user they can't create brands or are "locked to one brand" because their key shows `role: member` — a member who owns brands sees and creates as many as they want.
|
|
22
22
|
|
|
23
23
|
The active brand resolves **folder > pointer > key default**: on a multi-brand install each brand has a subfolder (marked by `.exodus-brand.json`), and running from inside one targets that brand automatically — it wins over the `{ activeBrand: "<slug>" }` pointer in `.exodus/state.json` that `brand use` writes. `brand current` tells you which one applied.
|
|
24
24
|
|
|
@@ -50,7 +50,7 @@ Shows role, the active brand (marked `★`), and the full accessible list. Flags
|
|
|
50
50
|
### Create a new brand
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
npx @aicopycoders/exodus brand create "
|
|
53
|
+
npx @aicopycoders/exodus brand create "Evergreen Fitness"
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
Creates a brand the user owns (any user can — no admin role required), then switches the CLI into it and, on multi-brand installs, sets up its subfolder. Brand names are scoped per user, so this succeeds even if another account already has a brand by that name (the slug is auto-disambiguated under the hood). Tell the user the next step is the primer (`exodus, set up my brand primer`). If the dashboard ever reports a name conflict for a brand the user is *sure* they've never made, that's a separate bug — not a sign the name is taken for them.
|
|
@@ -23,6 +23,7 @@ Hook gate (manual mode — review hooks in Claude Code):
|
|
|
23
23
|
on pause the run prints a numbered hook pool, then:
|
|
24
24
|
genesis continue --id <runId> --hooks 1,3,5 write the picked hooks (one ad each)
|
|
25
25
|
genesis regenerate --id <runId> [--steering "…"] re-roll the pool (reject + steer)
|
|
26
|
+
genesis reject --id <runId> abandon the run (terminal "superseded")
|
|
26
27
|
genesis hooks --id <runId> re-print the pool (fresh-session resume)
|
|
27
28
|
```
|
|
28
29
|
|
|
@@ -157,6 +158,14 @@ npx @aicopycoders/exodus genesis regenerate --id <runId> --steering "lead with t
|
|
|
157
158
|
- 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.
|
|
158
159
|
- 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.
|
|
159
160
|
|
|
161
|
+
**C. Abandon the run (reject):** when the user is done with this run entirely — "kill it", "forget this one", or they're starting over with a different brief:
|
|
162
|
+
```bash
|
|
163
|
+
npx @aicopycoders/exodus genesis reject --id <runId>
|
|
164
|
+
```
|
|
165
|
+
- The run lands in the terminal **superseded** state — not failed, not awaiting anything — instead of sitting at the hook gate forever.
|
|
166
|
+
- If they replaced it with a new run, link the two: `--superseded-by <newRunId>`.
|
|
167
|
+
- Terminal: a rejected run cannot be continued or re-rolled afterward.
|
|
168
|
+
|
|
160
169
|
**Fresh session / lost the list?** Re-fetch and reprint the pool any time with:
|
|
161
170
|
```bash
|
|
162
171
|
npx @aicopycoders/exodus genesis hooks --id <runId>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: exodus-winners
|
|
3
|
+
description: Mine the member's OWN Meta ad account for its winning ads and import them into Exodus as generative fuel. The member's Claude Code session holds BOTH the official Meta Ads MCP (their Facebook login, their machine) and the Exodus CLI; this skill orchestrates the whole journey — point them at the Meta MCP, pick one ad account, run a NO-NUMBERS winner-definition interview, mine the account via the MCP, confirm each pick visually, then push a winner-package JSON with `npx @aicopycoders/exodus winners import` and run the two-phase video gap-filler. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("exodus, import my winning ads", "exodus winners", "exodus, mine my ad account for winners", "exodus, which of my own ads are winners"), named this skill or /exodus-winners, ran an `npx @aicopycoders/exodus winners` command, or the `exodus` hub skill routed here. Never claim generic asks ("what are my best ads", "pull my Meta ads", "analyze my Facebook ads") without Exodus context — in shared folders those may belong to the user's other tools; if the user did not say exodus, this skill is not for them. This is the member's OWN account's winners, NOT competitor swipes — competitor swipe mining lives on the dashboard's Swipe Mining surfaces, not here. The bare word "Genesis" without "exodus" refers to the member's own Genesis API key and personal bot recipes, NOT to Exodus.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```operator-guide
|
|
7
|
+
Subcommands:
|
|
8
|
+
exodus winners import <file.json | -> [--dry-run] [--no-wait] [--json] Push a winner package (- reads stdin)
|
|
9
|
+
exodus winners status <importId> [--json] Re-poll an import later
|
|
10
|
+
exodus winners list [--json] Winners Exodus already holds
|
|
11
|
+
|
|
12
|
+
Key flags:
|
|
13
|
+
--dry-run Local schema check + server dry-run: would-create vs would-update per winner,
|
|
14
|
+
plus a Scrape Creators key warning if it's missing. Zero writes, no media upload.
|
|
15
|
+
--no-wait Return the importId immediately instead of polling to outcomes
|
|
16
|
+
--json Machine-readable output
|
|
17
|
+
|
|
18
|
+
Facts baked into the CLI:
|
|
19
|
+
• Scopes to your active brand (exodus brand current). Switch with the exodus-brand skill.
|
|
20
|
+
• Requires the member's Scrape Creators key (Settings → Keys) — the own-page match scrape bills them.
|
|
21
|
+
• asset paths in the package resolve RELATIVE TO THE JSON FILE'S FOLDER.
|
|
22
|
+
• Re-push never duplicates rows: verdict snapshots replace wholesale; gap-filled rows
|
|
23
|
+
upgrade in place; winners absent from a re-push are untouched. It DOES re-run the
|
|
24
|
+
billed match scrape + enrichment — push updates, don't push as a retry reflex.
|
|
25
|
+
|
|
26
|
+
Meta side: the official Meta Ads MCP (https://mcp.facebook.com/ads) supplies all account data.
|
|
27
|
+
This skill NEVER calls the Meta Graph API directly and NEVER calls ads_library_search.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
# Winners — Mine Your Own Ad Account and Import the Champions
|
|
31
|
+
|
|
32
|
+
This is the agent-side journey for **Own-Brand Winners**: turn the member's real Meta ad account into a set of designated winners that Exodus stores as generative fuel. The session holds the member's **own Meta Ads MCP** (their Facebook OAuth) alongside the Exodus CLI — you drive both.
|
|
33
|
+
|
|
34
|
+
The member is a **creative strategist, not a media buyer**. This entire journey is data *collection and preparation* — the analysis happens later, server-side, on the stored winners. So the interview **never asks a single numeric question**: no budgets, no ROAS targets, no thresholds, no "top how many?". You infer the mechanics and confirm in plain language. Make that rule impossible to break — if you catch yourself about to ask for a number, stop and infer it instead.
|
|
35
|
+
|
|
36
|
+
The arc, in order: **MCP check → pick one account → winner-definition interview → mine via MCP → 30k-foot view → visual confirmation → write the package → dry-run → import → video gap-filler.** Persist the definition so re-mining re-asks nothing already settled.
|
|
37
|
+
|
|
38
|
+
## 0. Meta Ads MCP must be connected
|
|
39
|
+
|
|
40
|
+
Everything here reads from the **official Meta Ads MCP** — a remote MCP server at `https://mcp.facebook.com/ads`, OAuth'd in the user's browser with their own Facebook login. We don't own that flow; there is exactly one setup pointer and no troubleshooting beyond it.
|
|
41
|
+
|
|
42
|
+
**If the `ads_*` Meta tools are not present in this session, stop.** Tell the user to add `https://mcp.facebook.com/ads` as a remote MCP server and complete the browser OAuth, then come back. Do not attempt any workaround, any Graph API call, or any other connection method.
|
|
43
|
+
|
|
44
|
+
## 1. Account selection — the required first step, pinned forever
|
|
45
|
+
|
|
46
|
+
Call `ads_get_ad_accounts`. It returns **every** account the login can touch — often a dozen-plus across several businesses. For each: `ad_account_id`, `ad_account_name`, `business_name`, `is_queryable`, `not_queryable_reason`, currency.
|
|
47
|
+
|
|
48
|
+
- Show only **queryable** accounts (`is_queryable: true`). For any flagged one, surface its `not_queryable_reason` so the user understands why it's absent — don't silently drop it.
|
|
49
|
+
- **Account names can be empty strings** — fall back to the id and business name so the list is still legible.
|
|
50
|
+
- The user picks **one**. That account is **pinned** in the definition file (`adAccountId` + name) and is **never re-asked** and never allowed to silently drift. Multi-account handling beyond this single pin is out of scope — one account per definition file.
|
|
51
|
+
|
|
52
|
+
## 2. The winner-definition interview — prescriptive, no numbers
|
|
53
|
+
|
|
54
|
+
You are not collecting preferences here so much as confirming inferences. Four things get settled, and only one of them ever shows the user a choice.
|
|
55
|
+
|
|
56
|
+
**a) Objective → metric is invisible.** Ads group by their campaign **objective**; each group's success metric is the account's own self-labeled result type — Meta hands it to you already labeled (`results: {"value": "98 (Website appointments scheduled)"}`, `cost_per_result: {"value": "$91.64 USD (Website appointments scheduled)"}` — the string lives under `.value`). **Never ask the user to pick a metric.** The account already declared it. Offer one free-text escape hatch only — *"If your definition of a winner is different from 'the ads that drove the most results,' tell me in your own words."* — and record whatever they say **verbatim** as `customDefinition`, applied agent-side. Do not turn their words into a number.
|
|
57
|
+
|
|
58
|
+
**b) Funnel mapping is infer-then-confirm.** Propose, per campaign, an **awareness level** from the Schwartz five — `unaware` / `problem-aware` / `solution-aware` / `product-aware` / `most-aware` — inferred from the campaign objective plus the campaign/adset **names** (naming is read-only creative context; see §6). Present the whole proposed map **once** for a single confirm/adjust pass, then save it. On a re-mine, only **new** campaigns get proposed; already-mapped ones stay put.
|
|
59
|
+
|
|
60
|
+
**c) The winner rule is fixed — volume over efficiency, all-time, no caps.** State it, don't negotiate it:
|
|
61
|
+
|
|
62
|
+
> Within each objective group, over the account's full lifetime (`date_preset: "maximum"`), the winners are the **smallest set of ads that together carry ~80% of that group's lifetime results**, where each qualifying ad has **at least 10 results** (a materiality floor — one lucky conversion is not a winner). No top-N cap.
|
|
63
|
+
|
|
64
|
+
- **Union with per-format cuts.** Run the same 80%/≥10 rule three ways: **overall**, **within video only**, and **within image only**, then union the results. This guarantees both lanes populate even when one format dominates spend. Each winner records which cut(s) it cleared as `selectionBasis`: any of `overall` / `top-video` / `top-image`.
|
|
65
|
+
- **Flat curve honesty.** If contribution is flat — no elbow, results spread evenly across hundreds of ads — say so plainly and show the top contributors instead of pretending there's a clean winner set. Never fabricate outliers to hit the 80% line.
|
|
66
|
+
|
|
67
|
+
**d) There are no losers at v1.** You designate winners only. Don't ask about, or invent, a "worst ads" pass.
|
|
68
|
+
|
|
69
|
+
The defaults behind this rule (`window: "maximum"`, `resultsFloor: 10`, `contributionLine: 0.80`) live in the definition file and are **edited in the file, never interrogated**. Selection is **format-agnostic** — you never ask "video or image?"; the per-format cuts already guarantee top images *and* top videos import.
|
|
70
|
+
|
|
71
|
+
## 3. Mining mechanics — the MCP facts that make or break this
|
|
72
|
+
|
|
73
|
+
Mine on `ads_get_ad_entities`. These are hard-won live-probe facts; encode them into how you call, not into a doc you can look up later:
|
|
74
|
+
|
|
75
|
+
- **Sort server-side, threshold agent-side.** Metric *filtering* on `ads_get_ad_entities` consistently **500s** — never filter by a metric. Metric *sorting* works. Sort by **results descending** and pull **the complete group — every page** — over `date_preset: "maximum"`; you need the group's full results total (the 80% denominator), so a sorted prefix is never enough. Then apply the 80%/≥10 rule yourself in memory.
|
|
76
|
+
- **Retry once on a 500.** Transient 500s happen even on valid calls; an identical retry usually succeeds. Retry a failed call once before treating it as real.
|
|
77
|
+
- **Metric values are display strings, not numbers** — `"$8,980.38 USD"`, `"4.20%"`, `"Not available"` for zero-delivery ads. Keep them **verbatim** for the verdict snapshot. You still need to *rank* by them agent-side — parse a working copy for the math, but never mutate the string you store. (Account level also returns a numeric `amount_spent_cents` if you need a clean total.)
|
|
78
|
+
- **`results` / `cost_per_result` self-label the outcome** (`.value` = `"98 (Website appointments scheduled)"`). That label is the metric — carry it into `resultType`.
|
|
79
|
+
- **Hierarchy:** ads carry `campaign_id` + `adset_id`; `objective` lives on the **campaign**. There is no `campaign_name` at ad level — get campaign/adset names from a **campaign-level / adset-level call**, then join by id.
|
|
80
|
+
- **Always pass explicit `fields`** on every by-id lookup (`ads_get_creatives`, `ads_get_ad_images`, `ads_get_ad_videos`). The "omit fields → all fields" default is **broken** — a bare by-id lookup returns only `{id}`.
|
|
81
|
+
- **Creative content needs a re-fetch by `creative_ids`.** Listing mode returns ids/names only. A creative detail call gives `body` (primary text), `title` (headline), `call_to_action_type`, `image_hash`, `image_url` (signed CDN), and `effective_object_story_id` (`<pageId>_<postId>` — its **prefix is your page id**, the scrape target and own-brand row key).
|
|
82
|
+
- **`pageName` comes from the user, not from Meta.** The page tools can return `"(unknown)"` for the page name, and `ads_library_search` (which carries it) is off-limits — so once you have the page id, **confirm the page's name with the user in one line** ("This account posts as page 10715… — that's your <name> page, right?"). It names their own-brand entry in Exodus; don't guess it.
|
|
83
|
+
- **Non-US accounts need `country` set.** The package's `source.country` defaults to `US`, and a wrong country makes the server-side Ad Library match silently miss. Infer it from the account (currency, business) and confirm alongside the page name when it isn't clearly US.
|
|
84
|
+
- **Images: download the bytes at interview time, not push time.** `ads_get_ad_images` (by hash) returns a full-res signed CDN `url` that **expires** (watch the `oe=` param). Grab it while you're confirming the winner, save it locally, and reference the local file in the package.
|
|
85
|
+
- **Video: there is no download URL. Never promise one.** `ads_get_ad_videos` gives `title`, `length`, and a poster `picture` only — no source/download URL. Grab the **poster** image for video winners; the actual mp4 comes from the user in the gap-filler (§8).
|
|
86
|
+
- **One `breakdowns` value per call.** Stay on `ads_get_ad_entities` for raw data — the `ads_insights_*` tools are opinionated narrative wrappers (trend/funnel prose), not the numbers you're ranking.
|
|
87
|
+
- **Pass the user's real ask through `advertiser_request`** on each tool call — it's Meta's audit surface; give it the user's actual words.
|
|
88
|
+
- **Never call `ads_library_search`.** Winner↔Ad-Library matching happens **server-side in Exodus** after import; the old title-fingerprint approach is dead. Your job ends at the account data.
|
|
89
|
+
|
|
90
|
+
## 4. The 30,000-foot view first
|
|
91
|
+
|
|
92
|
+
Before you confirm a single ad, lead with the **account summary** — the strategist wants the shape before the specimens:
|
|
93
|
+
|
|
94
|
+
> "1,041 ads, 12,400 purchases all-time. 37 ads produced ~80% of them — and 29 of those 37 are video. That format skew is itself a finding."
|
|
95
|
+
|
|
96
|
+
The format skew, the count of winners, the share they carry — that framing *is* strategic value. Deliver it, then move to confirmation.
|
|
97
|
+
|
|
98
|
+
## 5. Visual confirmation — the human gate
|
|
99
|
+
|
|
100
|
+
This is a HITL gate; treat it like one. For each proposed winner, render it with **`ads_get_ad_preview`** (an actual in-context creative image) and show it. The strategist:
|
|
101
|
+
|
|
102
|
+
- **strikes** picks that don't belong,
|
|
103
|
+
- **adds** any obvious winner the rule missed,
|
|
104
|
+
- and may attach an optional free-text **note** per winner (`strategistNote`).
|
|
105
|
+
|
|
106
|
+
Their edits win over the algorithm. Don't argue the rule against their eyes — they made these ads. Only after this pass do you compose verdicts and write the package.
|
|
107
|
+
|
|
108
|
+
## 6. Naming is read-only
|
|
109
|
+
|
|
110
|
+
Campaign / adset / ad names feed two things: your **awareness inference** (§2b) and the winner's `sourceNames` as creative context. That's all. **No naming-conventions guidance** — nobody is asked to rename anything, and the account's naming hygiene is not your concern.
|
|
111
|
+
|
|
112
|
+
## 7. The verdict snapshot & the winner-package JSON
|
|
113
|
+
|
|
114
|
+
Every winner carries a **verdict snapshot** composed to the schema below. The metrics stay as **display strings verbatim** — never parsed into numbers in what you store — and you compose one plain-language sentence per winner. A **fresh snapshot is written on every re-mine; snapshots are never patched.**
|
|
115
|
+
|
|
116
|
+
```jsonc
|
|
117
|
+
{
|
|
118
|
+
"version": 1,
|
|
119
|
+
"source": {
|
|
120
|
+
"adAccountId": "act_123…",
|
|
121
|
+
"adAccountName": "Ground Co",
|
|
122
|
+
"pageId": "10715…", // scrape target + own-brand row key (from effective_object_story_id prefix)
|
|
123
|
+
"pageName": "Ground Co",
|
|
124
|
+
"country": "US", // optional, default US — non-US accounts false-unmatch otherwise
|
|
125
|
+
"minedAt": "2026-07-14T18:30:00Z"
|
|
126
|
+
},
|
|
127
|
+
"winners": [
|
|
128
|
+
{
|
|
129
|
+
"accountAdId": "1202…", // the natural key; REQUIRED
|
|
130
|
+
"campaignId": "1201…",
|
|
131
|
+
"adSetId": "1202…",
|
|
132
|
+
"sourceNames": { "campaign": "…", "adSet": "…", "ad": "…" }, // verbatim, read-only
|
|
133
|
+
"createdTime": "2025-11-02T…",
|
|
134
|
+
"format": "video", // "video" | "image"; REQUIRED
|
|
135
|
+
"bodyText": "…", // verbatim; bodyText OR headline required
|
|
136
|
+
"headline": "…", // optional
|
|
137
|
+
"ctaType": "SHOP_NOW", // optional
|
|
138
|
+
"effectiveObjectStoryId": "pageId_postId", // optional
|
|
139
|
+
"verdict": {
|
|
140
|
+
"sentence": "Winner by contribution: 2,214 purchases (6.2% of account) @ $18.20, all-time through Jul 2026", // REQUIRED
|
|
141
|
+
"resultType": "Purchases", // the self-label from results.value's parenthetical
|
|
142
|
+
"results": "2,214", // Meta's display strings, never converted to numbers —
|
|
143
|
+
"resultsShare": "6.2%", // resultsShare is the one YOU derive (share of group total)
|
|
144
|
+
"spend": "$40,297",
|
|
145
|
+
"costPerResult": "$18.20",
|
|
146
|
+
"asOf": "2026-07-14",
|
|
147
|
+
"objective": "OUTCOME_SALES",
|
|
148
|
+
"awarenessLevel": "product-aware", // from the confirmed mapping (§2b)
|
|
149
|
+
"selectionBasis": ["overall", "top-video"], // which cut(s) it cleared
|
|
150
|
+
"strategistNote": "…" // optional, from §5
|
|
151
|
+
},
|
|
152
|
+
"assets": {
|
|
153
|
+
"imagePath": "./media/1202….jpg", // optional — downloaded at interview time
|
|
154
|
+
"videoPath": "./media/1202….mp4", // optional — user-supplied file (gap-filler)
|
|
155
|
+
"posterPath": "./media/1202…-poster.jpg" // optional
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
All verdict metrics are display strings verbatim. `assets` is optional everywhere. A **malformed winner is rejected individually server-side** (with a reason in the ledger) — it never aborts the whole import.
|
|
163
|
+
|
|
164
|
+
## 8. Import — dry-run, push, then the video gap-filler
|
|
165
|
+
|
|
166
|
+
Run these in this exact order.
|
|
167
|
+
|
|
168
|
+
**1. Write the package to disk.** Put it at `state/own-brand-winners/import-<date>.json` (user-inspectable and re-runnable), with the interview-time downloaded media in a folder alongside it. **Asset paths are relative to the JSON file's folder** — so `./media/…jpg` next to `import-<date>.json` resolves correctly.
|
|
169
|
+
|
|
170
|
+
**2. Dry-run first, always:**
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npx @aicopycoders/exodus winners import state/own-brand-winners/import-<date>.json --dry-run --json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Show the user the would-create vs would-update split. **If the dry-run reports the Scrape Creators key missing, surface it now** — a real import will fail without it; route them to Settings → Keys (scrapecreators.com). Fix any would-reject rows, then push for real:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx @aicopycoders/exodus winners import state/own-brand-winners/import-<date>.json --json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Default polling waits for outcomes. Present the **per-winner outcome table** it returns — each row is `matched` / `gap-filled` / `partial (missing: …)` / `rejected (reason)` — plus the created-vs-updated split.
|
|
183
|
+
|
|
184
|
+
**3. The two-phase video gap-filler.** Do **not** open with "find me 12 mp4 files." Push everything first; the report names exactly which video winners came back `partial (missing: video-file)` (expected — MCP has no video download). Then ask the user for **just those** files — they made the ads, they usually have them. Write a **second import JSON** containing only the partial winners: copy each winner's **full object** from the first import file (the server validates every push the same way — a stripped-down winner missing its `verdict`, `format`, or copy is rejected, not merged), same `source` block, and add the `videoPath`. Re-push it — the idempotent upgrade path completes those rows in place (the identical verdict re-replacing itself is harmless):
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx @aicopycoders/exodus winners import state/own-brand-winners/gapfill-<date>.json --json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Pick gap-fill candidates by their `missing` array **containing** `video-file` — an unmatched video winner usually lists `transcript` too. The re-push fills the file gap, but transcription runs as a **separate sweep after the import finalizes**, so the row typically still reads `partial (missing: transcript)` right away. That's not a failure — tell the user, and re-check `exodus winners list` later for the transcript to land.
|
|
191
|
+
|
|
192
|
+
Any winner the user can't supply a file for **stays amber and visible** in `exodus winners list` — that's fine, not a failure.
|
|
193
|
+
|
|
194
|
+
**4. "What does Exodus already hold?" is `exodus winners list`** — never keep the imported-winners ledger agent-side; it lives server-side and this is how you read it.
|
|
195
|
+
|
|
196
|
+
**5. Re-push safety** (from the CLI itself): re-pushing never duplicates rows — verdict snapshots replace wholesale, gap-filled rows upgrade in place, winners absent from a re-push are left untouched. But it is not free: every push re-uploads referenced assets, re-runs enrichment, and re-fires the Scrape Creators match scrape (billed to the member) — so re-push when there's something to update, not as a retry reflex.
|
|
197
|
+
|
|
198
|
+
## 9. Definition persistence
|
|
199
|
+
|
|
200
|
+
Persist the interview to `state/own-brand-winners.json`, keyed by **brand slug** (`state/` survives skill refreshes). It holds:
|
|
201
|
+
|
|
202
|
+
- pinned `adAccountId` + name (§1),
|
|
203
|
+
- `campaignAwarenessMap` (§2b),
|
|
204
|
+
- editable `defaults` — `{ "window": "maximum", "resultsFloor": 10, "contributionLine": 0.80 }`,
|
|
205
|
+
- `customDefinition` (the §2a escape-hatch text, verbatim),
|
|
206
|
+
- `lastMinedAt`.
|
|
207
|
+
|
|
208
|
+
Re-running re-asks **nothing already pinned** — only genuinely new campaigns get a confirm pass. Losing the file costs a short re-interview, never duplicate rows (dedup is server-side on `accountAdId`). Defaults are changed by editing the file, never by interrogating the user.
|
|
209
|
+
|
|
210
|
+
## Failure handling
|
|
211
|
+
|
|
212
|
+
- **Meta `ads_*` tools not in the session** — stop and point at `https://mcp.facebook.com/ads` (§0). No workaround exists here.
|
|
213
|
+
- **`ads_get_ad_entities` 500 with a metric `filtering`** — that's the known filter bug, not a transient. Drop the filter, sort instead, threshold agent-side (§3).
|
|
214
|
+
- **Any tool 500s once** — retry the identical call once before reporting it.
|
|
215
|
+
- **A by-id lookup returns only `{id}`** — you omitted `fields`. Re-call with explicit `fields` (§3).
|
|
216
|
+
- **Signed image URL 403 / expired** — it lapsed since the interview. Re-fetch `ads_get_ad_images` and download again immediately.
|
|
217
|
+
- **Dry-run says Scrape Creators key missing** — real import will fail; Settings → Keys first.
|
|
218
|
+
- **`partial (missing: video-file)`** — expected for video winners; run the §8 gap-filler.
|
|
219
|
+
- **Import poll timed out** — the import is still running server-side; re-poll with `npx @aicopycoders/exodus winners status <importId>` rather than re-pushing.
|
|
220
|
+
- **Import auth / wrong brand** — `npx @aicopycoders/exodus doctor`, then confirm `npx @aicopycoders/exodus brand current` is the brand you mean (switch via the `exodus-brand` skill). Winners scope to the active brand.
|
|
221
|
+
|
|
222
|
+
## Not the right skill for
|
|
223
|
+
|
|
224
|
+
- **Competitor swipe mining** — this skill is the member's OWN ad account only. Mining competitors' ads lives on the dashboard's **Swipe Mining** surfaces (and the CLI swipe commands), not here.
|
|
225
|
+
- **Naming-conventions guidance** — names are read-only inputs; nobody is asked to rename anything.
|
|
226
|
+
- **Designating losers** — winners only at v1.
|
|
227
|
+
- **Scheduled / automated re-mining cadence** — re-run the skill when you want a fresh snapshot; there's no scheduler.
|
|
228
|
+
- **Multi-account handling beyond the single pin** — one account per definition file.
|
|
229
|
+
- **Meta MCP connection troubleshooting beyond the one URL**, and **any direct Meta Graph API call** — out of bounds; the MCP is the only Meta surface.
|
|
230
|
+
- **Writing copy or rendering images from these winners** — that happens downstream once they're stored (`exodus-genesis`, `exodus-image`); this skill only collects and imports.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: exodus-workflow
|
|
3
|
+
description: Run, chain, build, and edit saved Exodus Workflows — the multi-node automations that wire Genesis bots, primers, briefs, and image nodes together on the dashboard canvas — from the CLI. Use it to list a brand's workflows, describe what a workflow needs before running it (inputs, per-workspace primer prerequisites, outputs), run one and collect its outputs, chain one workflow's output into another's input, browse the bot catalog, and author or update a workflow contract via export → edit → import. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("exodus, run my launch workflow", "exodus, what does this workflow need", "exodus, build a workflow that turns a swipe into hooks then ads", "list my exodus workflows", "exodus, edit that workflow"), named this skill or /exodus-workflow, ran an `npx @aicopycoders/exodus workflow` command, or the `exodus` hub skill routed here. Never claim generic "workflow"/"automation" requests ("automate this", "build me a workflow") without Exodus context — in shared folders those may belong to the user's other tools; if the user did not say exodus, this skill is not for them. The bare word "Genesis" without "exodus" refers to the member's own Genesis API key and personal bot recipes, NOT to Exodus.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```operator-guide
|
|
7
|
+
Subcommands:
|
|
8
|
+
exodus workflow list [--json] List the brand's saved workflows
|
|
9
|
+
exodus workflow describe <workflowId|name> [--json] Inputs, prerequisites, outputs
|
|
10
|
+
exodus workflow bots [--category <cat>] [--slug <slug>] [--json] Bot catalog / one bot's spec
|
|
11
|
+
exodus workflow run <workflowId|name> [--input key=value ...] [--wait] [--json] Run it
|
|
12
|
+
exodus workflow status [--id <runId>] [--json] Poll a run / read its outputs
|
|
13
|
+
exodus workflow export <workflowId|name> [--out <file>] Dump the contract JSON
|
|
14
|
+
exodus workflow import <file> [--update <workflowId>] [--dry-run] [--json] Author/edit
|
|
15
|
+
|
|
16
|
+
Key flags:
|
|
17
|
+
--input key=value Repeatable run input; the value may contain "="
|
|
18
|
+
--input key=@path Load the value from a file (path resolved from the cwd);
|
|
19
|
+
use key=@@text to keep a leading "@" as a literal character
|
|
20
|
+
--wait Poll until the run reaches a terminal status, then print outputs
|
|
21
|
+
--id <runId> Run id for `status` detail
|
|
22
|
+
--category <cat> (bots) Filter the catalog to one category id
|
|
23
|
+
--slug <slug> (bots) Show a single bot's full port + param spec
|
|
24
|
+
--out <file> (export) Write JSON to a file instead of stdout
|
|
25
|
+
--update <workflowId> (import) Update this workflow in place (optimistic-concurrency
|
|
26
|
+
guarded by the export's updatedAt — a 409 means re-export first)
|
|
27
|
+
--dry-run (import) Validate + resolve refs without writing; returns issues
|
|
28
|
+
|
|
29
|
+
Auth: Bearer (EXODUS_API_KEY). The canvas lives on the dashboard at /workflows.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
# Workflow — Run, Chain, and Build Saved Automations
|
|
33
|
+
|
|
34
|
+
An Exodus **Workflow** is a saved graph the member builds on the dashboard canvas (`/workflows`): brief and primer sources feed Genesis bots, bots feed other bots or an image node, and an Output node collects the deliverables. This skill is the CLI operator for those graphs — discover and run them, chain one into the next, and author or edit the contract without opening a browser.
|
|
35
|
+
|
|
36
|
+
Three jobs, three sections below. Read the request and jump to the one that fits; don't walk all three when the user just wants a run.
|
|
37
|
+
|
|
38
|
+
## 1. Discover & run
|
|
39
|
+
|
|
40
|
+
The mistake to avoid is firing a run blind and wasting it on a missing input or an unmet prerequisite. Always **`describe` before `run`**.
|
|
41
|
+
|
|
42
|
+
### Step 1 — List
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @aicopycoders/exodus workflow list
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Prints the brand's saved workflows (name, id, node/edge counts, timestamps). If the user named the workflow already, skip straight to `describe`.
|
|
49
|
+
|
|
50
|
+
### Step 2 — Describe (read inputs, prereqs, outputs)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @aicopycoders/exodus workflow describe "Launch Flow"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Accepts a workflow id **or** a name. It reports three things:
|
|
57
|
+
|
|
58
|
+
- **Inputs** — each free input the run needs: field name, source (`text`, `swipe-ad`, `swipe-bundle`, `organic-url`, `ad-url`), whether it's required, and a description. These are the `--input key=value` pairs you'll pass.
|
|
59
|
+
- **Prerequisites** — the per-workspace primer markers the graph's primer nodes depend on, each shown **✓ stored** or **✗ missing** for *this brand*. **If any prerequisite is ✗, warn the user before running** — the run will not produce good output (or will fail) without it. Route them to the `exodus-primer` / `exodus-foundation` skill to fill the gap first, rather than burning a run.
|
|
60
|
+
- **Outputs** — the deliverables the Output node collects (text or image, with labels), so you know what to expect back.
|
|
61
|
+
|
|
62
|
+
### Step 3 — Gather inputs
|
|
63
|
+
|
|
64
|
+
Fill every required input. For a free-text brief you can compose a **bespoke brief from in-session research** — the winning angle you just worked out, the swipe you analyzed — and pass it inline (`--input brief="..."`) or, if it's long, write it to a project file and pass it with `@file` (see Chain, below). For URL/swipe sources, pass the value the source expects.
|
|
65
|
+
|
|
66
|
+
### Step 4 — Run and collect
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @aicopycoders/exodus workflow run "Launch Flow" --input brief="new cortisol offer, problem-aware" --wait
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`--wait` polls to a terminal status and prints the run's collected outputs — text inline, image URLs. Without `--wait` the command returns the `runId` and you poll it yourself:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx @aicopycoders/exodus workflow status --id <runId>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Report the outputs plus a short take. Don't call a run "done" off the kickoff line alone — a run reaches `completed`, `partial`, or `failed`; a `partial` means some nodes were skipped or failed, so read the status before declaring victory.
|
|
79
|
+
|
|
80
|
+
## 2. Chain — feed one workflow's output into the next
|
|
81
|
+
|
|
82
|
+
The chaining surface is the run's flattened outputs. After a `--wait` run (or once `status` shows terminal), pull the machine-readable outputs:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx @aicopycoders/exodus workflow status --id <runId> --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The response carries a top-level `outputs` array — each entry is `{ nodeId, botSlug?, type, label, text?, imageUrl?, imageId? }`. Extract the text you want (e.g. `.outputs[].text`) and pass it into the next workflow's input:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Inline, for short outputs
|
|
92
|
+
npx @aicopycoders/exodus workflow run "Ad Writer" --input hooks="<text from run A>" --wait
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**The file-as-persistence convention.** For anything reusable or long — a brief, a sales letter, a hook pool — write the text to a **markdown file in the project** and pass it with `@file`. This is the deliberate v1 alternative to a server-side store: the file *is* the persistence layer, versioned in the project alongside the brand's other artifacts.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Save run A's text output to a project file, then feed it to workflow B
|
|
99
|
+
npx @aicopycoders/exodus workflow status --id <runId> --json # copy .outputs[].text into state/hooks.md
|
|
100
|
+
npx @aicopycoders/exodus workflow run "Ad Writer" --input hooks=@state/hooks.md --wait
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`@path` is resolved from the current directory. If the literal value you need actually starts with `@`, escape it as `@@` (e.g. `--input handle=@@brand` passes the string `@brand`).
|
|
104
|
+
|
|
105
|
+
Use this pattern to compose pipelines the canvas doesn't wire directly: run the research/hook workflow, persist its best output as a file, then run the writing workflow off that file — reproducibly, because the file stays.
|
|
106
|
+
|
|
107
|
+
## 3. Build & edit — author a workflow contract
|
|
108
|
+
|
|
109
|
+
The documented way to learn the contract format is to **export an existing workflow and study it** — there is no separate schema doc. The loop is: learn the bots → learn the format → compose → dry-run until clean → import → review on the canvas.
|
|
110
|
+
|
|
111
|
+
### Step 1 — Learn the bot catalog
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx @aicopycoders/exodus workflow bots # full catalog, grouped by category
|
|
115
|
+
npx @aicopycoders/exodus workflow bots --category writing # one category
|
|
116
|
+
npx @aicopycoders/exodus workflow bots --slug new-hook-bot # one bot's full port + param spec
|
|
117
|
+
npx @aicopycoders/exodus workflow bots --json # raw catalog for machine use
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Per bot you get its **input ports** (id, what it accepts — `text` / `primer` / `image`, whether it's required, and for primer ports which `primerKinds` gate it — `body` / `hook` / `headline` / `summary`), its **params** (select/text/toggle/number/multiselect, with options and defaults), and its output type. This is what tells you which node can wire into which — a port only accepts an edge whose source produces a type it lists in `accepts`.
|
|
121
|
+
|
|
122
|
+
### Step 2 — Learn the format by exporting
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx @aicopycoders/exodus workflow export "Launch Flow" --out launch.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Study `launch.json`: it's the `exodus-workflow` contract — `nodes` (each with `id`, `kind` = `brief`/`bot`/`primer`/`image`/`output`, a `config`, and an optional `position`) and `edges` (`source`/`sourceHandle` → `target`/`targetHandle`, the handles being port ids). An export also carries `workflowId` + `updatedAt` at the top — those are update anchors, not part of the graph.
|
|
129
|
+
|
|
130
|
+
### Step 3 — Compose the contract
|
|
131
|
+
|
|
132
|
+
Author the JSON by hand or by adapting an export. **Node positions are optional** — leave them out and the server auto-layouts the graph on import; you don't need to compute coordinates.
|
|
133
|
+
|
|
134
|
+
### Step 4 — Dry-run like a compiler loop
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx @aicopycoders/exodus workflow import launch.json --dry-run
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`--dry-run` validates and resolves refs without writing. It returns compiler-style issues — each names the **node id**, the **port id** where relevant, and a concrete **remedy** (the exact edit that fixes it). Codes you'll see include `dangling-edge`, `unknown-port`, `type-mismatch`, `missing-required-input`, `duplicate-input`, `cycle`, `unknown-kind`, and `bad-config`. Fix, re-run `--dry-run`, repeat until it's clean — same loop as fixing type errors.
|
|
141
|
+
|
|
142
|
+
### Step 5 — Import (create or update)
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Create a new workflow
|
|
146
|
+
npx @aicopycoders/exodus workflow import launch.json
|
|
147
|
+
|
|
148
|
+
# Update an existing workflow in place
|
|
149
|
+
npx @aicopycoders/exodus workflow import launch.json --update wf_123
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then **tell the user to review it on the canvas at `/workflows`** — the CLI writes the graph; the human eye confirms it reads right.
|
|
153
|
+
|
|
154
|
+
**Two rules for edits (`--update`):**
|
|
155
|
+
|
|
156
|
+
- **Always export a backup first.** v1 has no version history — a destructive `--update` overwrites the live workflow with no undo. `export --out backup.json` before you import over it.
|
|
157
|
+
- **A 409 means the canvas moved.** `--update` sends the export's `updatedAt` as an optimistic-concurrency guard. If someone edited the workflow on the dashboard since your export, the import 409s. Re-export the current workflow, reapply your edits onto the fresh contract, and import again — never force past a 409, or you'll clobber their change.
|
|
158
|
+
|
|
159
|
+
## Failure handling
|
|
160
|
+
|
|
161
|
+
- **`describe` shows a ✗ prerequisite** — don't run yet. Fill the primer via `exodus-primer` / `exodus-foundation`, then re-check `describe`.
|
|
162
|
+
- **Run comes back `partial` or `failed`** — read `status --id <runId>` (drop `--json` for a readable view) to see which node failed and why before re-running.
|
|
163
|
+
- **`import --dry-run` issues** — fix the exact node/port each issue names using its remedy; don't guess.
|
|
164
|
+
- **`import --update` 409** — re-export, reapply, re-import (see Step 5). This is a conflict, not a bug.
|
|
165
|
+
- **Auth / whoami failure** — run `npx @aicopycoders/exodus doctor` and confirm `EXODUS_API_KEY`.
|
|
166
|
+
- **Wrong brand** — workflows are per-brand. `npx @aicopycoders/exodus brand current` to confirm you're on the brand you think you are; switch with the `exodus-brand` skill.
|
|
167
|
+
|
|
168
|
+
## Not the right skill for
|
|
169
|
+
|
|
170
|
+
- **Running a single Genesis writing pass** (brief → copy in two voices) — that's `exodus-genesis`. Reach here when the member has a *saved multi-node workflow* to run or build.
|
|
171
|
+
- **One-off image renders** — `exodus-image` / `exodus-creative` / `exodus-template`.
|
|
172
|
+
- **Building the primer a workflow depends on** — `exodus-primer` (winning ads) or `exodus-foundation` (no ads).
|
|
173
|
+
- **Finding a past run's output doc** — `exodus-browse`.
|
package/dist/bin/exodus.js
CHANGED
|
@@ -16,7 +16,10 @@ const OTHER_COMMANDS = new Set([
|
|
|
16
16
|
"drive",
|
|
17
17
|
"doctor",
|
|
18
18
|
"init",
|
|
19
|
+
"update",
|
|
19
20
|
"migrate",
|
|
21
|
+
"segment",
|
|
22
|
+
"workflow",
|
|
20
23
|
]);
|
|
21
24
|
const HIDDEN_COMMANDS = new Set([
|
|
22
25
|
"creative",
|
|
@@ -76,7 +79,7 @@ async function buildHelp() {
|
|
|
76
79
|
const fmt = (rows) => rows.map(([n, s]) => ` ${n.padEnd(18)} ${s}`.trimEnd()).join("\n");
|
|
77
80
|
const commonOptions = [
|
|
78
81
|
"Common Options:",
|
|
79
|
-
" --awareness <level> Awareness level: unaware, problem-aware, solution-aware, product-aware",
|
|
82
|
+
" --awareness <level> Awareness level: unaware, problem-aware, solution-aware, product-aware, most-aware",
|
|
80
83
|
" --wait Wait for completion (default)",
|
|
81
84
|
" --no-wait Return immediately after starting",
|
|
82
85
|
" --help, -h Show help (top-level or per-subcommand)",
|