gooseworks 0.4.2 → 0.5.0-dev.3.8661ef81

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 (130) hide show
  1. package/README.md +69 -21
  2. package/dist/agents/staging.d.ts +38 -0
  3. package/dist/agents/staging.d.ts.map +1 -0
  4. package/dist/agents/staging.js +309 -0
  5. package/dist/agents/staging.js.map +1 -0
  6. package/dist/auth/credentials.d.ts +1 -0
  7. package/dist/auth/credentials.d.ts.map +1 -1
  8. package/dist/auth/credentials.js +26 -8
  9. package/dist/auth/credentials.js.map +1 -1
  10. package/dist/auth/oauth-server.d.ts.map +1 -1
  11. package/dist/auth/oauth-server.js +18 -2
  12. package/dist/auth/oauth-server.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +29 -5
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +117 -63
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/fetch.d.ts +1 -0
  18. package/dist/commands/fetch.d.ts.map +1 -1
  19. package/dist/commands/fetch.js +64 -29
  20. package/dist/commands/fetch.js.map +1 -1
  21. package/dist/commands/formats.js +2 -2
  22. package/dist/commands/formats.js.map +1 -1
  23. package/dist/commands/graphics/publish-flow.js +1 -1
  24. package/dist/commands/graphics/publish-flow.js.map +1 -1
  25. package/dist/commands/install.d.ts.map +1 -1
  26. package/dist/commands/install.js +57 -4
  27. package/dist/commands/install.js.map +1 -1
  28. package/dist/commands/launch.d.ts +3 -0
  29. package/dist/commands/launch.d.ts.map +1 -0
  30. package/dist/commands/launch.js +35 -0
  31. package/dist/commands/launch.js.map +1 -0
  32. package/dist/commands/login.d.ts.map +1 -1
  33. package/dist/commands/login.js +15 -3
  34. package/dist/commands/login.js.map +1 -1
  35. package/dist/commands/logout.d.ts.map +1 -1
  36. package/dist/commands/logout.js +3 -1
  37. package/dist/commands/logout.js.map +1 -1
  38. package/dist/commands/skills.d.ts +50 -0
  39. package/dist/commands/skills.d.ts.map +1 -0
  40. package/dist/commands/skills.js +160 -0
  41. package/dist/commands/skills.js.map +1 -0
  42. package/dist/commands/styles.js +2 -2
  43. package/dist/commands/styles.js.map +1 -1
  44. package/dist/commands/update.d.ts.map +1 -1
  45. package/dist/commands/update.js +37 -6
  46. package/dist/commands/update.js.map +1 -1
  47. package/dist/commands/video-local.d.ts +9 -0
  48. package/dist/commands/video-local.d.ts.map +1 -0
  49. package/dist/commands/video-local.js +83 -0
  50. package/dist/commands/video-local.js.map +1 -0
  51. package/dist/commands/video-save.d.ts +3 -0
  52. package/dist/commands/video-save.d.ts.map +1 -0
  53. package/dist/commands/video-save.js +83 -0
  54. package/dist/commands/video-save.js.map +1 -0
  55. package/dist/config.d.ts.map +1 -1
  56. package/dist/config.js +11 -3
  57. package/dist/config.js.map +1 -1
  58. package/dist/environment.d.ts +9 -0
  59. package/dist/environment.d.ts.map +1 -0
  60. package/dist/environment.js +103 -0
  61. package/dist/environment.js.map +1 -0
  62. package/dist/index.js +12 -38
  63. package/dist/index.js.map +1 -1
  64. package/dist/lib/graphics-cache.d.ts.map +1 -1
  65. package/dist/lib/graphics-cache.js +2 -2
  66. package/dist/lib/graphics-cache.js.map +1 -1
  67. package/dist/lib/video-local-captions.d.ts +83 -0
  68. package/dist/lib/video-local-captions.d.ts.map +1 -0
  69. package/dist/lib/video-local-captions.js +261 -0
  70. package/dist/lib/video-local-captions.js.map +1 -0
  71. package/dist/lib/video-local-runtime.d.ts +8 -0
  72. package/dist/lib/video-local-runtime.d.ts.map +1 -0
  73. package/dist/lib/video-local-runtime.js +201 -0
  74. package/dist/lib/video-local-runtime.js.map +1 -0
  75. package/dist/lib/video-save-mcp.d.ts +9 -0
  76. package/dist/lib/video-save-mcp.d.ts.map +1 -0
  77. package/dist/lib/video-save-mcp.js +162 -0
  78. package/dist/lib/video-save-mcp.js.map +1 -0
  79. package/dist/lib/video-save.d.ts +77 -0
  80. package/dist/lib/video-save.d.ts.map +1 -0
  81. package/dist/lib/video-save.js +440 -0
  82. package/dist/lib/video-save.js.map +1 -0
  83. package/dist/program.d.ts +2 -0
  84. package/dist/program.d.ts.map +1 -0
  85. package/dist/program.js +54 -0
  86. package/dist/program.js.map +1 -0
  87. package/dist/skills/custom-video-skill.d.ts +3 -0
  88. package/dist/skills/custom-video-skill.d.ts.map +1 -0
  89. package/dist/skills/custom-video-skill.js +6 -0
  90. package/dist/skills/custom-video-skill.js.map +1 -0
  91. package/dist/skills/installer.d.ts +12 -6
  92. package/dist/skills/installer.d.ts.map +1 -1
  93. package/dist/skills/installer.js +82 -38
  94. package/dist/skills/installer.js.map +1 -1
  95. package/dist/skills/master-skill.d.ts +34 -28
  96. package/dist/skills/master-skill.d.ts.map +1 -1
  97. package/dist/skills/master-skill.js +2181 -858
  98. package/dist/skills/master-skill.js.map +1 -1
  99. package/dist/skills/package-freshness.d.ts +16 -0
  100. package/dist/skills/package-freshness.d.ts.map +1 -0
  101. package/dist/skills/package-freshness.js +36 -0
  102. package/dist/skills/package-freshness.js.map +1 -0
  103. package/dist/skills/releases.d.ts +12 -0
  104. package/dist/skills/releases.d.ts.map +1 -0
  105. package/dist/skills/releases.js +155 -0
  106. package/dist/skills/releases.js.map +1 -0
  107. package/dist/skills/routes.d.ts.map +1 -1
  108. package/dist/skills/routes.js +12 -6
  109. package/dist/skills/routes.js.map +1 -1
  110. package/dist/skills/staging-content.d.ts +3 -0
  111. package/dist/skills/staging-content.d.ts.map +1 -0
  112. package/dist/skills/staging-content.js +12 -0
  113. package/dist/skills/staging-content.js.map +1 -0
  114. package/dist/utils/browser-preflight.d.ts +29 -0
  115. package/dist/utils/browser-preflight.d.ts.map +1 -0
  116. package/dist/utils/browser-preflight.js +338 -0
  117. package/dist/utils/browser-preflight.js.map +1 -0
  118. package/package.json +11 -3
  119. package/skills/goose-ads/SKILL.md +486 -239
  120. package/skills/goose-product-photos/SKILL.md +138 -60
  121. package/skills/goose-video/SKILL.md +537 -259
  122. package/skills/goose-video-local/SKILL.md +1372 -236
  123. package/skills/gooseworks/SKILL.md +300 -32
  124. package/skills/make-custom-video/SKILL.md +697 -0
  125. package/skills/manifest.json +12 -0
  126. package/skills/routes.json +13 -6
  127. package/dist/skills/fetcher.d.ts +0 -18
  128. package/dist/skills/fetcher.d.ts.map +0 -1
  129. package/dist/skills/fetcher.js +0 -118
  130. package/dist/skills/fetcher.js.map +0 -1
@@ -11,7 +11,7 @@ description: >
11
11
  references a product to photograph. Unlike goose-ads (ad creative) this produces clean PRODUCT
12
12
  photos that can then feed the ad workflow.
13
13
  category: ads
14
- version: 0.2.0
14
+ version: 0.3.0
15
15
  author: GooseWorks
16
16
  tags: [gooseworks, ads, product-photos, photoshoot, product, ecommerce, studio, lifestyle, on-model]
17
17
  ---
@@ -19,26 +19,93 @@ tags: [gooseworks, ads, product-photos, photoshoot, product, ecommerce, studio,
19
19
  # GooseWorks Product Photos — branded product photography
20
20
 
21
21
  The GooseWorks Product Photos skill. You **pick a brand + product and submit one generation**;
22
- the **backend** runs the whole pipeline (compose the shot prompt → generate on `gpt_image_2` →
23
- judge for product fidelity → auto-retry a few times for free) and stores the results. You do NOT
22
+ the **backend** runs the whole pipeline (compose the shot prompt → generate → judge for product
23
+ fidelity → auto-retry a few times for free) and stores the results. You do NOT
24
24
  generate images, call a model, or manage files — this is the exact same workflow the Product
25
25
  Photos studio uses, so the skill and the app can never drift. The point is to **enrich a brand's
26
26
  usable product imagery** — approved photos join the brand kit and can then feed the ad workflow
27
27
  (`goose-ads`).
28
28
 
29
- ## Prerequisite — the GooseWorks MCP server is REQUIRED
30
-
31
- Everything goes through the `mcp__gooseworks__*` tools. If they are not available, **stop and
32
- tell the user to run `gooseworks install --claude --mcp`** (and restart Claude Code). There is no
33
- HTTP/file fallback.
29
+ It shoots a **physical catalog product** (apparel, beauty, consumer goods). A software
30
+ screenshot or an app mockup is not a product photo: that is an image edit, not this skill.
31
+
32
+ ## How to talk to the customer (applies to every message you send them)
33
+
34
+ The customer is a marketer or founder, not an engineer. Everything in this skill about tools,
35
+ fields, files, polling, models and pipeline steps is for YOU. Never pass it on to them.
36
+
37
+ - **Say what they get and what they need to decide, never how it's made.**
38
+ Bad: "Checking the media-proxy helpers for voiceover timestamps and lipsync." / "Generated with
39
+ gpt-image-2 at high quality, mouth closed, passes QC."
40
+ Good: "Recording the voices now." / "Here are your two hosts."
41
+ - **Never mention** tool, file or field names, ids, JSON, commands, scripts, installed software,
42
+ uploads, model or vendor names, timings in seconds or frames, retries, polling, internal statuses
43
+ or your own quality checks. Fix what you can quietly. If they ask how something works, explain it
44
+ simply.
45
+ - **Update them only at milestones they care about**: choices confirmed, script and voices done,
46
+ visuals done, ready to review, finished. One short line each, then stay quiet until the next
47
+ milestone. No update for setup, downloads, uploads or saving.
48
+ - **Money is in credits, with a number**: "the full video uses about N credits". Never "the cheap
49
+ pieces" or "the expensive render".
50
+ - **Problems:** say what it means for them and what they can do, in one or two sentences. No error
51
+ codes or stack traces. Only raise a problem that changes something for them.
52
+ - **Use their words**: script, voice, hook, scene, host, ending, logo, the product shot.
53
+ - **No filler**: no "Great question!", "Certainly!", "I hope this helps!", no restating what they
54
+ just said at length.
55
+
56
+ Example. Instead of a dozen lines about render tools, scripts, uploads and portraits, send:
57
+ "Got it: warm tone, home podcast studio, Brielle as the skeptic and Mark as the believer." then
58
+ "Script and voices are done (about 34 seconds)." then "Ready for you to review: the script, voices,
59
+ both hosts and the ending. You can also review your recipe ingredients in the app: <link>. Say go
60
+ here and I'll make the full video (about N credits)."
61
+
62
+ ## Use current instructions for new work
63
+
64
+ Before a new task in a terminal host, run `gooseworks skills status` once. It compares the
65
+ installed entry files, running CLI and published npm release. An older CLI needs a package
66
+ upgrade before `gooseworks update`; update alone only uses that CLI's bundled instructions.
67
+ Preserve local edits or unknown install provenance. Review/back up before explicitly replacing
68
+ modified files; never quietly reinstall over them. If the release check is unavailable, report
69
+ that freshness is unknown rather than claiming the local copy is latest.
70
+
71
+ For a new recipe run, fetch its package from the connected catalog. Retain the returned
72
+ `version` / `contentHash` and every dependency's hash with the saved package. When reusing a
73
+ saved fetch JSON, `gooseworks fetch <slug> --saved-package <file>` returns the current package
74
+ and a hash comparison without changing that file. With MCP, if the advertised `catalog_fetch`
75
+ schema accepts them, send `saved_content_hash` and `saved_dependency_hashes`; otherwise fetch
76
+ normally and compare the returned hashes yourself. Missing hashes mean unknown, not current.
77
+ The server cannot inspect a client's saved files; hash metadata does not certify later edits.
78
+ Fetch current packages into a new run directory and report stale saved instructions. Preserve
79
+ an existing approved run's recorded package; changing that harness requires a reviewed change
80
+ and approval before spending. Hosted installed snapshots use the existing Skills Update action.
81
+ Skill content and a host's cached MCP tool schemas are separate: refreshing one does not refresh
82
+ the other. Check the actual advertised tools before using new fields.
83
+
84
+ ## Prerequisite — the GooseWorks connector tools
85
+
86
+ Everything below runs through the GooseWorks connector's tools, by the names this skill uses.
87
+ Match on the tool name: a coding agent may show a server prefix (for example
88
+ `mcp__gooseworks__photos_generate`), a chat app may not.
89
+
90
+ If a tool named here is missing, the GooseWorks connection or its tool list is stale: ask the
91
+ user to reconnect or refresh GooseWorks in their app's connector settings. Installing or
92
+ updating the `gooseworks` CLI never fixes a missing connector tool, so never send a chat-app
93
+ user to a terminal for it. Only a terminal coding agent (Claude Code, Codex, Cursor) that has no
94
+ GooseWorks tools at all connects them, in that terminal, with `gooseworks install --mcp` plus
95
+ `--claude`, `--codex` or `--cursor`, then a restart.
96
+
97
+ Older notes or skill copies may name tools the connector no longer lists. Use the tool this skill
98
+ names instead; `catalog_fetch { type: "skill", slug: "gooseworks-guide" }` maps every old name.
34
99
 
35
100
  ## Start from the brand context — don't re-ask what it already answers
36
101
 
37
102
  If the `gooseworks` router handed you brand context, USE IT. If you were invoked directly, call
38
- `brand_get_context` yourself first. It answers most of the setup questions below, so **do not ask
39
- the user for them**:
103
+ `brand_read { brand_id, sections: ["summary","kit","products","learnings"] }` yourself first, then
104
+ `knowledge_search { brand_id, query: "<the shoot, in the user's words>" }` for saved rules and past
105
+ feedback (an archived photo's reason is saved as a `dont` rule). They answer most of the setup
106
+ questions below, so **do not ask the user for them**:
40
107
 
41
- - **Which product?** — the context's `products[]` are the real catalog entries. Offer them; never
108
+ - **Which product?** — the context's `products` are the real catalog entries. Offer them; never
42
109
  invent a product or ask the user to describe one you can already see.
43
110
  - **What does it look like / what is it made of?** — grounded in the product's stored images and
44
111
  description. Never guess a material, colorway, or silhouette.
@@ -50,79 +117,90 @@ the user for them**:
50
117
  Ask only for the genuinely open choices: the shot `category`, how many photos, quality, and
51
118
  whether a human model is wanted (which needs explicit consent — see the rules).
52
119
 
53
- ## Identity & credits
120
+ ## Credits — state the total, then get a yes
54
121
 
55
- - One agent-scoped token authenticates the tools; they resolve your org automatically. Never
56
- print the token. (You may pass an optional `target` to operate on a specific agent/org, exactly
57
- as the other GooseWorks tools; omit it to use your pinned scope.)
58
- - **Credits are handled by the backend.** `generate_product_photos` reserves the estimated cost up
59
- front and bills only the photos that pass the judge — **automatic retries are free**, and a photo
60
- the judge can't get right (`flagged`) is shown but **never billed**. Call
61
- `estimate_product_photos` first to quote the cost; `get_ad_credits` shows the balance.
122
+ - One token authenticates the tools and resolves your org; never print it. Omit `target`.
123
+ - **Quote first.** Call `photos_generate` with the exact arguments you will submit plus
124
+ `dry_run: true`. It reserves nothing and returns `creditsPerOutput` and `totalCredits`. Pass
125
+ `count` and `quality` explicitly in both calls so the quote matches the run.
126
+ - **Nothing paid runs without the user's explicit yes in this chat, given after you state that
127
+ credit total.** Then submit the same call without `dry_run`.
128
+ - The submit reserves the quoted credits and bills only the photos that pass the judge:
129
+ **automatic retries are free**, and a photo the judge can't get right (`flagged`) is shown but
130
+ **never billed**. The balance is `credits.available_credits` from `account_whoami`. If the
131
+ wallet is short, say so plainly with the total and the balance, and stop.
62
132
 
63
133
  ## The tools
64
134
 
65
135
  **Pick the brand + product**
66
- - `list_ad_brands` — the user's ad brands (get a `brand_id`; also carries `slug`).
67
- - `list_brand_products { brand_id, search?, page?, page_size? }` — the brand's imported products.
68
- Pick a `product_id` to shoot. `search` matches name / type / variant / SKU.
69
- - `import_product { brand_id, kind, url, product_name? }` — import a product if it isn't in the
70
- catalog yet. `kind` is `product_url` (a single product page), `shopify_store` (a store URL →
71
- imports the catalog), or `image_url` (a direct image; requires `product_name`). Returns an import
72
- row with an `id`; if its `status` isn't `complete`, poll `get_product_import` until it is, then
73
- `list_brand_products` to find the new product. (File uploads aren't available over MCP — use a URL.)
74
- - `get_product_import { import_id }` — poll an import until `status` is `complete` or `failed`.
136
+ - `brand_read` with no `brand_id` — the user's brands (each row's `id` is the `brand_id`).
137
+ - `brand_read { brand_id, sections: ["products"], products_query: "<name>" }` — the brand's
138
+ imported products in `products.items`; a product's `id` is the `product_id` to shoot.
139
+ `products_query` matches name / type / variant / SKU / description.
140
+ - Import a product that isn't in the catalog yet (free):
141
+ `brand_update { brand_id, patch: { products: [{ import_url, import_kind, name? }] } }`.
142
+ `import_kind` is `product_url` (a single product page), `shopify_store` (a store URL → imports
143
+ the catalog), or `image_url` (a direct image; also needs `name`). It returns `jobs[]`: poll
144
+ `job_get { job_id, kind: "product_import" }` until it finishes, then read the products again.
145
+ To add a photo the user attached to an existing product:
146
+ `media_upload { brand_id, scope: "product", scope_id: <product_id>, kind: "reference", source: { type: "bytes", filename, content_base64 } }`.
75
147
 
76
148
  **Generate**
77
- - `estimate_product_photos { count, quality? }` — cost preview (per-photo + total credits). `count`
78
- is 1, 2, 4, or 8; `quality` is `low` | `medium` | `high` (default `medium`). Reserves nothing.
79
- - `generate_product_photos { brand_id, product_id, variant_id?, category, controls?, prompt?,
80
- count?, quality?, reference_image_urls?, attestation_accepted? }` — **the one call that makes
81
- photos.** `category` is `apparel` | `beauty` | `cpg` (seeds sensible scene/framing defaults).
82
- Omit `controls` to use the category preset; pass `prompt` as free-text steering **added on top of**
83
- the settings (it doesn't replace them). Returns a generation with an `id` **immediately** — poll
84
- `get_product_photo_generation` until done, then read each `outputs[].final_image_url`.
149
+ - `photos_generate { brand_id, product_id, variant_id?, category, controls?, prompt?, count,
150
+ quality, reference_image_urls?, attestation_accepted?, dry_run? }` — **the one call that makes
151
+ photos** (and, with `dry_run: true`, its free quote). `category` is `apparel` | `beauty` |
152
+ `cpg` (seeds sensible scene/framing defaults). `count` is 1, 2, 4, or 8; `quality` is
153
+ `low` | `medium` | `high`. Omit `controls` to use the category preset; pass `prompt` as
154
+ free-text steering **added on top of** the settings (it doesn't replace them). Returns the
155
+ generation with its `id` **immediately**.
85
156
  **If you request a human model** (`controls.model.presence` is not `none`) you MUST pass
86
157
  `attestation_accepted: true` to confirm the user has the rights for model imagery.
87
- - `get_product_photo_generation { generation_id }` — poll until `status` is `complete`,
88
- `partial_failure`, or `failed`. Each `outputs[]` entry has its own `status` and, once ready, a
158
+ - `photos_read { brand_id, generation_id }` — poll until `status` is `complete`,
159
+ `partial_failure`, or `failed` (`job_get { job_id: <generation id>, kind: "photo_generation" }`
160
+ works too). Each `outputs[]` entry has its own `id`, `status` and, once ready, a
89
161
  `final_image_url`. A `flagged` output is the best attempt but wasn't billed.
90
162
 
91
163
  **Use the results**
92
- - `list_product_photos { brand_id, archived? }` — the brand's generated photos (`archived: false`
93
- = active, `true` = archived).
94
- - `approve_product_photo { output_id }` — approve a photo: links it to the product and makes it
95
- available in the **brand kit**, so `goose-ads` can use it. **Photos are not used anywhere until
96
- approved.**
97
- - `archive_product_photo { output_id, reason? }` — archive a photo; archived photos are **excluded**
98
- from ad generation.
164
+ - `photos_read { brand_id, archived?, product_id?, status? }` — the brand's generated photos
165
+ (`archived: false` = active, `true` = archived).
166
+ - `photos_update { brand_id, output_id, action: "approve" }` — approve a photo: links it to the
167
+ product and makes it available in the **brand kit**, so `goose-ads` can use it. **Photos are
168
+ not used anywhere until approved.**
169
+ - `photos_update { brand_id, output_id, action: "archive", reason? }` — archive a photo; archived
170
+ photos are **excluded** from ad generation, and the reason is saved as a `dont` rule.
99
171
 
100
172
  ## Workflow — shoot a product
101
173
 
102
- 1. **Load the brand context** (`brand_get_context`, or reuse what the router passed you) and
103
- **resolve the brand + product.** `list_ad_brands` → `brand_id`. `list_brand_products` → pick a
104
- `product_id` from the catalog you already know about. If the product genuinely isn't there,
105
- `import_product` (poll `get_product_import`).
106
- 2. **Quote the cost.** `estimate_product_photos { count, quality }` → tell the user credits.
107
- 3. **Generate.** `generate_product_photos { brand_id, product_id, category, count, quality, prompt? }`.
108
- Build `prompt` from the brand's voice/positioning you already have — don't interview the user for it.
109
- Returns a generation `id` right away.
110
- 4. **Poll.** `get_product_photo_generation { generation_id }` until terminal; hand back each
174
+ 1. **Load the brand context** (`brand_read` + `knowledge_search`, or reuse what the router passed
175
+ you) and **resolve the brand + product.** Pick a `product_id` from the catalog you already
176
+ know about. If the product genuinely isn't there, import it and poll the import.
177
+ 2. **Quote the cost.** `photos_generate` with `dry_run: true` and the exact `brand_id`,
178
+ `product_id`, `category`, `count` and `quality` → tell the user the credit total and wait
179
+ for their explicit yes.
180
+ 3. **Generate.** The same `photos_generate` call without `dry_run` (add `prompt` built from the
181
+ brand's voice/positioning you already have — don't interview the user for it). Returns a
182
+ generation `id` right away.
183
+ 4. **Poll.** `photos_read { brand_id, generation_id }` every ~20-30s until terminal; show each
111
184
  `final_image_url`.
112
- 5. **Approve the keepers.** Show the results and let the user pick; `approve_product_photo` the ones
113
- they'd publish (that's what puts them in the brand kit for ads), `archive_product_photo` the rest.
185
+ 5. **Approve the keepers.** Show the results and let the user pick; `photos_update` with
186
+ `action: "approve"` the ones they'd publish (that's what puts them in the brand kit for ads),
187
+ `action: "archive"` the rest.
114
188
 
115
189
  ## Rules
116
190
 
191
+ - **Connector tools only** — a missing tool means the GooseWorks connection is stale: ask the
192
+ user to reconnect or refresh GooseWorks. Never send a chat-app user to a terminal or a CLI
193
+ install.
117
194
  - **Never invent product facts.** The backend grounds the shot on the product's real images; don't
118
195
  describe a product you can't see.
119
196
  - **Use the brand context instead of interviewing the user.** Product, audience, voice, positioning,
120
- logo/colors/fonts all come from `brand_get_context` / the brand kit. Ask only for the shot
197
+ logo/colors/fonts all come from `brand_read` / the brand kit. Ask only for the shot
121
198
  category, count, quality, and model consent.
122
- - **Ask before spending.** Quote the estimate and confirm `count` / `quality` before
123
- `generate_product_photos` — it reserves credits.
199
+ - **Ask before spending.** State the dry-run credit total and get the user's explicit yes before
200
+ the real `photos_generate` — it reserves credits.
124
201
  - **Poll, don't re-submit.** A generation that's still `running` is not stuck; re-submitting
125
- double-bills. Only a `failed` generation should be retried.
202
+ double-bills. A `failed` generation may still hold `flagged` photos (shown, never billed):
203
+ show those first, and run again only after a new quote and the user's yes.
126
204
  - **Model imagery needs consent.** Only set a human model when the user asks, and pass
127
205
  `attestation_accepted: true`.
128
206
  - **Approval is the hand-off to ads.** Remind the user that only **approved** photos reach the brand