@aicopycoders/exodus 2026.7.200-beta.5 → 2026.7.200-beta.7

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,167 @@
1
+ # Meta Ads MCP — live probe findings (wayfinder #563)
2
+
3
+ Probed 2026-07-14 against Brad's real accounts via the **official Meta Ads MCP**
4
+ (`https://mcp.facebook.com/ads`, remote HTTP MCP, OAuth-in-browser). All calls
5
+ read-only. Primary probe account: **Matt Beard FPI** (act `…820395`, ~$62.9k
6
+ spend / 1.07M impressions in the trailing 90 days). Feeds the enrichment-path
7
+ ticket (#565) and the bridge contract (#566) on map #562.
8
+
9
+ ## TL;DR for the specs
10
+
11
+ 1. **The winner query works end-to-end**: per-ad `amount_spent`, `results`
12
+ (labeled with the result type), `cost_per_result`, `ctr`, `cpm`,
13
+ `impressions`, `lead`, `purchase_roas` over any date preset or custom
14
+ range, sortable by any metric. A "top ads by spend, then threshold on
15
+ cost-per-result" winner definition is executable **agent-side over sorted
16
+ results** — server-side *metric filtering* consistently 500s (see gotchas).
17
+ 2. **The Ad Library cross-reference exists but is fuzzy**: `ads_library_search`
18
+ by `page_ids` returns the public **`adArchiveId` directly** (the result `id`;
19
+ `ad_snapshot_url` = `facebook.com/ads/library/?id=<archiveId>`). But library
20
+ results carry only `ad_creative_link_title` + creation/delivery timestamps —
21
+ **no body text, no ad-account ad id** — so account-ad → library-ad mapping is
22
+ a **fingerprint match** (creative `title` == library `ad_creative_link_title`,
23
+ ± creation-time proximity), not a direct key. No tool/field exposes
24
+ `adArchiveId` from the account side.
25
+ 3. **Images are fully retrievable; videos are not.** Image detail gives a
26
+ signed full-res CDN `url` (+ `url_128`, `permalink_url`, dimensions) —
27
+ downloadable now, but the URL **expires** (`oe=` param). Video detail gives
28
+ `length`, poster `picture`, and a *relative* `permalink_url` — **no source /
29
+ download URL**. This confirms the map's scrape-first asset strategy, with
30
+ MCP image-pull as gap-filler and **no MCP gap-filler for video files**.
31
+ 4. **Naming comes through intact** at every level (`name` on ad/adset/campaign,
32
+ creative `name`, video `title` — titles even encode aspect ratios like
33
+ `-9x16`). Naming-conventions guidance can rely on free-text ad names plus
34
+ `campaign_id`/`adset_id` for hierarchy grouping.
35
+ 5. **Setup surface is one URL**: add `https://mcp.facebook.com/ads` as a remote
36
+ MCP server; OAuth happens in the browser with the user's own Facebook login.
37
+ Per-account gates: `is_ads_mcp_enabled` and `is_queryable` (disabled/flagged
38
+ accounts come back with a human-readable `not_queryable_reason`).
39
+
40
+ ## Tool-by-tool findings
41
+
42
+ ### `ads_get_ad_accounts`
43
+ Returns **every** account the user can access — Brad sees 13 across 5
44
+ businesses (own + client accounts), each with `ad_account_id`,
45
+ `ad_account_name`, owning `business_id`/`business_name`, `account_status`,
46
+ `currency`, `is_ads_mcp_enabled`, `is_queryable`, `not_queryable_reason`.
47
+ Two disabled accounts surfaced with the flag reason string. **Multi-account
48
+ selection is a real UX step the skill must handle** (fog item on the map,
49
+ now sharpened). Account names can be empty strings.
50
+
51
+ ### `ads_get_ad_entities` (the insights workhorse)
52
+ - Levels: `account` / `campaign` / `adset` / `ad`. Metrics require
53
+ `date_preset` (e.g. `last_90d`, `maximum`) or `time_range` JSON;
54
+ `time_increment` gives time series.
55
+ - Real sample (ad level, last_90d, sorted `amount_spent_descending`):
56
+
57
+ ```json
58
+ {
59
+ "id": "120241083993850557",
60
+ "name": "PT-Intro-me-and-ai-Image-Promoted",
61
+ "effective_status": "ADSET_PAUSED",
62
+ "creative_id": "952613677477713",
63
+ "campaign_id": "120239743848310557",
64
+ "adset_id": "120241083975980557",
65
+ "amount_spent": "$8,980.38 USD",
66
+ "results": {"value": "98 (Website appointments scheduled)"},
67
+ "cost_per_result": {"value": "$91.64 USD (Website appointments scheduled)"},
68
+ "impressions": "212,816", "ctr": "4.20%", "cpm": "$42.20 USD",
69
+ "lead": "7", "created_time": "April 17, 2026"
70
+ }
71
+ ```
72
+ - **Values are formatted display strings**, not numbers (`"$8,980.38 USD"`,
73
+ `"4.20%"`, `"Not available"` for zero-delivery ads). Exception: account
74
+ level also returns numeric `amount_spent_cents`. The bridge/skill must
75
+ parse display strings (or treat them as verdict-snapshot text, which is
76
+ what the map's verdict-snapshot design wants anyway).
77
+ - `results`/`cost_per_result` self-label the optimization outcome ("Website
78
+ appointments scheduled", "Website purchases") — ideal raw material for the
79
+ verdict snapshot, and it makes result-type explicit across mixed-objective
80
+ accounts.
81
+ - Field catalog via `ads_get_field_context`: `spend` aliases `amount_spent`;
82
+ `lead`, `purchase_roas`, `results`, `cost_per_result`, `clicks`, video
83
+ metrics (`video_thruplay_watched_actions`, `video_p50_watched_actions` —
84
+ metric-only, not filterable/sortable) exist. **Not present**: `purchase`,
85
+ `cost_per_purchase`, `cost_per_lead`, `roas`, `campaign_name`/`adset_name`
86
+ (use ids + a campaign-level call), `link_click`, `hook_rate`,
87
+ `ad_archive_id`, `preview_shareable_link`, `effective_object_story_id`
88
+ (creative-side only).
89
+ - Hierarchy: ads carry `campaign_id` + `adset_id`; `objective` lives on
90
+ campaign/ad, `optimization_goal` on adset.
91
+
92
+ ### `ads_get_creatives`
93
+ The content payload. By id, returns `body` (primary text, full), `title`
94
+ (headline), `call_to_action_type`, `object_type`, `image_hash`, `image_url`
95
+ (signed CDN), `thumbnail_url`, `object_story_id` +
96
+ `effective_object_story_id` (`<pageId>_<postId>`) and
97
+ `effective_instagram_media_id`. Listing mode returns only
98
+ id/name/account_id/status — **must re-fetch by `creative_ids` for content**.
99
+ `link_url` was absent on the probed `SHARE`-type creative (link lives in the
100
+ story post). The `<pageId>_<postId>` story id is the strongest identity hook
101
+ for enrichment (post permalink = `facebook.com/<pageId>/posts/<postId>`).
102
+
103
+ ### `ads_get_ad_images` / `ads_get_ad_videos`
104
+ - Image by hash: `url` full-res signed CDN JPEG + `url_128` + `permalink_url`
105
+ + `width/height` + timestamps. Downloadable **now**; URL expires later.
106
+ - Video by id: `title`, `length` (seconds float), poster `picture`, relative
107
+ `permalink_url` (`/<pageId>/videos/<videoId>/…`). **No source URL.**
108
+ - **Gotcha (both tools + creatives): the documented "omit `fields` → all
109
+ fields" default is broken on by-id lookups** — a `video_ids` lookup without
110
+ `fields` returned only `{id}`. Always pass explicit `fields`.
111
+
112
+ ### `ads_library_search`
113
+ By `page_ids` (page id from the creative's `effective_object_story_id` prefix
114
+ or `ads_get_ad_account_pages`): returns per ad `id` (**= adArchiveId**),
115
+ `page_id`, `page_name`, `ad_creative_link_title`, `ad_creation_time`,
116
+ `ad_delivery_start_time`, `ad_snapshot_url`, `currency`, plus
117
+ `estimated_total_count` (48 for the probed page). Requires the caller to have
118
+ an active ad account. No body text, no media URLs → cross-reference by
119
+ title + time fingerprint, then hand the `adArchiveId` to the existing
120
+ ScrapeCreators pipeline for full public payload (media, transcription).
121
+
122
+ ### `ads_get_ad_preview`
123
+ Returns an actual **rendered creative image** as in-context image content +
124
+ an authenticated, expiring `preview_url` iframe (business.facebook.com). Good
125
+ for HITL confirmation inside the skill ("is THIS the winner you mean?");
126
+ not a durable asset source.
127
+
128
+ ### `ads_get_ad_account_pages`
129
+ Returns `page_id`s but `page_name` came back `"(unknown)"` on the probed
130
+ account — get the human page name from the library search result instead.
131
+
132
+ ## Gotchas / reliability
133
+
134
+ - **Metric filtering 500s.** Every `filtering` on `ad.amount_spent`
135
+ (GREATER_THAN, dollar or cent values) → `Internal Server Error`, repeatably.
136
+ Attribute filters (`ad.effective_status IN [ACTIVE]`) work. Metric *sorting*
137
+ works. → Winner definitions execute as: sort by metric server-side, pull top
138
+ N, threshold agent-side.
139
+ - **Transient 500s** happen on entity queries too (one wide 12-field call
140
+ failed once, then an identical narrower call succeeded; a later identical
141
+ call with the same fields also succeeded). Retry-once is a sane default.
142
+ - Only ONE `breakdowns` value per call; only documented params exist.
143
+ - The `ads_insights_*` tools (`performance_trend`, `advertiser_context`, etc.)
144
+ are opinionated analysis wrappers (trend narratives, funnel context), not
145
+ raw data — the skill's mining procedure should stay on `ads_get_ad_entities`.
146
+ - Every tool takes an `advertiser_request` verbatim-user-words param — a
147
+ Meta-side audit surface; the skill should pass the user's actual ask through.
148
+
149
+ ## What this settles for the open tickets
150
+
151
+ - **#565 (enrichment)**: key = fingerprint match into Ad Library
152
+ (`title` + creation time → `adArchiveId`) → ScrapeCreators; MCP `image_url`
153
+ is the image gap-filler (download immediately — signed URLs expire); there is
154
+ **no video-file gap-filler** via MCP — video enrichment depends on the ad
155
+ still being scrapeable (or the post being public via
156
+ `effective_object_story_id`).
157
+ - **#566 (bridge contract)**: payload fields available agent-side per winner:
158
+ ad id, name, campaign/adset ids, creative id, body, title, CTA type,
159
+ story/IG ids, image hash + bytes, video id + length + poster, metrics as
160
+ display strings (+ result-type labels), account id/currency, page id,
161
+ matched `adArchiveId` (best-effort). Numbers arrive as formatted strings —
162
+ the contract should either accept snapshot strings verbatim or require the
163
+ agent to parse.
164
+ - **#564 (interview)**: winner definitions must be expressible as
165
+ sort-metric + date-window + agent-side thresholds; `results` self-labeling
166
+ means the definition can be per-result-type; multi-account users (13 visible
167
+ here) need an explicit account-selection step.
@@ -0,0 +1,178 @@
1
+ # Native-Audio Dialogue — per-model way to make `dialogue` actually get SPOKEN (in English)
2
+
3
+ **Research date:** 2026-07-13
4
+ **For:** Build ticket #547 (send storyboard `dialogue` to the native-audio video models). Feeds map #545.
5
+ **Scope:** The four v1 picker models, all via Kie.ai — `veo3_fast` (Veo 3.1 Fast), `veo3` (Veo 3.1 Quality), `bytedance/seedance-2` (Seedance 2.0), `kling-3.0/video` (Kling 3.0).
6
+ **Method:** Primary = docs.kie.ai model pages (fetchable), Google's official Veo prompting guide (cloud.google.com + ai.google.dev), fal.ai model guides, Kling official blog (blocked to automated fetch — corroborated via fal + community). Community tricks flagged inline. Builds on the #529 catalog (`workspace/references/kie-video-models.md`) and the #533 bake-off (`workspace/output/bakeoff-533/`).
7
+ **Confidence labels:** `[docs]` verified on a first-party API/prompting doc · `[official-guide]` first-party prompting guide (Google/Kling/ByteDance) · `[vendor-guide]` fal.ai / infra vendor guide · `[community]` community-reported, use with caution.
8
+
9
+ ---
10
+
11
+ ## The core problem (confirmed in code)
12
+
13
+ `buildClipPrompt` in `scout/src/workflow/video/clips.ts` never reads `scene.dialogue`. It only threads `videoPrompt` + `voText` (and `voText` is dropped entirely when native audio is ON). So on a native-audio run the model is told `generate_audio: true` / `sound: true` with **no words to say**, and it invents speech-shaped babble. Every fix below is about getting the `scene.dialogue[]` lines (`{ speaker, line }`, see `convex/lib/workflow/storyboardContract.ts`) into the prompt in the format each family actually speaks — in English, without burning in captions.
14
+
15
+ **None of the four models accepts spoken dialogue as a structured API field.** On all four, through Kie, dialogue is **in-prompt text only**. (Details in §"Structured audio params".)
16
+
17
+ ---
18
+
19
+ ## TL;DR table
20
+
21
+ | Family | Dialogue embedding format (recommended) | Force English | Subtitle guard | Structured audio params (Kie) | Pacing guidance |
22
+ |---|---|---|---|---|---|
23
+ | **Veo 3.1 Fast / Quality** (`veo3_fast`,`veo3`) | Inline attribution **with quotes**, per Google's own guide: `She says, in English: "…"`. Multi-speaker = separate attributed sentences. | Write English inside the quotes **and** add an explicit clause `spoken in English (American accent)`. Keep `enableTranslation:false` (it translates the *prompt* to English and can paraphrase quoted lines). No language param. `[docs]`/`[official-guide]` | **No `negativePrompt` param on Kie's `/veo/generate`** — in-prompt only. Keep `ANTI_SUBTITLE_CORE` first; community: quotes can trigger captions, so the anti-caption clause is load-bearing for Veo. `[docs]`/`[community]` | **None.** Audio always on, not togglable; no voice/reference-audio field. `[docs]` | Keep each line short; dialogue lives inside a 4/6/8s clip. Google: short lines sync best. `[official-guide]` |
24
+ | **Seedance 2.0** (`bytedance/seedance-2`) | **Speaker label + colon + double quotes**: `The senior woman, arms crossed, says: "…"`. Short lines; insert written beats (`She pauses, then continues:`) as resync anchors between lines. `[vendor-guide]` | Add per-scene `American accent, conversational tone` + write English text. Mandarin is its most-consistent lip-sync language, so English **must** be stated explicitly or it drifts. `[vendor-guide]`/`[community]` | No first-party subtitle guidance found. Keep `ANTI_SUBTITLE_CORE`. Quoted lines are the documented format, so the core's on-screen-text ban complements (not conflicts). `[community]` | `generate_audio` bool (default true). `reference_audio_urls` (≤3, 2–15s, wav/mp3) exists — a voice/style reference lever, **not** a documented dialogue-content field. `[docs]` | ~**12 words / 10s**, ~**20 words / 15s** (~1.2 wps); sync drops past 10s; record "mental" pace ~80% of natural. **Tighter than the repo's 17 chars/s VO budget.** `[vendor-guide]` |
25
+ | **Kling 3.0** (`kling-3.0/video`) | **Bracketed speaker label + tone, colon, quotes**: `[Character A: Exhausted Partner, trembling voice]: "…"`. Consistent labels (no pronouns); for multi-speaker insert a switch beat. `[official-guide]`/`[vendor-guide]` | Specify language in the prompt (`spoken in English`) — helps phoneme mapping. Supports EN natively but also CN/JA/KO/ES, so state it. No language param. `[official-guide]`/`[community]` | No first-party subtitle guidance found. Keep `ANTI_SUBTITLE_CORE`. `[community]` | `sound` bool. `kling_elements[].audio` (5–30s) is an element-level audio lever, not a dialogue-voice field. `[docs]` | Short, single-speaker segments sync best; use Master/`pro` mode when lip-sync is scrutinized; medium shots > extreme close-ups. `[official-guide]` |
26
+
27
+ ---
28
+
29
+ ## Recommended rider / format per API family (implementation-ready for #547)
30
+
31
+ ### Where it slots into the existing rider stack
32
+
33
+ `buildClipPrompt` today (native audio ON) emits, in order:
34
+
35
+ 1. `ANTI_SUBTITLE_CORE`
36
+ 2. `MOTION_POLICY`
37
+ 3. `FIRST_FRAME_RIDER` (if a Scene Frame is wired)
38
+ 4. `body` (the `videoPrompt`)
39
+
40
+ **Add a DIALOGUE block as a new final section, ONLY when `nativeAudio === true` AND `scene.dialogue.length > 0`.** Rules:
41
+
42
+ - Keep `ANTI_SUBTITLE_CORE` **first and unchanged** — it is the only subtitle guard Veo has (no `negativePrompt` param), and its wording bans *on-screen* text ("no dialogue text, no transcript text"), which does not suppress *spoken* audio.
43
+ - The dialogue block is **family-specific** (Veo/Seedance/Kling format differently), so it must be built where the family is known. Two clean options:
44
+ - **(A)** Thread `scene.dialogue` + `family` into `buildClipPrompt` and branch there.
45
+ - **(B)** Keep `buildClipPrompt` family-agnostic; in `generateSceneClip` (which already has `spec.family`) append a `buildDialogueBlock(family, scene.dialogue)` string to the returned prompt.
46
+ Recommend **(B)** — smallest change, keeps `buildClipPrompt` pure, and the per-family request builders already live next to it.
47
+ - `buildClipPrompt` currently accepts `voText` but **not** `dialogue`. #547 must thread `scene.dialogue` through `generateSceneClip` (it already has the full `scene`).
48
+ - When `nativeAudio === false`, do **not** emit dialogue (current behavior — mouths shut, VO track). Unchanged.
49
+
50
+ ### Concrete template strings
51
+
52
+ Let `LANG = "spoken in English (natural American accent)"`.
53
+
54
+ **Veo (`veo3_fast`, `veo3`)** — inline, quoted, per Google's guide:
55
+ ```
56
+ DIALOGUE (on-camera, lip-synced, {LANG}; no on-screen captions):
57
+ <Speaker A description> says: "<line 1>"
58
+ <Speaker B description> replies: "<line 2>"
59
+ ```
60
+ - Single speaker → one attributed sentence.
61
+ - Use the storyboard `speaker` string as the attribution ("The woman", "The barista"). Quotes around the exact words per Google's official example: `A woman says, "We have to leave now."`
62
+ - Do **not** set `enableTranslation:true` (it translates/paraphrases the prompt to English and can rewrite the quoted line). Current code already sends `enableTranslation:false` — keep it.
63
+
64
+ **Seedance (`bytedance/seedance-2`)** — label + colon + quotes, short lines, beats:
65
+ ```
66
+ DIALOGUE ({LANG}, conversational tone; lip-synced to the speaker):
67
+ <Speaker A description> says: "<short line>"
68
+ [She pauses, then continues:] "<next short line>"
69
+ ```
70
+ - Colon-then-quotes is the fal-documented Seedance format: `The senior woman, arms crossed, says: "You ran that client call better than I would have."`
71
+ - Split anything over ~10–12 words into two lines with a written beat between them (`She pauses, then continues:`) — Seedance uses written beats as resync anchors.
72
+ - The `American accent, conversational tone` clause is the sourced mitigation for its Mandarin-lip-sync bias — include it every scene.
73
+
74
+ **Kling (`kling-3.0/video`)** — bracketed labelled speakers:
75
+ ```
76
+ DIALOGUE ({LANG}):
77
+ [<Speaker A label>, <tone>]: "<line>"
78
+ [<Speaker B label>, <tone>]: "<line>"
79
+ ```
80
+ - Consistent bracket labels, no pronouns (`[Character A: Exhausted Partner, trembling voice]: "You never listen to me."`).
81
+ - Map the storyboard `speaker` to the label and (optionally) infer a tone from `scene.narrative`.
82
+ - Prefer `mode: "pro"` when dialogue is central (official: use Master/high mode for scrutinized lip-sync). Current `klingModeFromResolution` already honors the operator's std/pro pick.
83
+
84
+ ### Request-builder param changes
85
+
86
+ - **`buildVeoBody`** — no new params. `enableTranslation:false` stays. Dialogue rides in `prompt`. (Kie Veo exposes no audio/voice/negativePrompt field — verified on the docs.)
87
+ - **`buildSeedanceInput`** — no *required* new params; `generate_audio` already tracks the toggle. Optional future lever: `reference_audio_urls` for voice consistency (§Cross-scene). Dialogue rides in `prompt`.
88
+ - **`buildKlingInput`** — no new params; `sound` already tracks the toggle. Dialogue rides in `prompt`.
89
+
90
+ **Net:** #547 is a **prompt-assembly change, not a request-schema change.** The only transport-level opportunity (Seedance `reference_audio_urls`) is optional and unverified for dialogue voice.
91
+
92
+ ---
93
+
94
+ ## Per-model deep dives
95
+
96
+ ### Google Veo 3.1 (`veo3_fast`, `veo3`) — Kie `/api/v1/veo/generate`
97
+
98
+ - **Dialogue format — quotes, inline attribution.** Google's official *Ultimate prompting guide for Veo 3.1* and the Gemini API docs both use quotation marks with a speaker + verb: `A woman says, "We have to leave now."` and `"This must be the key," he murmured.` The guide lists three audio cue types — Dialogue (quotes), Sound Effects (describe explicitly), Ambient — and says the model "captures the nuance of these cues to generate a synchronized soundtrack." `[official-guide]`
99
+ - **Colon-vs-quotes tension (must reconcile).** A widespread community technique claims you should use a **colon and NO quotation marks** (`the man says: hello there`) because Veo "may interpret quotation marks as text to display" and burn in captions. `[community]` This directly contradicts Google's own quoted examples. Resolution for #547: **follow Google (quotes) for reliable lip-sync**, and neutralize the caption risk with the explicit anti-subtitle clause (which we already have) rather than dropping quotes. If Veo captions still appear in testing, the fallback is the colon-no-quotes variant — treat as a tuning knob, not the default.
100
+ - **Subtitle suppression.** Kie's `/veo/generate` exposes **no `negativePrompt` field** (verified — the param list is prompt/model/aspect_ratio/duration/resolution/generationType/imageUrls/enableTranslation/watermark/callBackUrl only; ai.google.dev's Gemini-API Veo page likewise shows no negativePrompt for the dialogue flow). So the only caption lever is **in-prompt text** — exactly what `ANTI_SUBTITLE_CORE` provides. Community adds "no subtitles"/"no captions" phrasing and keeping dialogue under the 8s clip length so the model "focuses on visuals and audio rather than captions." `[docs]`/`[community]`
101
+ - **English forcing.** No language param. `enableTranslation` "enable[s] prompt translation to English … before video generation" — i.e. it translates *your prompt*, not the spoken output; for an already-English prompt it's a no-op at best and a paraphrase risk at worst, so **keep it false**. Force English by (a) writing the words in English inside the quotes and (b) an explicit `spoken in English (American accent)` clause; Veo "will handle the accent and lip-sync automatically" from the written language. `[docs]`/`[community]`
102
+ - **Audio toggle.** "All videos ship with background audio by default"; rarely suppressed for sensitive scenes. Not togglable on Kie — matches `audioTogglable:false` in `models.ts` (code already forces `nativeAudio=true` for the veo family). `[docs]`
103
+ - **Bake-off note (#533):** Veo 3.1 Fast native-audio clip generated in ~10s wall (anchored) — the fastest of the four; the audio dialogue shoot-out cell is `dlg-veo3-quality.mp4` / `clipref-s1.mp4`. Sora 2 was excluded (paused on Kie). See `workspace/output/bakeoff-533/index.html`.
104
+
105
+ Sources: [docs.kie.ai/veo3-api/generate-veo-3-video](https://docs.kie.ai/veo3-api/generate-veo-3-video) · [Ultimate prompting guide for Veo 3.1 (Google Cloud)](https://cloud.google.com/blog/products/ai-machine-learning/ultimate-prompting-guide-for-veo-3-1) · [Gemini API Veo docs](https://ai.google.dev/gemini-api/docs/veo) · [GlobalGPT Veo 3.1 dialogue guide](https://www.glbgpt.com/hub/how-to-make-characters-speak-in-veo-3-1-the-ultimate-guide-to-dialogue-audio-lip-sync/) `[community]` · [VidAU "no subtitles"](https://www.vidau.ai/veo-3-prompt-no-subtitles/) `[community]`
106
+
107
+ ### ByteDance Seedance 2.0 (`bytedance/seedance-2`) — Kie jobs API
108
+
109
+ - **Dialogue format — label, colon, double quotes.** fal.ai's Seedance 2.0 prompting guide: "Put the spoken words in double quotes, and the model voices them, matching the lip movement to each line," e.g. `The senior woman, arms crossed, says: "You ran that client call better than I would have."` For multi-speaker, hold a two-shot then cut to the speaker, "lips matched to each line." Keep lines short — "Long monologues drift out of sync, so I split a speech into a couple of shorter lines and let the cuts carry it." `[vendor-guide]`
110
+ - **Written beats as resync anchors.** "Insert a written beat ('She pauses, then continues:') between sentences — Seedance uses written beats as resync anchors." Directly useful for splitting a scene's multiple `dialogue` lines. `[vendor-guide]`
111
+ - **English forcing / drift.** Seedance supports 8+ languages incl. English/Mandarin, but "Mandarin produces the most consistent lip sync," and separate generations of the same character drift in accent unless pinned. Sourced mitigation: **"Specify accent explicitly in every scene prompt: 'American accent, conversational tone.'"** No API language param. The specific "drifts to Chinese/Mandarin" failure the map calls out is **community-reported and consistent with these vendor guides, but I could not pin a primary Reddit/X thread this session** — treat the drift as real-risk, the accent clause as the sourced fix. `[vendor-guide]`/`[community]`
112
+ - **Subtitle risk.** No first-party statement that quoted Seedance dialogue burns in captions; no first-party suppression guidance found. Keep `ANTI_SUBTITLE_CORE`. `[community]`
113
+ - **Pacing (important — conflicts with the repo budget).** fal / UGC-Copilot guidance: **~12 words per 10s, ~20 words per 15s (~1.2 words/sec)**; "short sentences … five to ten words per line is the sweet spot"; sync quality "drops past 10" seconds. The storyboard gate (`VO_CHARS_PER_SEC = 17`, `sceneSpeechSec`) budgets ~17 chars/s ≈ ~3 words/s — **~2.5× looser than Seedance actually speaks cleanly.** A scene that "fits" at 17 cps can still overflow / rush / desync on Seedance native audio. **Recommendation for #547 (or a fast-follow):** use a tighter chars-per-second budget for native-audio *dialogue* (≈9–10 cps, ~1.2–1.5 wps) than for VO narration. Seedance does not hard-truncate; it rushes and desyncs. `[vendor-guide]`
114
+ - **Bake-off note (#533):** Seedance native audio was slowest by far — measured ≈24 min/clip, which is why `POLL_TIMEOUT_VIDEO_MS` was raised to 35 min in `kie-video.ts`. It won "best native audio" per the map, but at a steep latency/cost. `dlg-seedance.mp4` vs the silent control `dlg-seedance-silent.mp4` vs pixr mux `dlg-elevenlabs-mux.mp4`.
115
+
116
+ Sources: [docs.kie.ai/market/bytedance/seedance-2](https://docs.kie.ai/market/bytedance/seedance-2) · [fal.ai Seedance 2.0 prompting guide](https://fal.ai/learn/tools/seedance-2-0-prompting-guide) · [UGC-Copilot Seedance native audio guide](https://ugccopilot.ai/blog/seedance-2-native-audio-generation-guide/) `[community]` · [Cutout.pro Seedance audio guide](https://www.cutout.pro/learn/blog-seedance-2-0-audio-guide/) `[community]`
117
+
118
+ ### Kling 3.0 (`kling-3.0/video`) — Kie jobs API
119
+
120
+ - **Dialogue format — bracketed labelled speakers.** fal.ai's Kling 3.0 guide: `[Character Label, tone/emotion]: "dialogue text"`, e.g. `[Character A: Exhausted Partner, trembling frustrated voice]: "You never listen to me."` Structured naming (P1): "Character labels must be consistent. Avoid pronouns or synonyms." For sequencing/multi-speaker, insert a switch beat ("this is when the speaker switches"). `[vendor-guide]`
121
+ - **Official audio behavior.** Kling's own blog (Kling VIDEO 3.0 / 3.0 Omni "Native Audio") — dialogue, sound and lip movement are generated together. Best-practice guidance (corroborated across Kling + fal + community): short simple sentences beat rapid complex speech; **specify the language in the prompt to help phoneme mapping**; medium shots sync better than extreme close-ups; avoid overlapping dialogue — single-speaker segments sync much better; use Master/high mode when lip-sync will be scrutinized. `[official-guide]`/`[community]` (Note: the official Kling blog returns HTTP 446 to automated fetch; content is corroborated via the search excerpt + fal guide, not a direct page read.)
122
+ - **Language support / forcing.** Supports Chinese, English, Japanese, Korean, Spanish + dialects/accents (American/British/Indian English). No language param — state `spoken in English` in the prompt. `[official-guide]`/`[community]`
123
+ - **Kie param nuance.** On Kie, the native-audio toggle is `sound` (bool) and `prompt` "takes effect when multi_shots is false" — our builder sends `multi_shots:false`, so the single `prompt` (with the dialogue block) is the right channel. Multi-shot mode would need the `multi_prompt[]` array instead (each shot's `prompt` ≤500 chars); v1 does not use it. `[docs]`
124
+ - **Bake-off note (#533):** `dlg-kling.mp4`, ~103s gen — much faster than Seedance, comparable to Veo Quality.
125
+
126
+ Sources: [docs.kie.ai/market/kling/kling-3-0](https://docs.kie.ai/market/kling/kling-3-0) · [Kling official: Kling Video 3.0 Omni Native Lip Sync & Audio](https://kling.ai/blog/kling-video-3-omni-native-lip-sync-audio-guide) (446 to automated fetch) · [fal.ai Kling 3.0 prompting guide](https://blog.fal.ai/kling-3-0-prompting-guide/) · [Imagine.art Kling 3.0 prompt guide](https://www.imagine.art/blogs/kling-3-0-prompt-guide) `[community]`
127
+
128
+ ---
129
+
130
+ ## Cross-scene voice consistency (map fog — levers that exist)
131
+
132
+ The problem: each scene is an independently-generated clip, so the same actor gets a **different synthesized voice** each time. No picker model exposes a first-class "voice ID" through Kie's jobs/veo API. Levers found, none verified to solve it:
133
+
134
+ - **Seedance `reference_audio_urls`** `[docs]` — up to 3 audio clips (2–15s, wav/mp3), also accepts `asset://{assetId}`. Documented as a *reference* input (style/voice conditioning), **not** as the dialogue-content source. **Fog:** it *might* anchor timbre if the same reference clip is passed to every scene — but the docs don't say it transfers voice identity, and whether the model speaks the *prompt* text in the *reference* voice is untested. This is the single most promising Kie-native lever; worth an empirical probe before relying on it. Seedance is also the only one of the four with any audio-input field.
135
+ - **Kling `kling_elements[].audio`** `[docs]` — per-element audio (5–30s) tied to an `@element`. Element references are aimed at *visual* character consistency (2–4 images/element); the audio slot is under-documented for voice transfer. **Fog.**
136
+ - **Veo** — **no audio input at all.** No lever for cross-scene voice. `[docs]`
137
+ - **Higher-level "voice fingerprint" features** (e.g. fal's "AI Twins" persona voice-locking) exist at the infra layer but are **not exposed through Kie's jobs API**, so not usable in this pipeline as-is. `[community]`
138
+
139
+ **Practical read for v1:** there is no reliable cross-scene voice-consistency lever through Kie today. If it matters, the more controllable path remains the old pixr architecture — render silent + one ElevenLabs voice muxed across all scenes (the `dlg-elevenlabs-mux.mp4` control in #533). Keep cross-scene voice in the fog for #547; if pursued, the first experiment is Seedance `reference_audio_urls` with a fixed reference clip.
140
+
141
+ ---
142
+
143
+ ## Duration / pacing behavior (per family)
144
+
145
+ - **Seedance:** ~12 words/10s, ~20 words/15s; sync degrades past ~10s and with long lines; does **not** hard-truncate — it rushes and drifts out of sync. Written beats resync it. `[vendor-guide]`
146
+ - **Veo:** fixed 4/6/8s clips; keep dialogue short enough to land inside the clip; Google/community both say short lines sync best and staying under 8s reduces caption bleed. `[official-guide]`/`[community]`
147
+ - **Kling:** 3–15s; short single-speaker segments sync best; no published words/sec number found. `[official-guide]`
148
+ - **Repo gate today:** `sceneSpeechSec = max(voText, dialogue) ÷ 17 chars/s` fits *both* tracks to `durationSec` and fails loud rather than truncating. For **VO narration** 17 cps is reasonable; for **native lip-synced dialogue** it is too loose (Seedance ≈ 7–8 cps of clean speech). **Actionable:** consider a separate, tighter cps for native-audio dialogue-fit. (Flagged as a recommendation; whether #547 changes the budget or just formats the block is a scoping call.)
149
+
150
+ ---
151
+
152
+ ## Open questions / unverifiable this session
153
+
154
+ 1. **Veo colon-vs-quotes for captions** — Google officially uses quotes; the "quotes burn captions, use a colon" claim is community-only and contradicts the official examples. Not resolvable without a live Veo-on-Kie A/B. Default to quotes + anti-caption clause; keep colon-no-quotes as a fallback knob. `[community]`
155
+ 2. **Seedance→Chinese drift** — consistent with vendor guides ("Mandarin most consistent," "specify accent every scene") but no primary Reddit/X thread pinned this session. The accent clause is the sourced mitigation; the drift itself is community-level. `[community]`
156
+ 3. **Whether `ANTI_SUBTITLE_CORE`'s "no dialogue text" wording ever suppresses *spoken* audio** — it targets on-screen typography, so it *shouldn't*, but a paranoid model could misread it. Low risk; worth watching in the first native-audio+dialogue test. Not observed in #533 (which ran without a dialogue block).
157
+ 4. **Seedance `reference_audio_urls` as a voice-consistency lever** — exists in the API but its effect on spoken-dialogue voice is undocumented. Needs an empirical probe.
158
+ 5. **Kie Veo `negativePrompt`** — absent from Kie's `/veo/generate` param list (Vertex AI Veo has one; Kie's endpoint does not expose it). Confident but worth a 1-line confirm if #547 wants a param-based caption guard.
159
+ 6. **Exact per-family words/sec** — only Seedance publishes numbers; Veo/Kling give qualitative "keep it short." Any tighter dialogue-fit budget will be model-family-specific and partly empirical.
160
+
161
+ ---
162
+
163
+ ## Sources
164
+
165
+ Primary / first-party:
166
+ - [docs.kie.ai — Veo 3.1 generate](https://docs.kie.ai/veo3-api/generate-veo-3-video), [Seedance 2.0](https://docs.kie.ai/market/bytedance/seedance-2), [Kling 3.0](https://docs.kie.ai/market/kling/kling-3-0)
167
+ - [Google Cloud — Ultimate prompting guide for Veo 3.1](https://cloud.google.com/blog/products/ai-machine-learning/ultimate-prompting-guide-for-veo-3-1)
168
+ - [Google AI for Developers — Generate videos with Veo 3.1 (Gemini API)](https://ai.google.dev/gemini-api/docs/veo)
169
+ - [Kling.ai — Kling Video 3.0 Omni Native Lip Sync & Audio](https://kling.ai/blog/kling-video-3-omni-native-lip-sync-audio-guide) (446 to automated fetch; corroborated indirectly)
170
+
171
+ Vendor guides (fal.ai):
172
+ - [Seedance 2.0 prompting guide](https://fal.ai/learn/tools/seedance-2-0-prompting-guide), [Kling 3.0 prompting guide](https://blog.fal.ai/kling-3-0-prompting-guide/)
173
+
174
+ Community (labeled inline):
175
+ - [GlobalGPT Veo 3.1 dialogue/lip-sync](https://www.glbgpt.com/hub/how-to-make-characters-speak-in-veo-3-1-the-ultimate-guide-to-dialogue-audio-lip-sync/) · [VidAU Veo no-subtitles](https://www.vidau.ai/veo-3-prompt-no-subtitles/) · [UGC-Copilot Seedance native audio](https://ugccopilot.ai/blog/seedance-2-native-audio-generation-guide/) · [Cutout.pro Seedance audio](https://www.cutout.pro/learn/blog-seedance-2-0-audio-guide/) · [Imagine.art Kling 3.0](https://www.imagine.art/blogs/kling-3-0-prompt-guide)
176
+
177
+ Local prior art:
178
+ - `workspace/references/kie-video-models.md` (#529 catalog) · `workspace/output/bakeoff-533/index.html` + `manifest.json` (#533 native-audio bake-off) · `scout/src/workflow/video/clips.ts`, `models.ts`, `kie-video.ts`, `storyboard.ts` · `convex/lib/workflow/storyboardContract.ts`
@@ -0,0 +1,160 @@
1
+ # Own-Brand Winners — enrichment path design (wayfinder #565)
2
+
3
+ Designed 2026-07-14 from the [MCP probe findings](meta-ads-mcp-probe.md) (#563)
4
+ plus a code audit of the live swipe rail (`scout/src/scrapecreators.ts`,
5
+ `scout/src/trigger/swipe-mine.ts`, `swipe-transcribe.ts`, `image-classify.ts`,
6
+ `convex/swipeAds.ts`, `convex/imageClassification.ts`). Answers: what is the
7
+ exact path from a designated winner (ad-account identity) to a complete stored
8
+ package? Feeds the bridge contract (#566) and the build specs (#567).
9
+
10
+ ## TL;DR decisions
11
+
12
+ 1. **Match server-side on ad copy, not agent-side on the MCP title fingerprint.**
13
+ The bridge payload carries the account-side ground truth (body, title,
14
+ created_time, format, page id); a new import task scrapes the brand's own
15
+ page once via the existing `fetchCompanyAds` and matches winners to library
16
+ ads by **normalized body+title equality** (tiebreak: earliest `start_date`).
17
+ Full ScrapeCreators snapshots include body text — a far stronger key than
18
+ the library-search `title + time` fingerprint the probe had to settle for,
19
+ and it drops `ads_library_search` from the skill entirely.
20
+ 2. **One new Trigger task (`own-brand-import`), everything downstream as-is.**
21
+ The task reuses the scrapecreators.ts helpers + the `/api/v2/swipes/batch`
22
+ upsert, stores **only matched winners**, then fires the existing
23
+ `swipe-transcribe` and `image-classify` sweeps, which pick own-brand rows up
24
+ with zero changes (both sweep global "missing transcript" / "unclassified"
25
+ states).
26
+ 3. **Gap-filler = agent-pushed assets for unmatched winners** (ad stopped
27
+ running): text is always complete from MCP; images are downloaded by the
28
+ agent immediately (signed CDN URLs expire) and uploaded as bytes via the
29
+ existing Convex `generateUploadUrl` pattern; **video files have no MCP
30
+ source — the skill asks the user for their own creative file** (it's their
31
+ ad), else the package is stored partial.
32
+ 4. **Mirror winner media into Convex storage at import.** fbcdn URLs expire;
33
+ competitor rows survive because the daily re-scrape refreshes them, but
34
+ winners are long-lived fuel imported once. Store the storage **serve URL in
35
+ the existing `imageUrl`/`videoUrl` fields** so every downstream consumer
36
+ (pickers, classify, transcribe, gallery) works unchanged.
37
+ 5. **Own-brand `brands` rows must be flagged out of the daily swipe-mine cron**
38
+ (`isOwnBrand: true`, mine filters it): otherwise the nightly sweep scrapes
39
+ the user's whole page and stores every active ad — violating winners-only.
40
+ 6. **Completeness is an explicit stored field**, not derived: `complete` |
41
+ `partial` (+ what's missing), surfaced amber in the UI tab (the #478
42
+ partial-delivery pattern).
43
+
44
+ ## Stage 0 — what the agent holds per winner (input to the bridge)
45
+
46
+ Per the probe, after the winner interview designates ads, the agent can gather
47
+ via MCP (`ads_get_ad_entities` + `ads_get_creatives` + `ads_get_ad_images` /
48
+ `ads_get_ad_videos`, all with explicit `fields`):
49
+
50
+ | Field | Source | Notes |
51
+ |---|---|---|
52
+ | account ad id, name, campaign/adset ids | entities | hierarchy + provenance |
53
+ | `created_time` | entities | matching tiebreak + daysRunning fallback |
54
+ | body (primary text), title (headline), CTA type | creative | ground truth for matching |
55
+ | `effective_object_story_id` (`<pageId>_<postId>`) | creative | **page id prefix** = scrape target |
56
+ | image hash → full-res signed URL | ad_images | **download immediately**, expires |
57
+ | video id, length, poster URL | ad_videos | no source URL — cannot download |
58
+ | metrics as display strings + result-type labels | entities | verdict snapshot material |
59
+
60
+ The bridge payload (#566) should carry all of the above per winner, plus the
61
+ page id once per import.
62
+
63
+ ## Stage 1 — matching: winner → `adArchiveId`
64
+
65
+ Runs inside `own-brand-import` (server-side, ScrapeCreators BYOK key snapshot
66
+ exactly like `swipe-mine`'s manual-run pattern):
67
+
68
+ 1. `fetchCompanyAds(pageId)` — one paginated scrape of the user's own page.
69
+ Returns full snapshots: `body.text`, `title`, `start_date`, media URLs,
70
+ `collation_count`.
71
+ 2. For each winner: **normalize** (trim, collapse whitespace, strip zero-width
72
+ chars, normalize curly quotes) and match on **body equality**; require
73
+ title equality too when both sides have a title.
74
+ 3. Ambiguity: multiple library ads with identical copy are creative variants /
75
+ republishes — any is acceptable (same creative); pick the **earliest
76
+ `start_date`** (longest `daysRunning`, best longevity signal for pickers).
77
+ 4. Fallback when the account creative has no body (some `SHARE`-type creatives
78
+ keep copy on the story post): match on unique title + `created_time`
79
+ proximity (±72h). Non-unique → treat as unmatched (gap-filler), never guess.
80
+ 5. Matched → optionally `fetchAdLibraryAdById(archiveId, get_transcript=true)`
81
+ for the inline transcript (the idea-ad-library / workflow-executor pattern);
82
+ store it at insert so most video winners never wait for the transcribe cron.
83
+
84
+ **Failure modes:** page scrape empty (wrong page id, zero active ads) → all
85
+ winners unmatched → gap-filler; the Ad Library only lists **running** ads for
86
+ non-political advertisers, so any stopped winner is *expected* to be unmatched
87
+ — that's the designed-for case, not an error. One spec-time item:
88
+ `fetchCompanyAds` hardcodes `country: "US"` — the import path must make this
89
+ configurable (or "ALL" if the endpoint accepts it) or non-US advertisers will
90
+ false-unmatch.
91
+
92
+ ## Stage 2 — gap-filler protocol (unmatched winners)
93
+
94
+ What MCP can and cannot supply fills the row directly:
95
+
96
+ - **Copy**: body/title/CTA come from the creative — always complete. No
97
+ library `linkUrl`/`linkDescription`; acceptable loss (fields stay empty).
98
+ - **Image**: the agent downloads the signed CDN URL **at interview time** (not
99
+ at push time — it expires) and the CLI uploads bytes: request an upload URL
100
+ (the `creativeSuite.generateUploadUrl` service pattern), PUT the file, record
101
+ the storage id. Import stores the serve URL in `imageUrl`.
102
+ - **Video**: no MCP file. Ladder: (a) the skill asks the user to point at
103
+ their own creative file — they made the ad, they usually have it — CLI
104
+ uploads it; (b) no file → store partial: copy + poster image (poster URL
105
+ *is* downloadable now) and no transcript. Once a video lands in storage, its
106
+ serve URL in `videoUrl` makes the existing `pendingTranscripts` sweep
107
+ transcribe it automatically (note: `transcribeVideo` rejects >25MB —
108
+ acceptable v1 cap, flag oversize uploads as partial-transcript).
109
+ - **Identity**: gap-filled rows get a synthetic archive id `own:<accountAdId>`
110
+ — stable, unique, satisfies the `by_adArchiveId` dedup index, can't collide
111
+ with numeric library ids. `isActive: false`, `startDate` from
112
+ `created_time`.
113
+
114
+ ## Stage 3 — reuse audit
115
+
116
+ | Piece | Verdict |
117
+ |---|---|
118
+ | `scrapecreators.ts` helpers (fetch, extract*, classifyFormat, transcribe) | **as-is** — the import task composes them |
119
+ | `/api/v2/swipes/batch` → `swipeAds.upsertBatch` | **as-is** — winners-only array in, dedup by archive id |
120
+ | `swipe-transcribe` (`pendingTranscripts` sweep) | **as-is** — global format=video-missing-transcript scan includes own-brand rows; also fire it at import end |
121
+ | `image-classify` (`getUnclassifiedSwipeAds`) | **as-is** — sweeps `classificationStatus === undefined`; own-brand rows enter automatically |
122
+ | Consumption (`swipeAdToText`, pickers, `listForWorkspacePublic`) | **as-is** — own-brand rows are swipeAds under a workspace brand; they flow into every picker. Spec check (per map): run-dialog top-N ranks by `daysRunning` — matched winners inherit real longevity, gap-filled ones get `created_time`-derived values, and the winner fields below let pickers boost designated winners explicitly |
123
+ | `swipe-mine` task | **not reused** (fetch-everything shape violates winners-only); its building blocks are |
124
+ | Daily `swipe-mine` cron | **needs a filter**: skip `isOwnBrand` brands (decision 5) |
125
+ | New code | one Trigger task `own-brand-import` + one HTTP route to start it (BYOK key snapshot like `swipe.startMineRun`) + the CLI ingestion command (#566's contract) |
126
+
127
+ ## Stage 4 — completeness contract
128
+
129
+ A package is **usable generative fuel** when:
130
+
131
+ - **every winner**: verdict snapshot (composed sentence + metric display
132
+ strings) — non-negotiable, it's the point of the feature — plus body or
133
+ title, and provenance (account ad id, campaign/adset ids).
134
+ - **image winner**: + full-res image (storage-mirrored). Missing image →
135
+ `partial` (copy bots still work; ref-match image engines can't use it).
136
+ - **video winner**: + transcript (from library inline, sweep, or uploaded
137
+ file). Video file itself is desired (gallery, future variations) but
138
+ transcript is the fuel; no transcript AND no video → `partial`.
139
+
140
+ Store it explicitly: `enrichmentStatus: "complete" | "partial"` +
141
+ `enrichmentMissing: string[]` (e.g. `["video-file", "transcript"]`), set by the
142
+ import task, re-evaluated when a sweep fills a gap. UI shows amber on partial
143
+ rows with the missing list (the #478 pattern).
144
+
145
+ New optional `swipeAds` columns this implies for #567 (all additive):
146
+ winner verdict text + metrics snapshot, `designatedAt`, source account
147
+ ad/campaign/adset ids, `enrichmentSource: "library-match" | "agent-push"`,
148
+ `enrichmentStatus` / `enrichmentMissing`, `imageStorageId` / `videoStorageId`
149
+ (lifecycle), plus `brands.isOwnBrand`.
150
+
151
+ ## Open items handed to #566 / #567
152
+
153
+ - Bridge contract (#566): exact payload shape per winner (Stage 0 table), how
154
+ the CLI transports image/video bytes (upload-URL handshake vs multipart),
155
+ idempotency on re-push (upsert by account ad id → synthetic/matched archive
156
+ id), and where the "ask the user for the video file" step lives in the skill.
157
+ - Spec (#567): country parameter on the import scrape; winner-boost rule in
158
+ top-selection heuristics; amber partial UI in the Swipe Mining tab; whether
159
+ matched winners' media also gets storage-mirrored at import (recommended:
160
+ yes, both paths, decision 4).
@@ -5,6 +5,7 @@ import { sniffImageType, BRAND_IMAGE_MAX_BYTES } from "../lib/image-sniff.js";
5
5
  import { getActiveBrand, setActiveBrand, clearActiveBrand, findWorkspaceRoot, } from "../lib/state.js";
6
6
  import { detectLayout, ensureBrandDir, resolveActiveBrand, brandStateDir, brandDirFor, } from "../lib/layout.js";
7
7
  import { promptYesNo } from "../lib/prompts.js";
8
+ import { pkgRef } from "../lib/channel.js";
8
9
  const AUTO_END_MARKER = "<!-- exodus:auto-section-end — manual brand notes below survive `brand use` refreshes -->";
9
10
  export const helpText = `
10
11
  exodus brand — list, switch, and inspect brands available to your CLI
@@ -180,7 +181,7 @@ async function runCreate(name) {
180
181
  console.log(`✓ Wrote ${refresh.pathRel}.`);
181
182
  }
182
183
  console.log(`\nNext: set up the brand's primer to unlock the pipelines —\n` +
183
- ` say "exodus, set up my brand primer" (or run \`npx @aicopycoders/exodus primer\`).`);
184
+ ` say "exodus, set up my brand primer" (or run \`npx ${pkgRef()} primer\`).`);
184
185
  }
185
186
  async function runDelete(slug, flags) {
186
187
  if (!slug)