gooseworks 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,6 +61,18 @@ Examples — all of these route to \`goose-ads\`, not the data flow: "remix this
61
61
 
62
62
  All commands below auto-load credentials from \`~/.gooseworks/credentials.json\`. If a command exits with "Not logged in", tell the user to run: \`npx gooseworks login\`. To log out: \`npx gooseworks logout\`.
63
63
 
64
+ ### CLI-free environments (cowork / headless)
65
+
66
+ If the \`gooseworks\` CLI binary isn't available (e.g. Anthropic cowork) but the
67
+ \`mcp__gooseworks__*\` tools are connected, use the MCP equivalents instead of shelling out:
68
+ - \`gooseworks search <q>\` → the **\`search_skills\`** MCP tool.
69
+ - \`gooseworks fetch <slug>\` → the **\`fetch_skill\`** MCP tool (same content/scripts/files/deps).
70
+ - \`gooseworks credits\` → the **\`get_ad_credits\`** MCP tool.
71
+
72
+ Discovery and fetching a skill's instructions work fully CLI-free this way. Note: the paid data
73
+ proxy (\`gooseworks call <provider> <path>\`) still requires the CLI for now — if a task needs it
74
+ and no CLI is present, tell the user that step must run where the \`gooseworks\` CLI is installed.
75
+
64
76
  To check credit balance:
65
77
  \`\`\`bash
66
78
  gooseworks credits
@@ -226,7 +238,7 @@ description: >
226
238
  app uses) — credits are reserved and billed server-side. Analytics recipes are fetched from
227
239
  goose-skills on demand.
228
240
  category: ads
229
- version: 2.1.0
241
+ version: 2.2.0
230
242
  author: GooseWorks
231
243
  tags: [gooseworks, ads, remix, static-ad, brand, creative, image, analytics, meta-ads, performance]
232
244
  ---
@@ -309,6 +321,29 @@ decide — fine, but prefer sending the app defaults for predictable parity.
309
321
  the template; \`"edit"\` makes a targeted change to a specific render (\`prompt\` + \`source_render_id\`
310
322
  required); \`"exact"\` runs \`prompt\` verbatim against that render's references. Returns a
311
323
  single-item batch — poll it with \`get_remix_batch\`.
324
+ - \`set_creative_feedback { render_id, rating?, comment?, reasons? }\` — record the user's reaction
325
+ to a generated image (the SAME happy/neutral/sad + comment + reason chips the app captures). Use
326
+ it whenever the user reacts ("love this one" / "the logo is wrong"). \`render_id\` is a RENDER id
327
+ from \`get_remix_batch\` / \`list_brand_creatives\`, not a project/batch id. \`reasons\` are quick
328
+ chips (wrong_product, brand_or_logo_wrong, off_brand, text_garbled, weak_copy, ai_or_distorted).
329
+
330
+ ### Plan mode — review the plan BEFORE generating (optional)
331
+
332
+ For users who want to approve each ad's plan before spending credits (the app's "Plan it" flow):
333
+
334
+ - \`submit_remix_batch { ..., requires_approval: true }\` — composes each creative's plan and PAUSES.
335
+ **No credits are reserved and no image renders** until you approve.
336
+ - \`list_ad_approvals { brand_id? }\` — poll this; returns \`{ items, counts }\`. While a creative is
337
+ \`composing\`, wait; once \`awaiting_approval\`, show its \`plan\` (composed prompt + refs + quality)
338
+ to the user.
339
+ - \`revise_ad_plan { project_id, message?, variant_label? }\` — recompose from a chat steer, still
340
+ free. Poll \`list_ad_approvals\` until it's \`awaiting_approval\` again.
341
+ - \`approve_ad_plan { project_id | batch_id }\` — approve ONE creative (\`project_id\`) or the whole
342
+ batch (\`batch_id\`). **This is the step that reserves credits and renders.** Then poll
343
+ \`get_remix_batch\` and hand back links as usual.
344
+
345
+ Only offer plan mode when the user asks to review/approve first — the default path generates
346
+ immediately.
312
347
 
313
348
  ## Reading the brand & picking inputs (still MCP, read-only)
314
349
 
@@ -327,6 +362,33 @@ decide — fine, but prefer sending the app defaults for predictable parity.
327
362
  image as a private template, then remix it like any other.
328
363
  - \`get_ad_project\` / \`append_project_message\` — inspect a creative / leave a note on its thread.
329
364
 
365
+ ## Keep the brand kit in sync — reconcile, then update (ASK first)
366
+
367
+ The brand kit is the source of truth every generation reads. During ANY task, when the user
368
+ **tells you something about the brand or asks to change something brand-level** — a different
369
+ tagline, audience, voice, a product's name/price/description, "our logo is X", "we don't sell Y
370
+ anymore", a new product photo — treat it as a possible kit update, don't just use it for this one
371
+ ad and forget it:
372
+
373
+ 1. **Check it against the kit.** \`get_brand_kit { brand_id }\` and see whether what the user said
374
+ matches, is missing from, or contradicts the kit.
375
+ 2. **If it's already in the kit and matches** — nothing to do; proceed.
376
+ 3. **If it's new or different — ASK before writing.** Confirm in one line: *"Want me to update
377
+ the brand kit so this sticks for future ads?"* Only persist on a yes (or when the user clearly
378
+ asked you to change the brand). Don't silently mutate the kit, and don't nag on trivia.
379
+ 4. **Persist with the write tools** (partial — only the fields you pass are touched; each edit is
380
+ recorded as a user override that later re-research won't clobber):
381
+ - \`update_brand_kit { brand_id, description?, audience?, voice?, instructions?, brand_type?,
382
+ value_props?, primary_color?, accent_color? }\` — the structured kit fields.
383
+ - \`upsert_brand_product { brand_id, ... }\` / \`delete_brand_product\` — manage products.
384
+ - \`add_brand_product_image { brand_id, ... }\` / \`remove_brand_reference_image\` — product /
385
+ reference photos.
386
+ 5. **Confirm what changed** and continue the task. (Logo, colors, and fonts are owned by the
387
+ backend research pass — prefer \`update_ad_brand\` / the research flow for those, not free text.)
388
+
389
+ This is the parity gap the app closes in-product: a brand fact the user gives mid-task should be
390
+ able to flow back into the kit — with their ok — instead of being lost.
391
+
330
392
  ## Picking templates — ASK the user; don't freelance from the catalog
331
393
 
332
394
  When the user wants to make ads but has NOT named a specific template (id/slug/Community
@@ -475,6 +537,15 @@ run through the \`gooseworks\` CLI (\`gooseworks fetch\` / \`gooseworks call\`),
475
537
  Generate when they paste the app's copyable remix prompt back (or submit the surprise picks
476
538
  directly if they'd rather not review).
477
539
  - **Ask the styling** — Keep original (default) vs Match brand — before you submit.
540
+ - **Reconcile brand facts into the kit** — when the user states or changes something brand-level
541
+ mid-task, check it against \`get_brand_kit\` and, with their ok, persist it via \`update_brand_kit\`
542
+ / \`upsert_brand_product\` / \`add_brand_product_image\` so it sticks for future ads. Ask first;
543
+ never silently mutate the kit.
544
+ - **Record feedback** — when the user reacts to a generated image, capture it with
545
+ \`set_creative_feedback { render_id, rating/comment/reasons }\` so the quality loop learns.
546
+ - **Plan mode is opt-in** — only compose-and-pause (\`requires_approval: true\` → \`list_ad_approvals\`
547
+ → \`approve_ad_plan\`) when the user wants to review before spending credits; otherwise generate
548
+ immediately.
478
549
  - **Don't busy-loop** — poll \`get_remix_batch\` on a sensible interval (~20-30s); a \`queued\`
479
550
  batch is waiting on research and will start on its own.
480
551
  `;
@@ -503,7 +574,7 @@ description: >
503
574
  template, or asks to remix a video ad. Unlike goose-ads (static images, generated server-side),
504
575
  video renders locally and reports progress + the result back through the gooseworks MCP tools.
505
576
  category: ads
506
- version: 0.1.0
577
+ version: 0.2.0
507
578
  author: GooseWorks
508
579
  tags: [gooseworks, ads, video, remix, imessage, local-render, byoa]
509
580
  ---
@@ -518,14 +589,43 @@ you *what to make*; read both, and this doc wins on any conflict about the envir
518
589
  You run inside the user's own Claude Code session (they pasted an instruction with a project
519
590
  id). The app NEVER runs you — it is the viewer + review surface; you are the renderer.
520
591
 
521
- ## Prerequisite — MCP + a local toolchain (Phase 0 preflight)
592
+ ## CLI-free environments (cowork / headless)
593
+
594
+ You may be running WITHOUT the \`gooseworks\` CLI binary (e.g. Anthropic cowork). The
595
+ \`mcp__gooseworks__*\` tools work over the MCP connection regardless, so wherever this skill
596
+ says to shell out, use the MCP equivalent:
597
+
598
+ - \`gooseworks fetch <slug>\` → the **\`fetch_skill\`** MCP tool (returns the same content/scripts/
599
+ files/dependencySkills). \`gooseworks search <q>\` → **\`search_skills\`**.
600
+ - \`gooseworks credits\` → the **\`get_ad_credits\`** MCP tool.
601
+ - \`gooseworks doctor\` → do the manual toolchain check in the preflight below.
602
+
603
+ ## Prerequisite — MCP + a render toolchain (Phase 0 preflight)
522
604
 
523
605
  - The \`mcp__gooseworks__*\` tools are REQUIRED. If they're unavailable, stop and tell the user
524
- to run \`gooseworks install --claude --mcp\` and restart Claude Code. There is no REST fallback.
525
- - This is a LOCAL render. Run \`gooseworks doctor\` FIRST — it checks login, the MCP server,
526
- **ffmpeg** + **ffprobe**, and **Playwright Chromium** in one shot. If it reports any ✗, relay
527
- the exact fix it prints (e.g. \`brew install ffmpeg\`, \`npx playwright install chromium\`) and
528
- stop — don't half-render.
606
+ to connect the GooseWorks MCP server (or run \`gooseworks install --claude --mcp\` on the CLI)
607
+ and restart. There is no REST fallback.
608
+ - **The render runs wherever THIS agent runs, and it needs a real toolchain: \`ffmpeg\` +
609
+ \`ffprobe\` + a Playwright **Chromium**.** Establish it in this priority order, and do NOT start
610
+ rendering until one is confirmed:
611
+ 1. **CLI present →** run \`gooseworks doctor\` (checks login, MCP, ffmpeg/ffprobe, Playwright
612
+ Chromium in one shot). Fix any ✗ with the command it prints, then continue.
613
+ 2. **No CLI →** check the toolchain yourself: \`ffmpeg -version\`, \`ffprobe -version\`, and a
614
+ Playwright Chromium probe (\`npx playwright --version\` and, if needed, \`npx playwright install
615
+ chromium\`). If all resolve, continue.
616
+ 3. **Docker available →** this is the most reliable way to get the toolchain in a sandbox that
617
+ lacks it: run the render steps inside the prebuilt image
618
+ **\`ghcr.io/gooseworks-ai/goose-video-render\`** (ffmpeg + ffprobe + Playwright Chromium baked
619
+ in), mounting the project working directory. Use Docker whenever the host is missing ffmpeg or
620
+ Chromium and \`docker\` is on PATH. (Note: nested Docker is usually disabled inside managed
621
+ sandboxes like cowork — treat this as an option, not a guarantee.)
622
+ 4. **None of the above works →** STOP and tell the user plainly, e.g.: *"Video rendering needs
623
+ ffmpeg + a Playwright Chromium (or Docker) on the machine running this agent. This environment
624
+ doesn't have them and I can't install them here. Options: (a) enable/allow Docker so I can use
625
+ the goose-video-render image, (b) install ffmpeg + \`npx playwright install chromium\`, or
626
+ (c) run this skill locally in your own Claude Code where the toolchain is available."* Do not
627
+ half-render or fake a result. Static image ads (the \`goose-ads\` skill) do NOT need any of this
628
+ and work anywhere — offer that as the fallback if they just want an ad now.
529
629
 
530
630
  ## Identity, token, credits
531
631
 
@@ -639,13 +739,35 @@ ingredients out one at a time.
639
739
  \`update_render_status { render_id, status: "running" }\`. The render row tracks status only
640
740
  (queued / running / complete / failed) — narrate fine-grained progress with
641
741
  \`append_project_message\` instead.
642
- 3. QC by watching: run the \`watch\` skill on the master — verify bubble/beat order + SFX, that
643
- the brand's product (not the source's) is shown, the end card has the real wordmark + code,
644
- and the duration is within ~20% of the source. For UGC/Seedance formats there are no captions
645
- and often no local Whisper key: extract frames with ffmpeg for the visual pass, and get the
646
- spoken transcript by running \`fal-ai/whisper\` through the SAME \`fal-proxy\` (upload the audio,
647
- pass its \`get_download_url\` as \`audio_url\`) — confirm the transcript matches the script and the
648
- brand name is pronounced right, with no word-repeat dysfluency, BEFORE spending the render credit.
742
+ 3. **MANDATORY final-video review gate — review EVERY finished master before \`set_final_render\`,
743
+ whatever the format (UGC or not).** The render credit is already spent (\`submit_render\` in 4.2);
744
+ this gate stands between a rendered master and PINNING/publishing it, so a bad render never gets
745
+ set as final. A master that looks fine on a still can still have a mis-voiced word, a caption
746
+ drifting off its line, a beat out of order, or a deformation — review the actual VIDEO, not
747
+ stills. Run the passes that APPLY to this format:
748
+ - **Audio ↔ script** — any master with SPEECH (VO or native/Seedance voice); **skip for
749
+ music-only / no-speech formats.** \`review-ugc-render\` is format-agnostic despite the name —
750
+ a deterministic Whisper transcript-vs-script diff, not UGC-specific: persist the approved
751
+ spoken lines to \`working/approved-script.txt\`, then \`gooseworks fetch review-ugc-render\` and
752
+ run \`review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json
753
+ working/review-verdict.json\` (exit 0 PASS / 2 FAIL / 3 ERROR). It blocks a mis-voiced word
754
+ (approved "human-vetted" → "human witted"), a dropped phrase, or silence. It routes Whisper
755
+ through the gooseworks proxy when \`OPENAI_BASE_URL\` is set; with no backend at all, run
756
+ \`fal-ai/whisper\` via \`fal-proxy\` (upload the audio, pass its \`get_download_url\` as \`audio_url\`)
757
+ and diff the transcript yourself.
758
+ - **Captions / subtitles** — ANY captioned format (the most common non-UGC defect); **skip for
759
+ UGC/Seedance masters, which carry no subtitle track.** Concrete check: diff the caption file
760
+ you burned (SRT/ASS) against the SAME Whisper transcript + word timings from the audio pass —
761
+ every caption line must match the heard/scripted words and sit within ~0.3s of when they're
762
+ spoken; then in the visual pass below, OCR-read the burned caption off 4–5 sampled frames to
763
+ confirm it's actually on screen at that time and not colliding with a hyperframe or the end
764
+ card. Mismatched text or >0.3s drift fails the gate.
765
+ - **Visual + structure** — always: run the \`watch\` skill on the master — beat/scene order + SFX,
766
+ the brand's product (not the source's) is shown, the end card has the real wordmark + code, no
767
+ deformation/artifact, duration within ~20% of the source.
768
+ If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, rebuild captions)
769
+ and re-review — only a clean pass proceeds to \`set_final_render\`. **This gate is universal: it
770
+ runs from the master skill for every format, so a recipe never has to opt in.**
649
771
  4. Publish: \`get_upload_url { target: { type: "agent", agent_id: ADS_AGENT } }\` → PUT the master
650
772
  and poster **under the project folder** (see Identity's path-prefix rule) — to
651
773
  \`agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4\` and
@@ -667,8 +789,10 @@ content }\` — never sit silent on a queue > 90s.
667
789
 
668
790
  Media APIs go through GooseWorks proxies with your agent token; do NOT use an SDK's default host
669
791
  (your token isn't a FAL/ElevenLabs token → 401). Base = \`<api_base>/api/internal/<proxy>\`; pass
670
- \`?token=<api_key>&agent_id=<agent_id>\` (agent_id bills the Ads agent). FAL = \`fal-proxy\` (+
671
- \`fal-storage-proxy\` to host a local image and get a CDN URL); ElevenLabs = \`elevenlabs-proxy\`
792
+ \`?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>\` (agent_id bills the Ads agent;
793
+ \`project_id\` = the id of the project you're rendering — it attributes this generation's credits to
794
+ that ad project so the user sees per-project spend in the app. ALWAYS pass it). FAL = \`fal-proxy\`
795
+ (+ \`fal-storage-proxy\` to host a local image and get a CDN URL); ElevenLabs = \`elevenlabs-proxy\`
672
796
  (VO / music bed).
673
797
 
674
798
  **FAL queue gotcha** (#1 waste of generations): submit returns \`status_url\`/\`response_url\` on
@@ -684,24 +808,26 @@ def _cfg():
684
808
  c = json.loads(pathlib.Path(os.path.expanduser("~/.gooseworks/credentials.json")).read_text())
685
809
  return c["api_base"].rstrip("/"), c["api_key"], c.get("agent_id")
686
810
 
687
- def _params(tok, agent):
811
+ def _params(tok, agent, project_id=None):
688
812
  p = {"token": tok}
689
813
  if agent: p["agent_id"] = agent
814
+ if project_id: p["project_id"] = project_id # attributes the spend to this ad project
690
815
  return p
691
816
 
692
- def fal_generate(model_path, payload, timeout_s=180, poll_s=3):
817
+ def fal_generate(model_path, payload, project_id=None, timeout_s=180, poll_s=3):
693
818
  """model_path e.g. 'fal-ai/nano-banana-2/edit' (the recipe names the model).
819
+ Pass project_id = the ad project you're rendering so credits attribute to it.
694
820
  Returns the result image URL (a public *.fal.media CDN URL)."""
695
821
  api_base, tok, agent = _cfg()
696
822
  base = api_base + "/api/internal/fal-proxy"
697
- sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent), json=payload).json()
823
+ sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent, project_id), json=payload).json()
698
824
  to_proxy = lambda u: base + urlparse(u).path
699
825
  status_url, response_url = to_proxy(sub["status_url"]), to_proxy(sub["response_url"])
700
826
  deadline = time.time() + timeout_s
701
827
  while time.time() < deadline:
702
- st = requests.get(status_url, params=_params(tok, agent)).json()
828
+ st = requests.get(status_url, params=_params(tok, agent, project_id)).json()
703
829
  if st.get("status") == "COMPLETED":
704
- return requests.get(response_url, params=_params(tok, agent)).json()["images"][0]["url"]
830
+ return requests.get(response_url, params=_params(tok, agent, project_id)).json()["images"][0]["url"]
705
831
  if st.get("status") in ("FAILED", "ERROR"):
706
832
  raise RuntimeError(f"FAL failed: {st}")
707
833
  time.sleep(poll_s)
@@ -709,7 +835,7 @@ def fal_generate(model_path, payload, timeout_s=180, poll_s=3):
709
835
  \`\`\`
710
836
 
711
837
  ElevenLabs (VO / music) is the same shape against \`<api_base>/api/internal/elevenlabs-proxy\`
712
- with \`?token=&agent_id=\`. Feed FAL a local image by storing it (\`get_upload_url\`) and passing its
838
+ with \`?token=&agent_id=&project_id=\`. Feed FAL a local image by storing it (\`get_upload_url\`) and passing its
713
839
  \`get_download_url\` presigned URL as an \`image_urls\` / \`audio_url\` entry — this is the reliable
714
840
  path. (\`fal-storage-proxy\` may 404 depending on the install; don't block on it — prefer the
715
841
  \`get_download_url\` presigned URL.)
@@ -722,6 +848,8 @@ path. (\`fal-storage-proxy\` may 404 depending on the install; don't block on it
722
848
  the template needs), mirror the whole set with \`update_ad_project_script\`, and get the user's
723
849
  approval in-session BEFORE rendering — always (review-once).
724
850
  - **submit_render LAST**; \`output_url\` = the durable render-file URL, never a CDN URL.
851
+ - **Always pass \`project_id\` on media-proxy calls** (fal / ElevenLabs) so the credits attribute
852
+ to this ad project — that's what lets the user see per-project spend in the app.
725
853
  - **Verify a real, non-empty MP4** (watch it) before marking the render complete.
726
854
  - **Reuse the brand** when its research is complete; never re-research.
727
855
  - On a hard error (auth/quota/model/timeout) set the render \`failed\` with a short
@@ -1 +1 @@
1
- {"version":3,"file":"master-skill.js","sourceRoot":"","sources":["../../src/skills/master-skill.ts"],"names":[],"mappings":";;AA4BA,wCAMC;AAWD,sDA8KC;AAiBD,0DA2QC;AAcD,8DA6OC;AAvtBD,oDAAoD;AACpD,SAAgB,cAAc;IAC5B,OAAO;QACL,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,EAAE,EAAE;QACxD,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,uBAAuB,EAAE,EAAE;QACzD,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE;KAC9D,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4KR,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAgB,uBAAuB;IACrC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyQR,CAAC;AACF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,yBAAyB;IACvC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2OR,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"master-skill.js","sourceRoot":"","sources":["../../src/skills/master-skill.ts"],"names":[],"mappings":";;AA4BA,wCAMC;AAWD,sDA0LC;AAiBD,0DAsUC;AAcD,8DAsSC;AAv1BD,oDAAoD;AACpD,SAAgB,cAAc;IAC5B,OAAO;QACL,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,EAAE,EAAE;QACxD,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,uBAAuB,EAAE,EAAE;QACzD,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE;KAC9D,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwLR,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAgB,uBAAuB;IACrC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoUR,CAAC;AACF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,yBAAyB;IACvC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoSR,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gooseworks",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "GooseWorks CLI — give your coding agent real data tools",
5
5
  "bin": {
6
6
  "gooseworks": "./dist/index.js"
@@ -10,7 +10,8 @@
10
10
  "build": "tsc",
11
11
  "dev": "tsx src/index.ts",
12
12
  "test": "jest",
13
- "prepublishOnly": "npm run build && npm test"
13
+ "generate:skills": "tsx scripts/generate-skill-files.ts",
14
+ "prepublishOnly": "npm run generate:skills && npm run build && npm test"
14
15
  },
15
16
  "dependencies": {
16
17
  "chalk": "^4.1.2",
@@ -0,0 +1,324 @@
1
+ ---
2
+ name: goose-ads
3
+ slug: goose-ads
4
+ description: >
5
+ GooseWorks ads skill — create, edit, AND analyze ad creative. Remix a static (image) ad
6
+ template into a branded ad for the user's product, edit/re-roll an existing creative,
7
+ research a brand for ads, OR analyze ad performance (Meta/Google campaign diagnostics,
8
+ creative fatigue, CAC & lead quality, competitor ad intelligence, ad angles & hooks). Use
9
+ when the user says "remix this ad", references a static ad template id/slug, asks to "make
10
+ an ad", "edit this ad", "research my brand", or asks to analyze/diagnose ad campaigns.
11
+ Generation runs through the GooseWorks backend's single cloud workflow (the same one the ads
12
+ app uses) — credits are reserved and billed server-side. Analytics recipes are fetched from
13
+ goose-skills on demand.
14
+ category: ads
15
+ version: 2.2.0
16
+ author: GooseWorks
17
+ tags: [gooseworks, ads, remix, static-ad, brand, creative, image, analytics, meta-ads, performance]
18
+ ---
19
+
20
+ # GooseWorks Ads — create, edit & analyze
21
+
22
+ The GooseWorks ads skill. Two jobs:
23
+
24
+ 1. **Create / edit ad creative** — a **thin wrapper** over the backend's single generation
25
+ workflow. You pick the brand + template(s) and submit ONE batch; the **backend** runs the
26
+ whole pipeline (compose → generate → persist → judge), reserves and bills credits, and
27
+ stores the renders. You do NOT generate images, call FAL, manage render rows, or upload
28
+ files — those are gone. This is the exact same workflow the GooseWorks ads app uses, so the
29
+ skill and the app can never drift.
30
+ 2. **Analyze ad performance** — fetch ad-analytics recipes from goose-skills on demand
31
+ (these are unrelated to generation; see "Analyze / intelligence" below).
32
+
33
+ ## Prerequisite — the GooseWorks MCP server is REQUIRED
34
+
35
+ Everything goes through the `mcp__gooseworks__*` tools. If they are not available, **stop and
36
+ tell the user to run `gooseworks install --claude --mcp`** (and restart Claude Code). There is
37
+ no HTTP/file fallback — the REST ad endpoints are session-cookie-only and reject your token.
38
+
39
+ ## Identity & credits
40
+
41
+ - One agent-scoped token authenticates the `gooseworks` MCP tools. Never print it. The tools
42
+ resolve your org automatically — you do NOT resolve an "Ads agent" or pass `target` for the
43
+ generation tools.
44
+ - **Credits are handled entirely by the backend.** `submit_remix_batch` reserves the estimated
45
+ cost up front (it errors with `insufficient_credits` if the wallet is short — relay the
46
+ message and stop) and bills only the images that actually complete. Call
47
+ `estimate_remix_batch` first to tell the user the cost; `gooseworks credits` shows balance.
48
+
49
+ ## Defaults — match the app (priority: frontend, then backend)
50
+
51
+ When the user doesn't specify, submit with the **ads app's** defaults so skill output matches
52
+ what they'd get in the UI. **Pass these explicitly:**
53
+
54
+ - `variants`: **1** per template
55
+ - `ratios`: **["4:5"]** (Meta feed vertical)
56
+ - `engine`: **"gpt_image_2"**
57
+ - `quality`: **"medium"**
58
+ - `preserve_source_styling`: **ASK the user** — "Keep original" (the template's own
59
+ colours/fonts → `preserve_source_styling: true`) vs "Match brand" (restyle to the brand
60
+ palette/fonts → `preserve_source_styling: false`). This mirrors the app's Styling control.
61
+ **The default is "Keep original"** — if the user doesn't answer or doesn't care, send `true`.
62
+
63
+ If the user asks for something the app exposes (more variants, a different ratio like 1:1 or
64
+ 9:16, a faster engine, higher quality), pass that instead. Omitting a field lets backend policy
65
+ decide — fine, but prefer sending the app defaults for predictable parity.
66
+
67
+ ## The generation tools (the new, single-workflow surface)
68
+
69
+ - `submit_remix_batch { brand_id, items, prompt?, product_name?, preserve_source_styling?,
70
+ reference_image_urls?, allow_without_product_image?, engine?, quality? }` — **the one call
71
+ that makes ads.** `items` is `[{ template_id, variants?, ratios? }]` (≤20 templates).
72
+ Returns the batch with a `links` block (`brand_url` + per-creative `app_url`). If the brand's
73
+ research isn't finished yet the batch comes back `status: "queued"` — it auto-runs the moment
74
+ research completes; tell the user it'll appear shortly, don't error.
75
+ - `estimate_remix_batch { items, engine?, quality? }` — cost preview (images, credits_per_image,
76
+ total_credits, available_credits). `template_id` accepts a uuid OR a slug. Reserves nothing. Use
77
+ to quote the cost first. Check `unknown_template_ids` in the response — any token there didn't
78
+ resolve (submit would 404 on it); don't quote a cost that silently dropped a bad id.
79
+ - `get_remix_batch { batch_id }` — poll status. Returns each creative with its renders and
80
+ `completed`/`failed`/`pending` counts, plus `links`. A creative is done when its `pending` is 0
81
+ — NOT when `current_render_url` is set (during a regenerate that field still points at the prior
82
+ image). Each render carries `age_seconds` (since queued) and `elapsed_seconds` (time generating):
83
+ use them to tell a slow-but-healthy render from a stuck one. A render only failed when its
84
+ `status` is `"failed"` — never assume a stall and re-submit, that double-bills.
85
+ - `list_brand_creatives { brand_id, limit?, offset? }` — the brand's gallery feed (newest
86
+ first) + `brand_url`. Alternative poll target; also use to show everything made for a brand.
87
+ - `surprise_me_templates { brand_id, count? }` — the **"Surprise me" recommender**. Picks
88
+ brand-relevant templates (SAME logic as the web /create "Surprise me" button — templates
89
+ whose category overlaps the brand float to the top, bucketed + shuffled so picks stay fresh).
90
+ Returns the picked templates (id, slug, title, image, ratio) AND a ready-to-open `create_url`
91
+ (the /create page with `cli=true` and the picks pre-selected). This is how you recommend
92
+ templates — do NOT hand-pick from the raw catalog yourself (see "Picking templates" below).
93
+ - `regenerate_creative { project_id, mode?, prompt?, source_render_id?, ... }` — **edit / re-roll
94
+ one existing creative** through the same pipeline. `mode: "variation"` (default) re-rolls from
95
+ the template; `"edit"` makes a targeted change to a specific render (`prompt` + `source_render_id`
96
+ required); `"exact"` runs `prompt` verbatim against that render's references. Returns a
97
+ single-item batch — poll it with `get_remix_batch`.
98
+ - `set_creative_feedback { render_id, rating?, comment?, reasons? }` — record the user's reaction
99
+ to a generated image (the SAME happy/neutral/sad + comment + reason chips the app captures). Use
100
+ it whenever the user reacts ("love this one" / "the logo is wrong"). `render_id` is a RENDER id
101
+ from `get_remix_batch` / `list_brand_creatives`, not a project/batch id. `reasons` are quick
102
+ chips (wrong_product, brand_or_logo_wrong, off_brand, text_garbled, weak_copy, ai_or_distorted).
103
+
104
+ ### Plan mode — review the plan BEFORE generating (optional)
105
+
106
+ For users who want to approve each ad's plan before spending credits (the app's "Plan it" flow):
107
+
108
+ - `submit_remix_batch { ..., requires_approval: true }` — composes each creative's plan and PAUSES.
109
+ **No credits are reserved and no image renders** until you approve.
110
+ - `list_ad_approvals { brand_id? }` — poll this; returns `{ items, counts }`. While a creative is
111
+ `composing`, wait; once `awaiting_approval`, show its `plan` (composed prompt + refs + quality)
112
+ to the user.
113
+ - `revise_ad_plan { project_id, message?, variant_label? }` — recompose from a chat steer, still
114
+ free. Poll `list_ad_approvals` until it's `awaiting_approval` again.
115
+ - `approve_ad_plan { project_id | batch_id }` — approve ONE creative (`project_id`) or the whole
116
+ batch (`batch_id`). **This is the step that reserves credits and renders.** Then poll
117
+ `get_remix_batch` and hand back links as usual.
118
+
119
+ Only offer plan mode when the user asks to review/approve first — the default path generates
120
+ immediately.
121
+
122
+ ## Reading the brand & picking inputs (still MCP, read-only)
123
+
124
+ - `get_brand_kit { brand_id }` — the CANONICAL brand context (name, description, audience,
125
+ voice, brandType, valueProps, colors, typography, logoUrl, `products[]`, presigned
126
+ `referenceImages[]`). Read this to choose `product_name` and any `reference_image_urls`.
127
+ - `list_ad_brands { query? }` / `get_ad_brand { brand_id }` — find/fetch a brand. Pass `query` to
128
+ filter by name (case-insensitive) instead of listing every brand; rows are lean (no `brand_kit` —
129
+ read `get_brand_kit` for the full kit).
130
+ - `get_static_ad_template { template_id }` — resolve a template (slug OR uuid; public catalog
131
+ AND your org's private templates). Confirms it exists before you submit.
132
+ - `remix_community_ad { community_id }` — a **Community** ad id is an `ad_project` id, not a
133
+ template id. Call this FIRST to snapshot it into a private template, then use the returned
134
+ template `id` in `items`.
135
+ - `create_user_ad_template { workspace_path }` — "bring your own ad": upload the user's own
136
+ image as a private template, then remix it like any other.
137
+ - `get_ad_project` / `append_project_message` — inspect a creative / leave a note on its thread.
138
+
139
+ ## Keep the brand kit in sync — reconcile, then update (ASK first)
140
+
141
+ The brand kit is the source of truth every generation reads. During ANY task, when the user
142
+ **tells you something about the brand or asks to change something brand-level** — a different
143
+ tagline, audience, voice, a product's name/price/description, "our logo is X", "we don't sell Y
144
+ anymore", a new product photo — treat it as a possible kit update, don't just use it for this one
145
+ ad and forget it:
146
+
147
+ 1. **Check it against the kit.** `get_brand_kit { brand_id }` and see whether what the user said
148
+ matches, is missing from, or contradicts the kit.
149
+ 2. **If it's already in the kit and matches** — nothing to do; proceed.
150
+ 3. **If it's new or different — ASK before writing.** Confirm in one line: *"Want me to update
151
+ the brand kit so this sticks for future ads?"* Only persist on a yes (or when the user clearly
152
+ asked you to change the brand). Don't silently mutate the kit, and don't nag on trivia.
153
+ 4. **Persist with the write tools** (partial — only the fields you pass are touched; each edit is
154
+ recorded as a user override that later re-research won't clobber):
155
+ - `update_brand_kit { brand_id, description?, audience?, voice?, instructions?, brand_type?,
156
+ value_props?, primary_color?, accent_color? }` — the structured kit fields.
157
+ - `upsert_brand_product { brand_id, ... }` / `delete_brand_product` — manage products.
158
+ - `add_brand_product_image { brand_id, ... }` / `remove_brand_reference_image` — product /
159
+ reference photos.
160
+ 5. **Confirm what changed** and continue the task. (Logo, colors, and fonts are owned by the
161
+ backend research pass — prefer `update_ad_brand` / the research flow for those, not free text.)
162
+
163
+ This is the parity gap the app closes in-product: a brand fact the user gives mid-task should be
164
+ able to flow back into the kit — with their ok — instead of being lost.
165
+
166
+ ## Picking templates — ASK the user; don't freelance from the catalog
167
+
168
+ When the user wants to make ads but has NOT named a specific template (id/slug/Community
169
+ ad/upload), do NOT silently browse the raw catalog and hand-pick for them. Instead run this
170
+ short ask flow — it mirrors the web app and keeps the human in the loop:
171
+
172
+ 1. **Ask what kind of ads they want** — the angle/offer/theme/season, the vibe, and which
173
+ product from the brand kit to feature. This shapes both the template choice and your steering
174
+ `prompt`. Keep it to one or two quick questions.
175
+ 2. **Ask how to pick templates: "Choose explicitly" or "Surprise me".**
176
+ - **Surprise me** (they want you/the app to pick) → call
177
+ `surprise_me_templates { brand_id, count }` and hand the user the returned `create_url`.
178
+ It opens /create in **CLI mode** with the picks pre-selected, a preview modal, and the
179
+ **copyable remix prompt at the bottom** (in place of the Generate input). They can swap
180
+ picks and copy that prompt. If they'd rather you "just make them" without reviewing in the
181
+ app, you MAY submit the `surprise_me_templates` picks directly (skip to submit).
182
+ - **Choose explicitly** (they want to browse and select) → hand the user this URL, with the
183
+ active brand's slug filled in:
184
+ `https://make.gooseworks.ai/create?brand=<brand-slug>&cli=true`
185
+ In CLI mode the app shows the copyable remix prompt at the bottom (dismissable / switchable
186
+ back to the UI composer). They browse, select templates, and copy the prompt.
187
+ 3. **Ask the styling** — "Keep original" (default) vs "Match brand" — per the Defaults section.
188
+ 4. **Close the loop.** When the user **pastes back the copyable remix prompt** from the app
189
+ (it names the brand + the templates they chose), THAT is your cue to generate: resolve the
190
+ named template(s), then `submit_remix_batch` with the app defaults + the styling they chose.
191
+
192
+ If the user already named a template (id/slug), a Community ad, or an upload, skip the ask flow
193
+ for template choice — they've chosen — but still confirm the styling default and steer the prompt.
194
+
195
+ ## Workflow — make ads from a template
196
+
197
+ 1. **Resolve the brand.** `list_ad_brands` by name/site → `get_brand_kit { brand_id }`. If the
198
+ kit's `researchStatus` isn't `complete`, you can still submit (the batch queues and runs when
199
+ research finishes) — just tell the user. Use the kit to pick `product_name` (a real entry from
200
+ `products[]`, not a guess) and, if the user supplied product photos, `reference_image_urls`.
201
+ 2. **Pick the template(s) via the ask flow above** (kind of ads → Choose explicitly vs Surprise
202
+ me → styling). Once you have concrete ids: `get_static_ad_template { template_id }` for each.
203
+ For a Community ad, `remix_community_ad` first; for an uploaded image, `create_user_ad_template`
204
+ first.
205
+ 3. **(Optional) Craft the steering prompt.** The `prompt` is OPTIONAL — this is where the skill
206
+ adds value: turn the user's intent (from step 1) into a concise steering note (e.g. tone,
207
+ season, emphasis). Don't over-specify; the backend pipeline + brand kit handle palette, fonts,
208
+ product swap.
209
+ 4. **(Optional) Quote the cost.** `estimate_remix_batch { items, engine, quality }` → tell the user.
210
+ 5. **Submit ONE batch.** `submit_remix_batch { brand_id, items, prompt?, product_name?, engine,
211
+ quality, preserve_source_styling }` using the app defaults above and the styling the user chose.
212
+ Keep the returned `batch_id` and `links`.
213
+ 6. **Poll until done.** `get_remix_batch { batch_id }` (or `list_brand_creatives`) every ~20-30s
214
+ until every creative's `pending` is 0. Most images finish in a few minutes; text-heavy templates
215
+ and `quality: high` take longer. Read each render's `elapsed_seconds` rather than guessing — a
216
+ render that's still `running` is healthy; do NOT re-submit thinking it stalled (that double-bills).
217
+ 7. **Hand back the links** from the batch's `links` block — `brand_url` (gallery) and each
218
+ creative's `app_url` — copied verbatim. Never end on just "done" or a file path.
219
+
220
+ ## Workflow — edit an existing ad
221
+
222
+ User wants to tweak a creative they already made → `regenerate_creative`:
223
+ - "make another version / different take" → `mode: "variation"` (optionally new `prompt`,
224
+ `product_name`, `ratios`).
225
+ - "change X in this exact image" → `mode: "edit"`, `source_render_id` = the render to edit,
226
+ `prompt` = the change.
227
+ - "run exactly this prompt on the product" → `mode: "exact"`, `source_render_id` + `prompt`.
228
+ Then poll with `get_remix_batch` and hand back the links, same as above.
229
+
230
+ ## Brand research
231
+
232
+ Prefer the backend's result: `get_brand_kit { brand_id }`. If `researchStatus` is
233
+ `complete`, REUSE it — never re-research.
234
+
235
+ **The split — backend owns visuals, you own the qualitative depth:**
236
+
237
+ - **Backend LIGHT pass (automatic).** `create_ad_brand` with a `website_url` kicks off the same
238
+ backend research the web app uses, in `mode: "light"`: it resolves the **authoritative logo,
239
+ colors, and fonts** (Brandfetch + context.dev) plus a baseline kit, then flips
240
+ `research_status` to `complete` — usually under a minute. You can't reproduce those visual
241
+ signals locally, so **never re-derive logo/colors/fonts.** (Web onboarding via `/api/ads/onboard`
242
+ runs the full thing; nothing to do but read it.)
243
+ - **Your DEEP pass (local, agentic).** You add the qualitative depth the light pass leaves thin —
244
+ positioning, audience segments, voice, brandType, value props, proof points, products — grounded
245
+ on the actual site.
246
+
247
+ **CLI brand-research flow:**
248
+
249
+ 1. `create_ad_brand { name, website_url }` → keep `brand_id` + `slug`. The brand comes back with
250
+ `research_status: "pending"` (light pass in flight).
251
+ 2. **Wait for the backend light pass:** poll `get_brand_kit { brand_id }` until `researchStatus`
252
+ is `complete` (usually <60s). Now the kit has authoritative logo/colors/fonts + a baseline.
253
+ At this point generation is already unblocked — but do the deep pass to make it good.
254
+ 3. **Deep research locally:** `gooseworks fetch brand-research` and follow its phases. **Ground
255
+ every fact on the fetched site** — if the site can't be read, say so and ask the user; never
256
+ guess a category from the brand name alone.
257
+ 4. **Write the pack** with `write_file` under `agent-config/brands/<slug>/`:
258
+ - the `brand-research/*.md` docs + `brand-assets/manifest.json` (human-readable pack), AND
259
+ - `brand-research/kit-patch.json` — the STRUCTURED fields the web UI renders. Field-for-field
260
+ contract; only what you put here reaches the kit. Shape:
261
+ `{ positioning?: string, audience?: string, voice?: string, brandType?: string, tagline?: string, valueProps?: string[], proofPoints?: string[], products?: [{ name, description?, link?, pricing?, imageUrls?: string[] }] }`
262
+ (`brandType` ∈ product | saas | service | agency | restaurant | fashion | beauty | fitness |
263
+ finance | education | health). Only URLs already in our storage for product images.
264
+ - **Do NOT set logo / colors / fonts here** — the backend light pass already owns those.
265
+ 5. **Persist it:** `finalize_brand_research { brand_id }` merges `kit-patch.json` into the kit
266
+ NON-CLOBBERINGLY (it will NOT overwrite the backend's visuals or any user edit), then re-confirms
267
+ `research_status: complete`.
268
+ 6. **Verify:** `get_brand_kit { brand_id }` — confirm the qualitative fields you wrote are present
269
+ before generating.
270
+
271
+ **If the brand has NO website**, the backend light pass can't run (nothing to fetch) — do the whole
272
+ thing locally (steps 3–6) and finalize; an un-finalized brand has no kit for generation and leaves
273
+ no artifact to debug a wrong run (this is how a bad local classification, e.g. mislabelling a SaaS
274
+ as a "drink company", used to vanish without a trace).
275
+
276
+ ## Analyze / intelligence (fetched recipes — NOT generation)
277
+
278
+ These are analysis recipes you fetch from goose-skills with `gooseworks fetch <slug>` and
279
+ follow; they do NOT touch the generation tools or credits-for-images. Pick the closest match;
280
+ if unsure, `gooseworks search "<what the user wants>"` first:
281
+ - **Campaign performance diagnosis** ("why is my Meta/Google campaign underperforming",
282
+ creative fatigue, learning phase, pacing, auction overlap) → `gooseworks fetch meta-ads-analyzer`
283
+ (or `ad-campaign-analyzer` for cross-platform).
284
+ - **Lead/CAC quality** ("are these ads driving qualified leads", true CAC vs vanity CPA,
285
+ Scale/Keep/Investigate/Cut) → `gooseworks fetch ad-lead-quality-analyzer`.
286
+ - **Competitor ad intelligence** ("what ads are competitors running") →
287
+ `gooseworks fetch competitor-ad-intelligence` (Meta Ad Library: `meta-ad-scraper`;
288
+ Google: `google-ad-scraper`).
289
+ - **Creative ideation** (ad angles, winning hooks) → `gooseworks fetch ad-angle-miner` /
290
+ `gooseworks fetch trending-ad-hook-spotter`.
291
+ - **Policy / landing-page checks** → `gooseworks fetch meta-ad-policy-checker` /
292
+ `gooseworks fetch ad-to-landing-page-auditor`.
293
+
294
+ Save their scripts to `/tmp/gooseworks-scripts/<slug>/` and follow their instructions. These
295
+ run through the `gooseworks` CLI (`gooseworks fetch` / `gooseworks call`), like the GTM skills.
296
+
297
+ ## Rules
298
+
299
+ - **MCP required** — if `mcp__gooseworks__*` is unavailable, stop and tell the user to run
300
+ `gooseworks install --claude --mcp`.
301
+ - **One backend workflow** — generation is `submit_remix_batch` / `regenerate_creative` ONLY.
302
+ Do NOT call FAL, the media proxy, `submit_render`, `update_render_status`, or upload render
303
+ files yourself; do NOT `gooseworks fetch` a local remix recipe to generate. The backend owns it.
304
+ - **Always end a successful run with the links** from the batch's `links` block (`brand_url` +
305
+ each creative's `app_url`), copied verbatim. Never end on just "done" or a file path.
306
+ - **Quote cost before generating** when it's non-trivial (use `estimate_remix_batch`), and
307
+ relay `insufficient_credits` plainly if the submit is rejected — don't retry blindly.
308
+ - **Don't hand-pick templates silently.** If the user didn't name a template, run the ask flow
309
+ (kind of ads → Choose explicitly vs Surprise me → styling). "Surprise me" goes through
310
+ `surprise_me_templates`; "Choose explicitly" sends them to `/create?brand=<slug>&cli=true`.
311
+ Generate when they paste the app's copyable remix prompt back (or submit the surprise picks
312
+ directly if they'd rather not review).
313
+ - **Ask the styling** — Keep original (default) vs Match brand — before you submit.
314
+ - **Reconcile brand facts into the kit** — when the user states or changes something brand-level
315
+ mid-task, check it against `get_brand_kit` and, with their ok, persist it via `update_brand_kit`
316
+ / `upsert_brand_product` / `add_brand_product_image` so it sticks for future ads. Ask first;
317
+ never silently mutate the kit.
318
+ - **Record feedback** — when the user reacts to a generated image, capture it with
319
+ `set_creative_feedback { render_id, rating/comment/reasons }` so the quality loop learns.
320
+ - **Plan mode is opt-in** — only compose-and-pause (`requires_approval: true` → `list_ad_approvals`
321
+ → `approve_ad_plan`) when the user wants to review before spending credits; otherwise generate
322
+ immediately.
323
+ - **Don't busy-loop** — poll `get_remix_batch` on a sensible interval (~20-30s); a `queued`
324
+ batch is waiting on research and will start on its own.