gooseworks 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +43 -11
  2. package/dist/commands/doctor.d.ts +29 -5
  3. package/dist/commands/doctor.d.ts.map +1 -1
  4. package/dist/commands/doctor.js +107 -63
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/fetch.d.ts +1 -0
  7. package/dist/commands/fetch.d.ts.map +1 -1
  8. package/dist/commands/fetch.js +64 -29
  9. package/dist/commands/fetch.js.map +1 -1
  10. package/dist/commands/install.d.ts.map +1 -1
  11. package/dist/commands/install.js +37 -4
  12. package/dist/commands/install.js.map +1 -1
  13. package/dist/commands/login.d.ts.map +1 -1
  14. package/dist/commands/login.js +8 -3
  15. package/dist/commands/login.js.map +1 -1
  16. package/dist/commands/skills.d.ts +34 -0
  17. package/dist/commands/skills.d.ts.map +1 -0
  18. package/dist/commands/skills.js +109 -0
  19. package/dist/commands/skills.js.map +1 -0
  20. package/dist/commands/update.d.ts.map +1 -1
  21. package/dist/commands/update.js +17 -6
  22. package/dist/commands/update.js.map +1 -1
  23. package/dist/commands/video-local.d.ts +9 -0
  24. package/dist/commands/video-local.d.ts.map +1 -0
  25. package/dist/commands/video-local.js +83 -0
  26. package/dist/commands/video-local.js.map +1 -0
  27. package/dist/commands/video-save.d.ts +3 -0
  28. package/dist/commands/video-save.d.ts.map +1 -0
  29. package/dist/commands/video-save.js +83 -0
  30. package/dist/commands/video-save.js.map +1 -0
  31. package/dist/index.js +7 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/video-local-captions.d.ts +83 -0
  34. package/dist/lib/video-local-captions.d.ts.map +1 -0
  35. package/dist/lib/video-local-captions.js +261 -0
  36. package/dist/lib/video-local-captions.js.map +1 -0
  37. package/dist/lib/video-local-runtime.d.ts +8 -0
  38. package/dist/lib/video-local-runtime.d.ts.map +1 -0
  39. package/dist/lib/video-local-runtime.js +201 -0
  40. package/dist/lib/video-local-runtime.js.map +1 -0
  41. package/dist/lib/video-save-mcp.d.ts +9 -0
  42. package/dist/lib/video-save-mcp.d.ts.map +1 -0
  43. package/dist/lib/video-save-mcp.js +162 -0
  44. package/dist/lib/video-save-mcp.js.map +1 -0
  45. package/dist/lib/video-save.d.ts +77 -0
  46. package/dist/lib/video-save.d.ts.map +1 -0
  47. package/dist/lib/video-save.js +440 -0
  48. package/dist/lib/video-save.js.map +1 -0
  49. package/dist/skills/custom-video-skill.d.ts +3 -0
  50. package/dist/skills/custom-video-skill.d.ts.map +1 -0
  51. package/dist/skills/custom-video-skill.js +6 -0
  52. package/dist/skills/custom-video-skill.js.map +1 -0
  53. package/dist/skills/installer.d.ts +12 -6
  54. package/dist/skills/installer.d.ts.map +1 -1
  55. package/dist/skills/installer.js +41 -15
  56. package/dist/skills/installer.js.map +1 -1
  57. package/dist/skills/master-skill.d.ts +26 -10
  58. package/dist/skills/master-skill.d.ts.map +1 -1
  59. package/dist/skills/master-skill.js +1534 -286
  60. package/dist/skills/master-skill.js.map +1 -1
  61. package/dist/skills/package-freshness.d.ts +16 -0
  62. package/dist/skills/package-freshness.d.ts.map +1 -0
  63. package/dist/skills/package-freshness.js +36 -0
  64. package/dist/skills/package-freshness.js.map +1 -0
  65. package/dist/skills/releases.d.ts +12 -0
  66. package/dist/skills/releases.d.ts.map +1 -0
  67. package/dist/skills/releases.js +152 -0
  68. package/dist/skills/releases.js.map +1 -0
  69. package/dist/skills/routes.d.ts.map +1 -1
  70. package/dist/skills/routes.js +13 -1
  71. package/dist/skills/routes.js.map +1 -1
  72. package/dist/utils/browser-preflight.d.ts +29 -0
  73. package/dist/utils/browser-preflight.d.ts.map +1 -0
  74. package/dist/utils/browser-preflight.js +338 -0
  75. package/dist/utils/browser-preflight.js.map +1 -0
  76. package/package.json +2 -2
  77. package/skills/goose-ads/SKILL.md +124 -16
  78. package/skills/goose-product-photos/SKILL.md +55 -3
  79. package/skills/goose-video/SKILL.md +369 -433
  80. package/skills/goose-video-local/SKILL.md +1405 -0
  81. package/skills/gooseworks/SKILL.md +200 -13
  82. package/skills/make-custom-video/SKILL.md +490 -0
  83. package/skills/manifest.json +12 -0
  84. package/skills/routes.json +15 -1
@@ -2,440 +2,376 @@
2
2
  name: goose-video
3
3
  slug: goose-video
4
4
  description: >
5
- GooseWorks video ads — remix a video ad template (iMessage chat-reveal, more coming) into a
6
- branded video ad for the user's product. Renders LOCALLY on the user's machine (Playwright +
7
- ffmpeg + GooseWorks media proxies) and saves the finished MP4 back to the project over MCP.
8
- Use when the user says "make the video for project <id>", "for video batch <id>", references a
9
- video ad project/batch or template, or asks to remix a video ad. Unlike goose-ads (static images,
10
- generated server-side), video renders locally and reports progress + the result back through the
11
- gooseworks MCP tools.
5
+ Start a video ad in the same chat. Resolve the brand and suggest supported formats with a
6
+ picker or the returned text choices. An agent with a verified shell makes the video with
7
+ goose-video-local; a chat host hands the same project to the GooseWorks coworker. Review
8
+ one complete template plan and total credits before production. Custom videos retain two authenticated review gates in this chat.
12
9
  category: ads
13
- version: 0.3.0
10
+ version: 3.0.0
14
11
  author: GooseWorks
15
- tags: [gooseworks, ads, video, remix, imessage, local-render, byoa]
12
+ tags: [gooseworks, ads, video, local-render, coworker, chat]
16
13
  ---
17
14
 
18
- # GooseWorks Video Ads — local remix runtime
19
-
20
- You produce **video** ad creative on the user's OWN machine and sync the result back to the
21
- GooseWorks app over MCP. This document is the **runtime contract** (auth, credits, the media
22
- proxies, data I/O, the review gate). A separate **recipe skill** — fetched per format — tells you
23
- *what to make* (the pieces, prompts, models, order of assembly).
24
-
25
- **Division of authority — read both, but when they disagree THIS doc wins on the environment AND the
26
- review/approval flow.** The recipe governs WHAT to make; this doc governs WHEN you pause, generate,
27
- and spend. In particular: a recipe may spell out a **multi-phase, multi-gate** flow — "generate the
28
- still [GATE] → approve → author the prompt [GATE] → approve → render [GATE] → approve", several
29
- separate pauses. **Do NOT run it that way.** Collapse every one of those gates into the single
30
- **review-once** flow below: one review set, one approval (Step 3). Take the recipe's pieces, prompts
31
- and models; ignore its intermediate pauses. This is the exact contradiction that confused past runs
32
- (GOOSE-2542) — there is no ambiguity: review-once wins.
33
-
34
- You run inside the user's own Claude Code session (they pasted an instruction with a project
35
- id). The app NEVER runs you — it is the viewer + review surface; you are the renderer.
36
-
37
- ## CLI-free environments (cowork / headless)
38
-
39
- You may be running WITHOUT the `gooseworks` CLI binary (e.g. Anthropic cowork). The
40
- `mcp__gooseworks__*` tools work over the MCP connection regardless, so wherever this skill
41
- says to shell out, use the MCP equivalent:
42
-
43
- - `gooseworks fetch <slug>` → the **`fetch_skill`** MCP tool (returns the same content/scripts/
44
- files/dependencySkills). `gooseworks search <q>` → **`search_skills`**.
45
- - `gooseworks credits` → the **`get_ad_credits`** MCP tool.
46
- - `gooseworks doctor` → do the manual toolchain check in the preflight below.
47
-
48
- ## Report problems so we can fix them (telemetry — do this, don't skip it)
49
-
50
- If anything blocks or degrades this run — a media/proxy call fails or errors, a required input or
51
- asset is missing, a recipe instruction is ambiguous or contradictory, the render toolchain won't set
52
- up, or you hit a bug you can't work around — **report it** so the team gets visibility and can fix
53
- the skill. It's fire-and-forget, never counts against you, and never blocks your work.
54
-
55
- - **First, set a stable run id** so every event (yours + the auto-logged media calls) groups together:
56
- `export GW_RUN_ID="vid-<project_or_batch_id>"` (and `export GW_SKILL="<recipe-slug>"`) in the
57
- shell you render from. The media proxies read `GW_RUN_ID` automatically.
58
- - **CLI present →** `gooseworks log "<what happened>" --event-type <type> --level error --details '{"error":"...","step":"...","model":"..."}'`
59
- - **No CLI (cowork / headless) →** the **`log_cli_event`** MCP tool with the same fields (pass `run_id`).
60
- - `--event-type`: `api_failure` (a proxy/model call failed) · `missing_input` · `blocker` ·
61
- `confusion` (unclear/contradictory instruction) · `error` (a bug) · `step`/`info` (progress notes).
62
- - Put the **real error text + the step you were on** in `--details`. Paid FAL/ElevenLabs calls
63
- ALREADY auto-log their own failures, so focus your manual logs on what the proxy can't see:
64
- missing inputs, confusing/contradictory recipe instructions, toolchain/setup failures, and bugs.
65
- - Logging is FOR US — it does not replace telling the user. When a problem blocks the run, still
66
- explain it to the user (and ask if you need a decision); just also `log` it so we can fix the skill.
67
-
68
- ## Prerequisite — MCP + a render toolchain (Phase 0 preflight)
69
-
70
- - The `mcp__gooseworks__*` tools are REQUIRED. If they're unavailable, stop and tell the user
71
- to connect the GooseWorks MCP server (or run `gooseworks install --claude --mcp` on the CLI)
72
- and restart. There is no REST fallback.
73
- - **The render runs wherever THIS agent runs, and it needs a real toolchain: `ffmpeg` +
74
- `ffprobe` + a Playwright **Chromium**.** Establish it in this priority order, and do NOT start
75
- rendering until one is confirmed:
76
- 1. **CLI present →** run `gooseworks doctor` (checks login, MCP, ffmpeg/ffprobe, Playwright
77
- Chromium in one shot). Fix any ✗ with the command it prints, then continue.
78
- 2. **No CLI →** check the toolchain yourself: `ffmpeg -version`, `ffprobe -version`, and a
79
- Playwright Chromium probe (`npx playwright --version` and, if needed, `npx playwright install
80
- chromium`). If all resolve, continue.
81
- 3. **Docker available →** this is the most reliable way to get the toolchain in a sandbox that
82
- lacks it: run the render steps inside the prebuilt image
83
- **`ghcr.io/gooseworks-ai/goose-video-render`** (ffmpeg + ffprobe + Playwright Chromium baked
84
- in), mounting the project working directory. Use Docker whenever the host is missing ffmpeg or
85
- Chromium and `docker` is on PATH. (Note: nested Docker is usually disabled inside managed
86
- sandboxes like cowork — treat this as an option, not a guarantee.)
87
- 4. **None of the above works →** STOP and tell the user plainly, e.g.: *"Video rendering needs
88
- ffmpeg + a Playwright Chromium (or Docker) on the machine running this agent. This environment
89
- doesn't have them and I can't install them here. Options: (a) enable/allow Docker so I can use
90
- the goose-video-render image, (b) install ffmpeg + `npx playwright install chromium`, or
91
- (c) run this skill locally in your own Claude Code where the toolchain is available."* Do not
92
- half-render or fake a result. Static image ads (the `goose-ads` skill) do NOT need any of this
93
- and work anywhere — offer that as the fallback if they just want an ad now.
94
-
95
- ## Identity, token, credits
96
-
97
- - Read `~/.gooseworks/credentials.json` → `api_key` (your agent token), `api_base`, `agent_id`.
98
- Never print the token.
99
- - **CRITICAL — target the org-default Ads agent on EVERY file op.** The app serves project files
100
- (the render-file route) from the org's DEFAULT agent, but MCP file writes default to your
101
- token's pinned agent — which can be a DIFFERENT agent, so a render written with the default
102
- scope is **invisible in the app**. First resolve the Ads agent: `list_accessible_scopes` → the
103
- scope with `is_org_default: true` (the ORG default — NOT the `is_default` / `default_agent_id`
104
- fields, which are the *user's* default agent and are often a DIFFERENT agent). Its `agent_id` is
105
- `ADS_AGENT` (name "Ads agent", slug `org-default`; usually also the `agent_id` in
106
- credentials.json). Then pass `target: { type: "agent", agent_id: ADS_AGENT }` on EVERY
107
- `get_upload_url` / `get_download_url` / `write_file` / `list_directory` / `read_file` — NEVER
108
- omit `target`.
109
- - **CRITICAL — publish under the PROJECT FOLDER, not the workspace root (the #1 "video renders but
110
- is invisible" bug).** `get_upload_url` stores at `<ADS_AGENT>/files/<path>` verbatim, but the
111
- render-file route reads from
112
- `<ADS_AGENT>/files/agent-config/brands/<brand_slug>/projects/<project_id>/<path>`
113
- (see backend `resolveProjectFileKey`). So EVERY publish/preview `path` MUST be prefixed with
114
- `agent-config/brands/<brand_slug>/projects/<project_id>/` — e.g. upload to
115
- `agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4`, NEVER bare
116
- `working/final.mp4`. A bare path 404s in the app even though the render row AND a bare-path
117
- `get_download_url` both "succeed" (they resolve the wrong key). The render `output_url` still
118
- stays the project-relative `...render-file?path=working/final.mp4` — the route re-prepends the
119
- prefix itself. Always verify with `get_download_url` on the FULL `agent-config/...` path (must
120
- be non-empty; curl it for HTTP 200) BEFORE marking the render complete.
121
- - Media generation (FAL / ElevenLabs) through the GooseWorks proxies is the **REAL spend** — billed
122
- to the agent per call as you generate (Step 4). `submit_render { kind: "full" }` additionally
123
- debits **1 nominal ad credit when the render ROW is opened** (a bookkeeping fee, NOT the render's
124
- true cost) — so open it only once you actually have a rendered master (Step 4.1/4.2), and never
125
- re-submit on a guess (that double-bills). The final-video QC gate (Step 4.3) then sits between
126
- that master and PINNING it. Call `get_ad_credits` first; the user can check `gooseworks credits`.
127
-
128
- ## Step 0 — project id, or video BATCH id? (fan out before anything else)
129
-
130
- The handoff you were pasted is EITHER a single `project <id>` OR a `video batch <id>`. A batch is
131
- the app's "N concepts" flow: one composer submission fans out into **N independent concept projects**
132
- (the user picked a concept count, default 3), and the app expects EACH to be rendered. **Handle both:**
133
-
134
- - **`project <id>`** → you have one project. Treat it as a batch of one and continue to Step 1.
135
- - **`video batch <id>`** → call `get_ad_video_batch { video_batch_id }`. It returns every child
136
- concept under `projects[]` — each is a normal project with its own `id`, `variant_index`
137
- (Concept 1..N), and its own `creative_brief` (the per-concept angle/hook/offer/message). **You
138
- MUST process every concept, not just the first** — dropping concepts 2..N is the #1 batch bug.
139
-
140
- **Loop shape (one agent, sequential, ONE approval for the whole batch):**
141
- 1. Run **Step 1 + Step 1.5 + Step 2 + Step 3-assemble** for EACH concept project (each has its own
142
- `project_id`, brief, and `working/` folder — never cross-write between concepts).
143
- 2. Mirror EVERY concept's review set (Step 3's `update_ad_project_script` per project), then stop
144
- for **ONE** approval that covers all concepts — show the per-concept credit estimate and the
145
- batch total. Set the batch to `review` (`update_ad_video_batch { status: "review" }`).
146
- 3. On approval, set the batch to `rendering` and run **Step 4 (the expensive render)** for each
147
- concept **sequentially** (finish Concept 1's master before starting Concept 2 — one machine can't
148
- render them in parallel). Deliver each (Step 5). When all concepts are pinned, set the batch to
149
- `complete`.
150
-
151
- If a single concept fails, keep going with the rest, mark that concept blocked, and report which
152
- ones shipped — never abort the whole batch on one bad concept. Everything below (Steps 1–5) is
153
- written per-project; a batch just runs it N times with the shared approval gate above.
154
-
155
- ## Step 1 — resolve the project, source, brand
156
-
157
- 1. `get_ad_project { project_id }` → keep `brand_id`, `source_sample_id`, `name`, `status`, the
158
- **top-level** `app_url` + `brand_url` (returned alongside `project`, NOT inside it — the links
159
- you hand the user for the in-app review in Step 3 and delivery in Step 5), AND the user's
160
- **`creative_brief`**, project **`assets`**, `character_id`, `default_voice_id` — these are the
161
- authoritative inputs the user chose in the composer (see Step 1.5). Do NOT discard them.
162
-
163
- ### Step 1.5 — the project brief is AUTHORITATIVE (honor it; don't re-ask)
164
-
165
- The composer already collected the user's creative direction onto the project. **Read it and treat
166
- it as ground truth — it OVERRIDES the template recipe's defaults, and it REPLACES the clarifying
167
- questions you would otherwise ask.** Only fall back to the recipe default (then, last, to asking)
168
- for a field the brief leaves empty. Map the fields you WILL honor:
169
-
170
- - `creative_brief.productName` / `.offer` / `.angle` → the product, offer/code, and angle. Do
171
- **not** ask "which product / what offer / what angle" if these are set.
172
- - `creative_brief.concept` (on a batch child) → this concept's **`angle` / `hook` / `offer` /
173
- `message` / `note`** — the per-concept differentiator. Honor it verbatim; it's WHY the user asked
174
- for N concepts. `angle: "auto"` or empty means "you choose."
175
- - Project `assets` + `creative_brief.reference_image_urls` → the user's **own reference images**.
176
- Use them as the product/brand refs (alongside the brand kit), don't ignore them for generic recipe
177
- assets.
178
- - `character_id` → the avatar/creator to use. `default_voice_id` → the voice for any VO (put its
179
- NAME in the review `subtitle`). Use these instead of picking your own.
180
- - `creative_brief.ratio` / `.durationSeconds` → target aspect ratio + length. Honor when the
181
- format's render pipeline supports it; if the format physically can't (e.g. a fixed phone-mockup
182
- aspect), keep the format's native value and note the constraint in the review rather than silently
183
- ignoring the request.
184
- - `polish_policy` (`standard` | `extra`) → `extra` means spend the extra pass on QC/polish.
185
- 2. `get_ad_template { template_id: source_sample_id }` → the source video: `media_url`,
186
- `recipe`, `format` (e.g. "imessage"), `extracted_script`, `how_to`, `remix_spec`.
187
- 3. Brand gate: `get_brand_kit { brand_id }`. If `researchStatus` is `complete`, REUSE it —
188
- never re-research. If not, run brand research first (`gooseworks fetch brand-research`,
189
- follow it, then `finalize_brand_research { brand_id }`) before continuing.
190
-
191
- ## Step 2 — read the template's recipe (it carries everything; NO hardcoded format map)
192
-
193
- The ad format is a **template (data) in the ad_sample DB**, not a per-format skill.
194
- `get_ad_template(source_sample_id)` returns the template's `recipe` — a self-contained brief you
195
- read and execute. **Do NOT map `format` to a hardcoded recipe slug** (there is no such table):
196
-
197
- - `recipe.format` — the format label (e.g. `vignette`), for display only.
198
- - `recipe.atoms` — the **capabilities** this template composes (e.g. `create-video-seedance-2-fal`,
199
- `create-image-gpt-image-fal`, `review-ugc-render`, `watch`). `gooseworks fetch <name>` each — they
200
- live in `skills/ads/capabilities/` and are reused across templates (so they cache).
201
- - `recipe.instructions` — the **playbook** to follow: `instructions.inline` prose, or
202
- `instructions.doc_url` (an S3 markdown doc — fetch it).
203
- - `recipe.config` — every param (prompts, layout, timings, palette, model choices).
204
- - `recipe.inputs` — the brand-asset contract (which product / logo / offer this template needs).
205
- - `recipe.assets` — reference material as S3 links (reference render, style guide, example frames) —
206
- fetch as needed.
207
-
208
- Runtime: **read the recipe → `gooseworks fetch` each capability in `recipe.atoms` → follow
209
- `recipe.instructions` with `recipe.config` + the brand's bound `inputs`.** The template IS the recipe;
210
- there is no `format → recipe-slug` table and no per-format skill to fetch.
211
-
212
- Save each fetched capability's scripts + files under `/tmp/gooseworks-scripts/<name>/`. If a capability
213
- is a Node package (a phone-mockup renderer), `npm install` in its folder so its `generate.js` +
214
- Playwright resolve, and point the recorder's `NODE_PATH` at it.
215
-
216
- > **Migration note:** older phone-mockup formats (`imessage` / `chatgpt` / `apple-notes`) whose DB
217
- > recipe does not yet carry `atoms` / `instructions` still hold the legacy `recipe.thread` payload;
218
- > migrate them to this shape (capabilities + instructions in the DB) — do not reintroduce a CLI map.
219
-
220
- ## Step 3 — assemble the review set, then get ONE approval in the app (before the expensive render)
221
-
222
- This is a **review-once** flow: put the whole review set in the app, get ONE approval, then run the
223
- expensive render + any remaining paid work end-to-end. Never spend on the expensive render before
224
- approval, and don't drip pieces out one at a time and re-pause.
225
-
226
- **What goes in the review — show the REAL cheap pieces, PROMPT only the expensive render.** Split
227
- every piece three ways by cost, NOT just "free vs paid":
228
- - **FREE** (an iMessage / Apple-Notes HTML mockup, a text/CTA line — rendered locally, no proxy
229
- call) → generate NOW and mirror the real asset.
230
- - **CHEAP paid** — a single still/image, the creator/avatar frame, the end card, a short voiceover
231
- or music bed (each costs cents → roughly **≤ 100 credits**) → **generate these NOW too** and
232
- mirror the real asset. The few credits buy a real review: the user SEES the actual creator face
233
- and end card and HEARS the VO, instead of judging a prompt. **This OVERRIDES any recipe rule that
234
- says to gate ALL paid calls** — only the expensive render below is gated.
235
- - **EXPENSIVE paid** — the video take / final AI render (hundreds of credits) → do NOT generate.
236
- Put its **exact prompt/spec** (+ ref image URLs) in the tile. This is the ONE thing approved as a
237
- prompt (you can't preview a hundreds-of-credits video for free); it's generated only in Step 4.
238
-
239
- **The expensive render's exact prompt must be in the panel BEFORE you ask for approval** — so a
240
- single "go" runs it (plus any remaining paid work) without re-pausing mid-run.
241
-
242
- **Show every cost in CREDITS, never dollars.** 1 credit = $0.01 and media generations bill at
243
- provider-cost × 1.2, so **credits ≈ round-up(provider-$ × 120)** per generation, plus a flat
244
- **200-credit base per video**. Convert any $ figures to credits and show ONLY credits to the user —
245
- never print a "$…" amount.
246
-
247
- **Never assemble/stitch the finished video for review.** The review is of the individual pieces (or
248
- their prompts) — never a "full cascade" / "approved cut" clip. Building the whole video before
249
- approval defeats the gate (the user opens the review to an already-finished video) and wastes the
250
- render (GOOSE-2542). The full video is assembled ONLY in Step 4, after approval. A `video`
251
- ingredient here is only a genuinely separate SOURCE clip the format needs (e.g. supplied b-roll).
252
-
253
- 1. **Assemble every piece the format needs — not just the script.** Read the recipe for the exact
254
- list. For an iMessage video that's the **script** (bubble thread), the **conversation image(s)**,
255
- and the **end card**; richer templates add a hook frame, background, product shots, music bed, a
256
- creator/avatar, a voiceover… For each piece, decide FREE / CHEAP-paid / EXPENSIVE-paid (above):
257
- - **FREE or CHEAP paid** (≤ ~100 credits — HTML mockups, a still, the creator frame, the end
258
- card, a short VO/music bed) → generate it now and `get_upload_url` the asset to the project
259
- folder `agent-config/brands/<brand_slug>/projects/<project_id>/working/review/<name>` (same
260
- path-prefix rule as final publish — a bare `working/review/<name>` won't render in the panel);
261
- set that piece's `path` in `script_drafts` to the project-relative `working/review/<name>`.
262
- - **EXPENSIVE paid** (the video take / final render, hundreds of credits) → do NOT generate. Put
263
- the **exact prompt/spec** (and any ref image URLs) in the tile's `text` / `subtitle` so the
264
- user reviews what will be spent on. No `path` yet — it's generated in Step 4.
265
- Include the **estimated cost in CREDITS** (never dollars) of the cheap pieces already generated +
266
- the pending render, so the user approves knowing the total spend. **Answer clarifying questions
267
- from the project brief FIRST (Step 1.5)** — only ask the user for a field (angle, which product,
268
- offer/code) the `creative_brief` leaves empty AND the recipe can't default. Do not re-ask for
269
- anything the composer already captured.
270
- 2. **Mirror the whole ingredient set for review** — `update_ad_project_script { project_id,
271
- script_drafts, script }`. `script_drafts` is a structured payload of **container-tagged
272
- ingredients** so the app renders each piece the right way:
273
- `{ format, scenes?, ingredients: [{ container, label, subtitle?, path?, text? }] }`. Each
274
- ingredient's `container` tells the app HOW to show it:
275
- - `image` (a frame shown in the video), `endcard` (the end card), `avatar` (a character
276
- headshot), `background` → rendered as an image tile.
277
- - `voice` (a voiceover clip — put the voice NAME in `subtitle`), `music` (the bed),
278
- `audio` → rendered as an audio player.
279
- - `video` (a clip) → a video player. `text` (a copy line like the CTA) → a text tile.
280
- - `script` / `thread` / `note` / `conversation` → the written script (or set `scenes[]`
281
- for the podcast shape, or pass the readable `script` string).
282
- `path` = `working/review/<name>` (upload the preview asset first via `get_upload_url`); `url`
283
- works too. **Label every ingredient** ("Hook image", "End card", "Voiceover", "Background
284
- music", "HER"). The `update_ad_project_script` call itself writes no render and costs no credits
285
- (the cheap pieces you already generated above have their own small cost) — it just populates the
286
- review panel.
287
- 3. **STOP — the review happens in the APP's review panel, NOT in this chat.** You've mirrored the
288
- ingredients (3.2); now hand the user the project's `app_url` (from `get_ad_project`) and tell
289
- them to review the pieces there and hit **"Approve & render"**. That button gives them a short
290
- message to paste back into this session — THAT is your go-ahead. Do NOT paste the
291
- script/ingredients into the chat for a thumbs-up, and do NOT render until that approval comes
292
- back from the app. If they want changes (via the app's comments or here), regenerate the
293
- affected ingredient, call `update_ad_project_script` again, tell them it's refreshed in the
294
- app, and wait for a fresh approval. Only AFTER the app approval do Step 4. A single approval
295
- authorises the WHOLE remaining chain — generate every paid piece, render, self-QC, publish —
296
- with NO further pauses (that is exactly why every paid prompt must already be in the panel).
297
-
298
- ## Step 4 — render locally, report stages, publish
299
-
300
- 1. Now generate every PAID piece you showed as a prompt in Step 3 — the AI stills/video, voice,
301
- music, the end-card render — through the media proxies (below), each from its approved prompt.
302
- Then assemble per the recipe (Playwright record where needed → ffmpeg stitch → `mix-master`
303
- audio).
304
- 2. Open the row LAST: `submit_render { project_id, kind: "full" }` → keep `render_id`, then
305
- `update_render_status { render_id, status: "running" }`. The render row tracks status only
306
- (queued / running / complete / failed) — narrate fine-grained progress with
307
- `append_project_message` instead.
308
- 3. **MANDATORY final-video QC gate — YOU review EVERY finished master before `set_final_render`,
309
- whatever the format (UGC or not).** This is your own automated quality check, separate from the
310
- user's Step-3 approval — it does not go back to the user. The render row is already open (its
311
- nominal credit spent, `submit_render` in 4.2);
312
- this gate stands between a rendered master and PINNING/publishing it, so a bad render never gets
313
- set as final. A master that looks fine on a still can still have a mis-voiced word, a caption
314
- drifting off its line, a beat out of order, or a deformation — review the actual VIDEO, not
315
- stills. Run the passes that APPLY to this format:
316
- - **Audio ↔ script** — any master with SPEECH (VO or native/Seedance voice); **skip for
317
- music-only / no-speech formats.** `review-ugc-render` is format-agnostic despite the name —
318
- a deterministic Whisper transcript-vs-script diff, not UGC-specific: persist the approved
319
- spoken lines to `working/approved-script.txt`, then `gooseworks fetch review-ugc-render` and
320
- run `review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json
321
- working/review-verdict.json` (exit 0 PASS / 2 FAIL / 3 ERROR). It blocks a mis-voiced word
322
- (approved "human-vetted" → "human witted"), a dropped phrase, or silence. It routes Whisper
323
- through the gooseworks proxy when `OPENAI_BASE_URL` is set; with no backend at all, run
324
- `fal-ai/whisper` via `fal-proxy` (upload the audio, pass its `get_download_url` as `audio_url`)
325
- and diff the transcript yourself.
326
- - **Captions / subtitles** — ANY captioned format (the most common non-UGC defect); **skip for
327
- UGC/Seedance masters, which carry no subtitle track.** Concrete check: diff the caption file
328
- you burned (SRT/ASS) against the SAME Whisper transcript + word timings from the audio pass —
329
- every caption line must match the heard/scripted words and sit within ~0.3s of when they're
330
- spoken; then in the visual pass below, OCR-read the burned caption off 4–5 sampled frames to
331
- confirm it's actually on screen at that time and not colliding with a hyperframe or the end
332
- card. Mismatched text or >0.3s drift fails the gate.
333
- - **Visual + structure** — always: run the `watch` skill on the master — beat/scene order + SFX,
334
- the brand's product (not the source's) is shown, the end card has the real wordmark + code, no
335
- deformation/artifact, duration within ~20% of the source.
336
- If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, rebuild captions)
337
- and re-review — only a clean pass proceeds to `set_final_render`. **This gate is universal: it
338
- runs from the master skill for every format, so a recipe never has to opt in.**
339
- 4. Publish: `get_upload_url { target: { type: "agent", agent_id: ADS_AGENT } }` → PUT the master
340
- and poster **under the project folder** (see Identity's path-prefix rule) — to
341
- `agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4` and
342
- `.../working/final-thumb.jpg`. **Always target ADS_AGENT AND use the full project-folder path**
343
- — a bare `working/final.mp4`, even on the right agent, 404s in the app. Verify servable:
344
- `get_download_url { target: ADS_AGENT, path: "agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4" }`
345
- must return a non-empty URL (curl it for HTTP 200).
346
- Then `update_render_status { render_id, status: "complete", output_url, thumbnail_url }` where
347
- **output_url MUST be the durable render-file URL**
348
- `/api/ads/projects/<project_id>/render-file?path=working/final.mp4` (the app re-presigns it on
349
- every view) — NEVER a raw proxy/CDN URL (those expire). Same for `thumbnail_url`.
350
- 5. `set_final_render { project_id, render_id }` to pin it, then return the `app_url` +
351
- `brand_url` (from the project/links) verbatim. Never end on just "done" or a file path.
352
-
353
- Narrate each long step in one line via `append_project_message { project_id, role: "agent",
354
- content }` — never sit silent on a queue > 90s.
355
-
356
- ## Media generation — the GooseWorks proxies (queue loop)
357
-
358
- Media APIs go through GooseWorks proxies with your agent token; do NOT use an SDK's default host
359
- (your token isn't a FAL/ElevenLabs token → 401). Base = `<api_base>/api/internal/<proxy>`; pass
360
- `?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>` (agent_id bills the Ads agent;
361
- `project_id` = the id of the project you're rendering — it attributes this generation's credits to
362
- that ad project so the user sees per-project spend in the app. ALWAYS pass it). FAL = `fal-proxy`
363
- (+ `fal-storage-proxy` to host a local image and get a CDN URL); ElevenLabs = `elevenlabs-proxy`
364
- (VO / music bed).
365
-
366
- **FAL queue gotcha** (#1 waste of generations): submit returns `status_url`/`response_url` on
367
- `queue.fal.run` (the real host, not the proxy). Polling those 401s forever — rewrite their host
368
- to the proxy base (keep the path), re-add `?token=&agent_id=`. Only the final `*.fal.media`
369
- image is a real public URL. Helper:
370
-
371
- ```python
372
- import json, os, pathlib, time, requests
373
- from urllib.parse import urlparse
374
-
375
- def _cfg():
376
- c = json.loads(pathlib.Path(os.path.expanduser("~/.gooseworks/credentials.json")).read_text())
377
- return c["api_base"].rstrip("/"), c["api_key"], c.get("agent_id")
378
-
379
- def _params(tok, agent, project_id=None):
380
- p = {"token": tok}
381
- if agent: p["agent_id"] = agent
382
- if project_id: p["project_id"] = project_id # attributes the spend to this ad project
383
- return p
384
-
385
- def fal_generate(model_path, payload, project_id=None, timeout_s=180, poll_s=3):
386
- """model_path e.g. 'fal-ai/nano-banana-2/edit' (the recipe names the model).
387
- Pass project_id = the ad project you're rendering so credits attribute to it.
388
- Returns the result image URL (a public *.fal.media CDN URL)."""
389
- api_base, tok, agent = _cfg()
390
- base = api_base + "/api/internal/fal-proxy"
391
- sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent, project_id), json=payload).json()
392
- to_proxy = lambda u: base + urlparse(u).path
393
- status_url, response_url = to_proxy(sub["status_url"]), to_proxy(sub["response_url"])
394
- deadline = time.time() + timeout_s
395
- while time.time() < deadline:
396
- st = requests.get(status_url, params=_params(tok, agent, project_id)).json()
397
- if st.get("status") == "COMPLETED":
398
- return requests.get(response_url, params=_params(tok, agent, project_id)).json()["images"][0]["url"]
399
- if st.get("status") in ("FAILED", "ERROR"):
400
- raise RuntimeError(f"FAL failed: {st}")
401
- time.sleep(poll_s)
402
- raise TimeoutError("FAL polling exceeded timeout")
403
- ```
404
-
405
- ElevenLabs (VO / music) is the same shape against `<api_base>/api/internal/elevenlabs-proxy`
406
- with `?token=&agent_id=&project_id=`. Feed FAL a local image by storing it (`get_upload_url`) and passing its
407
- `get_download_url` presigned URL as an `image_urls` / `audio_url` entry — this is the reliable
408
- path. (`fal-storage-proxy` may 404 depending on the install; don't block on it — prefer the
409
- `get_download_url` presigned URL.)
410
-
411
- ## Rules
412
-
413
- - **MCP + ffmpeg + Playwright required** — run `gooseworks doctor` in Phase 0; stop with the
414
- exact fix it prints if anything is ✗.
415
- - **Assemble the whole review set first**, mirror it with `update_ad_project_script`, and get the
416
- user's approval **in the app's review panel** (the "Approve & render" button) BEFORE the expensive
417
- render — never ask for a thumbs-up in this chat (review-once, in-app).
418
- - **Show the REAL cheap pieces; PROMPT only the expensive render.** Generate the FREE + CHEAP-paid
419
- pieces (≤ ~100 credits — stills, creator frame, end card, short VO/music) and mirror the real
420
- assets; put ONLY the expensive video take/render in the panel as its exact prompt. That prompt
421
- must be in the panel before you ask to approve, so a single "go" runs the render + any remaining
422
- paid work (→ QC → publish) with no re-pausing.
423
- - **Costs in CREDITS, never dollars.** credits ≈ round-up(provider-$ × 120) per generation + a flat
424
- 200-credit base per video; never show a "$…" figure to the user.
425
- - **Never assemble the full video before approval.** The review shows the
426
- individual PIECES, never the finished cut (or their prompts) — not a
427
- stitched/composited cut; do not add a "full cascade" / finished-video clip
428
- as a review ingredient (GOOSE-2542). The assembled video is produced only in
429
- Step 4.
430
- - **submit_render only after the master is rendered** (Step 4.2), never on a guess; `output_url` =
431
- the durable render-file URL, never a CDN URL.
432
- - **Always pass `project_id` on media-proxy calls** (fal / ElevenLabs) so the credits attribute
433
- to this ad project — that's what lets the user see per-project spend in the app.
434
- - **Verify a real, non-empty MP4** (watch it) before marking the render complete.
435
- - **Reuse the brand** when its research is complete; never re-research.
436
- - On a hard error (auth/quota/model/timeout) set the render `failed` with a short
437
- `error_message` and stop — don't ship the source unchanged. **Also `log` it** (`gooseworks log`
438
- / `log_cli_event`, `--event-type api_failure|error`) so we can see + fix it (see "Report problems").
439
- - **Report blockers/bugs/confusing instructions via telemetry** (`gooseworks log` or the
440
- `log_cli_event` MCP tool) — not just to the user. Set `GW_RUN_ID` once so events group.
441
- - Always end a successful run with `app_url` + `brand_url`, verbatim.
15
+ # GooseWorks Video Ads — choose, review, make
16
+
17
+ ## How to talk to the customer (applies to every message you send them)
18
+
19
+ The customer is a marketer or founder, not an engineer. Everything in this skill about tools,
20
+ fields, files, polling, models and pipeline steps is for YOU. Never pass it on to them.
21
+
22
+ - **Say what they get and what they need to decide, never how it's made.**
23
+ Bad: "Checking the media-proxy helpers for voiceover timestamps and lipsync." / "Generated with
24
+ gpt-image-2 at high quality, mouth closed, passes QC."
25
+ Good: "Recording the voices now." / "Here are your two hosts."
26
+ - **Never mention** tool, file or field names, ids, JSON, commands, scripts, installed software,
27
+ uploads, model or vendor names, timings in seconds or frames, retries, polling, internal statuses
28
+ or your own quality checks. Fix what you can quietly. If they ask how something works, explain it
29
+ simply.
30
+ - **Update them only at milestones they care about**: choices confirmed, script and voices done,
31
+ visuals done, ready to review, finished. One short line each, then stay quiet until the next
32
+ milestone. No update for setup, downloads, uploads or saving.
33
+ - **Money is in credits, with a number**: "the full video uses about N credits". Never "the cheap
34
+ pieces" or "the expensive render".
35
+ - **Problems:** say what it means for them and what they can do, in one or two sentences. No error
36
+ codes or stack traces. Only raise a problem that changes something for them.
37
+ - **Use their words**: script, voice, hook, scene, host, ending, logo, the product shot.
38
+ - **No filler**: no "Great question!", "Certainly!", "I hope this helps!", no restating what they
39
+ just said at length.
40
+
41
+ Example. Instead of a dozen lines about render tools, scripts, uploads and portraits, send:
42
+ "Got it: warm tone, home podcast studio, Brielle as the skeptic and Mark as the believer." then
43
+ "Script and voices are done (about 34 seconds)." then "Ready for you to review: the script, voices,
44
+ both hosts and the ending. You can also review your recipe ingredients in the app: <link>. Say go
45
+ here and I'll make the full video (about N credits)."
46
+
47
+ ## Use current instructions for new work
48
+
49
+ Before a new task in a terminal host, run `gooseworks skills status` once. It compares the
50
+ installed entry files, running CLI and published npm release. An older CLI needs a package
51
+ upgrade before `gooseworks update`; update alone only uses that CLI's bundled instructions.
52
+ Preserve local edits or unknown install provenance. Review/back up before explicitly replacing
53
+ modified files; never quietly reinstall over them. If the release check is unavailable, report
54
+ that freshness is unknown rather than claiming the local copy is latest.
55
+
56
+ For a new recipe run, fetch its package from the connected catalog. Retain the returned
57
+ `version` / `contentHash` and every dependency's hash with the saved package. When reusing a
58
+ saved fetch JSON, `gooseworks fetch <slug> --saved-package <file>` returns the current package
59
+ and a hash comparison without changing that file. With MCP, if the advertised `catalog_fetch`
60
+ schema accepts them, send `saved_content_hash` and `saved_dependency_hashes`; otherwise fetch
61
+ normally and compare the returned hashes yourself. Missing hashes mean unknown, not current.
62
+ The server cannot inspect a client's saved files; hash metadata does not certify later edits.
63
+ Fetch current packages into a new run directory and report stale saved instructions. Preserve
64
+ an existing approved run's recorded package; changing that harness requires a reviewed change
65
+ and approval before spending. Hosted installed snapshots use the existing Skills Update action.
66
+ Skill content and a host's cached MCP tool schemas are separate: refreshing one does not refresh
67
+ the other. Check the actual advertised tools before using new fields.
68
+
69
+ ## Purpose
70
+
71
+ Resolve the brand, show supported formats and create one saved project. An agent with a verified
72
+ shell follows **`goose-video-local`**. A chat host delegates that same project to the GooseWorks
73
+ coworker, which renders in its sandbox. This is agent execution, not a recipe server-order API.
74
+
75
+ **Cards and execution are separate capabilities.** A terminal may render without drawing a
76
+ picker; a chat host may draw a picker while the coworker renders. Follow `card.display_hint`
77
+ and `available_here`, never a guess from the host's name. Template-remix review stays in this
78
+ chat. Custom videos use separate authenticated script and ingredient approvals in this same chat.
79
+ Studio is an optional review surface.
80
+
81
+ ## Custom videos: route before formats
82
+
83
+ For an original brief without a reference template, an Instagram reel/post URL or a direct video URL to study, fetch `catalog_fetch { type: "skill", slug: "make-custom-video" }` and follow it in this same session. It creates format:"custom", custom_mode:"generate" with the brief and optional reference_url. Growth executes in its managed sandbox; connected agents use their shell. Script and actual ingredients are reviewed and separately approved in the same chat before paid production; Studio is optional. Do not force a template choice or import the reference as a finished video.
84
+
85
+ ## Route first: is this a new video?
86
+
87
+ Hand off to **`goose-video-local`** now, and stop following this skill, for:
88
+
89
+ - an existing **project** id or a **video batch** id;
90
+ - the app's copy-for-Claude command (it names `goose-video-local`);
91
+ - "remix this video ad template" for a specific app template.
92
+
93
+ Use `goose-video-local` if it is installed; otherwise load it with
94
+ `catalog_fetch { type: "skill", slug: "goose-video-local" }` on the GooseWorks MCP (older clients:
95
+ `fetch_skill("goose-video-local")`). It reads the project first and says what to do with it.
96
+
97
+ When they ask **what** to make ("give me video ad ideas", "what angles should I use?", "what's
98
+ working for my competitors?"), fetch **`ad-angle-miner`** (`catalog_fetch { type: "skill", slug: "ad-angle-miner" }`,
99
+ using its advertised tools) and run it with the **video** output. It returns ranked
100
+ video ideas mapped to formats and hands the picked ones to `goose-video-local`.
101
+
102
+ Everything else, including "make me a video ad for <brand>", starts at step 1 below.
103
+
104
+ ## Keep the selected connection for the whole run
105
+
106
+ Call `account_whoami` on the connection that owns the brand/project before the first write.
107
+ Keep `environment.name` and the public `environment.api_origin` with this run. Respect the
108
+ user's selected production or staging connection. If multiple connections are available and
109
+ none was selected, resolve that once. Missing or unknown identity is uncertainty, not
110
+ permission to switch. Never infer the environment from credits, billing links or Node mode.
111
+
112
+ Reads, project creation, uploads, generation, polling and final updates all use that same
113
+ connection. If a production request fails, resume or report the failure on production;
114
+ never retry it on staging or recreate the project there. Before retrying a timed-out write,
115
+ read back the existing project or paid request on the selected connection.
116
+
117
+ Local/CLI proxy origin must match `environment.api_origin`. If it differs, use the MCP
118
+ relay on the selected connection (`GW_MEDIA_VIA=mcp`) before paid calls. Do not change
119
+ credentials or API origins to recover a failed write. An explicit user-requested move is
120
+ a separate operation, with the existing project and paid requests reconciled first.
121
+
122
+ ## Check assets for the selected format before spending
123
+
124
+ For a template, read its structured `asset_readiness` from
125
+ `video_catalog_list { kind: "formats", brand_id }`. `missing` names gaps;
126
+ `needs_review` means suitability is unverified, including older recipes without
127
+ structured requirements. `ready` describes assets only, not script or budget approval.
128
+ For a custom video, derive requirements from its actual approved scenes.
129
+
130
+ Inspect candidate files for the chosen product and format. A catalog photo can contain
131
+ multiple objects, other products or a person; its existence or approved status does not
132
+ make it a standalone image of the selected product. Check object count, framing, readable
133
+ print and real image bytes. A service conversation has no automatic packshot requirement.
134
+
135
+ Reuse a suitable approved image first. A free crop or cutout is a new file: keep the
136
+ original, inspect the result and include it in the normal ingredient review. When no
137
+ usable input exists, explain the gap before generation. Estimate any paid preparation
138
+ separately before spending. Record the selected asset and inspection in the project
139
+ review set; do not add a separate approval round.
140
+
141
+ ## Inputs
142
+
143
+ - A brand, usually named in the request. Resolve it from the accessible brands; one brand needs no question.
144
+ - The customer's goal, occasion, audience, selected angle and constraints when supplied; otherwise propose defaults from the verified kit.
145
+ - The selected format's required assets. Presence does not prove suitability; uncertain assets remain “needs review.”
146
+
147
+ ## Save durable brand answers, then verify them
148
+
149
+ Read the selected brand with `brand_read { brand_id, sections: ["summary", "kit", "products", "learnings", "onboarding"] }`
150
+ (fallback: `brand_get_context` with the same sections). Keep founder answers, user corrections,
151
+ research and your own hypotheses distinct. Reuse matching saved answers; ask only about gaps.
152
+
153
+ When the user asks to remember a rule, answers a brand interview, or explicitly corrects a
154
+ standing fact, save that answer in the same turn. The capture request authorizes those answers;
155
+ do not ask for approval again. A direction for this one video stays in its brief. If the scope
156
+ is genuinely ambiguous, ask whether it applies to future videos before saving a standing rule.
157
+
158
+ Use the **live registered schema**. Where supported, call `brand_update` with
159
+ `knowledge_intent: "user_correction"` and `user_statement` containing the user's exact,
160
+ verbatim answer, not your paraphrase or researched text.
161
+ For an inference or suggested improvement, use `knowledge_intent: "agent_proposal"`. Show the
162
+ before/after change from your prior read and proposed value; retain the returned proposal IDs
163
+ and say the user must accept it in the app. Link only a review surface actually returned by a
164
+ tool; the compact `knowledge_updates` response does not itself contain a diff or URL.
165
+ A pending proposal is not a saved fact. Never call an unavailable
166
+ tool or silently relabel research or your inference as something the user said.
167
+
168
+ The safe structured shape is `patch: { knowledge: { positioning?, audience?, voice?,
169
+ instructions?, brandType?, tagline?, valueProps? } }`, using only fields present in the live
170
+ schema. Inferred rules/taste go in an `instructions` proposal with a rationale, never in
171
+ `patch.facts`. Prefix every video-only preference in that proposed text with "Video preference:"
172
+ so it remains production direction after acceptance. Preserve unrelated instructions when
173
+ proposing a merged replacement.
174
+
175
+ | User answer | Canonical write |
176
+ | --- | --- |
177
+ | Primary audience, positioning or voice correction | `patch.knowledge: { audience/positioning/voice: <answer> }` (one actual key). During onboarding, `brand_onboarding { action: "review_research", review: { action: "correct", field, value } }` writes these existing corrections with provenance. |
178
+ | Founder story, customer pains, objections, buying trigger or useful audience detail without a structured field | `patch.facts: [{ kind: "insight", text }]`; retain attribution such as "Founder reports: …" rather than turn a belief into a verified result. |
179
+ | Required wording or pronunciation | `patch.facts: [{ kind: "must", text }]`; pronunciation is exactly `Pronounce "<term>" as "<say_as>"`. |
180
+ | Forbidden claim, word or visual | `patch.facts: [{ kind: "dont", text }]`. |
181
+ | Durable visual, voice or pacing preference | `patch.facts: [{ kind: "do", text }]` for a preference; `dont` for an avoidance; `template_hint` for a preferred format. Prefix video-only preferences with "Video preference:". |
182
+
183
+ Facts are existing `ad_brand_learning` rows with user provenance; they are not a second profile.
184
+ Update a matching rule by its returned `id` instead of adding duplicates. Preserve unrelated
185
+ rules and the user's exact meaning. Only use the legacy facts shape for explicitly user-authored
186
+ answers when the live schema lacks intent fields; agent suggestions still need a proposal path.
187
+
188
+ **Claims and plans have separate gates.** A founder assertion or proof point is not an approved,
189
+ evidence-backed claim or consent to quote a customer. Use the existing evidence/claims and
190
+ operating-plan tools only if registered, following their proposal, evidence and confirmation
191
+ requirements. Never encode a spend cap, approver or emergency stop as a learning. If that write
192
+ path is missing, report the specific unsaved item and keep it pending for the supported review
193
+ surface; do not claim it was saved or create a parallel local profile.
194
+
195
+ After every write, **read back before saying saved**: use `brand_read` with `kit`, `learnings`
196
+ or `onboarding` as appropriate, or `brand_onboarding { action: "status", brand_id }` after an
197
+ onboarding answer. Verify the intended field/rule, its source and the absence of a conflicting
198
+ duplicate. A generic success response, pending proposal, ignored key or truncated result is not
199
+ proof. Report partial saves honestly. Carry the verified rules into the current task and the
200
+ routed skill; claims still pass their own safety gate.
201
+
202
+ ## Video taste — reuse examples and preferences
203
+
204
+ When asked to capture video taste, or when the user volunteers a durable video preference,
205
+ first read the saved learnings and `media_list { brand_id, scope: "brand", scope_id: brand_id,
206
+ tags: ["video-taste"], limit: 100 }`. Follow `next_cursor` before deciding an example is absent.
207
+ Do not force a taste interview before an unrelated task or ask again for an existing preference.
208
+
209
+ Save what the user has already supplied first. Then ask only the missing useful question, for
210
+ example: "What do you like about this video—its pace, voice, captions, or look?" An inaccessible
211
+ link can still be saved as a link with the user's explanation; do not pretend you watched it.
212
+
213
+ - **Direct clip or video file:** register with `media_upload { brand_id, scope: "brand",
214
+ scope_id: brand_id, kind: "video", source: { type: "url", url }, tags: ["video-taste", "reference-only"],
215
+ metadata: { purpose: "video_taste", source_url: url, provenance: "user", captured_at: <ISO timestamp>,
216
+ preference: <the user's explanation> } }`. For a file use the live file/bytes upload flow and
217
+ `media_confirm` after a presigned upload. Registration of a URL does not copy or inspect it.
218
+ - **Instagram/post/page link:** the same registration with `kind: "document"`; it is a link
219
+ bookmark, not downloadable footage or an indexed transcript. Do not fabricate a direct clip URL.
220
+ - **Preferences:** save the user's reasons, likes and dislikes through the facts mapping above.
221
+ Read and preserve existing facts before updating one. Do not invent `video_preferences` or
222
+ new `video_lab` keys; the live kit patch accepts only its documented asset fields.
223
+ - **Deduplicate:** reuse a matching returned media row, then `media_update` its title/tags/metadata
224
+ if needed; preserve existing metadata and tags. Do not create another row for the same example.
225
+ Read back with `media_list` and `brand_read` learnings before claiming it was saved.
226
+ Check the returned row belongs to this brand: URL deduplication may return another brand's
227
+ existing row. Do not relabel that row or claim success unless the current brand's scoped read
228
+ actually returns it. Report an unsaved association if no supported attach path is available.
229
+
230
+ Never put third-party taste examples into kit reference images: `kind: "reference"` at brand
231
+ scope writes there. The tags and metadata above record purpose and provenance; **they do not
232
+ grant or enforce usage rights**. Study the structure, pacing and look only. Never use the example's
233
+ footage, face, product, testimonial or claims in a new ad without independently verified permission.
234
+
235
+ Read video-only entries prefixed `Video preference:` from both saved learnings and
236
+ `kit.instructions`, including accepted proposals. Keep them out of required or forbidden
237
+ dialogue. Build a brief from the verified readback: preferred pace, voice, caption treatment, visual style,
238
+ formats to favour/avoid, reference links and the user's reasons. Say what is still unknown.
239
+ Pass it with the brand rules into the existing video workflow. A one-video request overrides a
240
+ default for that project; it does not silently rewrite the brand's standing preference.
241
+
242
+ ## Composed Atoms
243
+
244
+ Use the live canonical schemas: brand_read, brand_create, video_catalog_list,
245
+ video_project_upsert, video_project_read, catalog_fetch, goose_run_task and job_cancel.
246
+ An old tool name in a recipe is not grounds for terminal-update advice in a chat host.
247
+
248
+ ## Paid media: images, clips, voice
249
+
250
+ **No FAL_KEY, ElevenLabs key or `fal_client` is ever needed.** Use
251
+ `data_post_provider { provider: "fal", path: <model id, e.g. "fal-ai/nano-banana/edit">, body: <model input>, project_id }`
252
+ then `job_get { job_id }`; voice/music use provider:"elevenlabs". Upload local inputs through
253
+ media_upload first. **A missing key is never a blocker.** `photos_generate` is **not** a general
254
+ image tool: it photographs physical catalog products only. Use goose-video-local for the
255
+ runtime, paid approval, asset saving and quality checks.
256
+
257
+ ## Workflow
258
+
259
+ ### 1. Resolve the brand, quietly when you can
260
+
261
+ Call brand_read without brand_id to list accessible brands. Resolve the customer's name from
262
+ actual returned names. One brand, or exactly one match: state it and continue. Several possible
263
+ brands: show names and websites and ask once. No match: show accessible brands and offer to add
264
+ one only when the customer asks and supplies its website; never guess the URL.
265
+
266
+ Read `brand_read { brand_id, sections: ["summary","kit","products","learnings"] }` before
267
+ choosing a format. Reconcile explicit brand corrections through the durable capture rules.
268
+ A direct video request does not require first-campaign acceptance. Follow any returned
269
+ request-specific setup requirement; never mark research complete or accept a campaign to
270
+ bypass setup. Keep the original request.
271
+
272
+ ### 2. Keep the goal and propose defaults
273
+
274
+ Never ask what the ad is for before showing formats. Keep the customer's exact direction.
275
+ Otherwise propose defaults from the verified kit's products, audience, offer and voice. Show
276
+ those defaults in the saved plan so any can be changed. Never invent proof or product facts.
277
+ Idea requests still follow ad-angle-miner with the video output.
278
+
279
+ ### 3. Show the picker or its text fallback
280
+
281
+ Call `video_catalog_list { kind: "formats", brand_id }`. On the customer's computer, when you
282
+ can actually execute shell commands, include `client: { shell: true }`; in a chat host omit
283
+ that claim. Inside a coworker sandbox follow its reported capabilities, never claim to be the
284
+ customer's computer. Respect requested limit and next_cursor for more item pages; the card may
285
+ contain a full picker independently of the item page.
286
+
287
+ - **card.display_hint:"widget"**: the picker replaces a format table. Write at most one short
288
+ acknowledgement, print no table or list of the formats, and wait for the selection.
289
+ - **Otherwise**: print card.text_summary as returned. Without a card, show returned rows with
290
+ names, faithful descriptions, needs and demo links, respecting the requested page size.
291
+ Write "no demo yet" when absent. Never put links only in a question control.
292
+ - A format whose card contradicts what they asked for is never Suggested. Never offer
293
+ available_here:false as an executable choice. Relay not_available_here in one short line.
294
+ - Missing required assets are missing; unknown suitability is “needs review.” Inspect the
295
+ selected format's candidates before spending. Never promise an unverified asset is ready.
296
+ - Template formats use **one plan, one approval with the total in credits**, not approval for
297
+ each paid step. Custom videos retain separate authenticated script/ingredient/budget gates in this chat.
298
+
299
+ ### 4. Check this machine can render it
300
+
301
+ - **Chat host without a shell:** skip local checks and delegate an available format in Step 5.
302
+ Never tell a chat host it needs Claude Code to start. Browser-only formats remain unavailable
303
+ here; give computer setup guidance only if asked how to make one.
304
+ - **Local shell:** run `gooseworks doctor --no-browser` for common setup (auth/MCP, Node 18+,
305
+ ffmpeg with libx264 + libass, ffprobe). Then fetch the selected template and its capabilities,
306
+ and install documented dependencies in its fetched folder. Do not guess a renderer from a
307
+ format name. For each Node renderer using default Playwright Chromium, run
308
+ `gooseworks doctor --renderer-script "/absolute/path/to/the/fetched/scripts/record.js"` with
309
+ the exact environment used to render, including NODE_PATH and PLAYWRIGHT_BROWSERS_PATH.
310
+ Use the actual script path. Custom browser launch settings need their equivalent exact-runtime
311
+ check. Non-browser capabilities need only their documented runtime checks.
312
+ Never create paid ingredients before the selected renderer passes. Repair and recheck under
313
+ existing setup permissions. If it cannot be fixed, re-list with client:{shell:false} and
314
+ delegate only a format available to the coworker; never silently change the selected format.
315
+ - **Coworker sandbox:** you are the renderer. Follow goose-video-local's sandbox checks; never
316
+ hand off recursively or claim browser capability that the sandbox lacks.
317
+
318
+ ### 5. Create the project and hand it off, in this session
319
+
320
+ 1. `video_project_upsert { brand_id, name, format: <template_id> }` with no brief (a brief creates
321
+ a concept batch). Include client:{shell:true} only for real local execution. Default
322
+ creation_intent:"format" keeps the style with this brand's content; source_remix requires an
323
+ explicit choice to use source content.
324
+ If they already chose a campaign/concept, include its verified campaign_id and optional
325
+ campaign_concept_id. Read the IDs from that brand's saved campaign; the concept must belong
326
+ to it. Omit unknown IDs and never infer a link or create a campaign solely to file a video.
327
+ 2. **Verified local shell or coworker sandbox:** load
328
+ `catalog_fetch { type: "skill", slug: "goose-video-local" }` and follow it on the same project_id.
329
+ Carry the customer's words, verified defaults, campaign and selected angle into its brief.
330
+ 3. **Chat host:** `goose_run_task { brand_id, project_id, message }` with the request, selected
331
+ format and defaults. Keep task_id. Continue questions/edits with the same task and project.
332
+ A saved free draft is not a started worker or a complete plan. Follow actual saved state.
333
+ 4. When follow.card_follows is true, the card follows progress: do not re-read it in a loop.
334
+ Read again on a customer reply, card action or requested update. A failure or missing saved
335
+ plan needs the returned recovery action; never claim completion or approve an empty plan.
336
+ Approval requires the current complete plan and total. Insufficient balance: offer a shorter
337
+ video or top-up. “Not now” keeps the saved plan.
338
+
339
+ Do not hand the customer a command to paste somewhere else.
340
+
341
+ ## Decision Rules
342
+
343
+ - One sentence is a complete request. Resolve the brand and show formats without a goal interview.
344
+ - One brand in the org: never ask which brand. Cards decide presentation; checks decide execution.
345
+ - Use canonical tools. Preserve campaign context, selected script angles and custom authenticated review gates.
346
+ - Never fabricate readiness, completion, approval or a second project.
347
+
348
+ ## Output
349
+
350
+ One project, one current saved plan in this chat, then the finished video. With a widget write one
351
+ line and do not add app_url, brand_url or duplicate video links. A text host gets text_summary
352
+ and its usable delivery link.
353
+
354
+ ## Quality Checks
355
+
356
+ - No forced goal question, duplicate table or unsupported shell claim.
357
+ - The exact renderer passed before local paid ingredients; uncertain assets stay uncertain.
358
+ - Selected campaign and angle survive; a single template project has no brief.
359
+ - Current complete plan and total precede approval. Custom script and ingredient gates remain independent; Studio is optional.
360
+
361
+ ## Failure Modes
362
+
363
+ | Symptom | Cause | Fix |
364
+ | --- | --- | --- |
365
+ | Picker plus table | Ignored display_hint | Widget: one line; otherwise text_summary |
366
+ | Terminal loses formats | Confused cards with execution | Declare real shell capability and check renderer |
367
+ | Chat told it cannot start | Only local execution considered | Create once and delegate to coworker |
368
+ | Worker ends without a plan | Assumed completion means ready | Follow actual failure/recovery state |
369
+ | Two projects for one video | Recreated instead of resuming | Continue same project and task |
370
+
371
+ ## Use uploaded footage
372
+
373
+ When the customer asks to use existing footage, check the actual advertised schemas. Use `media_search` with purpose `production`, kind `video`, the brand and current project when needed. Omit query spend for free retrieval. Read facts/scenes with `media_analyze`, then use `media_excerpt` action `inspect` to review actual bounded frames and timed transcript before selecting. A description or thumbnail URL alone is not visual review.
374
+
375
+ Freeze `source_excerpt:{asset_id,analysis_revision,scene_id,start_ms,end_ms,audio_mode}`. Revision is the original-byte SHA, scene may be null for a known user trim, bounds use integer milliseconds, and audio is original or muted. Call `media_excerpt` action `attach` with that exact selection and a stable idempotency key. The returned project remains unfinished and unapproved. Open its existing Studio review; preserve user locks and requested format, and keep footage separate from image packshot indexes. An incompatible format requires an explicit choice before changing it.
376
+
377
+ For custom production, fetch the current shared `video-production-harness` and follow script/ingredient gates. Download the verified original and trim its exact selected window at normal speed with the approved audio. Recheck current revision and production permission before consumption and final upload/completion. Save excerpt lineage in plan and ingredient readback. Missing optional semantics does not require another upload; known user-selected stored footage remains usable. Research and competitor references never become production footage. Selected originals require no paid generation; only newly generated/replaced ingredients consume the approved budget.