gooseworks 0.4.2 → 0.4.5-dev.15.facf60fb

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 (156) hide show
  1. package/README.md +84 -22
  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/device-flow.d.ts +79 -0
  11. package/dist/auth/device-flow.d.ts.map +1 -0
  12. package/dist/auth/device-flow.js +411 -0
  13. package/dist/auth/device-flow.js.map +1 -0
  14. package/dist/auth/login-mode.d.ts +25 -0
  15. package/dist/auth/login-mode.d.ts.map +1 -0
  16. package/dist/auth/login-mode.js +87 -0
  17. package/dist/auth/login-mode.js.map +1 -0
  18. package/dist/auth/oauth-server.d.ts +2 -2
  19. package/dist/auth/oauth-server.d.ts.map +1 -1
  20. package/dist/auth/oauth-server.js +28 -5
  21. package/dist/auth/oauth-server.js.map +1 -1
  22. package/dist/auth/paste-login.d.ts +27 -0
  23. package/dist/auth/paste-login.d.ts.map +1 -0
  24. package/dist/auth/paste-login.js +203 -0
  25. package/dist/auth/paste-login.js.map +1 -0
  26. package/dist/commands/doctor.d.ts +29 -5
  27. package/dist/commands/doctor.d.ts.map +1 -1
  28. package/dist/commands/doctor.js +116 -63
  29. package/dist/commands/doctor.js.map +1 -1
  30. package/dist/commands/fetch.d.ts +1 -0
  31. package/dist/commands/fetch.d.ts.map +1 -1
  32. package/dist/commands/fetch.js +64 -29
  33. package/dist/commands/fetch.js.map +1 -1
  34. package/dist/commands/formats.js +2 -2
  35. package/dist/commands/formats.js.map +1 -1
  36. package/dist/commands/graphics/publish-flow.js +1 -1
  37. package/dist/commands/graphics/publish-flow.js.map +1 -1
  38. package/dist/commands/install.d.ts.map +1 -1
  39. package/dist/commands/install.js +126 -26
  40. package/dist/commands/install.js.map +1 -1
  41. package/dist/commands/launch.d.ts +3 -0
  42. package/dist/commands/launch.d.ts.map +1 -0
  43. package/dist/commands/launch.js +35 -0
  44. package/dist/commands/launch.js.map +1 -0
  45. package/dist/commands/login.d.ts +21 -1
  46. package/dist/commands/login.d.ts.map +1 -1
  47. package/dist/commands/login.js +112 -30
  48. package/dist/commands/login.js.map +1 -1
  49. package/dist/commands/logout.d.ts.map +1 -1
  50. package/dist/commands/logout.js +7 -1
  51. package/dist/commands/logout.js.map +1 -1
  52. package/dist/commands/skills.d.ts +50 -0
  53. package/dist/commands/skills.d.ts.map +1 -0
  54. package/dist/commands/skills.js +160 -0
  55. package/dist/commands/skills.js.map +1 -0
  56. package/dist/commands/styles.js +2 -2
  57. package/dist/commands/styles.js.map +1 -1
  58. package/dist/commands/tool.d.ts +17 -0
  59. package/dist/commands/tool.d.ts.map +1 -0
  60. package/dist/commands/tool.js +239 -0
  61. package/dist/commands/tool.js.map +1 -0
  62. package/dist/commands/update.d.ts.map +1 -1
  63. package/dist/commands/update.js +37 -6
  64. package/dist/commands/update.js.map +1 -1
  65. package/dist/commands/video-local.d.ts +9 -0
  66. package/dist/commands/video-local.d.ts.map +1 -0
  67. package/dist/commands/video-local.js +83 -0
  68. package/dist/commands/video-local.js.map +1 -0
  69. package/dist/commands/video-save.d.ts +3 -0
  70. package/dist/commands/video-save.d.ts.map +1 -0
  71. package/dist/commands/video-save.js +83 -0
  72. package/dist/commands/video-save.js.map +1 -0
  73. package/dist/config.d.ts.map +1 -1
  74. package/dist/config.js +14 -3
  75. package/dist/config.js.map +1 -1
  76. package/dist/environment.d.ts +9 -0
  77. package/dist/environment.d.ts.map +1 -0
  78. package/dist/environment.js +105 -0
  79. package/dist/environment.js.map +1 -0
  80. package/dist/index.js +12 -38
  81. package/dist/index.js.map +1 -1
  82. package/dist/lib/graphics-cache.d.ts.map +1 -1
  83. package/dist/lib/graphics-cache.js +2 -2
  84. package/dist/lib/graphics-cache.js.map +1 -1
  85. package/dist/lib/mcp-client.d.ts +53 -0
  86. package/dist/lib/mcp-client.d.ts.map +1 -0
  87. package/dist/lib/mcp-client.js +196 -0
  88. package/dist/lib/mcp-client.js.map +1 -0
  89. package/dist/lib/video-local-captions.d.ts +83 -0
  90. package/dist/lib/video-local-captions.d.ts.map +1 -0
  91. package/dist/lib/video-local-captions.js +261 -0
  92. package/dist/lib/video-local-captions.js.map +1 -0
  93. package/dist/lib/video-local-runtime.d.ts +8 -0
  94. package/dist/lib/video-local-runtime.d.ts.map +1 -0
  95. package/dist/lib/video-local-runtime.js +201 -0
  96. package/dist/lib/video-local-runtime.js.map +1 -0
  97. package/dist/lib/video-save-mcp.d.ts +9 -0
  98. package/dist/lib/video-save-mcp.d.ts.map +1 -0
  99. package/dist/lib/video-save-mcp.js +162 -0
  100. package/dist/lib/video-save-mcp.js.map +1 -0
  101. package/dist/lib/video-save.d.ts +77 -0
  102. package/dist/lib/video-save.d.ts.map +1 -0
  103. package/dist/lib/video-save.js +440 -0
  104. package/dist/lib/video-save.js.map +1 -0
  105. package/dist/program.d.ts +2 -0
  106. package/dist/program.d.ts.map +1 -0
  107. package/dist/program.js +56 -0
  108. package/dist/program.js.map +1 -0
  109. package/dist/skills/custom-video-skill.d.ts +3 -0
  110. package/dist/skills/custom-video-skill.d.ts.map +1 -0
  111. package/dist/skills/custom-video-skill.js +6 -0
  112. package/dist/skills/custom-video-skill.js.map +1 -0
  113. package/dist/skills/installer.d.ts +12 -6
  114. package/dist/skills/installer.d.ts.map +1 -1
  115. package/dist/skills/installer.js +82 -38
  116. package/dist/skills/installer.js.map +1 -1
  117. package/dist/skills/master-skill.d.ts +34 -28
  118. package/dist/skills/master-skill.d.ts.map +1 -1
  119. package/dist/skills/master-skill.js +2264 -858
  120. package/dist/skills/master-skill.js.map +1 -1
  121. package/dist/skills/package-freshness.d.ts +16 -0
  122. package/dist/skills/package-freshness.d.ts.map +1 -0
  123. package/dist/skills/package-freshness.js +36 -0
  124. package/dist/skills/package-freshness.js.map +1 -0
  125. package/dist/skills/releases.d.ts +12 -0
  126. package/dist/skills/releases.d.ts.map +1 -0
  127. package/dist/skills/releases.js +155 -0
  128. package/dist/skills/releases.js.map +1 -0
  129. package/dist/skills/routes.d.ts.map +1 -1
  130. package/dist/skills/routes.js +12 -6
  131. package/dist/skills/routes.js.map +1 -1
  132. package/dist/skills/staging-content.d.ts +3 -0
  133. package/dist/skills/staging-content.d.ts.map +1 -0
  134. package/dist/skills/staging-content.js +12 -0
  135. package/dist/skills/staging-content.js.map +1 -0
  136. package/dist/utils/browser-preflight.d.ts +29 -0
  137. package/dist/utils/browser-preflight.d.ts.map +1 -0
  138. package/dist/utils/browser-preflight.js +338 -0
  139. package/dist/utils/browser-preflight.js.map +1 -0
  140. package/dist/utils/http.d.ts +7 -1
  141. package/dist/utils/http.d.ts.map +1 -1
  142. package/dist/utils/http.js +5 -1
  143. package/dist/utils/http.js.map +1 -1
  144. package/package.json +11 -3
  145. package/skills/goose-ads/SKILL.md +509 -239
  146. package/skills/goose-product-photos/SKILL.md +161 -60
  147. package/skills/goose-video/SKILL.md +592 -259
  148. package/skills/goose-video-local/SKILL.md +1409 -236
  149. package/skills/gooseworks/SKILL.md +327 -34
  150. package/skills/make-custom-video/SKILL.md +735 -0
  151. package/skills/manifest.json +12 -0
  152. package/skills/routes.json +13 -6
  153. package/dist/skills/fetcher.d.ts +0 -18
  154. package/dist/skills/fetcher.d.ts.map +0 -1
  155. package/dist/skills/fetcher.js +0 -118
  156. package/dist/skills/fetcher.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RENDER_UPDATE_KEY = exports.RENDER_OPEN_ARGS = exports.RENDER_ROW_TOOL = void 0;
3
4
  exports.getEntrySkills = getEntrySkills;
4
5
  exports.getEntrySkillNames = getEntrySkillNames;
5
6
  exports.getMasterSkillContent = getMasterSkillContent;
@@ -7,6 +8,7 @@ exports.getGooseAdsSkillContent = getGooseAdsSkillContent;
7
8
  exports.getGooseVideoSkillContent = getGooseVideoSkillContent;
8
9
  exports.getGooseVideoLocalSkillContent = getGooseVideoLocalSkillContent;
9
10
  exports.getGooseProductPhotosSkillContent = getGooseProductPhotosSkillContent;
11
+ exports.getMakeCustomVideoSkillContent = getMakeCustomVideoSkillContent;
10
12
  /**
11
13
  * The CLI installs two vendored ENTRY skills into ~/.agents/skills/:
12
14
  * - `gooseworks` — the PARENT router (getMasterSkillContent): GTM/data toolkit
@@ -22,16 +24,386 @@ exports.getGooseProductPhotosSkillContent = getGooseProductPhotosSkillContent;
22
24
  * `gooseworks install --with goose-graphics` or fetched on demand.
23
25
  *
24
26
  * Video (vendored here, GOOSE-3677):
25
- * - `goose-video` — order a video ad in chat; renders on the GooseWorks
26
- * server (getGooseVideoSkillContent). The only full copy of that body.
27
+ * - `goose-video` — the front door for a new video ad: brand → goal →
28
+ * format table → machine check → project → hand off to goose-video-local
29
+ * (getGooseVideoSkillContent). The only full copy of that body.
27
30
  * - `goose-video-local` — render an existing app project/batch LOCALLY
28
31
  * (Playwright + ffmpeg + media proxies) (getGooseVideoLocalSkillContent).
29
32
  *
30
33
  * Recipe skills (remix-graphic-ad-from-reference, brand-research, meta-ads-analyzer,
31
34
  * …) are NOT vendored here — they live in goose-skills and are fetched live on
32
- * demand via `gooseworks fetch <slug>`, so they're always current.
35
+ * demand from the connected catalog; saved packages retain their recorded hashes.
33
36
  */
34
37
  const routes_1 = require("./routes");
38
+ const custom_video_skill_1 = require("./custom-video-skill");
39
+ const STORED_FOOTAGE_GUIDANCE = "\n## Use uploaded footage\n\nWhen 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.\n\nFreeze `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.\n\nFor 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.\n";
40
+ /**
41
+ * How every entry skill talks to the customer. Shared so the five skills can't
42
+ * drift. The customer is a marketer or founder, not an engineer: everything the
43
+ * skills say about tools, fields, polling and pipeline steps is for the agent,
44
+ * and a run that narrated it ("checking the media-proxy helpers for voiceover
45
+ * timestamps and lipsync", "generated with gpt-image-2") read as noise.
46
+ */
47
+ const SKILL_FRESHNESS = `## 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
+ const VIDEO_GUIDE_COMPATIBILITY = `**Follow the connector's full-guide requirement when available.** Fetch
69
+ \`catalog_fetch { type: "skill", slug: "gooseworks-guide" }\` when the connector requires it.
70
+ The only older-server exception is below; it never removes brand preparation or approvals.
71
+
72
+ | Returned guide/workflow state on the selected connection | Required action |
73
+ | --- | --- |
74
+ | Guide returns \`not_found\`, and this same connection already returned the complete current matching video entry with its required dependencies | Continue with that authoritative entry's workflow, full brand preparation and existing approvals. |
75
+ | Guide has another error, no response, or incomplete content; or the matching entry/dependencies are missing, incomplete or from another connection | Stop before creative work, project writes or paid calls; resolve a compatible connection on this same environment. |
76
+
77
+ A missing custom entry or required custom tool/schema remains unavailable: never substitute
78
+ a template or an import to bypass it. Never hide other fetch errors or continue from cached
79
+ instructions merely because the guide was not found.`;
80
+ /**
81
+ * The Brain-first entry contract, shared by the router, goose-ads and every video
82
+ * entry. A fresh agent read the Kit, then asked the user for angles and proof
83
+ * the Brand Brain already held: no entry made the search a step, and several
84
+ * told it to ask. Searching is free and read-only, so it needs no approval.
85
+ */
86
+ const BRAIN_FIRST_ENTRY = `## Search the Brand Brain, then propose — before any creative choice or question
87
+
88
+ The Brand Kit is a summary. The brand's saved knowledge (its Brain) holds what the Kit does not:
89
+ rules from past feedback, approved and rejected creatives, customer evidence, approved claims,
90
+ reports and documents. **After \`brand_read\`, and before you choose an angle, claim, hook,
91
+ product emphasis, format or source ad — and before you ask the user for any brand fact — call
92
+ \`knowledge_search { brand_id, query }\`** when it is registered. It is free and read-only: no
93
+ approval, no announcement to the customer and no questionnaire.
94
+
95
+ 1. **Search for this task, not the whole Brain.** Run one short query (under 500 characters) in
96
+ the user's own words plus the product (for example "ads for <product>: what worked, what to
97
+ avoid") and one with
98
+ \`source_types: ["evidence", "claim", "learning", "creative", "document"]\` for proof and past
99
+ creative results. Add a query only for a specific open question. Reuse results from this run.
100
+ 2. **Keep these states distinct** and record which one each query returned:
101
+
102
+ | Result | Means | Do |
103
+ | --- | --- | --- |
104
+ | \`status: "ok"\` with matches | Saved knowledge exists | Use it; keep each fact's citation in your working brief |
105
+ | \`status: "empty"\` | Nothing saved matches this query | Say "no saved evidence for <topic>", never "the brand has no proof" |
106
+ | \`refresh_required\` | The results shown are current; some changed sources were left out | Use them; search again shortly for anything missing |
107
+ | \`building\` | The index is not ready | Retry once shortly, then continue with the gap stated |
108
+ | An error, or the tool is not registered | Retrieval failed or is unavailable | Retry an error once, then continue from \`brand_read\` and treat evidence as unchecked |
109
+ | An empty Kit field | Only that field is blank | Not a search result: still search before asking |
110
+
111
+ 3. **Let the findings change the plan.** A \`dont\`/\`must\` learning or a rejected creative rules
112
+ options out; an approved or well-rated past creative is a proven angle to lead with; a report
113
+ shows what worked. Claim-grade proof is a result with \`approved_ad_claim: true\` or the kit's
114
+ \`approvedClaims\` within their stated applicability; the kit's \`proofPoints\` are what the
115
+ backend allows an ad to state, used as written. Other Kit text, documents and performance
116
+ numbers are context, never public claims. Never invent an offer, price or result. Judge
117
+ relevance and skip results about another product or business.
118
+ 4. **Propose; don't interview.** Lead with one recommended direction (angle, product, source or
119
+ format) and a one-line reason naming what you found, with up to two alternatives. When several
120
+ directions fit (two audiences, products or campaigns), pick the one the evidence favours, such
121
+ as an active campaign or approved past creatives, and name the other as an alternative instead
122
+ of asking. Ask only for a decision the brand read and the search cannot settle, or for spend
123
+ approval. Never ask the user for a fact the Brain already answered.
124
+ Keep directions already chosen (the user's words, a project's creative brief, handed-off
125
+ defaults, approved plans): search only to fill empty fields and to apply saved rules. When a
126
+ returned card or picker presents the choice, your recommendation is its one acknowledgement
127
+ line and the saved plan's defaults, not a separate list.
128
+ 5. **Carry an evidence brief** into the routed skill, writer or plan: findings with citations,
129
+ the state of each query, what it ruled out and the open gaps. Tell the customer the findings
130
+ in plain words; the citations stay in the brief.
131
+
132
+ On an approved resume, keep the saved brief and evidence; search again only for a new decision.`;
133
+ /** One prerequisite for template, custom and delegated video entry. */
134
+ function videoEntryPreparation(slug) {
135
+ return `## Prepare the video workflow and brand before creative work
136
+
137
+ **Installed entry files are bootstrap instructions.** For new work started from an installed
138
+ file or an old chat, fetch \`catalog_fetch { type: "skill", slug: "${slug}" }\` on the selected
139
+ connection and read its returned content and dependencies before continuing. Use the already
140
+ fetched body when this entry came from that connection in this run; do not recursively fetch
141
+ the same entry. A CLI freshness warning does not block using the current connected package.
142
+ Keep the fetched package and hashes in a new run folder; do not replace edited installed files.
143
+ If current instructions cannot be loaded, resolve the connection before format suggestions,
144
+ script writing or production. Do not continue from an old installed workflow or session notes.
145
+
146
+ ${VIDEO_GUIDE_COMPATIBILITY}
147
+
148
+ **For an existing project or batch, read \`video_project_read\` first.** Determine its actual
149
+ route and saved review state before fetching a recipe or doing local work. Resume an approved
150
+ run with its recorded packages, brand inputs, script and ingredients; do not silently replace
151
+ them with today's release or brand rules. Follow returned preparation requirements and use
152
+ the existing affected review/approval flow for an intentional change. Reads, free drafts,
153
+ imports and existing-job retrieval do not grant permission for new paid production.
154
+
155
+ **For every new plan, resolve the brand and call**
156
+ \`brand_read { brand_id, sections: ["summary", "kit", "products", "learnings"] }\` **before**
157
+ suggesting formats, mining video angles, writing any script, creating a production plan or
158
+ handing the work to another agent. Use \`brand_get_context\` with those same four sections only
159
+ when advertised. Reuse a complete read from this run on the same connection and brand.
160
+ Onboarding completion, a summary-only response and earlier session notes do not supply the
161
+ kit, selected product facts or saved rules. If the full read is unavailable, resolve it before
162
+ creative work. Use known kit facts while research is incomplete; do not restart research
163
+ solely because its status is pending. Never invent a product fact or accept a campaign to
164
+ bypass preparation. Then search the Brand Brain (next section) before suggesting formats,
165
+ choosing an angle or asking the user for a brand fact.
166
+
167
+ Carry that context through custom/template/idea routing and delegation. Before writing, make
168
+ the workflow's brand-rules file from those sources: pronunciations, required spoken copy,
169
+ prohibited claims, real logo, font, palette and the selected product's supported facts.
170
+ Entries prefixed \`Video preference:\` and visual/pacing instructions are creative direction,
171
+ not spoken lines. Use the kit's audience, offer and voice as defaults without an interview.
172
+ The rules file mirrors context; it is not proof of a read or permission to spend. Keep the
173
+ route's existing script, ingredient and budget approvals, including its allowed previews.
174
+
175
+ **Use the server's brand bundle when the selected API returns it.** Project create/read may
176
+ return top-level
177
+ \`brand_context\`: \`{ version: 1, brand_id, digest, loaded_at, sections, brand, kit, products,
178
+ learnings }\`. Its sections cover summary, kit, products and learnings.
179
+
180
+ | Returned brand/project response on the selected connection | Required action |
181
+ | --- | --- |
182
+ | A complete authoritative version-1 \`brand_context\` is returned | Binding is required: use its contents for the plan/rules and include \`script_drafts.brand_context_digest = brand_context.digest\` with each new/changed script save. |
183
+ | An older API returns the actual four brand sections but no \`brand_context\` in either the brand read or project response | Prepare from those full sections, write the brand rules and follow the selected connection's existing approval flow and advertised fields. Do not fabricate a receipt or send nonexistent bundle/digest fields. |
184
+
185
+ An incomplete/malformed bundle or missing brand section is a preparation failure, never the
186
+ older-API exception. Once this connection returns an authoritative bundle, use its binding
187
+ contract. \`script_drafts.video_brand_context\` is server-owned: do not author, replace or forge
188
+ it, and do not invent a read receipt or hash local brand facts into one. Keep the full bundle
189
+ out of client review payloads. If managed generation returns HTTP 409
190
+ \`video_brand_context_required\`, reload the project and bind the plan to its returned bundle
191
+ through the normal review flow before retrying. That refusal never permits the older-API
192
+ fallback, skipping sections or retrying paid calls blindly. Unchanged approved legacy resumes
193
+ retain their package, context and approvals; an upgrade alone needs no extra approval.
194
+
195
+ **To apply a deliberate brand correction to a new or revised plan**, make a fresh, unfiltered
196
+ \`brand_read\` of those four sections. When it returns the authoritative bundle, use its
197
+ returned \`brand_context.digest\` in the complete revised \`script_drafts\` through the existing
198
+ \`video_project_upsert\` \`patch.script\` save; the server verifies the current brand and replaces
199
+ its snapshot with the saved plan. On an older API with no bundle contract, apply the actual
200
+ four-section correction and save the revised draft through its existing flow without inventing
201
+ digest fields. If a connection that already returned a bundle cannot supply the fresh bundle,
202
+ resolve that failure before rebinding. Review and approve the affected script, ingredients and
203
+ budget through the normal flow. Never refresh an ongoing approved run automatically; its
204
+ recorded package and brand inputs stay pinned until an intentional change.
205
+
206
+ ${BRAIN_FIRST_ENTRY}`;
207
+ }
208
+ /**
209
+ * Who makes an agent-made video: one rule, in the same words as the server's
210
+ * (gooseworks-app lib/who-makes-the-video.ts). The agent decides; the server
211
+ * never guesses it from the host.
212
+ */
213
+ const WHO_MAKES_THE_VIDEO = `**Who makes the video (one rule).** If you can run shell commands on the customer's own
214
+ computer, you make it there with \`goose-video-local\`: check the toolchain first and, if it
215
+ can't be fixed, hand it off. If you can't, hand it to the GooseWorks coworker with
216
+ \`goose_run_task { brand_id, project_id, message: the customer's words }\`; the one approval,
217
+ Stop and fixes stay in this chat. Inside the GooseWorks coworker's own sandbox you are the
218
+ coworker: make it yourself and never hand off. A hosted code sandbox is not the customer's
219
+ computer. Decide this yourself, never from the host's name; the server needs no flag for it.`;
220
+ const CUSTOMER_TALK = `## How to talk to the customer (applies to every message you send them)
221
+
222
+ The customer is a marketer or founder, not an engineer. Everything in this skill about tools,
223
+ fields, files, polling, models and pipeline steps is for YOU. Never pass it on to them.
224
+
225
+ - **Say what they get and what they need to decide, never how it's made.**
226
+ Bad: "Checking the media-proxy helpers for voiceover timestamps and lipsync." / "Generated with
227
+ gpt-image-2 at high quality, mouth closed, passes QC."
228
+ Good: "Recording the voices now." / "Here are your two hosts."
229
+ - **Never mention** tool, file or field names, ids, JSON, commands, scripts, installed software,
230
+ uploads, model or vendor names, timings in seconds or frames, retries, polling, internal statuses
231
+ or your own quality checks. Fix what you can quietly. If they ask how something works, explain it
232
+ simply.
233
+ - **Update them only at milestones they care about**: choices confirmed, script and voices done,
234
+ visuals done, ready to review, finished. One short line each, then stay quiet until the next
235
+ milestone. No update for setup, downloads, uploads or saving.
236
+ - **Money is in credits, with a number**: "the full video uses about N credits". Never "the cheap
237
+ pieces" or "the expensive render".
238
+ - **Problems:** say what it means for them and what they can do, in one or two sentences. No error
239
+ codes or stack traces. Only raise a problem that changes something for them.
240
+ - **Use their words**: script, voice, hook, scene, host, ending, logo, the product shot.
241
+ - **No filler**: no "Great question!", "Certainly!", "I hope this helps!", no restating what they
242
+ just said at length.
243
+
244
+ Example. Instead of a dozen lines about render tools, scripts, uploads and portraits, send:
245
+ "Got it: warm tone, home podcast studio, Brielle as the skeptic and Mark as the believer." then
246
+ "Script and voices are done (about 34 seconds)." then "Ready for you to review: the script, voices,
247
+ both hosts and the ending. You can also review your recipe ingredients in the app: <link>. Say go
248
+ here and I'll make the full video (about N credits)."
249
+
250
+ ${SKILL_FRESHNESS}`;
251
+ // GOOSE-3937: a cloud agent's sandbox (ChatGPT agent, Meta AI, Grok) has a
252
+ // shell but no GooseWorks connector, because those apps load tools from their
253
+ // own settings. `gooseworks tool` calls the same MCP tools with the CLI login.
254
+ const CLI_TOOL_BRIDGE = `## No GooseWorks tools in this session? Call them through the CLI
255
+
256
+ The GooseWorks tools named here (for example \`account_whoami\`, \`brand_onboarding\`,
257
+ \`brand_read\`) normally come from the GooseWorks connector. If they are not registered in this
258
+ session but you have a shell with the \`gooseworks\` CLI signed in (a cloud sandbox such as
259
+ ChatGPT agent, Meta AI or Grok), call the same tool from the shell:
260
+
261
+ \`\`\`bash
262
+ npx gooseworks tool <name> '<arguments as a JSON object>'
263
+ \`\`\`
264
+
265
+ - Run \`npx gooseworks tool --list\` once first. It prints the server's rules for using the tools
266
+ and every tool name; \`npx gooseworks tool <name> --schema\` shows one tool's arguments.
267
+ - The result prints as JSON. When a tool answers with a one-line summary, the fields you act on
268
+ (such as \`next_step\` and ids) follow under "Data:". It exits 1 when the tool reports an error.
269
+ - Nothing renders as a widget here, so tell the user the result in plain words.
270
+ - The same rules apply as over the connector: state the credit total and get the user's yes
271
+ before paid work.
272
+ - If it says you are not logged in, run \`npx gooseworks login --device --no-wait\`, show the
273
+ user the link and code it prints, and run \`npx gooseworks login --device\` once they approve.
274
+
275
+ When the tools are registered in this session, call them directly instead.`;
276
+ const ENVIRONMENT_IDENTITY = `## Keep the selected connection for the whole run
277
+
278
+ Call \`account_whoami\` on the connection that owns the brand/project before the first write.
279
+ Keep \`environment.name\` and the public \`environment.api_origin\` with this run. Respect the
280
+ user's selected production or staging connection. If multiple connections are available and
281
+ none was selected, resolve that once. Missing or unknown identity is uncertainty, not
282
+ permission to switch. Never infer the environment from credits, billing links or Node mode.
283
+
284
+ Reads, project creation, uploads, generation, polling and final updates all use that same
285
+ connection. If a production request fails, resume or report the failure on production;
286
+ never retry it on staging or recreate the project there. Before retrying a timed-out write,
287
+ read back the existing project or paid request on the selected connection.
288
+
289
+ Local/CLI proxy origin must match \`environment.api_origin\`. If it differs, use the MCP
290
+ relay on the selected connection (\`GW_MEDIA_VIA=mcp\`) before paid calls. Do not change
291
+ credentials or API origins to recover a failed write. An explicit user-requested move is
292
+ a separate operation, with the existing project and paid requests reconciled first.`;
293
+ const ASSET_READINESS = `## Check assets for the selected format before spending
294
+
295
+ For a template, read its structured \`asset_readiness\` from
296
+ \`video_catalog_list { kind: "formats", brand_id }\`. \`missing\` names gaps;
297
+ \`needs_review\` means suitability is unverified, including older recipes without
298
+ structured requirements. \`ready\` describes assets only, not script or budget approval.
299
+ For a custom video, derive requirements from its actual approved scenes.
300
+
301
+ Inspect candidate files for the chosen product and format. A catalog photo can contain
302
+ multiple objects, other products or a person; its existence or approved status does not
303
+ make it a standalone image of the selected product. Check object count, framing, readable
304
+ print and real image bytes. A service conversation has no automatic packshot requirement.
305
+
306
+ Reuse a suitable approved image first. A free crop or cutout is a new file: keep the
307
+ original, inspect the result and include it in the normal ingredient review. When no
308
+ usable input exists, explain the gap before generation. Estimate any paid preparation
309
+ separately before spending. Record the selected asset and inspection in the project
310
+ review set; do not add a separate approval round.`;
311
+ /** Same canonical capture policy for the router and both video entrypoints. */
312
+ const DURABLE_BRAND_CAPTURE = `## Save durable brand answers, then verify them
313
+
314
+ Read the selected brand with \`brand_read { brand_id, sections: ["summary", "kit", "products", "learnings", "onboarding"] }\`
315
+ (older clients: \`brand_get_context\` with the same sections, only when that tool is advertised).
316
+ Keep founder answers, user corrections, research and your own hypotheses distinct. Reuse
317
+ matching saved answers; ask only about gaps.
318
+
319
+ When the user asks to remember a rule, answers a brand interview, or explicitly corrects a
320
+ standing fact, save that answer in the same turn. The capture request authorizes those answers;
321
+ do not ask for approval again. A direction for this one video stays in its brief. If the scope
322
+ is genuinely ambiguous, ask whether it applies to future videos before saving a standing rule.
323
+
324
+ Use the **live registered schema**. Where supported, call \`brand_update\` with
325
+ \`knowledge_intent: "user_correction"\` and \`user_statement\` containing the user's exact,
326
+ verbatim answer, not your paraphrase or researched text.
327
+ For an inference or suggested improvement, use \`knowledge_intent: "agent_proposal"\`. Show the
328
+ before/after change from your prior read and proposed value; retain the returned proposal IDs
329
+ and say the user must accept it in the app. Link only a review surface actually returned by a
330
+ tool; the compact \`knowledge_updates\` response does not itself contain a diff or URL.
331
+ A pending proposal is not a saved fact. Never call an unavailable
332
+ tool or silently relabel research or your inference as something the user said.
333
+
334
+ The safe structured shape is \`patch: { knowledge: { positioning?, audience?, voice?,
335
+ instructions?, brandType?, tagline?, valueProps? } }\`, using only fields present in the live
336
+ schema. Inferred rules/taste go in an \`instructions\` proposal with a rationale, never in
337
+ \`patch.facts\`. Prefix every video-only preference in that proposed text with "Video preference:"
338
+ so it remains production direction after acceptance. Preserve unrelated instructions when
339
+ proposing a merged replacement.
340
+
341
+ | User answer | Canonical write |
342
+ | --- | --- |
343
+ | 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. |
344
+ | 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. |
345
+ | Required wording or pronunciation | \`patch.facts: [{ kind: "must", text }]\`; pronunciation is exactly \`Pronounce "<term>" as "<say_as>"\`. |
346
+ | Forbidden claim, word or visual | \`patch.facts: [{ kind: "dont", text }]\`. |
347
+ | 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:". |
348
+
349
+ Facts are existing \`ad_brand_learning\` rows with user provenance; they are not a second profile.
350
+ Update a matching rule by its returned \`id\` instead of adding duplicates. Preserve unrelated
351
+ rules and the user's exact meaning. Only use the legacy facts shape for explicitly user-authored
352
+ answers when the live schema lacks intent fields; agent suggestions still need a proposal path.
353
+
354
+ **Claims and plans have separate gates.** A founder assertion or proof point is not an approved,
355
+ evidence-backed claim or consent to quote a customer. Use the existing evidence/claims and
356
+ operating-plan tools only if registered, following their proposal, evidence and confirmation
357
+ requirements. Never encode a spend cap, approver or emergency stop as a learning. If that write
358
+ path is missing, report the specific unsaved item and keep it pending for the supported review
359
+ surface; do not claim it was saved or create a parallel local profile.
360
+
361
+ After every write, **read back before saying saved**: use \`brand_read\` with \`kit\`, \`learnings\`
362
+ or \`onboarding\` as appropriate, or \`brand_onboarding { action: "status", brand_id }\` after an
363
+ onboarding answer. Verify the intended field/rule, its source and the absence of a conflicting
364
+ duplicate. A generic success response, pending proposal, ignored key or truncated result is not
365
+ proof. Report partial saves honestly. Carry the verified rules into the current task and the
366
+ routed skill; claims still pass their own safety gate.`;
367
+ /** The existing media library and learnings are sufficient for a minimal taste brief. */
368
+ const VIDEO_TASTE_CAPTURE = `## Video taste — reuse examples and preferences
369
+
370
+ When asked to capture video taste, or when the user volunteers a durable video preference,
371
+ first read the saved learnings and \`media_list { brand_id, scope: "brand", scope_id: brand_id,
372
+ tags: ["video-taste"], limit: 100 }\`. Follow \`next_cursor\` before deciding an example is absent.
373
+ Do not force a taste interview before an unrelated task or ask again for an existing preference.
374
+
375
+ Save what the user has already supplied first. Then ask only the missing useful question, for
376
+ example: "What do you like about this video—its pace, voice, captions, or look?" An inaccessible
377
+ link can still be saved as a link with the user's explanation; do not pretend you watched it.
378
+
379
+ - **Direct clip or video file:** register with \`media_upload { brand_id, scope: "brand",
380
+ scope_id: brand_id, kind: "video", source: { type: "url", url }, tags: ["video-taste", "reference-only"],
381
+ metadata: { purpose: "video_taste", source_url: url, provenance: "user", captured_at: <ISO timestamp>,
382
+ preference: <the user's explanation> } }\`. For a file use the live file/bytes upload flow and
383
+ \`media_confirm\` after a presigned upload. Registration of a URL does not copy or inspect it.
384
+ - **Instagram/post/page link:** the same registration with \`kind: "document"\`; it is a link
385
+ bookmark, not downloadable footage or an indexed transcript. Do not fabricate a direct clip URL.
386
+ - **Preferences:** save the user's reasons, likes and dislikes through the facts mapping above.
387
+ Read and preserve existing facts before updating one. Do not invent \`video_preferences\` or
388
+ new \`video_lab\` keys; the live kit patch accepts only its documented asset fields.
389
+ - **Deduplicate:** reuse a matching returned media row, then \`media_update\` its title/tags/metadata
390
+ if needed; preserve existing metadata and tags. Do not create another row for the same example.
391
+ Read back with \`media_list\` and \`brand_read\` learnings before claiming it was saved.
392
+ Check the returned row belongs to this brand: URL deduplication may return another brand's
393
+ existing row. Do not relabel that row or claim success unless the current brand's scoped read
394
+ actually returns it. Report an unsaved association if no supported attach path is available.
395
+
396
+ Never put third-party taste examples into kit reference images: \`kind: "reference"\` at brand
397
+ scope writes there. The tags and metadata above record purpose and provenance; **they do not
398
+ grant or enforce usage rights**. Study the structure, pacing and look only. Never use the example's
399
+ footage, face, product, testimonial or claims in a new ad without independently verified permission.
400
+
401
+ Read video-only entries prefixed \`Video preference:\` from both saved learnings and
402
+ \`kit.instructions\`, including accepted proposals. Keep them out of required or forbidden
403
+ dialogue. Build a brief from the verified readback: preferred pace, voice, caption treatment, visual style,
404
+ formats to favour/avoid, reference links and the user's reasons. Say what is still unknown.
405
+ Pass it with the brand rules into the existing video workflow. A one-video request overrides a
406
+ default for that project; it does not silently rewrite the brand's standing preference.`;
35
407
  /**
36
408
  * THE registry of entry skills (GOOSE-3190) — one list, four consumers:
37
409
  * - `gooseworks install` / `update` / login-refresh write exactly these dirs,
@@ -49,6 +421,7 @@ function getEntrySkills() {
49
421
  { name: 'goose-ads', content: getGooseAdsSkillContent() },
50
422
  { name: 'goose-video', content: getGooseVideoSkillContent() },
51
423
  { name: 'goose-video-local', content: getGooseVideoLocalSkillContent() },
424
+ { name: 'make-custom-video', content: getMakeCustomVideoSkillContent() },
52
425
  { name: 'goose-product-photos', content: getGooseProductPhotosSkillContent() },
53
426
  ];
54
427
  }
@@ -73,9 +446,10 @@ description: >
73
446
  GooseWorks growth coworker and specialist-skill router. Research brands, customers, competitors,
74
447
  creators, markets, and prospects; analyze ads and performance; create ads, product photos,
75
448
  graphics, and video; search and scrape public web and social data; find and enrich leads.
449
+ Capture founder answers, brand rules, audience depth, and video taste in the existing brand.
76
450
  Use it as the single GooseWorks entry point for brand growth, B2B, sales, research, and GTM work.
77
451
  category: general
78
- version: 1.0.0
452
+ version: 1.1.2
79
453
  author: GooseWorks
80
454
  tags: [gooseworks, data, scraping, search, reddit, twitter, linkedin, email, people, research, gtm, leads, prospecting]
81
455
  ---
@@ -86,9 +460,19 @@ You have access to GooseWorks — an AI coworker with specialist skills for rese
86
460
 
87
461
  This skill is also the **parent router** for the GooseWorks family. Data/GTM work you handle here (see "How to Use"); specialized work you hand off to a dedicated \`goose-*\` skill.
88
462
 
463
+ ${CUSTOMER_TALK}
464
+
465
+ ${CLI_TOOL_BRIDGE}
466
+
89
467
  ## Route to the right skill FIRST
90
468
 
91
- First apply the **Common company onboarding** gate below. Preserve the user's original request while onboarding, then continue with it as soon as onboarding is complete. Then load the brand context (**"Load the brand context FIRST"**, immediately below). After that, check whether the request belongs to a specialized domain. If so, **switch to that skill** instead of the data flow below:
469
+ First apply the **Common company onboarding** gate below. Preserve the user's original request while onboarding, then continue with it as soon as onboarding is complete. For video work, load the current matching workflow from the selected connection first: \`goose-video\` for a new request, \`make-custom-video\` for an explicit original/reference brief, or \`goose-video-local\` for an existing template project/batch. Read an existing project first to determine its actual route and retain its approved packages. Fetch with the advertised \`catalog_fetch { type: "skill", slug }\`; an installed copy or old chat is only a bootstrap. Then load the brand context (**"Load the brand context FIRST"**, immediately below), search the Brand Brain for the task (**"Search the Brand Brain, then propose"**), and follow the matching workflow with both. For other specialized work, **switch to that skill** after loading the brand instead of the data flow below:
470
+
471
+ ${VIDEO_GUIDE_COMPATIBILITY}
472
+
473
+ For "interview me about the brand", "save our brand rules", "refine our audience", or "remember
474
+ our video taste", stay here and follow **Guided brand capture** below. This extends the current
475
+ brand and onboarding flow; it does not create another onboarding checklist.
92
476
 
93
477
  | If the user wants… | Route to | How |
94
478
  | --- | --- | --- |
@@ -99,29 +483,40 @@ Examples — all of these route to \`goose-ads\`, not the data flow: "remix this
99
483
 
100
484
  ## Load the brand context FIRST (mandatory — before you route, and before you ask anything)
101
485
 
102
- **Call \`brand_get_context\` before the first substantive step of ANY task**, and before you route to a specialist skill. It is a cheap, read-only call that returns the brand's canonical facts:
486
+ **Call \`brand_read { brand_id, sections: ["summary", "kit", "products", "learnings", "onboarding"] }\` before the first substantive step of ANY task**, and before you route to a specialist skill. Older clients can use \`brand_get_context\` with the same sections only when that tool is advertised. It is a read-only call that returns the brand's canonical facts and saved rules:
487
+
488
+ For videos, read the current workflow first as described above, then load all four creative
489
+ sections (summary, kit, products and learnings) before suggesting formats, choosing angles or
490
+ writing a script. Onboarding facts alone are insufficient. Carry saved rules and kit assets
491
+ into the specialist's brand preparation. \`Video preference:\` rules describe the look, voice
492
+ and pacing; they are not lines to read aloud. On an approved resume, preserve the saved brand
493
+ inputs and packages; an intentional change uses the existing affected review/approval gates.
103
494
 
104
495
  | It returns | Use it for |
105
496
  | --- | --- |
106
497
  | **voice** — tone, style, banned phrasing | Any copy, script, caption, hook, or headline. Don't ask "what tone?" |
107
498
  | **products** — names, descriptions, pricing, links, imagery | Picking the product to feature. Don't ask "which product?" — offer the list. |
108
499
  | **audience** — segments, demographics, jobs-to-be-done | Targeting, angles, creator fit. Don't ask "who is this for?" |
109
- | **positioning** — category, value props, proof points, tagline | Angles, offers, competitive framing. Don't ask "what makes you different?" |
500
+ | **positioning** — category, value props, proof points, tagline | Angles, offers, competitive framing. Don't ask "what makes you different?" Kit proof points are proof an ad may state as written; other positioning text is context. |
110
501
  | **research status** — whether the brand's research pass has completed | Whether the facts are trustworthy yet, or still being filled in. |
111
502
 
112
503
  Then:
113
504
 
114
- 1. **Pass what it returned INTO the routed skill.** When you hand off to \`goose-ads\`, \`goose-video\`, \`goose-product-photos\`, \`goose-graphics\`, or a fetched Brand Growth recipe, carry the voice / products / audience / positioning with you. Do **not** make the routed skill re-derive them, and do **not** re-run brand research when the context is already there.
115
- 2. **Never re-ask the user for something the brand context already answers.** If a routed skill's own prose asks a question the context answers, the context wins — answer it yourself and move on. Ask only for what is genuinely missing or ambiguous.
505
+ 1. **Pass what it returned INTO the routed skill.** When you hand off to \`goose-ads\`, \`goose-video\`, \`goose-product-photos\`, \`goose-graphics\`, or a fetched Brand Growth recipe, carry the voice / products / audience / positioning with you, plus the evidence brief from the Brain search below. Do **not** make the routed skill re-derive them, and do **not** re-run brand research when the context is already there.
506
+ 2. **Never re-ask the user for something the brand context already answers.** If a routed skill's own prose asks a question the context or the Brain search answers, they win — answer it yourself and move on. Ask only for what is genuinely missing after both, or a decision that is the user's to make.
116
507
  3. **If research status is not complete**, say so in one line, use what you have, and continue. Only run brand research when the context comes back empty or the user asks for it.
117
- 4. **If \`brand_get_context\` is unavailable** (no MCP connection), fall back to \`get_brand_kit\` for the selected brand and treat its fields the same way. If neither is available, tell the user the GooseWorks MCP connection is needed rather than guessing brand facts.
118
- 5. **Treat it as read-only.** Writing brand facts back is the reconciliation flow in \`goose-ads\` (ask first, then \`update_brand_kit\`) — not something this router does.
508
+ 4. **If \`brand_read\` is unavailable**, refresh the GooseWorks connection or tool list. An older connection may expose \`brand_get_context\` / \`get_brand_kit\`; use those only when actually advertised. Never require a legacy tool name or guess brand facts.
509
+ 5. **A read grants no write permission.** Save explicit durable answers/corrections with the capture policy below. Propose agent-derived changes for review; never overwrite confirmed knowledge with research or a guess.
119
510
 
120
- Never invent a brand fact. If it isn't in the brand context and the user hasn't said it, ask.
511
+ Never invent a brand fact. If it isn't in the brand context and the user hasn't said it, search
512
+ the Brand Brain next. Ask the user only when that search cannot answer it and the answer is
513
+ theirs to give.
514
+
515
+ ${BRAIN_FIRST_ENTRY}
121
516
 
122
517
  ## Setup
123
518
 
124
- 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\`.
519
+ 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\`. In a cloud sandbox or over SSH, sign in with a code instead: run \`npx gooseworks login --device --no-wait\` yourself, show the user the link and code it prints, and run \`npx gooseworks login --device\` once they have approved. To log out: \`npx gooseworks logout\`.
125
520
 
126
521
  ### Choose the available runtime — MCP first, then CLI
127
522
 
@@ -129,20 +524,33 @@ Skills may describe a managed provider request as an environment-neutral operati
129
524
  \`provider\`, \`method\`, \`path\`, and optional \`query\` or \`body\`. Execute the operation through
130
525
  the first available runtime:
131
526
 
132
- 1. If the matching GooseWorks MCP tool is registered, use it. For ScrapeCreators, pass the
133
- operation directly to \`call_data_provider\`. This is the preferred path in ChatGPT, Cowork,
134
- and other terminal-free clients. Do not shell out and do not ask for a separate provider key.
527
+ 1. If the matching GooseWorks MCP tool is registered, use it. For ScrapeCreators, pass a GET
528
+ operation directly to \`data_call_provider\` (\`provider\`, \`path\`, \`query\`) and a POST
529
+ operation to \`data_post_provider\` (\`provider\`, \`path\`, \`body\`, optional \`query\`).
530
+ This is the preferred path in ChatGPT, Cowork, and other terminal-free clients.
531
+ Do not shell out and do not ask for a separate provider key.
135
532
  2. Otherwise, if a local terminal and the \`gooseworks\` CLI are available, translate the same
136
533
  operation into \`gooseworks call <provider> <path>\` with its method, query, and body options.
137
534
  3. Otherwise, follow the provider dependency's direct-key path only when the user has supplied
138
535
  their own key. If no runtime is available, explain what connection is missing; never pretend
139
536
  the provider call ran.
140
537
 
538
+ Managed provider calls are paid on the MCP and CLI runtimes alike (a ScrapeCreators call costs 1
539
+ credit today, and each result reports what it charged). Before a skill's first paid call, tell the
540
+ user roughly how many calls it will make and the credit total, and get their yes.
541
+
141
542
  The same selection applies to catalog and account operations. When the CLI is unavailable but the
142
- \`mcp__gooseworks__*\` tools are connected, use these equivalents:
143
- - \`gooseworks search <q>\` → the **\`search_skills\`** MCP tool.
144
- - \`gooseworks fetch <slug>\` → the **\`fetch_skill\`** MCP tool (same content/scripts/files/deps).
145
- - \`gooseworks credits\` → the **\`get_ad_credits\`** MCP tool.
543
+ GooseWorks MCP tools are connected, use these equivalents. Match on the tool name: a coding agent
544
+ may show a server prefix (for example \`mcp__gooseworks__catalog_search\`), a chat app may not.
545
+ - \`gooseworks search <q>\` → **\`catalog_search { type: "skill", query: "<q>" }\`**.
546
+ - \`gooseworks fetch <slug>\` → **\`catalog_fetch { type: "skill", slug: "<slug>" }\`** (same content/scripts/files/deps).
547
+ - \`gooseworks credits\` → **\`account_whoami\`** (the balance is \`credits.available_credits\`).
548
+
549
+ If one of these tools is missing, the GooseWorks connection or its tool list is stale: ask the
550
+ user to reconnect or refresh GooseWorks. Installing or updating the \`gooseworks\` CLI never fixes
551
+ a missing connector tool, so never send a chat-app user to a terminal for it. An agent that has
552
+ its own shell and a signed-in CLI can call the tool with \`npx gooseworks tool\` instead (see
553
+ "No GooseWorks tools in this session?" above).
146
554
 
147
555
  Discovery, skill fetching, and ScrapeCreators-backed Brand Growth workflows work fully CLI-free
148
556
  this way. Task skills own the endpoint and analysis workflow; this runtime rule owns how the same
@@ -172,16 +580,53 @@ When onboarding returns a review link, show that single link and ask the user to
172
580
  Use the host's native question controls. Ask one short group at a time and rely on the live tool schema for accepted values.
173
581
 
174
582
  1. **Start** — If status returns \`start\`, ask for the company website or Apple App Store URL. Also offer the optional hero product URL and “Where do you do your work?” choices: Slack, WhatsApp, iMessage, Claude Code, Claude, Codex, and ChatGPT. Call \`action: "start"\`; server-side research begins immediately. If status returns \`select_brand\`, ask which company/client to use. Otherwise reuse the only brand automatically.
175
- 2. **Your coworker** — Ask what they want to name their Growth Coworker. A text-only client may keep the default avatar; do not block on an image. Save with \`action: "save_coworker"\`.
176
- 3. **Your company** — Use the returned \`company_draft\` as the starting point and ask the user to verify or edit: what they sell (\`marketCategory\`), where people buy (\`appPlatforms\`), primary customer, customer problem, promised outcome, and optional differentiator. Save with \`action: "save_company"\`.
177
- 4. **Your taste** — In a terminal or CLI host, use the returned \`taste_url\`: open it when the host supports opening links and always show one clickable **Choose your taste in GooseWorks** link. Ask the user to heart or skip ads on that page, click **Continue** or **Skip this**, return to the agent, and reply \`done\`. Do not print, enumerate, or summarize \`taste_deck\` in the terminal. After \`done\`, call \`brand_onboarding { action: "status" }\` again and follow the refreshed \`next_step\`. In a chat host that renders images, show only the one image attached by the tool and save each Love/Skip decision with \`action: "save_taste"\`; send \`complete: true\` after three hearts or an explicit skip.
178
- 5. **First campaign** — Ask **“What’s happening right now?”**: launch \`launch\`, promotion \`promo\`, seasonal moment \`seasonal\`, or nothing special \`nothing\`, plus an optional note. Call \`action: "propose_campaign"\`, show the returned editable card (name, objective, offer, audience, 2–3 angles, CTA, and product URL), and save edits with \`action: "save_campaign"\`. Send \`accept: true\` only after approval; acceptance can start the complimentary first creatives.
179
- 6. **Where you are** — Ask monthly ad spend (\`none\`, \`under_1k\`, \`1k_5k\`, \`5k_25k\`, \`25k_plus\`), annual revenue (\`under_1m\`, \`1m_10m\`, \`10m_100m\`, \`100m_plus\`), the 90-day goal, current channels (an empty list is a valid “nothing yet”), and at least one channel they are willing to use. Channel values: \`paid_social\`, \`search_ads\`, \`content\`, \`creators\`, \`seo\`, \`communities\`, \`referrals\`, \`partnerships\`, \`outbound\`, \`app_stores\`, \`other\`. Save with \`action: "save_progress"\`.
180
- 7. **Review** — Show the returned founder, researched, and inferred facts with their provenance. The user may correct positioning, audience, voice, value propositions, proof points, or competitors through \`action: "review_research"\`. Complete the review even when research is still running, failed, or sparse; never trap the user waiting for it.
181
- 8. **Channels** — If \`channel_connected\` is already true, this is complete automatically. Otherwise ask whether they want to connect Slack, WhatsApp, or iMessage later, or skip for now. An explicit skip is valid; call \`action: "complete_channels"\`.
583
+ 2. **Your coworker** — The current flow accepts the default coworker automatically. If an older session returns \`coworker\`, refresh \`status\`; do not introduce a naming/avatar question. Rename only when the user asks and the live tool supports it.
584
+ 3. **Your company** — Use the returned \`company_draft\` plus the user's existing answers. Ask only to verify missing or ambiguous details: what they sell (\`marketCategory\`), where people buy (\`appPlatforms\`), primary customer, customer problem, promised outcome, and optional differentiator. Monthly Meta ad spend belongs here when absent: \`none\`, \`under_10k\`, \`10k_50k\`, \`50k_150k\`, \`150k_plus\`, or \`not_sure\`. Save the merged required company object with \`action: "save_company", company: { … }\`; read \`status\` back.
585
+ 4. **Your taste** — In a terminal or CLI host, use the returned \`taste_url\`: open it when the host supports opening links and always show one clickable **Choose your taste in GooseWorks** link. Ask the user to heart or skip ads on that page, click **Continue** or **Skip this**, return to the agent, and reply \`done\`. Do not print, enumerate, or summarize \`taste_deck\` in the terminal. After \`done\`, call \`brand_onboarding { action: "status" }\` again and follow the refreshed \`next_step\`. In a chat host that renders images, show only the one image attached by the tool and save each Love/Skip decision with \`action: "save_taste", taste: { hearted_ids, skipped_ids, complete }\`; set \`taste.complete: true\` after three hearts or an explicit skip.
586
+ 5. **First campaign** — Ask **“What’s happening right now?”**: launch \`launch\`, promotion \`promo\`, seasonal moment \`seasonal\`, or nothing special \`nothing\`, plus an optional note. Call \`action: "propose_campaign"\`, show the returned editable card (name, objective, offer, audience, 2–3 angles, CTA, and product URL), and save edits with \`action: "save_campaign"\`. Send \`campaign.accept: true\` only after approval; acceptance can start the complimentary first creatives.
587
+ 6. **Review** — Show the returned founder, researched, and inferred facts with their provenance. The user may correct positioning, audience, voice, value propositions, proof points, or competitors through \`action: "review_research", review: { action: "correct", field, value }\`. A proof-point edit does not approve a claim. Complete with \`review: { action: "complete" }\` even when research is still running, failed, or sparse; never trap the user waiting for it.
588
+ 7. **Channels** — If \`channel_connected\` is already true, this is complete automatically. Otherwise ask whether they want to connect Slack, WhatsApp, or iMessage later, or skip for now. An explicit skip is valid; call \`action: "complete_channels"\`.
589
+
590
+ The former revenue / 90-day-goal / \`save_progress\` screen is retired. Do not insert it into
591
+ onboarding. Ask those human-only questions later only when the user's task needs them.
182
592
 
183
593
  Do not ask for role, discovery source, who makes creatives, who manages ads, or a separate “what do you want to do first?” menu. Those belonged to the retired CLI questionnaire. The task the user already asked for is their first task.
184
594
 
595
+ ## Guided brand capture
596
+
597
+ Use this when the user requests a founder interview, audience/rules capture, or video taste.
598
+ Keep their original task pending. Load the current brand first, compare it with information
599
+ already volunteered in this chat, and **save known information first** using the canonical
600
+ mapping below. Do not run a long questionnaire as a prerequisite for making an ad.
601
+
602
+ For facts needed by the task but absent from the read, call \`knowledge_search\` first if it is
603
+ registered, as in **"Search the Brand Brain, then propose"**. Use returned citations and states
604
+ honestly: an empty, building or failed index is not proof that the brand has no answer. Do not re-scrape or ask the founder for a fact already
605
+ answered by trustworthy saved knowledge.
606
+
607
+ Ask one short group of missing human-only facts at a time, in plain language, with the relevant
608
+ known answer in that same question. "Skip" or "not sure" is valid. Examples, **only for gaps**:
609
+
610
+ | Gap | Useful question |
611
+ | --- | --- |
612
+ | Founder origin or conviction | "What made you start this, and what do you believe that alternatives get wrong?" |
613
+ | Audience depth | "Who buys first, what problem pushes them to act, and what nearly stops them?" |
614
+ | Buying trigger or alternatives | "What happens just before they look for you, and what do they use instead?" |
615
+ | Rules | "What must we always say or show, and what must we never say or imply?" |
616
+ | Proof | "What evidence supports that result, and do we have permission to quote the customer?" Keep unsupported claims pending. |
617
+ | Video taste | "Share a video you like and what you would keep or avoid: pace, voice, captions, or look." |
618
+
619
+ Keep skipped or uncertain answers as gaps in the brief; do not save them as confirmed facts.
620
+ Save each answered group and read it back before the next group. Stop when the requested capture
621
+ is covered or the user skips; resume from canonical saved answers after interruption. Present a
622
+ short brief containing verified answers, attribution, pending proposals and remaining gaps, then
623
+ continue the original task. Review happens in this chat plus any proposal review link returned
624
+ by the tools. Do not promise an unavailable evidence, claims or plan write.
625
+
626
+ ${DURABLE_BRAND_CAPTURE}
627
+
628
+ ${VIDEO_TASTE_CAPTURE}
629
+
185
630
  ## Brand Growth discovery
186
631
 
187
632
  Brand Growth is a collection inside the normal skill catalog, not a command or installable pack. Use these known routes when relevant, while preserving all existing B2B, sales, research, lead-generation, and data behavior:
@@ -190,7 +635,7 @@ Brand Growth is a collection inside the normal skill catalog, not a command or i
190
635
  | --- | --- |
191
636
  ${(0, routes_1.renderBrandGrowthTable)()}
192
637
 
193
- Fetch the named public skill before following it. You already called \`brand_get_context\` — hand the brand's voice, products, audience, and positioning to the fetched skill instead of letting it re-derive or re-ask them. Provider helpers such as \`scrapecreators-api\` and \`transcript-intelligence\` are dependencies, not user-facing results.
638
+ Fetch the named public skill before following it. You already called \`brand_read\` — hand the brand's voice, products, audience, and positioning to the fetched skill instead of letting it re-derive or re-ask them. Provider helpers such as \`scrapecreators-api\` and \`transcript-intelligence\` are dependencies, not user-facing results.
194
639
 
195
640
  For a multi-part request, repeat this routing check before each new job. Fetch and follow the
196
641
  closest outcome skill first (for example, \`comment-mining\`, \`creator-profile-teardown\`, or
@@ -320,30 +765,52 @@ The \`gooseworks\` CLI sends authenticated requests (Bearer \`GOOSEWORKS_API_KEY
320
765
 
321
766
  ## Rules
322
767
 
323
- 0. **Call \`brand_get_context\` before anything else**, pass what it returns into whatever skill you route to, and never re-ask the user for a fact it already answers (see "Load the brand context FIRST").
768
+ 0. **Read the canonical brand context before substantive work**, pass what it returns into whatever skill you route to, and never re-ask the user for a fact it already answers (see "Load the brand context FIRST").
324
769
  1. **Consider a GooseWorks skill when it fits the task** — scraping, research, lead gen, enrichment, especially at scale, behind auth, or from a specific source. For a quick lookup your built-in tools are fine; use your judgement and pick the best tool for the user.
325
- 2. **Before paid operations**, tell the user the estimated credit cost
326
- 3. **If a \`gooseworks\` command exits with "Not logged in"**: tell the user to run \`npx gooseworks login\`
770
+ 2. **Before paid operations**, tell the user the estimated credit cost and get their yes
771
+ 3. **If a \`gooseworks\` command exits with "Not logged in"**: tell the user to run \`npx gooseworks login\` (in a cloud sandbox or over SSH, use the code sign-in described in Setup)
327
772
  4. **Parse JSON responses** and present data in a readable format to the user
328
773
  5. **When running scripts**: save to \`/tmp/gooseworks-scripts/\`, install pip deps, then execute. NEVER pollute the user's project directory
329
774
  6. **Output files default to \`~/Gooseworks/\`** — always confirm with the user before saving
330
775
  7. **Prefer \`gooseworks call\` over raw curl** — if it returns an error, first fix the parameters (check types, required fields, format) and retry. Only fall back to raw curl if you have strong reason to believe it is a CLI bug, not a parameter issue.
331
776
  `;
332
777
  }
778
+ /**
779
+ * How an entry skill that runs on the GooseWorks connector says what to do when
780
+ * a tool is missing (QA-26 / VWR14). A chat agent that could not find a tool
781
+ * used to tell the customer to run `gooseworks install` in a terminal they do
782
+ * not have; a missing connector tool is fixed by reconnecting, never by a CLI.
783
+ */
784
+ function connectorPrerequisite(exampleTool, extra = '') {
785
+ return `## Prerequisite — the GooseWorks connector tools
786
+
787
+ Everything below runs through the GooseWorks connector's tools, by the names this skill uses.
788
+ Match on the tool name: a coding agent may show a server prefix (for example
789
+ \`mcp__gooseworks__${exampleTool}\`), a chat app may not.${extra}
790
+
791
+ If a tool named here is missing, the GooseWorks connection or its tool list is stale: ask the
792
+ user to reconnect or refresh GooseWorks in their app's connector settings. Installing or
793
+ updating the \`gooseworks\` CLI never fixes a missing connector tool, so never send a chat-app
794
+ user to a terminal for it. Only a terminal coding agent (Claude Code, Codex, Cursor) that has no
795
+ GooseWorks tools at all connects them, in that terminal, with \`gooseworks install --mcp\` plus
796
+ \`--claude\`, \`--codex\` or \`--cursor\`, then a restart.
797
+
798
+ Older notes or skill copies may name tools the connector no longer lists. Use the tool this skill
799
+ names instead; \`catalog_fetch { type: "skill", slug: "gooseworks-guide" }\` maps every old name.`;
800
+ }
333
801
  /**
334
802
  * Returns the goose-ads entry SKILL.md content (the `goose-ads` entry skill,
335
803
  * formerly `ads-remix`).
336
804
  *
337
805
  * This is the ads domain skill and a THIN WRAPPER over the backend's single ad
338
- * generation workflow (adRemixBatchesService, exposed via the new
339
- * mcp__gooseworks__*_remix_batch / regenerate_creative tools — the SAME workflow
340
- * the ads frontend uses). The skill no longer generates images, manages renders,
341
- * or uploads files itself; the backend reserves credits, runs the cloud pipeline,
342
- * and bills. The skill ALSO routes ad analytics/intelligence to recipe skills
343
- * fetched on demand from goose-skills. It is SEPARATE from the `gooseworks` GTM
344
- * skill — different domain, different tools — and Claude loads it (or the
345
- * `gooseworks` parent router hands off to it) when the user wants to make/edit an
346
- * ad, research a brand for ads, or analyze ad performance.
806
+ * generation workflow (adRemixBatchesService, exposed on the connector as
807
+ * `ads_generate` / `ads_creative_edit` — the SAME workflow the ads frontend
808
+ * uses). The skill never generates images, manages renders, or uploads files
809
+ * itself; the backend reserves credits, runs the cloud pipeline, and bills. The
810
+ * skill ALSO routes ad analytics/intelligence to recipe skills fetched on demand
811
+ * from goose-skills. Chat apps reach it through `catalog_fetch`, so every tool it
812
+ * names must be one the connector lists (QA-26 / VWR14; guarded by
813
+ * tests/skills/master-skill.test.ts against tests/fixtures/connector-tool-names.json).
347
814
  */
348
815
  function getGooseAdsSkillContent() {
349
816
  return `---
@@ -360,7 +827,7 @@ description: >
360
827
  app uses) — credits are reserved and billed server-side. Analytics recipes are fetched from
361
828
  goose-skills on demand.
362
829
  category: ads
363
- version: 2.4.0
830
+ version: 2.6.1
364
831
  author: GooseWorks
365
832
  tags: [gooseworks, ads, remix, static-ad, brand, creative, image, analytics, meta-ads, performance]
366
833
  ---
@@ -378,39 +845,70 @@ The GooseWorks ads skill. Two jobs:
378
845
  2. **Analyze ad performance** — fetch ad-analytics recipes from goose-skills on demand
379
846
  (these are unrelated to generation; see "Analyze / intelligence" below).
380
847
 
381
- ## Prerequisite — the GooseWorks MCP server is REQUIRED
848
+ It works the same in a chat app (ChatGPT, claude.ai, Cowork) and in a terminal coding agent:
849
+ everything except the few steps labelled terminal-only goes through the connector's tools.
850
+
851
+ ${CUSTOMER_TALK}
382
852
 
383
- Everything goes through the \`mcp__gooseworks__*\` tools. If they are not available, **stop and
384
- tell the user to run \`gooseworks install --claude --mcp\`** (and restart Claude Code). There is
385
- no HTTP/file fallback — the REST ad endpoints are session-cookie-only and reject your token.
853
+ ${CLI_TOOL_BRIDGE}
854
+
855
+ ${connectorPrerequisite('ads_generate', ' There is no HTTP or file fallback: the REST ad\nendpoints are session-cookie-only and reject your token.')}
386
856
 
387
857
  ## Start from the brand context — don't re-ask what it already answers
388
858
 
389
- If the \`gooseworks\` router handed you brand context, USE IT. If you were invoked directly, call
390
- \`brand_get_context\` first (falling back to \`get_brand_kit\` for the selected brand). It already
391
- answers most of what the flows below would otherwise ask the user:
859
+ If the \`gooseworks\` router handed you brand context and an evidence brief, USE THEM. If you were
860
+ invoked directly, call \`brand_read { brand_id, sections: ["summary","kit","products","learnings"] }\`
861
+ first, then search the Brand Brain (next section). Together they answer most of what the flows
862
+ below would otherwise ask the user:
392
863
 
393
- - **Which product to feature** → \`products[]\`. Offer the real catalog entries; never guess a
864
+ - **Which product to feature** → \`products.items\`. Recommend one real catalog entry; never guess a
394
865
  product name and never ask the user to list their products.
395
866
  - **The vibe / tone of the copy** → the brand's **voice**. Use it; don't ask "what tone?".
396
867
  - **Who the ad is for** → the brand's **audience**. Don't ask "who's the target?".
397
- - **The angle, offer framing, and what to claim** → **positioning**, value props, proof points.
868
+ - **The angle and offer framing** → **positioning** and value props, sharpened by the Brain:
869
+ lead with an angle past approved creatives proved, and drop anything a saved rule forbids.
870
+ - **What to claim** → only claim-grade proof: a search result with \`approved_ad_claim: true\`, the
871
+ kit's approved claims, or its proof points as written. Other Kit text, documents and
872
+ performance numbers are context.
398
873
  - **Logo, colors, fonts** → owned by the backend research pass. **Never re-derive them.**
399
874
  - **Whether the facts are trustworthy yet** → **research status**. If it isn't complete, say so in
400
875
  one line and continue; the batch queues and runs when research finishes.
401
876
 
402
- Ask only for what the context genuinely doesn't answer: the specific campaign intent (season,
403
- promo, which of several angles), the source ad, and anything the user must consent to.
877
+ Don't ask for the angle, product or tone: recommend them with a one-line reason. Ask only for a
878
+ decision the brand read and the Brain search cannot settle (for example an offer or season the
879
+ user hasn't mentioned) and for anything the user must consent to (rights, spend).
880
+
881
+ ${BRAIN_FIRST_ENTRY}
404
882
 
405
- ## Identity & credits
883
+ ## Credits — state the total, then get a yes
406
884
 
407
- - One agent-scoped token authenticates the \`gooseworks\` MCP tools. Never print it. The tools
408
- resolve your org automatically — you do NOT resolve an "Ads agent" or pass \`target\` for the
409
- generation tools.
410
- - **Credits are handled entirely by the backend.** \`submit_remix_batch\` reserves the estimated
411
- cost up front (it errors with \`insufficient_credits\` if the wallet is short — relay the
412
- message and stop) and bills only the images that actually complete. Call
413
- \`estimate_remix_batch\` first to tell the user the cost; \`gooseworks credits\` shows balance.
885
+ - One token authenticates the GooseWorks tools and resolves your org; never print it. The
886
+ generation tools need no \`target\`.
887
+ - **Nothing paid runs without the user's explicit yes in this chat, given after you state the
888
+ credit total.** The paid calls are \`ads_generate\` (without \`dry_run\`), every
889
+ \`ads_creative_edit\` action, \`ads_approval_decide\` with \`decision: "approve"\`, and every
890
+ \`data_call_provider\` / \`data_post_provider\` call (the data calls an analysis recipe or brand
891
+ research makes; \`gooseworks call\` in a terminal). Reads, dry runs, \`ads_creative_update\` and
892
+ \`request_campaign_generation\` (it only composes plans) are free.
893
+ - The balance is \`credits.available_credits\` from \`account_whoami\`; a dry-run \`estimate\` also
894
+ carries \`available_credits\`.
895
+ - The backend reserves the quoted credits when a paid call starts and bills only the images
896
+ that complete. A rejection with \`insufficient_credits\` means the wallet is short: tell the
897
+ user the total and their balance in plain words, offer fewer images or a top-up, and stop.
898
+ Never retry blindly.
899
+
900
+ How to get the total for each paid call:
901
+
902
+ | Paid call | Credit total to state |
903
+ | --- | --- |
904
+ | \`ads_generate\` with \`source.template_ids\` | The same call with \`dry_run: true\` returns \`estimate\`: \`total_credits\` for \`images\` images (plus \`credits_per_image\`, \`rates\` and \`unknown_template_ids\`). |
905
+ | \`ads_generate\` with \`source.community_ad_ids\` or \`source.creative_ids\` | No dry run (it returns \`not_available\`; these are priced when submitted). Pass \`quality\` explicitly (the user's choice, else \`high\`, the app default) and quote the images (each source's \`variants\` × its \`ratios\`) × \`estimate.rates.<quality>\` as the most it will cost (an engine without quality tiers costs the lower \`rates.low\`). \`rates\` comes with any template dry run; with no template at hand, dry-run one Surprise-me pick. |
906
+ | \`ads_creative_edit\` \`animate\` | The same call with \`dry_run: true\` returns its \`estimate\`. |
907
+ | \`ads_creative_edit\` \`regenerate\` / \`precision_edit\` | No dry run. Pass \`quality\` in the action's payload (without it \`precision_edit\` copies the source render's tier) and quote the images × \`estimate.rates.<quality>\` as the most it will cost: \`regenerate\` with \`mode: "variation"\` makes one per ratio (omitted \`ratios\` make three); \`edit\`, \`exact\` and \`precision_edit\` make one. |
908
+ | \`ads_creative_edit\` \`resize\` | No dry run and no quality setting: it renders at the server's default tier (\`high\` today). Quote one image per placement × the larger of \`estimate.rates.high\` and \`credits_per_image\` from a template dry run without \`quality\` (that dry run uses the server's default tier) as the most it will cost. \`platforms\` expands to every placement: meta 4, google 4, tiktok 2, linkedin 3, reddit 3, x 2; pass \`targets\` for fewer. |
909
+ | \`ads_creative_edit\` \`layerize\` | No dry run. It holds about 80 credits while it runs and charges the actual cost of the split (usually less). Say so and get the yes before sending \`layerize: { confirmed: true }\`. |
910
+ | \`ads_approval_decide\` approve | The \`credits\` that \`request_campaign_generation\` and \`ads_creative_read { brand_id, view: "approvals", batch_id }\` return for those plans. |
911
+ | \`data_call_provider\` / \`data_post_provider\` | The calls a recipe will make × their price: a ScrapeCreators call costs 1 credit today, and each result reports what it charged. A fal or ElevenLabs POST quotes free with the same call plus \`query: { quote_only: true }\`. State the rough total for the whole recipe once, before its first paid call. |
414
912
 
415
913
  ## Live MCP contract — inspect it before asking
416
914
 
@@ -423,79 +921,84 @@ Before each tool call:
423
921
  2. Fill required inputs already known from the Brand Kit, selected source, or conversation.
424
922
  3. Ask the user only for required inputs that cannot be inferred and for choices that materially
425
923
  change the result. Do not turn every optional field into a questionnaire.
426
- 4. Omit unspecified optional settings so the backend applies its current app defaults.
924
+ 4. Omit unspecified optional settings so the backend applies its current app defaults. Two
925
+ exceptions: always pass \`ratios\` on every source (omitted ratios make three images per
926
+ variant), and keep the same \`quality\` setting in the quote and the paid call (both omitted
927
+ for a template dry run, or the explicit \`quality\` the credits table asks for).
427
928
  5. If the live schema conflicts with this workflow, follow the live schema and report the drift
428
- with \`log_cli_event\`.
429
-
430
- ## The generation tools (the new, single-workflow surface)
431
-
432
- - \`submit_remix_batch\` — **the one call that makes ads.** Inspect its live schema and supply
433
- the required brand/source inputs plus any choices the user explicitly made.
434
- Returns the batch with a \`links\` block (\`brand_url\` + per-creative \`app_url\`). If the brand's
435
- research isn't finished yet the batch comes back \`status: "queued"\` — it auto-runs the moment
436
- research completes; tell the user it'll appear shortly, don't error.
437
- - \`estimate_remix_batch\` — cost preview. Reserves nothing. Use it to quote the cost first and
438
- check whether every selected source resolved before submitting.
439
- - \`get_remix_batch\` — poll status. Returns each creative with its renders and
440
- \`completed\`/\`failed\`/\`pending\` counts, plus \`links\`. A creative is done when its \`pending\` is 0
441
- — NOT when \`current_render_url\` is set (during a regenerate that field still points at the prior
442
- image). Each render carries \`age_seconds\` (since queued) and \`elapsed_seconds\` (time generating):
443
- use them to tell a slow-but-healthy render from a stuck one. A render only failed when its
444
- \`status\` is \`"failed"\` — never assume a stall and re-submit, that double-bills.
445
- - \`list_brand_creatives\` — the brand's gallery feed (newest
446
- first) + \`brand_url\`. Alternative poll target; also use to show everything made for a brand.
447
- - \`surprise_me_templates\` — the **"Surprise me" recommender**. Picks
448
- remixable Community creations (SAME logic as the web /create "Surprise me" button), shuffled
449
- so picks stay fresh. It does not use the retired curated third-party catalog.
450
- Returns the picked templates (id, slug, title, image, ratio) AND a ready-to-open \`create_url\`
451
- (the /create page with \`cli=true\` and the picks pre-selected). This is how you recommend
452
- templates — do NOT hand-pick from the raw catalog yourself (see "Picking templates" below).
453
- - \`regenerate_creative\` — edit or re-roll one existing creative through the same pipeline.
454
- Inspect the live schema to select the supported mode and required source inputs. Returns a
455
- single-item batch; poll it with \`get_remix_batch\`.
456
- - \`set_creative_feedback\` — record the user's reaction to a generated image. Use it whenever
457
- the user reacts; inspect the schema for the current rating and reason choices.
458
-
459
- ### Plan mode — review the plan BEFORE generating (optional)
460
-
461
- For users who want to approve each ad's plan before spending credits (the app's "Plan it" flow):
462
-
463
- - Use the approval option exposed by \`submit_remix_batch\` — it composes each creative's plan and PAUSES.
464
- **No credits are reserved and no image renders** until you approve.
465
- - \`list_ad_approvals\` — poll this. While a creative is
466
- \`composing\`, wait; once \`awaiting_approval\`, show its \`plan\` (composed prompt + refs + quality)
467
- to the user.
468
- - \`revise_ad_plan\` — recompose from a chat steer, still
469
- free. Poll \`list_ad_approvals\` until it's \`awaiting_approval\` again.
470
- - \`approve_ad_plan\` — approve one creative or the whole batch using the live schema.
471
- **This is the step that reserves credits and renders.** Then poll
472
- \`get_remix_batch\` and hand back links as usual.
473
-
474
- Only offer plan mode when the user asks to review/approve first — the default path generates
475
- immediately.
476
-
477
- ## Reading the brand & picking inputs (still MCP, read-only)
478
-
479
- - \`get_brand_kit\` — read the canonical brand context and available products/assets.
480
- - \`list_ad_brands\` / \`get_ad_brand\` — find and fetch the active brand.
481
- - \`list_user_ad_templates\` — list the org's own uploads and
482
- imported ads. Prefer \`relationship: "self"\` when the user wants to reuse their own ads;
483
- \`relationship: "competitor"\` is research/inspiration, never proof that the user owns the ad.
484
- - \`search_ad_templates\` — search remixable Community generations. The
485
- retired curated third-party catalog is not returned.
486
- - \`get_static_ad_template\` — resolve a source already owned by
487
- the org, including an own upload or a snapshotted Community creative. It does not resolve the
488
- retired curated third-party catalog.
489
- - \`remix_community_ad\` — turn a selected Community creative into a private remix source before
490
- submitting it. A Community ad id is an \`ad_project\` id, not a
491
- template id. Call this FIRST to snapshot it into a private template, then use the returned
492
- template \`id\` in \`items\`.
493
- - \`create_user_ad_template\` — upload a source image as a private template. Answer any
494
- ownership/rights input only from the user's explicit confirmation. Never claim rights for a
495
- competitor ad or an image found online.
496
- - \`get_ad_project\` / \`append_project_message\` — inspect a creative / leave a note on its thread.
497
-
498
- ## Keep the brand kit in sync — reconcile, then update (ASK first)
929
+ (see "Report problems" in the rules).
930
+
931
+ ## The ad tools
932
+
933
+ - \`ads_generate\` — **the one call that makes ads.** Needs \`brand_id\` and exactly one \`source\`:
934
+ \`template_ids: [{ template_id, variants?, ratios }]\` (the brand's own templates, Surprise-me
935
+ picks, Community rows whose \`item_type\` is \`template\`), \`community_ad_ids: [{ community_id,
936
+ variants?, ratios }]\` (Community rows whose \`item_type\` is \`creative\`; the backend snapshots
937
+ them), or \`creative_ids: [{ project_id, render_id?, variants?, ratios }]\` (remix the user's own
938
+ finished ads). Optional: \`product_name\` (a real product), \`prompt\` (a short steering note),
939
+ \`reference_image_urls\`, \`quality\`. \`dry_run: true\` quotes template sources and reserves
940
+ nothing. A real call GENERATES at once and returns \`{ job_id }\` (\`kind: "ads_batch"\`) and the
941
+ \`batch\`, which already carries its \`links\` (below). If the
942
+ brand's research is still running the batch is \`queued\` and starts on its own when research
943
+ finishes: tell the user it'll appear shortly, don't error.
944
+ - \`job_get { job_id, kind: "ads_batch" }\` — poll a batch. \`status\` is \`queued\`, \`running\`,
945
+ \`complete\`, \`partial_failure\` or \`failed\`; \`progress\` counts \`completed\` / \`failed\` /
946
+ \`pending\` images. \`result.creatives[]\` carry each creative's \`renders[]\` (\`id\`, \`status\`,
947
+ \`ratio\`, \`output_url\`, \`age_seconds\` since queued, \`elapsed_seconds\` generating). A creative
948
+ is done when its \`pending\` is 0, NOT when \`current_render_url\` is set (during a regenerate it
949
+ still points at the prior image). A render only failed when its \`status\` is \`"failed"\`: a slow
950
+ render is healthy, and re-submitting it double-bills. \`result.links\` holds the app links you end
951
+ the run with: \`brand_url\` (the brand's page, with all its ads) and \`creative_links: [{
952
+ project_id, app_url }]\` (each creative's page). \`brand_url\` is \`null\` and the list empty only
953
+ if the link lookup failed; the ads are still made. A server older than this skill returns no
954
+ \`links\` (and no \`app_url\` / \`brand_url\` on creative reads): then hand back the images
955
+ only and don't mention links.
956
+ - \`ads_creative_read { brand_id }\` — the brand's generated creatives, newest first (filter with
957
+ \`batch_id\`, \`tags\`, \`approved_only\`); each row has its \`app_url\` and the list has the
958
+ \`brand_url\`. \`creative_id\` reads one with its \`renders\`, plus \`creative.app_url\` and
959
+ \`creative.brand_url\`.
960
+ - \`ads_template_read\` — find or inspect a source (see "Picking source ads"). \`template_id\` reads one.
961
+ - \`ads_creative_edit { brand_id, creative_id, action }\` — every paid edit of one creative, one
962
+ \`action\` per call: \`regenerate\` (\`regenerate: { mode: "variation" }\` for another take;
963
+ \`mode: "edit"\` or \`"exact"\` with \`source_render_id\` and \`prompt\`), \`precision_edit\` (\`source_render_id\`
964
+ plus a \`note\` or region \`annotations\`), \`resize\` (\`source_render_id\` plus \`platforms\` or
965
+ \`targets\`), \`layerize\` (\`confirmed: true\`), \`animate\` (\`source_image_url\`; only when
966
+ \`account_whoami\` shows the Animate Images feature; for the full flow fetch the \`animate-image\`
967
+ skill). Returns \`{ job_id }\`: poll \`job_get\` with \`kind: "ads_batch"\` (regenerate, resize,
968
+ precision_edit) or \`kind: "animate"\`; for layerize read
969
+ \`ads_creative_read { brand_id, creative_id, include: ["layers"] }\`. While the brand's research is
970
+ still running, \`regenerate\`, \`resize\` and \`precision_edit\` refuse with
971
+ \`brand_research_in_progress\`: tell the user and try again when it finishes.
972
+ - \`ads_creative_update { brand_id, creative_id, patch }\` — free. \`patch.feedback: { render_id, rating:
973
+ "happy" | "neutral" | "sad", comment?, reasons? }\` records the user's reaction (it feeds the
974
+ quality loop); \`patch.tags\` replaces the creative's tags.
975
+ - \`ads_template_create { brand_id, source, rights_attested? }\` — register the user's own image as a
976
+ private source template (see "Upload" below).
977
+
978
+ ### Campaigns — the only plan-and-approve path
979
+
980
+ \`ads_generate\` has no plan step: the dry-run quote and the user's yes are the checkpoint, and a
981
+ submit generates at once. Never promise a review step before the images render. Plans exist only
982
+ for a campaign's concepts (planned with \`campaign_read\`, \`campaign_upsert\` and
983
+ \`add_campaign_concept\`; follow the connector guide for those):
984
+
985
+ 1. \`request_campaign_generation { campaign_id }\` composes the plans for the concepts with no
986
+ creatives yet (\`concept_ids\` re-runs chosen ones; \`count\` only for a number the user named). It
987
+ spends nothing and returns \`batch_ids\` and \`credits\`. Never say generation has started.
988
+ 2. Tell the user what will be made and the credit total, and wait for their explicit yes in this
989
+ chat. Never send them to a button in the app; a campaign link is only a place to look.
990
+ 3. Read \`ads_creative_read { brand_id, view: "approvals", batch_id }\` until that batch's plans are
991
+ \`awaiting_approval\` and none is \`composing\`. If its credit total differs from what they
992
+ agreed to, state the new total and ask again.
993
+ 4. \`ads_approval_decide { brand_id, decision: "approve", batch_id, user_quote: "<their exact words>" }\`
994
+ for each batch, then poll the batch ids it returns with \`job_get\` (\`kind: "ads_batch"\`) and
995
+ end with each batch's \`result.links\`, as in step 7 of the workflow below.
996
+
997
+ Before approving, a steer is free: \`ads_approval_decide { brand_id, creative_id, decision: "revise",
998
+ revise: { message } }\` recomposes that plan (read the approvals view again), and
999
+ \`decision: "edit_plan"\` patches it directly. Editing the campaign in between discards these plans.
1000
+
1001
+ ## Keep the brand kit in sync — reconcile, then save or propose
499
1002
 
500
1003
  The brand kit is the source of truth every generation reads. During ANY task, when the user
501
1004
  **tells you something about the brand or asks to change something brand-level** — a different
@@ -503,559 +1006,649 @@ tagline, audience, voice, a product's name/price/description, "our logo is X", "
503
1006
  anymore", a new product photo — treat it as a possible kit update, don't just use it for this one
504
1007
  ad and forget it:
505
1008
 
506
- 1. **Check it against the kit.** Call \`get_brand_kit\` for the active brand and see whether what the user said
1009
+ 1. **Check it against the kit.** Call \`brand_read\` with \`kit\` and \`learnings\` for the active brand and see whether what the user said
507
1010
  matches, is missing from, or contradicts the kit.
508
1011
  2. **If it's already in the kit and matches** — nothing to do; proceed.
509
- 3. **If it's new or different — ASK before writing.** Confirm in one line: *"Want me to update
510
- the brand kit so this sticks for future ads?"* Only persist on a yes (or when the user clearly
511
- asked you to change the brand). Don't silently mutate the kit, and don't nag on trivia.
512
- 4. **Persist with the write tools** (partial — only the fields you pass are touched; each edit is
513
- recorded as a user override that later re-research won't clobber):
514
- - \`update_brand_kit\` — structured brand fields.
515
- - \`upsert_brand_product\` / \`delete_brand_product\` — products.
516
- - \`add_brand_product_image\` / \`remove_brand_reference_image\` — product and reference photos.
1012
+ 3. **If it's new or different**, persist an explicit request to correct or remember the brand
1013
+ through the policy below; that request is already authorization. For an ambiguous one-ad
1014
+ direction, ask once whether it should stick. Agent-derived suggestions become proposals.
1015
+ 4. **Persist with the canonical write tools** (send only changed fields):
1016
+ - \`brand_update\` with explicit user-correction intent — structured brand fields or products;
1017
+ an inferred structured field uses agent-proposal intent instead. Follow the live schema.
1018
+ - \`media_upload\` / \`media_update\` — the user's own product and reference photos.
517
1019
  Inspect each live schema and send only the fields needed for the confirmed change.
518
- 5. **Confirm what changed** and continue the task. (Logo, colors, and fonts are owned by the
519
- backend research pass — prefer \`update_ad_brand\` / the research flow for those, not free text.)
1020
+ 5. **Read back what changed** before saying saved, then continue the task. (Use the research
1021
+ workflow for researched logo/colors/fonts; explicit user edits use only fields supported by
1022
+ the canonical tool schema and the correction policy below.)
520
1023
 
521
1024
  This is the parity gap the app closes in-product: a brand fact the user gives mid-task should be
522
1025
  able to flow back into the kit — with their ok — instead of being lost.
523
1026
 
1027
+ ${DURABLE_BRAND_CAPTURE}
1028
+
524
1029
  ## Picking source ads — use approved sources, not the retired catalog
525
1030
 
526
1031
  When the user wants to make ads but has NOT named a specific template (id/slug/Community
527
- ad/upload), do NOT silently browse the raw catalog and hand-pick for them. Instead run this
528
- short ask flow — it mirrors the web app and keeps the human in the loop:
529
-
530
- 1. **Ask what kind of ads they want** — the angle/offer/theme/season. **The brand context already
531
- gives you the vibe (voice), the audience, and the product catalog — do NOT ask for those.**
532
- Offer the real \`products[]\` to pick from rather than asking "which product?", and derive the
533
- tone from the brand's voice. This shapes both the source choice and your steering \`prompt\`.
534
- Keep it to one quick question about campaign intent.
535
- 2. **Ask how to pick a source: their own ads, Community, upload, or "Surprise me".**
536
- - **Their own ads** → use \`list_user_ad_templates\` to load the active brand's own sources and
537
- let them choose from the results.
538
- - **Community** → \`search_ad_templates\`, let them choose, then call \`remix_community_ad\`
539
- before submitting.
540
- - **Upload** → upload through the workspace and call \`create_user_ad_template\`. If its live
541
- schema requires an ownership or permission answer, only supply it after explicit confirmation.
542
- - **Surprise me** (they want you/the app to pick) → call \`surprise_me_templates\` for the active
543
- brand and hand the user the returned \`create_url\`.
544
- It opens /create in **CLI mode** with the picks pre-selected, a preview modal, and the
545
- **copyable remix prompt at the bottom** (in place of the Generate input). They can swap
546
- picks and copy that prompt. If they'd rather you "just make them" without reviewing in the
547
- app, you MAY submit the \`surprise_me_templates\` picks directly (skip to submit).
548
- - **Browse in the app** → hand the user this URL, with the
549
- active brand's slug filled in:
550
- \`https://make.gooseworks.ai/create?brand=<brand-slug>&cli=true\`
551
- In CLI mode the app shows the copyable remix prompt at the bottom (dismissable / switchable
552
- back to the UI composer). They browse the available own/Community sources and copy the prompt.
1032
+ ad/upload), do NOT silently browse the raw catalog and hand-pick for them. Instead send **one
1033
+ proposal** — it mirrors the web app and keeps the human in the loop without an interview:
1034
+
1035
+ 1. **Propose the direction yourself.** From the brand read and the evidence brief, recommend the
1036
+ product, angle/offer and tone, with a one-line reason naming what the Brain showed (a past
1037
+ approved angle, a rule it respects). **Do NOT ask what kind of ads they want, which product,
1038
+ or the vibe.** Keep any direction the user already gave. This shapes both the source choice
1039
+ and your steering \`prompt\`.
1040
+ 2. **Recommend a source in the same message: their own ads, Community, upload, or "Surprise me".**
1041
+ Default to their own approved ads when suitable ones exist, otherwise Surprise-me picks for
1042
+ the brand; list the other paths as one-line alternatives. You may resolve the picks and run the
1043
+ free dry run first so the proposal already carries the credit total. Show a list of sources as
1044
+ one table with every row and its image link, and mark your one suggestion.
1045
+ - **Their own ads** → \`ads_template_read { brand_id, mode: "mine", filters: { relationship: "self" } }\`
1046
+ and let them choose. \`mode: "competitor"\` rows are research and inspiration, never proof that
1047
+ the user owns the ad.
1048
+ - **Community** → \`ads_template_read { brand_id, mode: "query", query: "<the angle and look>" }\`
1049
+ ranks Community ads by meaning; each row's \`sourceId\` (with \`title\` and \`thumbnailUrl\`) goes
1050
+ in \`source.community_ad_ids[].community_id\`. To browse the feed instead, use \`mode:
1051
+ "community"\`: its rows carry \`item_type\`, and a \`template\` id goes in \`source.template_ids\`,
1052
+ a \`creative\` id in \`source.community_ad_ids\`. The backend snapshots a Community creative
1053
+ itself; there is no separate step. Let the user choose.
1054
+ - **Upload** → the user's own image becomes a private template: \`media_upload\` it (\`scope:
1055
+ "brand"\`, \`scope_id: <brand_id>\`, \`kind: "image"\`; in a chat app \`source.type: "bytes"\`), then
1056
+ \`ads_template_create { brand_id, source: { type: "media", media_id } }\` (or \`source: { type:
1057
+ "url", url }\` for a public image). Set \`rights_attested: true\` only after the user explicitly
1058
+ confirms they own it; this is the ownership/rights input. Never claim rights for a competitor
1059
+ ad or an image found online.
1060
+ - **Surprise me** (they want you/the app to pick) → \`ads_template_read { brand_id, mode:
1061
+ "surprise", count: 4 }\` picks remixable Community ads for the brand, shuffled so picks stay
1062
+ fresh. Every pick is a template: show them, and generate
1063
+ from the ones the user keeps (or from all of them if they said "just make them").
1064
+ - **Browse in the app** → the one link you build yourself: \`<app>/create?brand=<brand-slug>&cli=true\`,
1065
+ with the active brand's slug, where \`<app>\` is the origin of a \`brand_url\` a tool returned
1066
+ (the part before \`/?brand=\`: \`https://ads-staging.gooseworks.ai/?brand=acme\` gives
1067
+ \`https://ads-staging.gooseworks.ai\`), so it opens on the user's own environment. Reuse a
1068
+ \`brand_url\` you already have from this run, or read one cheaply with
1069
+ \`ads_creative_read { brand_id, limit: 1 }\`; if none comes back, use \`https://make.gooseworks.ai\`.
1070
+ In this mode the app shows a copyable remix prompt at the bottom (dismissable / switchable
1071
+ back to the UI composer). They browse their own and Community sources and copy the prompt.
553
1072
  3. **Close the loop.** When the user **pastes back the copyable remix prompt** from the app
554
1073
  (it names the brand + the templates they chose), THAT is your cue to generate: resolve the
555
- named source(s), inspect \`submit_remix_batch\`, and collect only its unresolved required inputs.
1074
+ named source(s) with \`ads_template_read\`, quote, and collect only the unresolved required inputs.
556
1075
 
557
1076
  If the user already named an owned source (id/slug), a Community ad, or an upload, skip the source
558
1077
  choice. Competitor ads may inform the angle or structure, but describe them as inspiration, never
559
- claim ownership, and never attest rights for the user.
1078
+ claim ownership, and never attest rights for the user. Never use the retired curated third-party
1079
+ catalog.
560
1080
 
561
1081
  ## Workflow — make ads from a template
562
1082
 
563
- 1. **Resolve the brand.** Use \`list_ad_brands\` by name/site, then call \`get_brand_kit\` for the
564
- selected brand. If the
565
- kit's \`researchStatus\` isn't \`complete\`, you can still submit (the batch queues and runs when
566
- research finishes) — just tell the user. Use the kit to pick \`product_name\` (a real entry from
567
- \`products[]\`, not a guess) and, if the user supplied product photos, \`reference_image_urls\`.
568
- 2. **Pick the source ad(s) via the ask flow above.** Once you have concrete ids:
569
- call \`get_static_ad_template\` for each.
570
- For a Community ad, \`remix_community_ad\` first; for an uploaded image, \`create_user_ad_template\`
571
- first.
1083
+ 1. **Resolve the brand and its evidence.** List brands with \`brand_read\` (no \`brand_id\`), then
1084
+ read the selected brand's summary, kit, products and
1085
+ learnings and search the Brand Brain as above. If research isn't \`complete\`, you can still
1086
+ submit (the batch queues and runs when research finishes) — just tell the user. Use the read to
1087
+ pick \`product_name\` (a real entry from \`products.items\`, not a guess) and, if the user supplied
1088
+ product photos, \`reference_image_urls\` (public URLs; \`media_upload\` a local file first).
1089
+ 2. **Pick the source ad(s) via the proposal above.** Read each chosen template with
1090
+ \`ads_template_read { brand_id, template_id }\`. Community creatives (a query row's \`sourceId\`,
1091
+ a feed row with \`item_type: "creative"\`) and the user's own finished ads go straight into
1092
+ \`source.community_ad_ids\` / \`source.creative_ids\`.
572
1093
  3. **(Optional) Craft the steering prompt.** The \`prompt\` is OPTIONAL — this is where the skill
573
1094
  adds value: turn the user's intent (from step 1) into a concise steering note (e.g. tone,
574
1095
  season, emphasis). Don't over-specify; the backend pipeline + brand kit handle palette, fonts,
575
1096
  product swap.
576
- 4. **Quote the cost.** Inspect and call \`estimate_remix_batch\`, then tell the user.
577
- 5. **Submit ONE batch.** Inspect the current \`submit_remix_batch\` schema, fill known required
578
- inputs, ask only for unresolved user decisions, and omit unspecified optional settings. Keep
579
- the returned \`batch_id\` and \`links\`.
580
- 6. **Poll until done.** Call \`get_remix_batch\` for the returned batch (or use
581
- \`list_brand_creatives\`) every ~20-30s
582
- until every creative's \`pending\` is 0. Most images finish in a few minutes; text-heavy templates
583
- and \`quality: high\` take longer. Read each render's \`elapsed_seconds\` rather than guessing — a
584
- render that's still \`running\` is healthy; do NOT re-submit thinking it stalled (that double-bills).
585
- 7. **Hand back the links** from the batch's \`links\` block — \`brand_url\` (gallery) and each
586
- creative's \`app_url\` — copied verbatim. Never end on just "done" or a file path.
1097
+ 4. **Quote the cost.** Call \`ads_generate\` with the exact arguments you will submit plus
1098
+ \`dry_run: true\` (template sources), or work out the total as in the credits table. Drop or
1099
+ replace anything in \`unknown_template_ids\`. State the image count and credit total in one line
1100
+ and wait for the user's explicit yes.
1101
+ 5. **Submit ONE batch** after that yes: the same \`ads_generate\` call without \`dry_run\`. Keep the
1102
+ returned \`job_id\`.
1103
+ 6. **Poll until done.** \`job_get { job_id, kind: "ads_batch" }\` every ~20-30s until every creative's
1104
+ \`pending\` is 0. Most images finish in a few minutes; text-heavy templates and \`quality: high\`
1105
+ take longer. Read each render's \`elapsed_seconds\` rather than guessing; do NOT re-submit
1106
+ thinking it stalled (that double-bills). If \`ads_generate\` refused with
1107
+ \`brand_research_required\`, the brand was never researched: run "Brand research" below, then
1108
+ quote and submit again.
1109
+ 7. **Hand back the ads with their links.** Show every finished image (\`renders[].output_url\` of
1110
+ completed renders) and say in one line what failed, if anything. Then give the links from the
1111
+ last \`job_get\`'s \`result.links\`, copied verbatim: the \`brand_url\` where all the brand's
1112
+ ads are, and the \`app_url\` of each creative with a finished image (match
1113
+ \`creative_links[].project_id\` to \`result.creatives[].id\`; skip a creative whose renders all
1114
+ failed). For a big batch (more than about 8 creatives) give the \`brand_url\` and offer the
1115
+ rest. Skip a missing or \`null\` link; never build an app URL yourself. Never end on just
1116
+ "done" or a file path.
587
1117
 
588
1118
  ## Workflow — edit an existing ad
589
1119
 
590
- User wants to tweak a creative they already made → use \`regenerate_creative\`. Infer whether they
591
- want another take, a targeted edit, or an exact instructed change from their request. Then inspect
592
- the live schema, ask only for any required source or instruction that is still missing, submit,
593
- poll with \`get_remix_batch\`, and hand back the links.
1120
+ User wants to tweak a creative they already made → \`ads_creative_edit\`. Read the creative first
1121
+ (\`ads_creative_read { brand_id, creative_id }\`) to get the render they mean. Infer the action
1122
+ from their request: another take (\`regenerate\`, \`mode: "variation"\`), a targeted change
1123
+ (\`regenerate\` with \`mode: "edit"\`, or \`precision_edit\` when they point at a region), an exact
1124
+ instructed change (\`mode: "exact"\`), new placements (\`resize\`), editable layers (\`layerize\`) or a
1125
+ short video (\`animate\`). Ask only for a required source or instruction that is still missing,
1126
+ state the credit total from the credits table, get the yes, submit, poll as above, and hand back
1127
+ the new images with their links: \`result.links\` from \`job_get\` for \`regenerate\`, \`resize\` and
1128
+ \`precision_edit\`; \`creative.app_url\` and \`creative.brand_url\` from \`ads_creative_read { brand_id,
1129
+ creative_id }\` for \`animate\` and \`layerize\`.
594
1130
 
595
1131
  ## Brand research
596
1132
 
597
- Prefer the backend's result: call \`get_brand_kit\` for the selected brand. If \`researchStatus\` is
598
- \`complete\`, REUSE it — never re-research.
1133
+ Prefer the backend's result. \`brand_read { brand_id, sections: ["summary"] }\`: if
1134
+ \`research_status\` is \`complete\`, REUSE it — never re-research. Proof points and the full product
1135
+ catalog keep arriving until \`enrichment.settled\` is true.
599
1136
 
600
1137
  **The split — backend owns visuals, you own the qualitative depth:**
601
1138
 
602
- - **Backend LIGHT pass (automatic).** \`create_ad_brand\` with a \`website_url\` kicks off the same
603
- backend research the web app uses, in \`mode: "light"\`: it resolves the **authoritative logo,
604
- colors, and fonts** (Brandfetch + context.dev) plus a baseline kit, then flips
605
- \`research_status\` to \`complete\` — usually under a minute. You can't reproduce those visual
606
- signals locally, so **never re-derive logo/colors/fonts.** (Web onboarding via \`/api/ads/onboard\`
607
- runs the full thing; nothing to do but read it.)
608
- - **Your DEEP pass (local, agentic).** You add the qualitative depth the light pass leaves thin —
609
- positioning, audience segments, voice, brandType, value props, proof points, products — grounded
610
- on the actual site.
611
-
612
- **CLI brand-research flow:**
613
-
614
- 1. Inspect and call \`create_ad_brand\` with the known brand identity and website, then keep its id
615
- and slug. The brand comes back with
616
- \`research_status: "pending"\` (light pass in flight).
617
- 2. **Wait for the backend light pass:** poll \`get_brand_kit\` for that brand until \`researchStatus\`
618
- is \`complete\` (usually <60s). Now the kit has authoritative logo/colors/fonts + a baseline.
619
- At this point generation is already unblocked — but do the deep pass to make it good.
620
- 3. **Deep research locally:** \`gooseworks fetch brand-research\` and follow its phases. **Ground
621
- every fact on the fetched site** — if the site can't be read, say so and ask the user; never
1139
+ - **Backend LIGHT pass (automatic).** A new brand comes from the connector's setup flow
1140
+ (\`brand_onboarding { action: "status" }\`, then its \`next_step\`). For an additional brand, check
1141
+ it is absent (\`brand_read\` with no \`brand_id\`), then \`brand_create { name, website_url }\` (free).
1142
+ The website starts the same backend research the web app uses: it resolves the
1143
+ **authoritative logo, colors, and fonts** plus a baseline kit and returns \`research_job_id\`; poll
1144
+ \`job_get { job_id: research_job_id, kind: "brand_research" }\` (or \`brand_read\`) until
1145
+ \`research_status\` is \`complete\` — usually under a minute. If it returns \`reused_existing: true\`,
1146
+ tell the user and use that brand. You can't reproduce those visual signals, so **never
1147
+ re-derive logo/colors/fonts.**
1148
+ - **Your DEEP pass (optional).** You add the qualitative depth the light pass leaves thin —
1149
+ positioning, audience, voice, brand type, value props, proof points, products — grounded on
1150
+ the actual site.
1151
+
1152
+ **Deep research flow:**
1153
+
1154
+ 1. Fetch the \`brand-research\` skill (\`catalog_fetch { type: "skill", slug: "brand-research" }\` in a
1155
+ chat app; \`gooseworks fetch brand-research\` in a terminal) and follow its phases. Its data
1156
+ calls are paid: state the credit total (credits table) and get the yes first. **Ground every
1157
+ fact on the brand's own site** — if the site can't be read, say so and ask the user; never
622
1158
  guess a category from the brand name alone.
623
- 4. **Write the pack** with \`write_file\` under \`agent-config/brands/<slug>/\`:
624
- - the \`brand-research/*.md\` docs + \`brand-assets/manifest.json\` (human-readable pack), AND
625
- - \`brand-research/kit-patch.json\` — the STRUCTURED fields the web UI renders. Field-for-field
626
- contract; only what you put here reaches the kit. Shape:
627
- \`{ positioning?: string, audience?: string, voice?: string, brandType?: string, tagline?: string, valueProps?: string[], proofPoints?: string[], products?: [{ name, description?, link?, pricing?, imageUrls?: string[] }] }\`
628
- (\`brandType\` ∈ product | saas | service | agency | restaurant | fashion | beauty | fitness |
629
- finance | education | health). Only URLs already in our storage for product images.
630
- - **Do NOT set logo / colors / fonts here** — the backend light pass already owns those.
631
- 5. **Persist it:** call \`finalize_brand_research\` for the brand. It merges \`kit-patch.json\` into the kit
632
- NON-CLOBBERINGLY (it will NOT overwrite the backend's visuals or any user edit), then re-confirms
633
- \`research_status: complete\`.
634
- 6. **Verify:** call \`get_brand_kit\` again and confirm the qualitative fields you wrote are present
635
- before generating.
636
-
637
- **If the brand has NO website**, the backend light pass can't run (nothing to fetch) — do the whole
638
- thing locally (steps 3–6) and finalize; an un-finalized brand has no kit for generation and leaves
639
- no artifact to debug a wrong run (this is how a bad local classification, e.g. mislabelling a SaaS
640
- as a "drink company", used to vanish without a trace).
1159
+ 2. **Save what you improved.**
1160
+ - **As proposals (the default):** \`brand_update { brand_id,
1161
+ knowledge_intent: "agent_proposal", rationale, patch: { knowledge: { positioning?, audience?,
1162
+ voice?, brandType?, tagline?, valueProps? } } }\`. They stay pending until the user accepts
1163
+ them in the app.
1164
+ - **As the full research pack (only when you wrote all of it; in a chat app, only for a brand
1165
+ with no website, see below):** with \`file_write\`, under
1166
+ \`agent-config/brands/<slug>/brand-research/\`, write all four docs \`brand-summary.md\`,
1167
+ \`visual-identity.md\`, \`audience.md\` and \`competitors.md\` (real content, at least a few
1168
+ sentences each) plus \`kit-patch.json\`: \`{ positioning?, audience?, voice?, brandType?,
1169
+ tagline?, valueProps?: string[], proofPoints?: [{ text, source_url }], products?: [{ name,
1170
+ description?, link?, pricing?, imageUrls?: string[] }] }\` with only the fields you improved;
1171
+ every proof point copied word for word from the brand's own page at \`source_url\`; product
1172
+ images only from URLs already in GooseWorks storage; **no logo / colors / fonts**. Then
1173
+ \`brand_update { brand_id, patch: { finalize_research: true } }\` merges it NON-CLOBBERINGLY
1174
+ (never over the backend's visuals or a user edit). **If any of the four docs is missing or
1175
+ nearly empty, finalize fails and marks the brand's research as failed**, so never finalize a
1176
+ partial pack.
1177
+ 3. **Verify:** \`brand_read\` again (\`summary\`, \`kit\`) and confirm what you saved before generating.
1178
+
1179
+ **If the brand has NO website**, the backend light pass can't run and generation goes ahead with
1180
+ an empty kit, so ads come out off-brand. Do the deep pass from what the user tells you and save the
1181
+ full research pack, so the brand has a kit and a record to debug a wrong run (this is how a bad
1182
+ classification, e.g. mislabelling a SaaS as a "drink company", used to vanish).
641
1183
 
642
1184
  ## Analyze / intelligence (fetched recipes — NOT generation)
643
1185
 
644
- These are analysis recipes you fetch from goose-skills with \`gooseworks fetch <slug>\` and
645
- follow; they do NOT touch the generation tools or credits-for-images. Pick the closest match;
646
- if unsure, \`gooseworks search "<what the user wants>"\` first:
1186
+ These are analysis recipes you fetch and follow; they don't use the generation tools, but their
1187
+ data calls are paid (credits table: state the total and get the yes before the first one). Fetch with \`catalog_fetch { type: "skill", slug: "<slug>" }\` in a chat app or
1188
+ \`gooseworks fetch <slug>\` in a terminal. Pick the closest match; if unsure, search first with
1189
+ \`catalog_search { type: "skill", query: "<what the user wants>" }\` (terminal:
1190
+ \`gooseworks search "<what the user wants>"\`):
647
1191
  - **Campaign performance diagnosis** ("why is my Meta/Google campaign underperforming",
648
- creative fatigue, learning phase, pacing, auction overlap) → \`gooseworks fetch meta-ads-analyzer\`
1192
+ creative fatigue, learning phase, pacing, auction overlap) → \`meta-ads-analyzer\`
649
1193
  (or \`ad-campaign-analyzer\` for cross-platform).
650
1194
  - **Lead/CAC quality** ("are these ads driving qualified leads", true CAC vs vanity CPA,
651
- Scale/Keep/Investigate/Cut) → \`gooseworks fetch ad-lead-quality-analyzer\`.
1195
+ Scale/Keep/Investigate/Cut) → \`ad-lead-quality-analyzer\`.
652
1196
  - **Competitor ad intelligence** ("what ads are competitors running") →
653
- \`gooseworks fetch competitor-ad-intelligence\` (Meta Ad Library: \`meta-ad-scraper\`;
654
- Google: \`google-ad-scraper\`).
655
- - **Creative ideation** (ad angles, winning hooks) → \`gooseworks fetch ad-angle-miner\` /
656
- \`gooseworks fetch trending-ad-hook-spotter\`.
657
- - **Policy / landing-page checks** → \`gooseworks fetch meta-ad-policy-checker\` /
658
- \`gooseworks fetch ad-to-landing-page-auditor\`.
1197
+ \`competitor-ad-intelligence\` (Meta Ad Library: \`meta-ad-scraper\`; Google: \`google-ad-scraper\`).
1198
+ - **Creative ideation** (ad angles, winning hooks) → \`ad-angle-miner\` / \`trending-ad-hook-spotter\`.
1199
+ - **Policy / landing-page checks** → \`meta-ad-policy-checker\` / \`ad-to-landing-page-auditor\`.
659
1200
 
660
- Save their scripts to \`/tmp/gooseworks-scripts/<slug>/\` and follow their instructions. These
661
- run through the \`gooseworks\` CLI (\`gooseworks fetch\` / \`gooseworks call\`), like the GTM skills.
1201
+ A recipe's provider calls go through the connector in a chat app (\`data_call_provider\` for GET,
1202
+ \`data_post_provider\` for POST) and through \`gooseworks call\` in a terminal; both are billed. Saving and running
1203
+ its scripts (under \`/tmp/gooseworks-scripts/<slug>/\`) is terminal-only: without a terminal, do
1204
+ that step's analysis yourself from the data the tools return, and never ask the user to run a
1205
+ command.
662
1206
 
663
1207
  ## Rules
664
1208
 
665
- - **MCP required** — if \`mcp__gooseworks__*\` is unavailable, stop and tell the user to run
666
- \`gooseworks install --claude --mcp\`.
667
- - **One backend workflow** — generation is \`submit_remix_batch\` / \`regenerate_creative\` ONLY.
1209
+ - **Connector tools only** — every step uses the tool names above. A missing tool means the
1210
+ GooseWorks connection is stale: ask the user to reconnect or refresh GooseWorks. Never send a
1211
+ chat-app user to a terminal or a CLI install.
1212
+ - **One backend workflow** — generation is \`ads_generate\` / \`ads_creative_edit\` ONLY.
668
1213
  Do NOT call FAL, the media proxy, \`submit_render\`, \`update_render_status\`, or upload render
669
- files yourself; do NOT \`gooseworks fetch\` a local remix recipe to generate. The backend owns it.
670
- - **Always end a successful run with the links** from the batch's \`links\` block (\`brand_url\` +
671
- each creative's \`app_url\`), copied verbatim. Never end on just "done" or a file path.
672
- - **Quote cost before generating** when it's non-trivial (use \`estimate_remix_batch\`), and
673
- relay \`insufficient_credits\` plainly if the submit is rejected — don't retry blindly.
674
- - **Use approved source paths.** If the user didn't name a source, run the ask flow (own ads,
675
- Community, upload, Surprise me, or browse in the app). "Surprise me" goes through
676
- \`surprise_me_templates\`; browsing uses \`/create?brand=<slug>&cli=true\`. Never use the retired
677
- curated third-party catalog.
678
- Generate when they paste the app's copyable remix prompt back (or submit the surprise picks
679
- directly if they'd rather not review).
1214
+ files yourself; do NOT fetch a local remix recipe to generate. The backend owns it.
1215
+ - **State the credit total and get an explicit yes before every paid call** (credits table above).
1216
+ Relay \`insufficient_credits\` plainly if the submit is rejected — don't retry blindly.
1217
+ - **No plan step for one-off ads.** Only a campaign's plans wait for approval
1218
+ (\`request_campaign_generation\` → \`ads_approval_decide\` with the user's words as \`user_quote\`).
1219
+ - **Always end a successful run with the finished images and their links**, copied verbatim:
1220
+ each creative's \`app_url\` and the \`brand_url\` from \`result.links\`, or from the creative
1221
+ read (\`creative.app_url\`, \`creative.brand_url\`) after \`animate\` or \`layerize\`. When the
1222
+ server returns no links, end with the images only. Never end on just "done" or a file path, and
1223
+ never build an app URL yourself (the one exception is the browse link below).
1224
+ - **Search the Brain before proposing.** After the brand read and before choosing an angle,
1225
+ claim or source — or asking for a brand fact — run the task's \`knowledge_search\` and carry
1226
+ its evidence brief. A failed or empty search is stated as such, never as "no evidence exists".
1227
+ - **Use approved source paths.** If the user didn't name a source, recommend one in the single
1228
+ proposal (own ads, Community, upload, Surprise me, or browse in the app). Surprise me is
1229
+ \`ads_template_read\` with \`mode: "surprise"\`; browsing uses \`/create?brand=<slug>&cli=true\`.
1230
+ Never use the retired curated third-party catalog. Generate when they paste the app's copyable
1231
+ remix prompt back, or when they accept the surprise picks.
680
1232
  - **Treat competitor ads as inspiration** — never attest rights, imply ownership, or promise to
681
1233
  copy a competitor's distinctive expression.
682
1234
  - **Reconcile brand facts into the kit** — when the user states or changes something brand-level
683
- mid-task, check it against \`get_brand_kit\` and, with their ok, persist it via \`update_brand_kit\`
684
- / \`upsert_brand_product\` / \`add_brand_product_image\` so it sticks for future ads. Ask first;
685
- never silently mutate the kit.
686
- - **Record feedback** — when the user reacts to a generated image, inspect and call
687
- \`set_creative_feedback\` so the quality loop learns.
688
- - **Plan mode is opt-in** — only use the live approval option, then \`list_ad_approvals\` and
689
- \`approve_ad_plan\`, when the user wants to review before spending credits; otherwise generate
690
- immediately.
691
- - **Don't busy-loop** — poll \`get_remix_batch\` on a sensible interval (~20-30s); a \`queued\`
692
- batch is waiting on research and will start on its own.
693
- - **Report problems so we can fix them** — when a batch fails/is rejected and you can't resolve it,
694
- a required brand input/asset is missing, or a recipe/instruction is ambiguous or contradictory,
695
- call the **\`log_cli_event\`** MCP tool (\`event_type\`: \`error\`/\`blocker\`/\`missing_input\`/\`confusion\`,
696
- with the real error + step in \`details\`) so the team gets visibility. Still tell the user too.
1235
+ mid-task, compare it with \`brand_read\`. Save explicitly authorized corrections through
1236
+ \`brand_update\` with correction intent and the user's exact statement, or the user's own
1237
+ images through \`media_upload\`; read back before saying saved. Proposed improvements stay
1238
+ pending. Ask only when it is unclear whether a one-ad direction should apply to future ads.
1239
+ - **Record feedback** — when the user reacts to a generated image, call \`ads_creative_update\`
1240
+ with \`patch.feedback\` so the quality loop learns.
1241
+ - **Don't busy-loop** — poll \`job_get\` on a sensible interval (~20-30s); a \`queued\` batch is
1242
+ waiting on research and will start on its own.
1243
+ - **Report problems so we can fix them** — when a batch fails or is rejected and you can't resolve
1244
+ it, a required brand input/asset is missing, or an instruction is ambiguous or contradictory:
1245
+ in a terminal run \`gooseworks log "<what happened>" --event-type <error|blocker|missing_input|confusion> --details '<json with the real error and step>'\`,
1246
+ or call the \`log_cli_event\` tool if your connection lists it. A chat app without either skips
1247
+ this step. Always tell the user too.
697
1248
  `;
698
1249
  }
699
- /**
700
- * Returns the goose-video entry SKILL.md content (GOOSE-3677).
701
- *
702
- * The server-rendered ORDERING flow: "make me a video ad for <brand>" → brand →
703
- * what it's for → format table with demos → the format's questions → a free
704
- * draft and a server quote → a script preview before the expensive work → the
705
- * finished video in the chat. Works in hosted connectors (nothing runs locally).
706
- * It routes existing app projects/batches to `goose-video-local` first.
707
- *
708
- * This is the ONLY full copy of the body. goose-lab's `order-video` is a stub
709
- * that points here; edit the ordering flow in this function.
710
- */
1250
+ /** Card-aware video entry. Shell execution and widget display are independent. */
711
1251
  function getGooseVideoSkillContent() {
712
1252
  return `---
713
1253
  name: goose-video
714
1254
  slug: goose-video
715
1255
  description: >
716
- Order a finished video ad without leaving the chat. Use it when the user says "make me a video
717
- ad for <brand>", "I want a video ad", or asks for a UGC / iMessage / explainer video. One
718
- sentence is enough: it picks the brand, asks what the ad is for, suggests formats in a table
719
- with demo links, asks that format's questions, and shows the script for approval before any
720
- real spend. It renders on the GooseWorks server, returns the video in the chat, and works in
721
- hosted connectors too. For an existing app video project or batch, it hands off to
722
- goose-video-local.
1256
+ Start a video ad in the same chat. Resolve the brand and suggest supported formats with a
1257
+ picker or the returned text choices. An agent that can run shell commands on the customer's
1258
+ computer makes the video with goose-video-local; otherwise the GooseWorks coworker makes it.
1259
+ Review one complete template plan and total credits before production. Custom videos retain two authenticated review gates in this chat.
723
1260
  category: ads
724
- version: 1.0.0
1261
+ version: 3.1.0
725
1262
  author: GooseWorks
726
- tags: [gooseworks, ads, video, order, server-render]
1263
+ tags: [gooseworks, ads, video, local-render, coworker, chat]
727
1264
  ---
728
1265
 
729
- # GooseWorks Video Ads — order a video in chat
730
-
731
- ## Purpose
732
-
733
- Produces one finished vertical video ad, rendered on the GooseWorks server and billed in credits. Examples: an animated iMessage, ChatGPT or Notes thread that ends on the brand's product, or a kinetic-type explainer. The live catalogue decides what can be ordered.
1266
+ # GooseWorks Video Ads — choose, review, make
734
1267
 
735
- **The customer starts with one sentence.** "Make me a video ad for Bioma" is the normal opening, not an edge case. They will not name a format, a tool or a step. Getting from that sentence to a good choice is this skill's job.
1268
+ ${CUSTOMER_TALK}
736
1269
 
737
- **The whole job happens in the chat.** The customer is in Claude Code, ChatGPT or a terminal. They will not open a browser to review a script, compare voices or watch a result. Choosing, previewing, approving and receiving all happen here, as text and links they can click. The app is for **payment and nothing else**.
1270
+ ${CLI_TOOL_BRIDGE}
738
1271
 
739
- **This is not the Video Ads Lab.** The lab is internal, admin-only and free. This spends a customer's money.
740
-
741
-
742
- ## Route first: is this a new order?
743
-
744
- This skill **orders a new video**. Hand off to **\`goose-video-local\`** and stop following this skill when the customer brings any of these:
1272
+ ## Purpose
745
1273
 
746
- - a **video batch** id ("for video batch <id>");
1274
+ Resolve the brand, show supported formats and create one saved project. This is agent
1275
+ execution, not a recipe server-order API.
1276
+
1277
+ ${WHO_MAKES_THE_VIDEO}
1278
+
1279
+ **Cards and execution are separate.** A terminal may render without drawing a picker; a chat
1280
+ app may draw a picker while the coworker renders. Follow \`card.display_hint\` for how to show
1281
+ things; who makes the video is the rule above. Template-remix review stays in this
1282
+ chat. Custom videos use separate authenticated script and ingredient approvals in this same chat.
1283
+ Studio is an optional review surface.
1284
+
1285
+ ${videoEntryPreparation('goose-video')}
1286
+
1287
+ ## Custom videos: check formats first
1288
+
1289
+ First check the catalog: when the brief names or implies a listed format (for example a street interview, testimonial, podcast or chat video), show that format and its fit through the format flow below, including its support status. Go custom only when no format fits and the customer chooses custom after hearing why; custom keeps that format's hard constraints. 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 that does not fit or that the customer declined, and do not import the reference as a finished video.
1290
+
1291
+ ## Run the format's route check before proposing it
1292
+
1293
+ A catalog row says what a format is best for and ranks it for the brand. It does not say whether
1294
+ the format can make the setup the customer asked for (how many people, mic only, no product).
1295
+ Some renderers declare a route selector that decides that; today that is the street interview
1296
+ (\`render-street-interview\`). Before you propose, script or create a street-interview project,
1297
+ template or custom, check the request against its routes:
1298
+
1299
+ | Street route | On screen | Needs | Makes |
1300
+ | --- | --- | --- | --- |
1301
+ | Guessing (\`product-guess\`) | Interviewer and up to four people | A physical product to hand over, photographed on its own | A finished video |
1302
+ | Conversation (\`mic-only\`, \`product-sample\`, \`concept-challenge\`) | Interviewer and one person | No product photo, phone, screen or UI | A script and prompt preview only, no finished video yet |
1303
+ | Street testimonial (its own format) | One person talking to camera, no interviewer | The creator still its recipe prescribes | A finished video, when the catalog lists it |
1304
+
1305
+ Neither interview route takes a photo of a person: people are described in text, so never
1306
+ propose putting a founder's or creator's face in one. Keep the customer's named setup: never
1307
+ change the interaction, mode or number of people just to make a check pass.
1308
+
1309
+ **Never offer guessing to software or a service.** The guessing route needs a physical product
1310
+ to hand over. When what the brand sells is software, an app or a service (nothing a person can
1311
+ hold), never offer or script guessing, not even with a laptop, phone or other device showing the
1312
+ app as the prop: the device is a prop, so \`offering_type\` stays \`digital\` or \`service\` and the
1313
+ selector says unsupported. A brand that sells a physical product, including a device or
1314
+ hardware that comes with an app, can still use guessing with that product. Offer the table's
1315
+ real alternatives instead: a one-person conversation (a script and prompt preview only, no
1316
+ finished video yet), the street testimonial if the catalog lists it and does not rule it out,
1317
+ or custom. Asking again never unlocks a route the table or the selector rules out.
1318
+
1319
+ When they asked for a street interview, the catalog may mark (newer servers do) the
1320
+ street-interview rows \`fit.ok: false\` for such a brand: they can't make a finished video for it.
1321
+ That verdict is about the finished video, so still offer the one-person conversation, saying
1322
+ plainly that it is a script and prompt preview only. If the brand read shows the brand sells the
1323
+ device itself but the catalog rules guessing out, tell the customer the catalog flagged it and
1324
+ confirm what they sell before offering guessing with that device.
1325
+
1326
+ - **Without a shell on the customer's computer**, decide from this table. Do not fetch the renderer: its
1327
+ package is far too large for a chat.
1328
+ - **With a shell** (the customer's computer or the coworker sandbox), also run its free
1329
+ selector, which makes no paid call. If the fetch or the run fails, decide from the table.
1330
+ 1. Fetch the renderer to a file, not into the conversation: its inline package is over a
1331
+ million characters. Either save the JSON from \`gooseworks fetch render-street-interview > <file.json>\`,
1332
+ then write each \`scripts\` entry as
1333
+ \`/tmp/gooseworks-scripts/render-street-interview/scripts/<name>\` and each \`files\` entry at
1334
+ \`/tmp/gooseworks-scripts/render-street-interview/<path>\`. Or call
1335
+ \`catalog_fetch { type: "skill", slug: "render-street-interview", delivery: "archive" }\`,
1336
+ extract the ZIP, check each file against its \`manifest.json\` hashes and copy the contents of its
1337
+ \`agent-config/skills/render-street-interview/\` folder into
1338
+ \`/tmp/gooseworks-scripts/render-street-interview/\`. Either way the selector reads
1339
+ \`references/street-reference-library.json\` from that folder.
1340
+ 2. Write a brief JSON. \`mode\` and \`interaction_type\` go in pairs: \`product-guess\` with
1341
+ \`product-guess\`, or \`conversation\` with \`mic-only\`, \`product-sample\` or \`concept-challenge\`.
1342
+ \`offering_type\` is exactly \`physical\`, \`service\` or \`digital\` (software, SaaS and apps are
1343
+ \`digital\`). \`participants\` is a whole number: people interviewed on screen, not counting
1344
+ the interviewer. Leave it out when the customer named no count; the selector then uses the
1345
+ route's usual cast. When the customer named no interaction, run each mode that could fit.
1346
+ 3. Run \`python3 /tmp/gooseworks-scripts/render-street-interview/scripts/prepare_script_context.py --brief <brief.json> --out <context.json>\`.
1347
+ Read \`status\` and \`brief_gaps\` in the output file, not the exit code: it exits 2 for every
1348
+ status except \`ready-for-writing\`. A Python traceback is not a verdict: the brief or the
1349
+ saved files are wrong (for example the reference library in the wrong folder). Fix them and
1350
+ run it again.
1351
+ 4. A run with any \`brief_gaps\` has not checked the route. Fix every gap and run it again.
1352
+
1353
+ **An unsupported setup is a stop.** \`unsupported-route\`, or a request the table rules out,
1354
+ means this setup can't be made. Tell the customer plainly, in one line, for example: "A
1355
+ three-person mic-only street interview isn't something we can make yet: that version takes one
1356
+ person, and the three-person version needs a physical product to hand over." Then offer the
1357
+ closest supported options in plain words, each with how it differs. With a shell, these are the
1358
+ selector's \`alternatives\`. Without one, take them from the table:
1359
+
1360
+ - when only the number of people was the problem, the same setup within its limit: guessing
1361
+ (a physical product only) with up to four people, a finished video; or a conversation with
1362
+ one person, a script preview only;
1363
+ - for something people can't hold (software, a service), a one-person conversation instead
1364
+ of guessing;
1365
+ - one person talking to camera with no interviewer, if the catalog lists that format;
1366
+ - a custom video, untested, that keeps the street format's limits.
1367
+
1368
+ Never use route names or the script's wording. Do not create the project or write a script for
1369
+ it. Go custom only if the customer picks it; a custom video keeps the street format's hard
1370
+ constraints, which make-custom-video lists.
1371
+
1372
+ **A supported setup.** With empty \`brief_gaps\`, any other status means the route exists.
1373
+ \`needs-reference\` means no observed reference ad matched yet: it does not block proposing the
1374
+ format; carry it into the handoff so the script step resolves or reports it before script
1375
+ approval. Carry the route too: which one, how many people, finished video or preview only, and
1376
+ no person photos. A preview-only route makes a script and prompt preview, not a finished video:
1377
+ say so before the customer chooses it.
1378
+
1379
+ ## Route first: is this a new video?
1380
+
1381
+ Hand off to **\`goose-video-local\`** now, and stop following this skill, for:
1382
+
1383
+ - an existing **project** id or a **video batch** id;
747
1384
  - the app's copy-for-Claude command (it names \`goose-video-local\`);
748
1385
  - "remix this video ad template" for a specific app template.
749
1386
 
750
- Those render on the customer's own machine. Use \`goose-video-local\` if it is installed; otherwise load it with \`fetch_skill("goose-video-local")\` on the GooseWorks MCP.
1387
+ Load the current \`goose-video-local\` entry with
1388
+ \`catalog_fetch { type: "skill", slug: "goose-video-local" }\` on the GooseWorks MCP (older clients:
1389
+ \`fetch_skill("goose-video-local")\`), unless already fetched on this connection in this run.
1390
+ It reads the project first and retains an approved run's recorded recipe packages.
751
1391
 
752
- A bare **project** id ("make the video for project <id>"): call \`video_project_read { brand_id, project_id }\` first. **Stay here** when it returns an \`order\`, or the project's \`script_drafts.recipe\` is set: that is an order made through this skill, so continue it on the same \`project_id\`. **Anything else** goes to \`goose-video-local\`.
1392
+ When they ask **what** to make ("give me video ad ideas", "what angles should I use?", "what's
1393
+ working for my competitors?"), fetch **\`ad-angle-miner\`** (\`catalog_fetch { type: "skill", slug: "ad-angle-miner" }\`,
1394
+ using its advertised tools) and run it with the **video** output. It returns ranked
1395
+ video ideas mapped to formats and hands the picked ones to \`goose-video-local\`.
753
1396
 
754
1397
  Everything else, including "make me a video ad for <brand>", starts at step 1 below.
755
1398
 
756
- ## Inputs
757
-
758
- - A brand, usually named in the opening sentence. Resolved to \`brand_id\`; with one brand in the org it needs no input.
759
- - What the ad is for, in the customer's words (optional; asked once, never forced). **It is sent with the order** as \`brief.prompt\` and reaches the script. It is not only for sorting the table.
760
- - Anything else they volunteer about the ad: who it's for, names or terms it must say, things to stay away from. These are never asked for; they're kept when the customer offers them.
761
- - The picked format's own answers (voice, music, angle…). The catalogue says which apply; some formats ask nothing.
762
-
763
- Never an input: a reference video (these formats don't use one), or a promo code (it comes from the brand).
764
-
765
- ## Composed Atoms
766
-
767
- MCP tools; the work happens in the app.
768
-
769
- - \`brand_list\`: brand NAME → \`brand_id\`. Pass \`query\` when they named one.
770
- - \`brand_create { name, website_url }\`: only when the customer asks to add a brand that isn't there. Free.
771
- - \`video_catalog_list { kind: "formats", brand_id }\`: what can be ordered. Each format has \`card.description\`, \`card.best_for\`, \`card.needs\`, \`ask[]\`, \`brand_requirements[]\`, \`default_credits\` and \`examples[]\` (demo videos: a curated sample first, then real runs). The response also carries \`brief_fields\`: the intent fields every format's \`brief\` accepts on top of its \`ask[]\` (\`prompt\`, \`audience\`, \`must_mention\`, \`avoid\`, \`notes\`).
772
- - \`video_catalog_list { kind: "voices" }\`: voices with playable \`preview_url\`s, for formats that speak.
773
- - \`video_project_upsert\`: the free draft. Returns \`project_id\`, \`quote\`, \`ready\`, \`missing\` and \`questions\` (0–2 things worth asking before the script is written; usually none).
774
- - \`video_project_upsert { project_id, patch: { brief } }\`: changes the brief, checked the same way as when it was created. A key replaces its value, \`null\` removes it. Free. Once ordered, only the brief fields change (never the format's own answers), and they reach the script on the next \`redraft\`. A brief change never moves the quote.
775
- - \`video_render_run { dry_run: true }\`: re-reads the price. Free.
776
- - \`video_render_run { kind: "partial" }\`: **writes the script and stops.** Reserves the quote and shows the script before the expensive work. Works for every orderable format, chat or voiceover.
777
- - \`video_render_run { kind: "full" }\`: finishes an approved preview.
778
- - \`video_render_run { kind: "edit", edit: { script } }\`: puts the customer's OWN words into the draft, verbatim, through the format's own guards. Free before approval, and the **only** route that keeps their copy. Offered where the format's \`edits.script\` is non-empty; refused as \`script_not_editable\` elsewhere.
779
- - \`video_render_run { kind: "redraft", reason }\`: another draft, before approval, written from why they turned this one down, or with no \`reason\` after a brief change (the change is the reason). It **re-runs the writer** either way, so every word (and the picture on a character format) is replaced — it does not keep copy the customer supplied. Same order, **no new hold**, but **not free**: it adds a few credits of model spend inside the hold already placed. Up to 3, and refused if it would pass the hold. \`dry_run: true\` says what it would add.
780
- - \`video_project_read { brand_id, project_id }\`: status, the drafted script while previewing, and the finished video. Returns an \`order\` object for recipe projects.
781
- - \`job_cancel { job_id }\`: declines a preview. \`job_id\` is the order id (the project id also works). Releases the whole hold.
782
-
783
- ## Workflow
784
-
785
- The opening is fixed: **brand → what it's for → suggested formats → first question.** Do each step without waiting for the customer to ask for it.
786
-
787
- ### 1. Resolve the brand, quietly when you can
788
-
789
- Call \`brand_list\`, with \`query\` when they named a brand. \`query\` is a case-insensitive substring match, so "Kolkata Chai" also finds "Kolkata Chai Co". If it finds nothing, call \`brand_list\` once more with no query before deciding.
1399
+ ${ENVIRONMENT_IDENTITY}
790
1400
 
791
- - **The org has exactly one brand** → use it. Say which in one line ("Making this for **Bioma**.") and move on. Don't ask.
792
- - **The name they said matches exactly one brand** → use it. Say which.
793
- - **Several match, or they named none and the org has several** → show a table (name, website) and ask. The wrong brand is a wasted order.
794
- - **Nothing matches** → say so, list the brands they do have in a table, and offer to add the new one here: "Or send me its website and I'll add it." With a website, call \`brand_create { name, website_url }\` (free). It starts brand research, which fills in the logo and colours in a few minutes. Say so, then carry on from step 2 while it runs. Before step 5, check \`brand_get_context\` shows \`research_status: complete\`; until then drafts are blocked. Adding a brand is not a trip to the app. Never create a brand they didn't ask for, and never guess the website.
795
-
796
- If the GooseWorks MCP's own instructions have you check onboarding first and it turns out unfinished, finish it, then come back here with the customer's original sentence.
797
-
798
- ### 2. Ask what the ad is for, in one open question
799
-
800
- Unless the opening sentence already said it, ask **one** plain question and wait:
801
-
802
- > What's this ad for? For example: launching something, a sale, explaining how it works, or showing real results. Anything you tell me helps me pick the right format.
803
-
804
- This is a free-text question: **no menu, no table, no list of formats yet.** Take whatever they say, even "not sure" or "just something good". Never ask it twice, and never block on it: a vague answer is an answer.
805
-
806
- **Keep the answer. It goes into the order in step 5**, not just into the table's order. Write it down as they said it; that becomes \`brief.prompt\`. If their words (here or in the opening) also say who the ad is for, a name or term the ad must say, or something to stay away from, note those too:
807
-
808
- | They said | Goes in | Example |
809
- |---|---|---|
810
- | What the ad is for (the whole answer) | \`prompt\` | "creative is the bottleneck for small teams, make it a problem-solver" |
811
- | Who it's for | \`audience\` | "heads of growth at seed-stage startups" |
812
- | A name or term it must say | \`must_mention\` (a list) | \`["Claude", "ChatGPT"]\` |
813
- | Something to keep out | \`avoid\` (a list) | \`["no villain", "don't talk down to marketers"]\` |
814
-
815
- Never ask for the last three and never fill them with a guess. A \`must_mention\` term the customer didn't say is a claim we made for them.
816
-
817
- Skip the question when the opening already names a goal ("…a video ad for our summer sale") or a format ("…an iMessage video ad"). With a format named, go to the table with that format first and marked, then step 4.
818
-
819
- ### 3. Suggest formats in a table, best fit first
820
-
821
- \`video_catalog_list { kind: "formats", brand_id }\`, then **always a markdown table in your message**.
822
-
823
- Order the rows by how well each format fits their answer. Judge fit from \`card.description\`, \`card.best_for\` **and the format's \`ask[]\` options** against what they said. An option can make a format fit: a cartoon explainer whose \`ask[]\` offers \`arc: hero-helper\` fits "a friendly hero mascot, no villain"; one without that option does not, because its card says the character is the problem and loses. **A format whose card contradicts what they asked for is never Suggested**, however well its keywords match. When nothing fits, say so before the table ("None of our formats does X; the closest is Y, which gives up Z") and still show the table. Mark **exactly one** row **Suggested** with a few words on why ("real results → before/after"); a close second can be **Also good**. One suggestion keeps "the suggested one" unambiguous. With no goal given, put the formats that have demos first.
824
-
825
- | | Format | What it looks like | Price | Demo |
826
- |---|---|---|---|---|
827
- | **Suggested** | iMessage chat reveal | Two friends texting; ends on your product | ~60 credits | [watch](https://…) |
828
- | **Also good** | Apple Notes reveal | A diary-style note typed out; ends on your product | ~15 credits | [watch](https://…) |
829
- | | Kinetic type explainer | Bold on-brand text timed to a voiceover | ~80 credits | [watch](https://…) |
830
-
831
- - **Demo** is \`examples[].output_url\`. When a format has none, write "no demo yet" in the cell; never leave it blank.
832
- - **"What it looks like" is \`card.description\`, quoted.** Copy it word for word; you may cut it at a sentence boundary, never re-word it. A paraphrase once turned "narrates how it gets beaten" into "narrates the fix", which made a villain format look right for a no-villain brief. \`card.best_for\` often carries a dollar figure; never copy it into the table.
833
- - **When the pick depends on an option, say which.** "Cartoon explainer (as a friendly helper)" in the Format cell, and pre-fill that answer in step 4.
834
- - **Price** is \`default_credits\`, written approximately ("~60 credits"). It is a guide for choosing. The price they agree to is the server's quote in step 5.
835
- - **Say which formats won't take their words.** A format whose \`edits.script\` is empty writes its own copy and accepts no hand edit; the only lever is a redraft, which rewrites everything. Put "writes its own words" in that row's "What it looks like" cell. A customer who arrives with a script already written needs to know this **before** they pick, not after they hand it over.
836
- - **Formats the brand may not be able to run** go last, with \`card.needs\` in plain words, e.g. "needs real before/after photos". Don't hide them; don't suggest them. Judge logo and product photos from the \`brand_list\` row. For anything else (before/after photos, say) you can't see, treat it as missing rather than make extra calls. \`video_project_upsert\` returns \`ready\`/\`missing\`, which is the real check.
837
-
838
- **Print the table in your message, THEN ask which one** ("Want the suggested one, or another?"). Never put the formats only inside a structured question control: it renders plain option labels, not links, so the customer would be picking a format they were never able to watch. That happened on the first real run: the picker appeared, the demos didn't, and the customer had to ask where they were. A question control may follow the table to capture the answer; it never replaces it.
839
-
840
- ### 4. Ask the picked format's questions (only these)
841
-
842
- Its \`ask[]\` list is the whole question set. Anything with \`source: "recipe"\` is the recipe's call, never the customer's. **An empty \`ask[]\` means no questions**: say so ("This one needs nothing else from you") and go straight to step 5.
843
-
844
- Ask them all in **one message**, the choice tables first. Put yes/no questions that have a default at the end as a statement they can override ("Music on and a selfie in the thread; say if you want either off"). Every question with an optional answer gets a "leave it to the writer" option.
845
-
846
- **The rule most easily got wrong:** anything the customer should *see or hear before choosing* goes in a **markdown table in your message**. The structured question control only captures the answer afterwards; it cannot render a link, so a demo or a voice sample placed only in the widget is a choice nobody can actually evaluate. Table first, question second. Yes/no questions need no table, and a choice with no sample (an angle list) is still a table, just without that column.
847
-
848
- | Angle | The thread | Demo |
849
- |---|---|---|
850
- | friend-asks-friend | A friend notices something and asks | [watch](…) |
851
- | setup-flex | You send a photo, the friend reacts | [watch](…) |
852
- | swap-moment | You quit something worse for this | [watch](…) |
853
- | feature-as-punchline | The product's own mechanic is the reveal | [watch](…) |
1401
+ ${ASSET_READINESS}
854
1402
 
855
- Same for voices: \`video_catalog_list { kind: "voices" }\` gives name, gender, accent and a \`preview_url\`. Put the name in a link so it plays. This was the clearest moment of the first real run: a voice table with playable samples. Do the same for any avatar or style choice a format exposes.
856
-
857
- A choice with an \`enum\` and no samples (the cartoon explainer's \`style\`, say) is still a table: one row per value, described in the words of the field's own \`description\`, with "no demo yet" in the sample column. Don't invent what a style looks like.
858
-
859
- Never ask for a promo code.
860
-
861
- ### 5. Draft and price
862
-
863
- \`video_project_upsert { brand_id, name, format, brief: { …answers, prompt, audience?, must_mention?, avoid? } }\` is free. \`brief\` holds the picked format's \`ask[]\` answers **plus** the step-2 intent. Leave out any intent field the customer never gave. If \`ready\` is false, **stop** and relay \`missing\` in plain words.
864
-
865
- The server refuses any other key with \`invalid_brief\` and names every key it accepts. Fix the brief from that message; never drop the customer's intent to make the error go away.
866
-
867
- **If \`questions\` is not empty, ask them now, in one message, before showing the price.** Each one says which brief field its answer fills (\`fills\`). Save the answers with \`video_project_upsert { project_id, patch: { brief: { <fills>: <their answer> } } }\`. The bounds:
868
- - **One round.** Never ask a follow-up, and never ask the same thing twice.
869
- - **Never required.** If they skip them or say "just make it", carry on with the draft as it is.
870
- - **Empty \`questions\` means ask nothing.** The server skips it when the brief already says what the ad is for and who it's for, and never asks what the brand record already knows. "Make me a video ad for X" stays a complete request.
871
-
872
- Show the price the draft returned (the \`quote\`), **never a number from this page or the catalogue**. If it differs from the table's "~N", the quote is right. Convert at **100 credits = $1**.
873
-
874
- ### 6. Write the script and show it, before the expensive work
875
-
876
- \`video_render_run { brand_id, project_id, kind: "partial" }\`.
877
-
878
- This reserves the credits and writes the script only: **no video is rendered.** It returns immediately with \`status: "previewing"\` and no script yet; the writing happens in the background. Poll \`video_project_read\` every 20 seconds (it lands in about 40) and read \`order.preview\`. Calling \`kind: "full"\` while it is still \`previewing\` is refused with \`preview_in_progress\`.
879
-
880
- \`order.preview\` is shaped by the format, and **every** orderable format has one. A preview was chat-only until 2026-09-25, and a kinetic-type customer approved a price and first saw the copy in the finished video.
881
-
882
- **A chat format** (iMessage / ChatGPT / Notes) fills \`thread\` (the message list), \`angle\`, \`cta_text\` and \`selfie_url\`. Show it as a readable transcript, not JSON:
883
-
884
- > **them:** wait why do you look so into your phone rn 😭
885
- > **me:** I literally just chose to betray the duke
886
- > **them:** omg is this a game or a book
887
-
888
- **\`selfie_url\` is a generated face — show it as a link.** These formats draw the selfie INSIDE the script step, so it never appears in \`anchor_images\` and \`stage\` stays \`"script"\`. It is still a face that will be in the ad, so it still needs their yes.
889
-
890
- **Any preview with \`order.preview.stage: "image"\`** stopped after the **picture**, not just the words, so they judge the face before paying for the video. Go by that field, never by a list of format names — it follows what the recipe's steps produce, so a format joins this branch without a skill edit. Today: the animated character, the reaction selfie, the podcast hosts, the UGC creator, and the voiceless dance story (four to six stills, and no spoken script at all — its stills and \`detail\` ARE the draft, so say that rather than reporting an empty script).
891
-
892
- Show every URL in \`order.preview.anchor_images\` as a link (the podcast format has two, one per host), alongside the script. Then say plainly, in this order: what they are looking at; that **nothing has been rendered yet and the pause is deliberate**, because the video is generated FROM this picture and changing the face now is cheap; that **approving starts the render and commits the credits already held**; and that cancelling instead releases the whole hold, so the picture costs them nothing. A pause with no reason given reads as a broken order.
893
-
894
- **A voiceover format** (kinetic type) fills \`beats\` instead, and \`thread\` is null. Each beat has \`vo_lines\` (what is spoken) and a \`beat\` label. \`detail.hyperframe.plan.slates[].props\` holds the words that go **on screen**, which for this format is most of the ad. Show both columns, in order:
895
-
896
- | # | On screen | Voiceover |
897
- |---|---|---|
898
- | 1 | Sunscreen that pills | Most sunscreens pill under makeup. |
899
- | 2 | Myth: all SPF is greasy → Fact: not a fluid one | This one is a fluid. It sinks in. |
900
-
901
- Read the spoken lines out as one paragraph underneath, so they can hear the pacing.
902
-
903
- Whatever the format, **quote it back verbatim.** Do not paraphrase, tidy or shorten the copy; they are approving the exact words that will be rendered.
904
-
905
- If \`order.preview.brief_check\` is present, read it before asking for approval:
906
- - \`missing_mentions\`: terms they asked for that the script never says. Tell them which, plainly ("It doesn't mention ChatGPT yet").
907
- - \`endorsement_flags\`: lines that make another brand sound like it endorses, partners with or ranks theirs. Show the line and say it has to change: a listed name may appear as "works with", never as "recommends" or "#1 for".
908
-
909
- If \`order.brief_changed_since_draft\` is present, the brief was changed after this draft was written. **Approving renders the draft as shown**, so redraft first (no reason needed), or tell them the change won't be in this video.
910
-
911
- Then ask: **use this, change it, or stop?**
912
-
913
- **One question decides how to change it: did they give you the actual WORDS, or did they tell you what is wrong?** Only the first route below keeps their words. The other two write new ones — which is right when the customer wants something different, and is a silent rewrite when they wanted what they wrote.
914
-
915
- - **They gave you the words** ("use these lines", "the end card must say Meet Goose", "keep this but change the second bubble") → \`video_render_run { kind: "edit", edit: { script: { … } } }\`. **This is the only route that keeps copy verbatim.** It puts their text through the format's own guards and costs nothing extra before approval. Send the shape the preview handed you: \`script.thread\` for a chat format, \`script.beats[i].vo_lines\` (one entry per beat, \`{}\` for a beat you are not changing) for a voiceover format, \`script.slates[{ beat_idx, props }]\` for on-screen words, \`script.cta_text\` for the end card, \`ingredient: "character"\` + \`script.character.description\` for the character itself.
916
- - **An edit before approval IS the approval**: it applies the copy and starts the render at once (status → \`running\`). So show the exact words you are about to send, get a yes, and only then send it. An unapproved fix stays a proposal in the chat.
917
- - Refused with \`script_not_editable\` → this format writes its own copy and takes no hand edit. **Say so, and do not paraphrase their lines into a redraft** — that hands them a video that is not what they wrote. Their real options are a redraft (new words, not theirs), a re-order with different answers, or cancel.
918
- - Refused with \`script_rejected\` → a line makes a claim the brand's facts don't support. Relay the reason and offer a rewrite; never argue it through.
919
- - **They changed the BRIEF, not the copy** — the problem it shows, who it's for, a name to say, something to avoid, a note like "keep it dry" → save it with \`video_project_upsert { project_id, patch: { brief } }\` (use \`notes\` for anything that isn't one of the other fields; keep their original \`prompt\`), then \`video_render_run { kind: "redraft" }\` with no \`reason\`. The new draft is written from the updated brief, so **the words will be new** — this changes the instructions, not the script. Same cost wording as below: no new order, no new hold, a few credits inside the hold.
920
- - **They only said what's wrong** ("too salesy", "the character looks like a mug") → ask **why** in one line, then \`video_render_run { kind: "redraft", reason: <their words> }\`. **A redraft RE-RUNS the writer: every word, and the picture on a character format, is replaced.** Their reason steers the next draft; it is not copied into it. Never put exact lines in \`reason\` expecting them back. Say what it costs, precisely: "no new order and no new hold; it adds about N credits to what this video costs, inside the hold" (N from the response's \`redraft.credits_estimate\`). **Never call it free.** Poll \`video_project_read\` until \`preview\` again and show the new draft plus \`order.spend\` (what each draft added, how much of the hold is left). A \`redraft_exceeds_hold\` or \`redraft_limit\` refusal means: approve a draft or cancel.
921
- - Different answers altogether (another format, another angle) → a fresh project, and cancel this one.
922
- - Stop → \`job_cancel { job_id: <order id> }\`. The whole hold is released and they pay nothing. Cancel only works while the order is \`queued\`, \`previewing\` or \`preview\`; once they approve and it is \`running\`, it is being made and cannot be refunded.
923
-
924
- This is the review. It happens here, not in the app.
925
-
926
- **If the server answers \`preview_not_available\`,** that is a bug: every sold format has a script preview. Say plainly "I can't show you the script for this one before it's made", and stop. Do not quietly skip the step and charge as if the gate had passed.
1403
+ ## Inputs
927
1404
 
928
- ### 7. Finish it
1405
+ - A brand, usually named in the request. Resolve it from the accessible brands; one brand needs no question.
1406
+ - The customer's goal, occasion, audience, selected angle and constraints when supplied; otherwise propose defaults from the verified kit.
1407
+ - The selected format's required assets. Presence does not prove suitability; uncertain assets remain “needs review.”
929
1408
 
930
- Only after they approve the script: \`video_render_run { brand_id, project_id, kind: "full" }\`.
1409
+ ${DURABLE_BRAND_CAPTURE}
931
1410
 
932
- Poll \`video_project_read { brand_id, project_id }\` **every 30 seconds.** Done means \`order.status\` is \`done\`; the same response then carries \`video_url\` (the project page) and \`mp4_url\` (the file).
1411
+ ${VIDEO_TASTE_CAPTURE}
933
1412
 
934
- **How long depends on the format.** One number is wrong by 2x across the catalogue, and a customer told "about 5 minutes" at minute 9 thinks it has failed:
1413
+ ## Composed Atoms
935
1414
 
936
- | Format | Typical | Give up after | Why |
937
- |---|---|---|---|
938
- | iMessage / ChatGPT / Notes chat reveal | ~5 min | 10 min | No video is generated; a browser renders the phone UI, then ffmpeg cuts it |
939
- | Kinetic type explainer | **~10 min** | 20 min | Two AI b-roll clips, a voiceover, a music bed, HTML slides and a stitch |
1415
+ Use the live canonical schemas: brand_read, brand_create, video_catalog_list,
1416
+ video_project_upsert, video_project_read, catalog_fetch, goose_run_task and job_cancel.
1417
+ An old tool name in a recipe is not grounds for terminal-update advice in a chat host.
940
1418
 
941
- An unlisted or new format: assume the longer budget. Say the number you are working to up front ("this takes about ten minutes; I'll keep checking"), and say something at the halfway mark rather than going silent. Waiting is not failing; silence feels like it.
1419
+ ## Paid media: images, clips, voice
942
1420
 
943
- ### 8. Deliver in the chat
1421
+ **No FAL_KEY, ElevenLabs key or \`fal_client\` is ever needed.** Use
1422
+ \`data_post_provider { provider: "fal", path: <model id, e.g. "fal-ai/nano-banana/edit">, body: <model input>, project_id }\`
1423
+ then \`job_get { job_id }\`; voice/music use provider:"elevenlabs". Upload local inputs through
1424
+ media_upload first. **A missing key is never a blocker.** \`photos_generate\` is **not** a general
1425
+ image tool: it photographs physical catalog products only. Use goose-video-local for the
1426
+ runtime, paid approval, asset saving and quality checks.
944
1427
 
945
- **Lead with \`video_url\`** — the project page, where the video plays and they can come back to it. Give \`mp4_url\` second and name it as the file ("and the raw MP4, if you want to download or upload it"). Never hand over the CloudFront \`.mp4\` on its own: it is a file, not a place — nothing to return to, nothing to edit from, and it reads like a debug artifact rather than a delivery. If \`video_url\` is missing, poll once more; never substitute the mp4 for it silently.
1428
+ ## Workflow
946
1429
 
947
- Say what the video is: length, ratio, what's in it. This is delivery **in the chat** — the link is the video, not an instruction to go to the app. If they want a change, offer to make another with different answers. That is a new order and a new charge; say so.
1430
+ ### 1. Resolve the brand, quietly when you can
948
1431
 
949
- The only reason to send someone to the app is **payment**: not enough credits, or a plan without video.
1432
+ Call brand_read without brand_id to list accessible brands. Resolve the customer's name from
1433
+ actual returned names. One brand, or exactly one match: state it and continue. Several possible
1434
+ brands: show names and websites and ask once. No match: show accessible brands and offer to add
1435
+ one only when the customer asks and supplies its website; never guess the URL.
1436
+
1437
+ Read \`brand_read { brand_id, sections: ["summary","kit","products","learnings"] }\` before
1438
+ choosing a format. Reconcile explicit brand corrections through the durable capture rules.
1439
+ A direct video request does not require first-campaign acceptance. Follow any returned
1440
+ request-specific setup requirement; never mark research complete or accept a campaign to
1441
+ bypass setup. Keep the original request.
1442
+
1443
+ ### 2. Keep the goal and propose defaults
1444
+
1445
+ Never ask what the ad is for before showing formats. Keep the customer's exact direction.
1446
+ Otherwise propose defaults from the verified kit's products, audience, offer and voice and the
1447
+ Brain search's evidence brief (proven angles, saved rules, approved claims). Show those defaults
1448
+ in the saved plan so any can be changed. Never invent proof or product facts.
1449
+ Idea requests still follow ad-angle-miner with the video output.
1450
+
1451
+ ### 3. Show the picker or its text fallback
1452
+
1453
+ Call \`video_catalog_list { kind: "formats", brand_id }\`. Every caller gets every format.
1454
+ Respect requested limit and next_cursor for more item pages; the card may
1455
+ contain a full picker independently of the item page.
1456
+
1457
+ - **card.display_hint:"widget"**: the picker replaces a format table. Write at most one short
1458
+ acknowledgement, print no table or list of the formats, and wait for the selection.
1459
+ - **Otherwise**: print card.text_summary as returned. Without a card, show returned rows with
1460
+ names, faithful descriptions, needs and demo links, respecting the requested page size.
1461
+ Write "no demo yet" when absent. Never put links only in a question control.
1462
+ - A format whose card contradicts what they asked for is never Suggested. A \`needs_browser\`
1463
+ format is made only on the customer's computer (the coworker has no browser): if you have no
1464
+ shell there and the customer picks one, say it needs GooseWorks running on their computer and
1465
+ offer another.
1466
+ - A row with \`fit.ok: false\` is ruled out for this brand. Never propose it unless the customer,
1467
+ after hearing why, still asks for it. For a street interview, insisting never unlocks a route
1468
+ the route check rules out; when they asked for one, the one-person conversation preview is the
1469
+ one exception (see the route check). When they ask about a ruled-out row, tell them why in
1470
+ plain words (its \`fit.reason\`), never the field name.
1471
+ - \`suggested\` ranks a row for the brand; it never means the row makes the setup they named.
1472
+ Never tell the customer a row is "exactly" what they asked for beyond what its card says. A
1473
+ street interview still needs the route check above first.
1474
+ - Missing required assets are missing; unknown suitability is “needs review.” Inspect the
1475
+ selected format's candidates before spending. Never promise an unverified asset is ready.
1476
+ - Template formats use **one plan, one approval with the total in credits**, not approval for
1477
+ each paid step. Custom videos retain separate authenticated script/ingredient/budget gates in this chat.
1478
+
1479
+ ### 4. Check this machine can render it
1480
+
1481
+ - **No shell on the customer's computer:** skip local checks and hand the project off in Step 5.
1482
+ Never tell the customer they need Claude Code to start. Give computer setup guidance only if
1483
+ asked how to make a \`needs_browser\` format.
1484
+ - **Local shell:** run \`gooseworks doctor --no-browser\` for common setup (auth/MCP, Node 18+,
1485
+ ffmpeg with libx264 (libass only for an ASS caption route), ffprobe). Then fetch the selected template and its capabilities,
1486
+ and install documented dependencies in its fetched folder. Do not guess a renderer from a
1487
+ format name. For each Node renderer using default Playwright Chromium, run
1488
+ \`gooseworks doctor --renderer-script "/absolute/path/to/the/fetched/scripts/record.js"\` with
1489
+ the exact environment used to render, including NODE_PATH and PLAYWRIGHT_BROWSERS_PATH.
1490
+ Use the actual script path. Custom browser launch settings need their equivalent exact-runtime
1491
+ check. Non-browser capabilities need only their documented runtime checks.
1492
+ Never create paid ingredients before the selected renderer passes. Repair and recheck under
1493
+ existing setup permissions. If it cannot be fixed, hand the project off in Step 5 (a
1494
+ \`needs_browser\` format can't be: offer another); never silently change the selected format.
1495
+ - **Coworker sandbox:** you are the renderer. Follow goose-video-local's sandbox checks; never
1496
+ hand off recursively or claim browser capability that the sandbox lacks.
1497
+
1498
+ ### 5. Create the project and hand it off, in this session
1499
+
1500
+ 1. \`video_project_upsert { brand_id, name, format: <template_id> }\` with no brief (a brief creates
1501
+ a concept batch). A street interview is created only after its route check found a supported
1502
+ setup. Default
1503
+ creation_intent:"format" keeps the style with this brand's content; source_remix requires an
1504
+ explicit choice to use source content.
1505
+ If they already chose a campaign/concept, include its verified campaign_id and optional
1506
+ campaign_concept_id. Read the IDs from that brand's saved campaign; the concept must belong
1507
+ to it. Omit unknown IDs and never infer a link or create a campaign solely to file a video.
1508
+ 2. **You make it** (a shell on the customer's computer, or the coworker sandbox): load
1509
+ \`catalog_fetch { type: "skill", slug: "goose-video-local" }\` and follow it on the same project_id.
1510
+ Carry the customer's words, verified defaults, campaign, selected angle and a street
1511
+ interview's route check result into its brief.
1512
+ 3. **No shell there:** \`goose_run_task { brand_id, project_id, message }\` with the request, selected
1513
+ format, defaults and a street interview's route check result. Keep task_id. Continue questions/edits with the same task and project.
1514
+ A saved free draft is not a started worker or a complete plan. Follow actual saved state.
1515
+ 4. When follow.card_follows is true, the card follows progress: do not re-read it in a loop.
1516
+ Read again on a customer reply, card action or requested update. A failure or missing saved
1517
+ plan needs the returned recovery action; never claim completion or approve an empty plan.
1518
+ Approval requires the current complete plan and total. Insufficient balance: offer a shorter
1519
+ video or top-up. “Not now” keeps the saved plan.
1520
+ 5. On the customer's yes, record it with \`video_project_upsert { brand_id, project_id, patch:
1521
+ { approve: { user_quote, total_credits } } }\`. Its reply says what comes next: the coworker
1522
+ gets the approval when it is making the video; otherwise you render it.
1523
+
1524
+ Do not hand the customer a command to paste somewhere else.
950
1525
 
951
1526
  ## Decision Rules
952
1527
 
953
- - **One sentence is a complete request.** Never answer "make me a video ad for X" by asking which format, which tool or what to do next. Run steps 1–3 and let the table do the asking.
954
- - **One brand in the org → never ask which brand.** State the one you used.
955
- - **Open question first, table second.** Don't lead with the whole catalogue. The goal question comes before any list, and the table comes ordered, with one suggestion.
956
- - **No approval of the SCRIPT → do not call \`kind: "full"\`.** The price gate is not the script gate. They approve a number in step 5 and the actual ad in step 6.
957
- - **More than two options → table in the message.** Every time. Options with a sample or demo the customer can't hear or see are not really choices. A question control may capture the answer after the table, never instead of it.
958
- - **\`ready: false\` → stop.** Ordering anyway fails and wastes their time.
959
- - **Unsure whether an order went through → read \`video_project_read\` first.** Never use the charging tool as a status probe. Only if the project shows nothing at all, re-call \`video_render_run\` on the SAME \`project_id\` (idempotent per project). Never create a second project; that is a second charge.
960
- - **Quote cards, never paraphrase them.** The card is the server's promise about the format. A reworded card can promise something the format cannot do.
961
- - **A contradiction with the card outranks every keyword match.** Check what they asked for against what the card and the \`ask[]\` options say the format does; a format that can't do the ask is never Suggested.
962
- - **They asked for a format that isn't in the catalogue** → it isn't available yet. Say so, show the table, and don't improvise a lab recipe.
963
- - **Run failed → say so plainly.** The credits are released automatically. Offer a retry; never retry unasked.
964
- - **Their words go in an \`edit\`, never in a \`redraft\` reason.** A \`reason\` steers the next draft; it is not copied into it. The moment a customer gives you actual copy, the only honest routes are \`kind: "edit"\` or telling them this format will not take it.
965
- - **Deliver the page, not the file.** \`video_url\` leads, \`mp4_url\` follows. A bare CloudFront link is not a delivery.
966
- - **The brief is the customer's words, not yours.** \`prompt\` is their answer, verbatim or close to it. \`audience\`, \`must_mention\` and \`avoid\` hold only what they said. Another brand in \`must_mention\` means they asked for it; it can be named as "works with", never as an endorsement or ranking.
1528
+ - One sentence is a complete request. Resolve the brand and show formats without a goal interview.
1529
+ - One brand in the org: never ask which brand. Cards decide presentation; checks decide execution.
1530
+ - Use canonical tools. Preserve campaign context, selected script angles and custom authenticated review gates.
1531
+ - Never fabricate readiness, completion, approval or a second project.
967
1532
 
968
1533
  ## Output
969
1534
 
970
- The finished video in the chat: \`video_url\` (the project page) first, \`mp4_url\` (the file) second, with its length and ratio.
971
-
972
- The credits leave the balance when the order is placed (a hold). They are **captured** on success or **released** on failure or cancellation. A balance that dropped does not prove a charge, so don't describe it to the customer that way.
1535
+ One project, one current saved plan in this chat, then the finished video. With a widget write one
1536
+ line and do not add app_url, brand_url or duplicate video links. A text host gets text_summary
1537
+ and its usable delivery link.
973
1538
 
974
1539
  ## Quality Checks
975
1540
 
976
- - A one-sentence opening got: the brand resolved (unasked when there is one), one open goal question, then a format table with demo links and one suggestion.
977
- - The table was ordered by the customer's goal, not the catalogue's order.
978
- - Every "What it looks like" cell is the card's own words. No Suggested format's card contradicts what the customer asked for; when nothing fit, you said so and named the closest with its trade-off.
979
- - Every multi-option question was a table in the message, with a demo or sample column wherever one exists.
980
- - Only the picked format's \`ask[]\` questions were asked.
981
- - The step-2 answer went into the order as \`brief.prompt\`, and no intent field held anything the customer didn't say.
982
- - They approved the **script**, not just the price, before the expensive work ran: the thread for a chat format, the on-screen words *and* the voiceover for a kinetic-type one.
983
- - You told them how long the render would take **for the format they picked**, and said something at the halfway mark instead of going quiet.
984
- - The quote you showed came from the server, never from memory or the catalogue.
985
- - Every line of copy the customer wrote is in the finished video word for word — it went through \`kind: "edit"\`, or you told them plainly that this format would not take it. No supplied copy was ever paraphrased into a \`redraft\` reason.
986
- - The video was delivered in the chat as \`video_url\` first and \`mp4_url\` second. You did not send them to the app for anything but payment.
987
- - You asked for no promo code or reference video, and made no product claim the brand's own facts don't support.
1541
+ - No forced goal question, duplicate table or unsupported shell claim.
1542
+ - The exact renderer passed before local paid ingredients; uncertain assets stay uncertain.
1543
+ - Selected campaign and angle survive; a single template project has no brief.
1544
+ - Current complete plan and total precede approval. Custom script and ingredient gates remain independent; Studio is optional.
988
1545
 
989
1546
  ## Failure Modes
990
1547
 
991
1548
  | Symptom | Cause | Fix |
992
1549
  | --- | --- | --- |
993
- | "Make me a video ad for X" got back "which format / what would you like?" | Treated the one-liner as incomplete | Run steps 1–3 unprompted: resolve the brand, ask the goal, show the table |
994
- | Asked which brand in a one-brand org | Skipped the count check in step 1 | Use the only brand and say which |
995
- | Opened with the full format list | Skipped the goal question | Ask what the ad is for first; order the table by the answer |
996
- | Demo links invisible to the customer | The choices went only into the structured question control | Table in the message first; the control only takes the answer |
997
- | \`format_not_available\` | Not an orderable format | Re-read the catalogue; offer what's in it |
998
- | \`brand_not_ready\` | Brand has no product photos or logo | Relay \`missing\` in plain words; stop |
999
- | \`invalid_brief\` | The brief carried a key this format doesn't accept (e.g. another format's question, or a made-up field like \`tone\`) | The message lists what is accepted. Move the customer's words into \`prompt\`/\`audience\`/\`must_mention\`/\`avoid\`; never drop them |
1000
- | The script ignores what they asked for | The step-2 answer wasn't sent (it only sorted the table), or they added something later that never reached the brief | Send it as \`brief.prompt\` in step 5. Anything they add later goes in with \`patch.brief\` (\`notes\`) and then a redraft. The preview's \`brief_check\` shows missing must-mention terms |
1001
- | Video not on this plan | Lite and trial have no video entitlement | Say so and point at the upgrade; this is the one app trip that's allowed |
1002
- | Insufficient credits | Wallet short | Nothing was created or charged; report the shortfall |
1003
- | Preview looks wrong | The script or the picture isn't what they wanted | They gave you the words → \`kind: "edit"\`. They changed the brief → \`patch.brief\` then a redraft with no reason. They only said what's wrong → \`kind: "redraft"\` with their reason. Same order, no new hold, a few credits inside it; never "free". Or \`job_cancel\` this one |
1004
- | **The customer wrote the script and the finished video says something else** | Their lines went into a \`redraft\` \`reason\`. A redraft re-runs the writer: the reason steers the next draft, it is never copied into it. Seen on staging — "Keep this exact draft, use these lines: …" came back paraphrased, twice, and was paid for both times | Copy goes in \`kind: "edit"\`. If the format refuses it (\`script_not_editable\`), say so and let them choose a redraft, a re-order or cancel — knowing the words will be new |
1005
- | Video started rendering before the customer approved | \`kind: "edit"\` on a preview applies the copy **and** starts the full render (status → \`running\`) | Show the exact words you're about to send, get a yes, then send the edit. An unapproved fix stays a proposal in the chat |
1006
- | \`draft_is_your_copy\` on a redraft | The current draft is the customer's own edited copy; another draft would replace their words | Correct behaviour. Edit their copy again with \`kind: "edit"\`, approve it, or cancel |
1007
- | \`script_rejected\` on an edit | A line makes a claim the brand's facts don't support | Relay the reason verbatim and offer a rewrite; nothing was charged |
1008
- | The customer was sent a raw \`cloudfront.net/….mp4\` | Delivered \`mp4_url\` (or \`order.output_url\`) instead of \`video_url\` | \`video_url\` leads and \`mp4_url\` follows. Both come back from \`video_project_read\` once \`order.status\` is \`done\` |
1009
- | \`recipe_brief_locked\` | You patched \`script_drafts\` raw on a format project, which would have skipped the brief's checks | Send the change as \`patch.brief\` instead |
1010
- | \`format_answers_locked\` | After ordering, you tried to change the format's own answer (voice, angle, selfie) through the brief | Those change what the run costs. Use \`kind: "edit"\` where the format lists it, or start a new project |
1011
- | Asked the customer three rounds of questions before anything was made | Treated \`questions\` as a form, or asked your own follow-ups | Ask what \`questions\` holds, once, in one message. Nothing else. Skipping is fine |
1012
- | \`preview_in_progress\` | You called \`full\` while the script was still being written | Keep polling \`video_project_read\` until \`order.preview\` appears |
1013
- | Cancel refused | The order is already \`running\`; they approved it | Say it's being made; a refund isn't possible now |
1014
- | No render after the format's budget (10 min chat, 20 min kinetic) | The worker didn't pick the run up | Credits are held, not spent. Say it's queued and you'll follow up; don't re-order |
1015
- | \`preview_not_available\` on an orderable format | A bug; every sold format has a script preview | Say so and stop. Do not order without showing the script |
1016
- | An edited script is refused (\`script_not_editable\`) | This format's writer takes no hand edit (its \`edits.script\` is empty). Only some formats do | Should have been said at the table. Their words cannot be rendered verbatim here: offer a redraft (new words), a re-order with different answers, or cancel — and never quietly paraphrase their copy into the redraft reason |
1017
- | A "hero, no villain" brief got the villain cartoon | The card was paraphrased ("narrates the fix") and the fit check never read what the format can't do | Quote \`card.description\`; check the ask against the card and \`ask[]\`; never Suggest a contradiction |
1018
- | Two videos, two charges | A second project was created instead of continuing the first | Continue on the SAME \`project_id\` |
1019
-
1020
- `;
1550
+ | Picker plus table | Ignored display_hint | Widget: one line; otherwise text_summary |
1551
+ | Agent with a shell hands off | Decided from the host, not the rule | Make it here; hand off only without a shell or a fixable toolchain |
1552
+ | Chat told it cannot start | Only local execution considered | Create once and delegate to coworker |
1553
+ | Worker ends without a plan | Assumed completion means ready | Follow actual failure/recovery state |
1554
+ | Two projects for one video | Recreated instead of resuming | Continue same project and task |
1555
+ ${STORED_FOOTAGE_GUIDANCE}`;
1021
1556
  }
1022
1557
  /**
1023
- * Returns the goose-video-local entry SKILL.md content (GOOSE-3677).
1558
+ * The canonical render-row actions for a client/agent-rendered video
1559
+ * (GOOSE-3725): `video_render_run kind:"full"` (no dry_run) on a sample-remix
1560
+ * project OPENS a queued ad_render row and returns `render_id`;
1561
+ * `video_render_run { render: { render_id, status, ... } }` UPDATES it
1562
+ * (← update_render_status). If those names change, edit ONLY these constants:
1563
+ * every mention in the goose-video-local skill text is built from them.
1564
+ */
1565
+ exports.RENDER_ROW_TOOL = 'video_render_run';
1566
+ exports.RENDER_OPEN_ARGS = 'kind: "full"';
1567
+ exports.RENDER_UPDATE_KEY = 'render';
1568
+ /**
1569
+ * Returns the goose-video-local entry SKILL.md content (GOOSE-3677, GOOSE-3726).
1024
1570
  *
1025
1571
  * The local-render runtime. VIDEO projects made in the app (template remixes,
1026
- * concept batches, anything that is not a recipe order) still render on the
1027
- * user's own Claude Code: the app pre-creates the project and hands the user a
1028
- * paste prompt; this skill fetches the per-format recipe, renders on the user's
1029
- * machine (Playwright + ffmpeg + the GooseWorks media proxies), mirrors the
1030
- * script for a free in-app review, then saves the finished MP4 back over MCP.
1031
- * It cannot be retired until a server worker consumes those renders
1032
- * (`submit_render` alias: sunset "after-render-worker").
1572
+ * concept batches, anything that is not a recipe order) render wherever the
1573
+ * agent runs: the user's own Claude Code (with or without the gooseworks CLI)
1574
+ * OR an agent inside the GooseWorks E2B workspace sandbox (canonical MCP tools
1575
+ * + Bash, no CLI, no credentials.json; media proxies via GW_MEDIA_PROXY_TOKEN).
1576
+ * It fetches the per-format recipe, renders (ffmpeg, PIL, Playwright where
1577
+ * available), mirrors the review set for a free in-app review, then saves the
1578
+ * finished MP4 back over MCP using the canonical tools (video_project_*,
1579
+ * catalog_fetch, media_upload, the video_render_run render-row actions).
1033
1580
  */
1034
1581
  function getGooseVideoLocalSkillContent() {
1035
1582
  return `---
1036
1583
  name: goose-video-local
1037
1584
  slug: goose-video-local
1038
1585
  description: >
1039
- Render an EXISTING GooseWorks video ad project or video batch on this machine (Playwright +
1040
- ffmpeg + GooseWorks media proxies) and save the finished MP4 back to the project over MCP. Use
1041
- when the app's "copy for Claude" command names goose-video-local, for "make the video for
1042
- project <id>" / "for video batch <id>", or to remix a video ad template locally. Needs a machine
1043
- with network egress and ffmpeg (local Claude Code or the desktop app), not a hosted connector.
1044
- To order a NEW video ad in chat, use goose-video instead.
1586
+ Render an EXISTING GooseWorks video ad project or video batch with ffmpeg (plus Playwright for
1587
+ phone-mockup formats) and the GooseWorks media proxies, then save the finished MP4 back to the
1588
+ project over MCP. Runs on the user's own machine (Claude Code / desktop app, with or without the
1589
+ gooseworks CLI) OR inside a GooseWorks workspace sandbox (canonical MCP tools + Bash, no CLI).
1590
+ Use for a client-side format project (created by goose-video), a template-remix project or a
1591
+ video batch. A copy-for-Claude command or project id must first be checked with
1592
+ video_project_read. An agent that can't run shell commands on the customer's computer hands this same project to the GooseWorks coworker. To start a NEW video ad in chat,
1593
+ use goose-video first.
1045
1594
  category: ads
1046
- version: 0.3.0
1595
+ version: 0.7.0
1047
1596
  author: GooseWorks
1048
- tags: [gooseworks, ads, video, remix, imessage, local-render, byoa]
1597
+ tags: [gooseworks, ads, video, remix, imessage, podcast, ugc, local-render, sandbox, byoa]
1049
1598
  ---
1050
1599
 
1051
1600
  # GooseWorks Video Ads — local remix runtime
1052
1601
 
1053
- You produce **video** ad creative on the user's OWN machine and sync the result back to the
1054
- GooseWorks app over MCP. This document is the **runtime contract** (auth, credits, the media
1055
- proxies, data I/O, the review gate). A separate **recipe skill** — fetched per format — tells you
1056
- *what to make* (the pieces, prompts, models, order of assembly).
1602
+ ${CUSTOMER_TALK}
1603
+
1604
+ ${CLI_TOOL_BRIDGE}
1605
+
1606
+ ${videoEntryPreparation('goose-video-local')}
1607
+
1608
+ ## Chat hosts and cards
1609
+
1610
+ ${WHO_MAKES_THE_VIDEO}
1611
+
1612
+ First perform the mandatory route check below. Then, if you can't run shell commands on the
1613
+ customer's computer, hand a verified template
1614
+ project to \`goose_run_task { brand_id, project_id, message }\`, or a verified template batch to
1615
+ \`goose_run_task { brand_id, batch_id, message }\` (never send both ids). Keep task_id and the
1616
+ same project or batch. Generated custom children keep their separate make-custom-video flow.
1617
+ Inside a coworker sandbox you are the renderer: never delegate recursively.
1618
+ With card.display_hint:"widget", say at most one line and never duplicate its plan, total or
1619
+ links. Otherwise print card.text_summary. Read again on customer input, not in a polling loop.
1620
+ A free saved draft is not proof that planning started or a complete plan exists.
1057
1621
 
1058
- **Division of authority — read both, but when they disagree THIS doc wins on the environment AND the
1622
+ ## Mandatory route check before any local work or spend
1623
+
1624
+ For every existing \`project_id\` (including one supplied by the app's copy-for-Claude command),
1625
+ call \`video_project_read { brand_id, project_id }\` **before** template lookup, toolchain setup,
1626
+ media-proxy calls, or a review-set upload. For a batch, inspect each child project.
1627
+
1628
+ - **A server-rendered order** — the response has \`creative_plan\`,
1629
+ \`project.creative_spec_revision_id\`, \`order.creative_spec_revision_id\`, a planning
1630
+ \`lifecycle\`, or an \`order\` / \`script_drafts.recipe\` on a server format → **stop.**
1631
+ Server video orders are paused, and there is **no vetted local node-execution API** for them:
1632
+ do not rebuild one locally. Tell the customer this project was made for the server flow, which
1633
+ is paused; offer to start the same ad on a client-side format (\`goose-video\`, fetched with
1634
+ \`catalog_fetch { type: "skill", slug: "goose-video" }\`, older clients:
1635
+ \`fetch_skill("goose-video")\`). An order already holding credits can be released with
1636
+ \`job_cancel\`.
1637
+ - **A client-side format or template remix** (a \`source_sample_id\` / \`template_id\` and none of
1638
+ the above) → continue below.
1639
+ - **Generated custom video** (\`project.custom_video_state.mode === "generate"\`) → fetch \`catalog_fetch { type: "skill", slug: "make-custom-video" }\`, follow it on this same project and stop following the template flow. Record independent authenticated script/ingredient approvals in this chat with the current custom_review.approval_quote phase, review_token and cumulative total; Studio is optional. A handed-off coworker cannot self-approve; do not report approval_not_required.
1640
+ - **Unclear** → read again or ask; never guess and generate. A copy prompt that names this skill
1641
+ is not proof of which kind the project is.
1642
+
1643
+ Continue below only for a verified client-side format or template remix.
1644
+
1645
+ For client-side formats and template remixes, you produce **video** ad creative wherever THIS agent runs and sync
1646
+ the result back to the GooseWorks app over MCP. This document is the **runtime contract** (auth,
1647
+ credits, the media proxies, data I/O, the review gate). A separate **recipe** — the template's
1648
+ \`recipe\`, plus the capability skills it names — tells you *what to make* (the pieces, prompts,
1649
+ models, order of assembly).
1650
+
1651
+ **Division of authority: read both, but when they disagree THIS doc wins on the environment AND the
1059
1652
  review/approval flow.** The recipe governs WHAT to make; this doc governs WHEN you pause, generate,
1060
1653
  and spend. In particular: a recipe may spell out a **multi-phase, multi-gate** flow — "generate the
1061
1654
  still [GATE] → approve → author the prompt [GATE] → approve → render [GATE] → approve", several
@@ -1064,19 +1657,143 @@ separate pauses. **Do NOT run it that way.** Collapse every one of those gates i
1064
1657
  and models; ignore its intermediate pauses. This is the exact contradiction that confused past runs
1065
1658
  (GOOSE-2542) — there is no ambiguity: review-once wins.
1066
1659
 
1067
- You run inside the user's own Claude Code session (they pasted an instruction with a project
1068
- id). The app NEVER runs you — it is the viewer + review surface; you are the renderer.
1660
+ The app NEVER runs this skill by itself — it is the viewer + review surface; you are the renderer.
1661
+
1662
+ ${ENVIRONMENT_IDENTITY}
1069
1663
 
1070
- ## CLI-free environments (cowork / headless)
1664
+ ${ASSET_READINESS}
1071
1665
 
1072
- You may be running WITHOUT the \`gooseworks\` CLI binary (e.g. Anthropic cowork). The
1073
- \`mcp__gooseworks__*\` tools work over the MCP connection regardless, so wherever this skill
1074
- says to shell out, use the MCP equivalent:
1666
+ ## Where am I running? (decide once, first)
1075
1667
 
1076
- - \`gooseworks fetch <slug>\` → the **\`fetch_skill\`** MCP tool (returns the same content/scripts/
1077
- files/dependencySkills). \`gooseworks search <q>\` → **\`search_skills\`**.
1078
- - \`gooseworks credits\` → the **\`get_ad_credits\`** MCP tool.
1079
- - \`gooseworks doctor\` → do the manual toolchain check in the preflight below.
1668
+ With no shell on the customer's computer at all, you don't render here: hand the project off
1669
+ (Chat hosts and cards above). Otherwise check in Bash, without printing any secret value:
1670
+
1671
+ \`\`\`bash
1672
+ [ -n "$GW_MEDIA_PROXY_TOKEN" ] && echo sandbox || echo local
1673
+ command -v gooseworks >/dev/null && echo cli || echo no-cli
1674
+ \`\`\`
1675
+
1676
+ | Mode | How you know | Skills / atoms | Media-proxy auth |
1677
+ |---|---|---|---|
1678
+ | **GooseWorks sandbox** | \`GW_MEDIA_PROXY_TOKEN\` is set | \`catalog_fetch { type: "skill" }\` | env: \`GW_MEDIA_PROXY_TOKEN\` + \`GW_*_PROXY_URL\` |
1679
+ | **Local, CLI installed** | \`gooseworks\` on PATH | \`gooseworks fetch <slug>\` or \`catalog_fetch\` | \`~/.gooseworks/credentials.json\` |
1680
+ | **Local, no CLI** (Claude desktop app / Codex without login) | neither | \`catalog_fetch { type: "skill" }\` | \`~/.gooseworks/credentials.json\` if present, else **paid media over the MCP** (below) |
1681
+
1682
+ The \`gooseworks\` CLI and \`~/.gooseworks/credentials.json\` are **optional**. Everything this skill
1683
+ needs from the app goes through the GooseWorks MCP tools below; the atoms' \`media_proxy.py\` reads
1684
+ credentials.json when it exists and falls back to the \`GW_MEDIA_PROXY_TOKEN\` env otherwise.
1685
+
1686
+ ### Paid media over the MCP: no key, no CLI needed
1687
+
1688
+ Every paid generation goes through the GooseWorks media proxy and is billed to the project. **You
1689
+ never need FAL_KEY, an ElevenLabs key or \`fal_client\`.** An atom, a recipe or an open-source
1690
+ skill that lists \`FAL_KEY\` in its environment is describing a standalone setup; here the proxy
1691
+ satisfies it. Never stop, and never ask anyone to set a key, because one is missing.
1692
+
1693
+ **A one-off image or clip — call the MCP directly.** A frame placed in a laptop, a product cutout,
1694
+ a creator still, a restyle, an animated shot, with any fal model (Nano Banana, GPT-image, Seedream,
1695
+ Seedance, Kling). No atom script is needed:
1696
+
1697
+ 1. A local input (a frame pulled from a screen recording, a screenshot) must be a public URL first:
1698
+ \`media_upload { brand_id, scope: "video_project", scope_id: project_id, source: { type: "file" | "bytes", … } }\`
1699
+ (no \`path\`). For a file, PUT its bytes to \`upload.url\` with \`upload.required_headers\`,
1700
+ then call \`media_confirm { brand_id, media_id: media.id }\`. Use the returned \`media.url\`.
1701
+ 2. \`data_post_provider { provider: "fal", path: <model id, e.g. "fal-ai/nano-banana/edit">, body: <model input>, project_id }\`
1702
+ returns \`{ job_id: "fal:<request_id>" }\`. Pass an \`idempotency_key\` so a retry isn't billed twice.
1703
+ 3. Poll \`job_get { job_id }\` every few seconds until \`complete\`; the \`*.fal.media\` URLs are in
1704
+ \`result\`. Download each and QC it (open the image) before using it.
1705
+
1706
+ For a set that must match (every laptop shot, every creator still), use ONE prompt and the same
1707
+ model for all of them and change only the input image. Voice and music:
1708
+ \`data_post_provider { provider: "elevenlabs", path: "/v1/text-to-speech/{voice_id}" | "/v1/music", body, project_id }\`
1709
+ (synchronous; the audio lands in the project folder).
1710
+
1711
+ \`photos_generate\` is **not** a general image tool: it only photographs a physical catalog
1712
+ product (apparel, beauty, CPG) and needs a \`product_id\`. A software screenshot or app mockup is a
1713
+ fal image edit, above.
1714
+
1715
+ **Atom scripts — the MCP relay.** With neither \`GW_MEDIA_PROXY_TOKEN\` nor
1716
+ \`~/.gooseworks/credentials.json\`, the atoms' \`media_proxy.py\` RELAYS each paid call through you
1717
+ instead of calling the proxies over HTTP. Before running any atom,
1718
+ \`export GW_PROJECT_ID=<project_id> GW_BRAND_ID=<brand_id>\` (every call is billed to that
1719
+ project). When a script **exits with code 3** it wrote a request file under
1720
+ \`working/mcp-requests/\`: make exactly that MCP call — fal:
1721
+ \`data_post_provider { provider: "fal", path, body, project_id }\` then \`job_get { job_id }\` until
1722
+ \`complete\`, saving \`result.output\`; ElevenLabs: \`data_post_provider { provider: "elevenlabs", … }\`,
1723
+ saving the reply; a local file: \`media_upload\` with its bytes, saving \`{"url": …}\`. Write that JSON
1724
+ to the request's \`save_result_to\` and **re-run the same command**; repeat until the script
1725
+ finishes. Same server proxy and price as the CLI path. If the CLI is logged in to a DIFFERENT
1726
+ environment than this MCP connector (prod vs staging), set \`GW_MEDIA_VIA=mcp\` so the spend lands
1727
+ where the project lives.
1728
+
1729
+ ## MCP tools — canonical names (use these)
1730
+
1731
+ Use the canonical GooseWorks MCP tools. Legacy names are listed only as a fallback for an older
1732
+ client that does not expose the canonical tool; never mix both for one step.
1733
+
1734
+ | Step | Canonical tool (use this) | Legacy fallback |
1735
+ |---|---|---|
1736
+ | Read a project / batch | \`video_project_read { brand_id, project_id }\` / \`video_project_read { brand_id, batch_id }\` | \`get_ad_project\` / \`get_ad_video_batch\` |
1737
+ | Template recipe | \`catalog_fetch { type: "template", slug: <source_sample_id> }\` | \`get_ad_template\` |
1738
+ | Capability skill (atom) + its scripts | \`catalog_fetch { type: "skill", slug }\` | \`gooseworks fetch <slug>\` / \`fetch_skill\` |
1739
+ | Brand kit, products, rules | \`brand_read { brand_id, sections: ["summary","kit","products","learnings"] }\` | \`brand_get_context\` / \`get_brand_kit\` |
1740
+ | Save a brand rule (a correction) | \`brand_update { brand_id, knowledge_intent: "user_correction", user_statement: <the user's exact words>, patch: { facts: [{ id?, kind, text }] } }\` | none |
1741
+ | Mirror the review set | \`video_project_upsert { brand_id, project_id, patch: { script: { script_drafts, script } } }\` | \`update_ad_project_script\` |
1742
+ | Project assets | \`video_project_upsert { …, patch: { assets: [...] } }\` | \`update_ad_project_asset\` |
1743
+ | Progress note | \`video_project_upsert { …, patch: { message: { role: "agent", content } } }\` | \`append_project_message\` |
1744
+ | Batch status | \`video_project_upsert { brand_id, batch_id, patch: { batch: { status } } }\` | \`update_ad_video_batch\` |
1745
+ | Upload a file to the project | \`media_upload { brand_id, scope: "video_project", scope_id: project_id, kind, path, source: { type: "file", filename, content_type } }\` → PUT → \`media_confirm { brand_id, media_id: media.id }\` | \`get_upload_url\` / \`get_ad_upload_url\` |
1746
+ | Save / find a finished piece (resume) | \`media_upload { …, path, ingredient_key, input_digest }\` / \`media_list { brand_id, scope: "video_project", scope_id: project_id, ingredient_key_prefix: "" }\` (see "Save as you go") | none |
1747
+ | Open the render row | \`${exports.RENDER_ROW_TOOL} { brand_id, project_id, ${exports.RENDER_OPEN_ARGS} }\` (no \`dry_run\`; returns \`render_id\`) | \`submit_render { project_id, kind: "full" }\` |
1748
+ | Update the render row | \`${exports.RENDER_ROW_TOOL} { brand_id, project_id, ${exports.RENDER_UPDATE_KEY}: { render_id, status, output_url?, thumbnail_url?, error_message?, quality_status?, quality_report? } }\` | \`update_render_status\` |
1749
+ | Pin the final render | \`video_project_upsert { brand_id, project_id, patch: { final_render_id: render_id } }\` | \`set_final_render\` |
1750
+ | Credits / identity | \`account_whoami\` | \`get_ad_credits\` |
1751
+
1752
+ \`list_accessible_scopes\` and the \`target: { type: "agent", agent_id }\` dance are **not needed**:
1753
+ \`media_upload\` with \`scope: "video_project"\` derives the right storage key (org-default Ads agent
1754
+ + brand slug + project folder) on the server.
1755
+
1756
+ ## Running in a GooseWorks sandbox
1757
+
1758
+ You are an agent inside the user's GooseWorks workspace sandbox (Claude Code harness, Bash, the
1759
+ GooseWorks MCP with canonical tools only). There is **no \`gooseworks\` CLI** and **no
1760
+ \`~/.gooseworks/credentials.json\`**. The environment carries \`GW_MEDIA_PROXY_TOKEN\`, \`GW_API_BASE\`,
1761
+ \`GW_FAL_PROXY_URL\`, \`GW_FAL_STORAGE_PROXY_URL\`, \`GW_ELEVENLABS_PROXY_URL\`, \`GW_WHISPER_PROXY_URL\`
1762
+ and usually \`GW_PROJECT_ID\`.
1763
+
1764
+ - **Formats:** only formats that need **no Chromium** run here — podcast skits and UGC /
1765
+ talking-head formats (ffmpeg + PIL assembly). If the template's recipe or an atom needs
1766
+ Playwright/Chromium with no PIL fallback (iMessage / ChatGPT / Notes phone mockups,
1767
+ hyperframes, HTML recorders), **stop before any spend** and say so plainly: "This format needs
1768
+ a browser renderer that this workspace doesn't have. Run it in your own Claude Code with the
1769
+ GooseWorks CLI (\`goose-video-local\`), or pick a podcast/UGC format." Do not half-render.
1770
+ - **Toolchain:** check \`ffmpeg -version\` and \`ffprobe -version\`. Install Python deps only when an
1771
+ atom needs them: \`pip install --quiet pillow\` when a render atom uses PIL (captions, end card),
1772
+ \`pip install --quiet requests\` if \`import requests\` fails. Never \`npx playwright install\` here.
1773
+ - **Atoms:** \`catalog_fetch { type: "skill", slug }\` returns \`content\`, \`scripts\`, \`files\` and
1774
+ \`dependencySkills\`. Write \`content\` to \`/tmp/gooseworks-scripts/<slug>/SKILL.md\`, each
1775
+ \`scripts\` entry to \`/tmp/gooseworks-scripts/<slug>/scripts/<name>\` and each \`files\` entry to
1776
+ \`/tmp/gooseworks-scripts/<slug>/<name>\` (keep the key as the relative path; do the same for
1777
+ each dependency skill). Run scripts from their \`scripts/\` folder so \`import media_proxy\` resolves.
1778
+ - **Project attribution:** \`export GW_PROJECT_ID=<project_id>\` in the shell **before running any
1779
+ atom** for that project (re-export per concept in a batch). The proxies bill the workspace
1780
+ agent and attribute spend to that project.
1781
+ - **Never call a provider with a raw key.** All FAL / ElevenLabs / Whisper traffic goes through
1782
+ the \`GW_*_PROXY_URL\` bases with \`GW_MEDIA_PROXY_TOKEN\`. \`FAL_KEY\` / \`ELEVENLABS_API_KEY\` in
1783
+ this environment are proxy tokens, not provider keys — never send them to \`fal.run\`,
1784
+ \`queue.fal.run\` or \`api.elevenlabs.io\` directly. Never print any token.
1785
+ - **Whisper for QC:** \`export OPENAI_BASE_URL="$GW_WHISPER_PROXY_URL/v1"\` before running
1786
+ \`review-ugc-render\` / \`watch\` transcripts.
1787
+ - **The operator may be absent.** Keep every quality gate and checkpoint below, but decide
1788
+ defaults yourself from the project brief and the recipe, and record them in the review set.
1789
+ Ask in the chat **only for a true taste call** (a creative choice the brief and recipe leave
1790
+ open and that changes the ad), never for a mechanical or recoverable decision. The Step 3
1791
+ approval still applies: ask for it in this chat and record the yes with
1792
+ \`video_project_upsert patch.approve\`.
1793
+ - **Outputs:** keep working files under \`/tmp/gooseworks-video/<project_id>/\` (local disk — never
1794
+ the s3fs workspace mount, which is slow and can drop writes); anything the user must see goes to
1795
+ the project via \`media_upload\` (never leave the result only in the sandbox). Every paid piece is
1796
+ also SAVED to the project as soon as it passes QC — see "Save as you go — and resume".
1080
1797
 
1081
1798
  ## Report problems so we can fix them (telemetry — do this, don't skip it)
1082
1799
 
@@ -1089,97 +1806,288 @@ the skill. It's fire-and-forget, never counts against you, and never blocks your
1089
1806
  \`export GW_RUN_ID="vid-<project_or_batch_id>"\` (and \`export GW_SKILL="<recipe-slug>"\`) in the
1090
1807
  shell you render from. The media proxies read \`GW_RUN_ID\` automatically.
1091
1808
  - **CLI present →** \`gooseworks log "<what happened>" --event-type <type> --level error --details '{"error":"...","step":"...","model":"..."}'\`
1092
- - **No CLI (cowork / headless) →** the **\`log_cli_event\`** MCP tool with the same fields (pass \`run_id\`).
1093
- - \`--event-type\`: \`api_failure\` (a proxy/model call failed) · \`missing_input\` · \`blocker\` ·
1809
+ - **No CLI (sandbox / cowork) →** from any atom's \`scripts/\` folder:
1810
+ \`python3 -c 'from media_proxy import gw_log; gw_log("<what happened>", event_type="blocker", level="error", details={"step": "..."})'\`
1811
+ (or the \`log_cli_event\` MCP tool if your client has it).
1812
+ - Event types: \`api_failure\` (a proxy/model call failed) · \`missing_input\` · \`blocker\` ·
1094
1813
  \`confusion\` (unclear/contradictory instruction) · \`error\` (a bug) · \`step\`/\`info\` (progress notes).
1095
- - Put the **real error text + the step you were on** in \`--details\`. Paid FAL/ElevenLabs calls
1814
+ - Put the **real error text + the step you were on** in the details. Paid FAL/ElevenLabs calls
1096
1815
  ALREADY auto-log their own failures, so focus your manual logs on what the proxy can't see:
1097
1816
  missing inputs, confusing/contradictory recipe instructions, toolchain/setup failures, and bugs.
1098
1817
  - Logging is FOR US — it does not replace telling the user. When a problem blocks the run, still
1099
- explain it to the user (and ask if you need a decision); just also \`log\` it so we can fix the skill.
1818
+ explain it to the user (and ask if you need a decision); just also log it so we can fix the skill.
1100
1819
 
1101
1820
  ## Prerequisite — MCP + a render toolchain (Phase 0 preflight)
1102
1821
 
1103
- - The \`mcp__gooseworks__*\` tools are REQUIRED. If they're unavailable, stop and tell the user
1822
+ - The GooseWorks MCP tools are REQUIRED. If they're unavailable, stop and tell the user
1104
1823
  to connect the GooseWorks MCP server (or run \`gooseworks install --claude --mcp\` on the CLI)
1105
1824
  and restart. There is no REST fallback.
1106
- - **The render runs wherever THIS agent runs, and it needs a real toolchain: \`ffmpeg\` +
1107
- \`ffprobe\` + a Playwright **Chromium**.** Establish it in this priority order, and do NOT start
1825
+ - **The render runs wherever THIS agent runs, and it needs a real toolchain:** \`ffmpeg\` +
1826
+ \`ffprobe\` always, plus a Playwright **Chromium** for browser-rendered formats (phone mockups,
1827
+ HTML end cards without a PIL fallback). Establish it in this priority order, and do NOT start
1108
1828
  rendering until one is confirmed:
1109
- 1. **CLI present →** run \`gooseworks doctor\` (checks login, MCP, ffmpeg/ffprobe, Playwright
1110
- Chromium in one shot). Fix any ✗ with the command it prints, then continue.
1111
- 2. **No CLI →** check the toolchain yourself: \`ffmpeg -version\`, \`ffprobe -version\`, and a
1112
- Playwright Chromium probe (\`npx playwright --version\` and, if needed, \`npx playwright install
1113
- chromium\`). If all resolve, continue.
1114
- 3. **Docker available →** this is the most reliable way to get the toolchain in a sandbox that
1115
- lacks it: run the render steps inside the prebuilt image
1829
+ 1. **Sandbox →** see "Running in a GooseWorks sandbox": ffmpeg + ffprobe (+ PIL on demand); no
1830
+ Chromium, so browser formats stop there.
1831
+ 2. **CLI present →** run \`gooseworks doctor --no-browser\` for login, MCP, Node 18+, ffmpeg
1832
+ with libx264, and ffprobe. Require libass only for a selected ASS caption route; caption-burn uses Pillow overlays and does not need it. This is common setup only. After fetching the selected
1833
+ capabilities in Step 2, check each browser renderer's actual launch before ANY paid
1834
+ ingredient. An unscoped \`gooseworks doctor\` checks only the calling folder's browser;
1835
+ it cannot certify a different fetched renderer.
1836
+ 3. **No CLI →** check the toolchain yourself: \`node --version\` (18+), \`ffmpeg -version\`,
1837
+ \`ffprobe -version\`, plus \`ffmpeg -hide_banner -encoders\` (libx264) and
1838
+ \`ffmpeg -hide_banner -filters\` (ass). After fetching, use the exact-package free launch
1839
+ check in Step 2; resolving a package or finding a cache folder does not prove its browser
1840
+ can launch. The \`watch\` QC step later needs the same ffmpeg and,
1841
+ for transcripts, a Whisper backend — without one it degrades to frames only.
1842
+ 4. **Docker available →** the most reliable way to get the toolchain on a host that lacks it:
1843
+ run the render steps inside the prebuilt image
1116
1844
  **\`ghcr.io/gooseworks-ai/goose-video-render\`** (ffmpeg + ffprobe + Playwright Chromium baked
1117
- in), mounting the project working directory. Use Docker whenever the host is missing ffmpeg or
1118
- Chromium and \`docker\` is on PATH. (Note: nested Docker is usually disabled inside managed
1119
- sandboxes like cowork — treat this as an option, not a guarantee.)
1120
- 4. **None of the above works →** STOP and tell the user plainly, e.g.: *"Video rendering needs
1121
- ffmpeg + a Playwright Chromium (or Docker) on the machine running this agent. This environment
1122
- doesn't have them and I can't install them here. Options: (a) enable/allow Docker so I can use
1123
- the goose-video-render image, (b) install ffmpeg + \`npx playwright install chromium\`, or
1124
- (c) run this skill locally in your own Claude Code where the toolchain is available."* Do not
1845
+ in), mounting the project working directory. (Nested Docker is usually disabled inside
1846
+ managed sandboxes — treat this as an option, not a guarantee.)
1847
+ 5. **None of the above works →** on the customer's computer, hand the project to the GooseWorks
1848
+ coworker with \`goose_run_task\` (a \`needs_browser\` format can't be: offer another). Inside the
1849
+ coworker sandbox, or when the customer wants it made on their own machine, STOP and tell the
1850
+ user plainly, e.g.: *"Video rendering needs
1851
+ ffmpeg (and, for this format, a Playwright Chromium) on the machine running this agent. This
1852
+ environment doesn't have them and I can't install them here. Options: (a) enable Docker so I
1853
+ can use the goose-video-render image, (b) install ffmpeg + \`npx playwright install chromium\`,
1854
+ or (c) run this skill in your own Claude Code where the toolchain is available."* Do not
1125
1855
  half-render or fake a result. Static image ads (the \`goose-ads\` skill) do NOT need any of this
1126
1856
  and work anywhere — offer that as the fallback if they just want an ad now.
1127
1857
 
1128
1858
  ## Identity, token, credits
1129
1859
 
1130
- - Read \`~/.gooseworks/credentials.json\` → \`api_key\` (your agent token), \`api_base\`, \`agent_id\`.
1131
- Never print the token.
1132
- - **CRITICAL — target the org-default Ads agent on EVERY file op.** The app serves project files
1133
- (the render-file route) from the org's DEFAULT agent, but MCP file writes default to your
1134
- token's pinned agent — which can be a DIFFERENT agent, so a render written with the default
1135
- scope is **invisible in the app**. First resolve the Ads agent: \`list_accessible_scopes\` → the
1136
- scope with \`is_org_default: true\` (the ORG default — NOT the \`is_default\` / \`default_agent_id\`
1137
- fields, which are the *user's* default agent and are often a DIFFERENT agent). Its \`agent_id\` is
1138
- \`ADS_AGENT\` (name "Ads agent", slug \`org-default\`; usually also the \`agent_id\` in
1139
- credentials.json). Then pass \`target: { type: "agent", agent_id: ADS_AGENT }\` on EVERY
1140
- \`get_upload_url\` / \`get_download_url\` / \`write_file\` / \`list_directory\` / \`read_file\` — NEVER
1141
- omit \`target\`.
1142
- - **CRITICAL — publish under the PROJECT FOLDER, not the workspace root (the #1 "video renders but
1143
- is invisible" bug).** \`get_upload_url\` stores at \`<ADS_AGENT>/files/<path>\` verbatim, but the
1144
- render-file route reads from
1145
- \`<ADS_AGENT>/files/agent-config/brands/<brand_slug>/projects/<project_id>/<path>\`
1146
- (see backend \`resolveProjectFileKey\`). So EVERY publish/preview \`path\` MUST be prefixed with
1147
- \`agent-config/brands/<brand_slug>/projects/<project_id>/\` — e.g. upload to
1148
- \`agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4\`, NEVER bare
1149
- \`working/final.mp4\`. A bare path 404s in the app even though the render row AND a bare-path
1150
- \`get_download_url\` both "succeed" (they resolve the wrong key). The render \`output_url\` still
1151
- stays the project-relative \`...render-file?path=working/final.mp4\` — the route re-prepends the
1152
- prefix itself. Always verify with \`get_download_url\` on the FULL \`agent-config/...\` path (must
1153
- be non-empty; curl it for HTTP 200) BEFORE marking the render complete.
1860
+ - **Sandbox:** the token is \`GW_MEDIA_PROXY_TOKEN\` (already scoped to this workspace's agent and
1861
+ org); the API base is \`GW_API_BASE\`. Never print either.
1862
+ - **Local:** read \`~/.gooseworks/credentials.json\` → \`api_key\` (your agent token), \`api_base\`,
1863
+ \`agent_id\`. Never print the token.
1864
+ - **Uploads go through \`media_upload\` with \`scope: "video_project"\`** — pass \`path\` = the
1865
+ project-relative path (\`working/final.mp4\`, \`working/review/end-card.png\`). The server stores it
1866
+ in the project folder of the org-default Ads agent (where the app's render-file route reads) and
1867
+ returns \`upload.url\` (presigned PUT), \`upload.required_headers\` and
1868
+ \`upload.render_file_url\`. PUT the bytes with exactly those headers, check the PUT returned
1869
+ 2xx, then call \`media_confirm { brand_id, media_id: media.id }\` and require success before
1870
+ using the file in ingredients or completing a render. This applies to project-path and
1871
+ path-less file uploads. Confirmation verifies the stored file; Goose performs this tool step
1872
+ without asking the user for another approval. On failure, report or repair the upload before
1873
+ continuing. Never hand-build storage paths or agent prefixes; a bare workspace upload is
1874
+ invisible in the app.
1154
1875
  - Media generation (FAL / ElevenLabs) through the GooseWorks proxies is the **REAL spend** — billed
1155
- to the agent per call as you generate (Step 4). \`submit_render { kind: "full" }\` additionally
1156
- debits **1 nominal ad credit when the render ROW is opened** (a bookkeeping fee, NOT the render's
1157
- true cost) — so open it only once you actually have a rendered master (Step 4.1/4.2), and never
1158
- re-submit on a guess (that double-bills). The final-video QC gate (Step 4.3) then sits between
1159
- that master and PINNING it. Call \`get_ad_credits\` first; the user can check \`gooseworks credits\`.
1876
+ per call as you generate (Step 4). The render row (\`${exports.RENDER_ROW_TOOL} ${exports.RENDER_OPEN_ARGS}\`) charges the flat
1877
+ **video base fee once, when a full render is reported \`complete\`**. Open one row immediately
1878
+ after recorded approval, BEFORE paid production (Step 4.1), and reuse that render_id for progress
1879
+ and completion. Never open a second row on a guess (a second completed row bills again). The final-video QC gate (Step 4.3) then sits between
1880
+ that master and PINNING it. Call \`account_whoami\` first to see the credit balance.
1881
+
1882
+ ## Save as you go — and resume (never pay twice for a piece)
1883
+
1884
+ A sandbox can die mid-run (timeout, restart, a new session picks the project up). Anything that
1885
+ lives only in \`/tmp\` is then gone, and regenerating it pays again. So: **save every piece to the
1886
+ project the moment it passes its QC, and start every run by loading what is already saved.**
1887
+ Working files stay in \`/tmp/gooseworks-video/<project_id>/\` (never the s3fs workspace mount);
1888
+ the project is the durable copy.
1889
+
1890
+ **Ingredient keys.** Give every planned piece a stable key before you generate it, the same on
1891
+ every run: \`vo/scene-03\`, \`vo/sample-her\`, \`still/her-base\`, \`still/scene-05\`,
1892
+ \`clip/scene-05\` (a lipsync / video clip), \`music/bed\`, \`endcard\`, \`captions\`, \`final\`,
1893
+ \`final-thumb\`. Upload path = \`working/<role>/<file>\` (\`working/vo/scene-03.mp3\`,
1894
+ \`working/clip/scene-05.mp4\`); the review set keeps its \`working/review/<name>\` paths.
1895
+
1896
+ **Input digest.** Name the exact inputs of each generation with \`input_digest\` from
1897
+ \`media_proxy\` (in every media capability's \`scripts/\` folder):
1898
+
1899
+ \`\`\`python
1900
+ from media_proxy import input_digest
1901
+ digest = input_digest(model_path, args) # the model + the EXACT payload you send
1902
+ \`\`\`
1903
+
1904
+ Hash only what decides the output (prompt, voice_id, model_id, seed, duration, aspect…). An input
1905
+ that is a presigned or proxy URL changes every run, so swap it for that input's own identity
1906
+ before hashing, e.g. \`{**args, "image_url": {"ingredient": "still/her-base", "digest": her_digest}}\`
1907
+ — then a changed still correctly invalidates every clip made from it. For a piece you build
1908
+ locally (ffmpeg stitch, PIL end card, captions) use \`input_digest("local/<step>", {params,
1909
+ inputs: {key: digest, …}})\`.
1910
+
1911
+ **1. At the START of every run (first run, resume, new sandbox), load what exists — one call:**
1912
+ \`media_list { brand_id, scope: "video_project", scope_id: project_id, ingredient_key_prefix: "",
1913
+ limit: 100 }\`. It returns ONE compact row per \`ingredient_key\` (the newest):
1914
+ \`{ id, ingredient_key, input_digest, kind, status, mime, bytes, url, path, created_at }\`. Every
1915
+ status except archived is included. Project-path uploads remain \`pending\` until
1916
+ \`media_confirm\` succeeds. Confirm a pending project-path upload before reusing it; if
1917
+ confirmation fails, repair the upload before treating it as a verified save.
1918
+ Page with \`cursor\` if \`next_cursor\` is set (keep the first row you see per key — it is the newest). Also read \`script_drafts.ingredients\` from
1919
+ \`video_project_read\`: it records which pieces were already approved in the review.
1920
+
1921
+ **2. For each planned piece:** compute its digest from the args you WOULD send now. If a saved row
1922
+ has the same \`ingredient_key\` AND the same \`input_digest\`, **download it instead of
1923
+ generating**:
1924
+
1925
+ \`\`\`bash
1926
+ curl -fsSL "$URL" -o /tmp/gooseworks-video/<project_id>/<path> # URL = that row's \`url\`
1927
+ \`\`\`
1928
+
1929
+ That \`url\` is a short-lived (~15 min) presigned S3 GET the server signs for you, so it needs no
1930
+ auth header — download right after listing (list again if it expired). **Never fetch the
1931
+ \`/api/ads/projects/<id>/render-file?path=…\` route from the sandbox:** it needs the app's
1932
+ browser session and answers 401 to a token. Check the file is non-empty and plays (ffprobe for
1933
+ audio/video, open the image); if the download fails or the file is broken, regenerate the piece.
1934
+ Only generate what is missing or whose digest changed — a changed digest means the inputs changed,
1935
+ so the old file is stale.
1936
+
1937
+ **Pass the digest to the proxy too:** \`fal_generate(..., input_digest=digest)\` (and
1938
+ \`fal_generate_video\` / \`fal_whisper\`). A piece that was generated but never saved (the sandbox
1939
+ died between the fal result and the upload) is then handed back by the proxy instead of paid for
1940
+ again, even though its input URLs changed. Only pass \`new_take=True\` when the user wants a
1941
+ different take of the same inputs.
1942
+
1943
+ **3. After EACH piece is generated AND passes its own QC, upload it right away** — don't batch
1944
+ the uploads to the end:
1945
+ \`media_upload { brand_id, scope: "video_project", scope_id: project_id, kind, path:
1946
+ "working/<role>/<file>", ingredient_key, input_digest, source: { type: "file", filename,
1947
+ content_type } }\` → PUT the bytes to \`upload.url\` with \`upload.required_headers\` →
1948
+ \`media_confirm { brand_id, media_id: media.id }\`. Require a successful confirmation.
1949
+ Kind: \`audio\` (VO), \`music\`, \`image\` (a still),
1950
+ \`video\` (a clip), \`endcard\`, \`document\` (captions / a JSON sidecar), \`render\` (the master),
1951
+ \`thumbnail\`. Re-uploading the same key is fine — the newest wins. A piece that FAILED QC is never
1952
+ uploaded under its key. **Save a piece's sidecars with it** under \`<key>.<name>\` — e.g. the VO's
1953
+ char-level timestamps as \`vo/scene-03.timestamps\` (\`kind: "document"\`, same digest). Captions are
1954
+ built from them; without them a resumed run has to fall back to Whisper timings, which mis-case
1955
+ brand names.
1956
+
1957
+ **4. Keep the approved review unchanged during production.** Record each confirmed piece's
1958
+ \`media_id\`, \`path\`, \`ingredient_key\` and \`input_digest\` locally; the confirmed media rows
1959
+ and render progress are the durable resume record. Before approval, mirror the draft ingredients
1960
+ as part of Step 3. After approval, do NOT write \`patch.script\` or \`script_drafts\` during any
1961
+ paid production, QC or repair step, or while provider work is pending. A review-set write clears
1962
+ approval and can stop the next paid step, including a batch concept. Save the final descriptive
1963
+ review once all paid work is finished and settled, as Step 4.5 requires.
1964
+
1965
+ Any material creative change or ANY change the user asked for in this chat must stop production:
1966
+ save the changed complete plan and full credit total, then obtain fresh approval before continuing.
1967
+ Never reuse the earlier yes for a changed plan. A repair that restores the approved choices can
1968
+ continue within the existing allowance and Stop guards without rewriting the approved review.
1969
+
1970
+ The \`final\` master and \`final-thumb\` poster (Step 4.4) carry \`ingredient_key\` too, so a
1971
+ resumed run that finds a passing \`final\` with the same digest only needs to publish.
1972
+
1973
+ ## Finished video, interrupted saving — resume the same version
1974
+
1975
+ If the final already passed QC and the customer says **"finish saving this video"**, start here.
1976
+ Do not restart the recipe, choose another format, open another render, generate a take, or run a
1977
+ paid transcript check again. Saving uses the **same render_id**. The ordinary first-completion
1978
+ video fee can still apply; its existing same-render idempotency prevents a duplicate fee. Never
1979
+ promise that all saving is free.
1980
+
1981
+ **Before the first final upload**, save the final review set and production manifest from Step
1982
+ 4.5/4.6 (including any QC repairs) only after all paid work is finished and settled. Preserve the
1983
+ approved creative choices; a material change requires the changed-plan approval flow first.
1984
+ After that final save, start no new provider or render work: prepare the checkpoint and finish
1985
+ the same opened render using its existing finishing allowance and guards. A released allowance
1986
+ or stopped/capped render requires normal recovery; never reopen or bypass approval.
1987
+ Write a durable local checkpoint outside the fetched-scripts cache. Keep the actual
1988
+ final, JPEG poster, structured passing quality report, and review evidence files alongside it in
1989
+ a retained local working folder. Record their SHA-256 **at the time those exact bytes pass QC**;
1990
+ never attach an old verdict to a newly hashed replacement. A sandbox's local disk can disappear:
1991
+ this checkpoint recovers connection interruptions, not lost storage. Save ingredients remotely
1992
+ as above and tell the customer if the checked local output is no longer available.
1993
+
1994
+ With the current CLI and its normal login to the selected environment, write a manifest like:
1995
+
1996
+ \`\`\`json
1997
+ {
1998
+ "brand_id": "<owning brand>", "project_id": "<same project>", "render_id": "<already opened render>",
1999
+ "input_digest": "<assembly input digest>",
2000
+ "final_path": "/absolute/retained/working/final.mp4",
2001
+ "poster_path": "/absolute/retained/working/final-thumb.jpg",
2002
+ "qc": {
2003
+ "final_sha256": "<64 lowercase hex characters recorded during final QC>",
2004
+ "poster_sha256": "<64 lowercase hex characters recorded during poster review>",
2005
+ "report_path": "/absolute/retained/working/quality-report.json",
2006
+ "evidence_paths": ["/absolute/retained/working/review/finished-ad.json"]
2007
+ }
2008
+ }
2009
+ \`\`\`
2010
+
2011
+ Run \`gooseworks video-save prepare --manifest <manifest.json> --checkpoint <retained/save-<render_id>.json>\`
2012
+ before any upload, then \`gooseworks video-save resume --checkpoint <same checkpoint>\` for both
2013
+ normal saving and recovery. The helper uses the saved login and canonical MCP connection; it does
2014
+ not copy credentials or use a raw Ads REST fallback. Its atomic private checkpoint binds the
2015
+ environment, account, project owner, project/render, final/poster bytes, assembly digest, passing
2016
+ quality report, review set, evidence files and stage receipts. Preserve it until delivery. Never
2017
+ edit the checkpoint to clear a failed check. Keep checkpoint and manifest files out of commits.
2018
+
2019
+ The helper reads remote state before writes. It keeps \`final\` / \`final-thumb\` ingredient keys
2020
+ and writes per-render paths \`working/final-<render_id>.mp4\` / \`working/final-<render_id>-thumb.jpg\`
2021
+ so saving this version cannot overwrite an older one. It confirms pending uploads, checks remote
2022
+ bytes against the QC hashes, and reuses confirmed media. A request with missing stored bytes can
2023
+ get a fresh upload URL at that same path. A lost completion reply is unknown until project read
2024
+ confirms the same render's output and passing report; then only final selection remains.
2025
+
2026
+ **No CLI / a host-selected MCP connection:** keep the same protocol with the host's canonical
2027
+ tools. Atomically write versioned JSON locally (write a private same-directory temporary file,
2028
+ flush/fsync, rename, then fsync the directory) before any upload and between each upload request,
2029
+ PUT, confirmation, completion and final selection. Persist only public environment identity,
2030
+ account/owner/project/render IDs, file paths/sizes/hashes, input digest, exact quality report and
2031
+ evidence fingerprints, saved-review digest, media IDs and stage states; **no tokens, session IDs,
2032
+ signed URLs or auth headers**. On reconnect call \`account_whoami\`,
2033
+ \`video_project_read { brand_id, project_id, include: ["renders"] }\` and
2034
+ \`media_list { brand_id, scope: "video_project", scope_id: project_id, ingredient_key, input_digest }\`
2035
+ on the original connection. Compare identities, local/evidence hashes and saved review; verify
2036
+ confirmed remote bytes too. Treat a lost reply as unknown and read back before retrying. Use only
2037
+ the missing \`media_upload\` → PUT → \`media_confirm\`, same-render \`video_render_run { render: … }\`
2038
+ callback and \`video_project_upsert\` with only \`patch.final_render_id\` and
2039
+ \`patch.final_selection_guard: { expected_final_render_id, expected_review_digest }\`; never a
2040
+ render-open call. The expected final is the last read's pin, including explicit \`null\`; the
2041
+ review digest is SHA-256 of recursively sorted-key JSON
2042
+ \`{script: project.script ?? null, script_drafts: project.script_drafts ?? null}\` saved at checkpoint
2043
+ creation. First confirm the selected server advertises this guard in \`tools/list\`. If absent,
2044
+ stop and request the normal server update/reconnect; an unguarded pin is unsafe. A
2045
+ \`final_selection_conflict\` means the choice or review changed: keep the checkpoint and explain
2046
+ that saving stopped without overwriting it. Retain the
2047
+ strict Step 4.3/4.4 report and Step 4.5 review-set requirements.
2048
+
2049
+ If authorization expired, reconnect/sign in through the normal host/CLI flow on the original
2050
+ connection, then resume. Missing tools are a reconnect requirement, not permission to switch
2051
+ environments. Missing/changed files, stale/missing QC, changed account/owner/review, another final
2052
+ selection, or a stopped/capped/failed/blocked render require diagnosis; do not reopen it or
2053
+ regenerate automatically. If saving stops, say: **"Your video is finished locally, but saving to
2054
+ Goose was interrupted."** Link the actual local file and preserve its checkpoint. Claim saved
2055
+ delivery only after a fresh project read verifies completion and selection of that exact render.
1160
2056
 
1161
2057
  ## Step 0 — project id, or video BATCH id? (fan out before anything else)
1162
2058
 
1163
- The handoff you were pasted is EITHER a single \`project <id>\` OR a \`video batch <id>\`. A batch is
2059
+ The handoff is EITHER a single \`project <id>\` OR a \`video batch <id>\`. A batch is
1164
2060
  the app's "N concepts" flow: one composer submission fans out into **N independent concept projects**
1165
2061
  (the user picked a concept count, default 3), and the app expects EACH to be rendered. **Handle both:**
1166
2062
 
1167
2063
  - **\`project <id>\`** → you have one project. Treat it as a batch of one and continue to Step 1.
1168
- - **\`video batch <id>\`** → call \`get_ad_video_batch { video_batch_id }\`. It returns every child
2064
+ - **\`video batch <id>\`** → call \`video_project_read { brand_id, batch_id }\`. It returns every child
1169
2065
  concept under \`projects[]\` — each is a normal project with its own \`id\`, \`variant_index\`
1170
2066
  (Concept 1..N), and its own \`creative_brief\` (the per-concept angle/hook/offer/message). **You
1171
2067
  MUST process every concept, not just the first** — dropping concepts 2..N is the #1 batch bug.
1172
2068
 
1173
- **Loop shape (one agent, sequential, ONE approval for the whole batch):**
1174
- 1. Run **Step 1 + Step 1.5 + Step 2 + Step 3-assemble** for EACH concept project (each has its own
1175
- \`project_id\`, brief, and \`working/\` folder — never cross-write between concepts).
1176
- 2. Mirror EVERY concept's review set (Step 3's \`update_ad_project_script\` per project), then stop
1177
- for **ONE** approval that covers all concepts — show the per-concept credit estimate and the
1178
- batch total. Set the batch to \`review\` (\`update_ad_video_batch { status: "review" }\`).
1179
- 3. On approval, set the batch to \`rendering\` and run **Step 4 (the expensive render)** for each
1180
- concept **sequentially** (finish Concept 1's master before starting Concept 2 — one machine can't
1181
- render them in parallel). Deliver each (Step 5). When all concepts are pinned, set the batch to
1182
- \`complete\`.
2069
+ **Loop shape (ONE approval for the batch, isolated work per concept):**
2070
+ 1. Run **Step 1 + Step 1.5 + Step 2 + Step 2.5 + Step 3-assemble** for EACH concept project (each
2071
+ has its own \`project_id\`, brief, \`GW_PROJECT_ID\` and \`working/\` folder — never cross-write
2072
+ between concepts). The brand read (Step 1 item 2) and \`brand-rules.json\` (Step 1.7) are per
2073
+ BRAND: do them once for the batch and copy the file into each concept's \`working/\`. The read is
2074
+ ~90K characters. Step 2.5's angle bank is shared per brand AND product: load or build it once, and
2075
+ give every concept whose angle is \`auto\` a DIFFERENT angle from that list, so the batch is N
2076
+ different ads, not one ad N times.
2077
+ 2. Mirror EVERY concept's review set (Step 3's \`video_project_upsert patch.script\` per project),
2078
+ then stop for **ONE** approval in this chat that covers all concepts. Save each render_estimate.total_credits
2079
+ (previews + render + the 200 base fee) and the batch total. Widget: one line; text: card.text_summary. Set the batch to \`review\` (\`video_project_upsert
2080
+ { brand_id, batch_id, patch: { batch: { status: "review" } } }\`).
2081
+ 3. On an explicit yes, record it ONCE for the whole batch: \`video_project_upsert { brand_id, batch_id,
2082
+ patch: { approve: { user_quote: "<their exact words>", total_credits: <the saved total> } } }\`. Check its \`not_ready\` list is
2083
+ empty (a concept listed there has no saved review set: save it, show it, ask again). If they
2084
+ approve only some concepts ("1 and 3 are good, redo 2"), record each approved one with its
2085
+ \`project_id\` instead, and redo the rest. Then set the batch to \`rendering\` and run **Step 4 (the expensive render)** for each
2086
+ concept with up to 8 isolated workers when the machine can sustain them. Retry
2087
+ concurrency_limit after retry_after_seconds; reduce concurrency on a limited machine. Deliver each (Step 5). When every concept is pinned, set the batch to
2088
+ \`complete\`. A concept the Step 4.3 gate leaves \`blocked\` cannot be pinned (a batch concept
2089
+ needs \`passed\`): finish the others, set the batch to \`blocked\`, and tell the user which
2090
+ concepts passed and which are blocked, with each one's failing checks.
1183
2091
 
1184
2092
  If a single concept fails, keep going with the rest, mark that concept blocked, and report which
1185
2093
  ones shipped — never abort the whole batch on one bad concept. Everything below (Steps 1–5) is
@@ -1187,18 +2095,25 @@ written per-project; a batch just runs it N times with the shared approval gate
1187
2095
 
1188
2096
  ## Step 1 — resolve the project, source, brand
1189
2097
 
1190
- 1. \`get_ad_project { project_id }\` → keep \`brand_id\`, \`source_sample_id\`, \`name\`, \`status\`, the
1191
- **top-level** \`app_url\` + \`brand_url\` (returned alongside \`project\`, NOT inside it — the links
1192
- you hand the user for the in-app review in Step 3 and delivery in Step 5), AND the user's
1193
- **\`creative_brief\`**, project **\`assets\`**, \`character_id\`, \`default_voice_id\` — these are the
1194
- authoritative inputs the user chose in the composer (see Step 1.5). Do NOT discard them.
2098
+ 1. \`video_project_read { brand_id, project_id }\` → keep \`brand_id\`, \`source_sample_id\`, \`name\`,
2099
+ \`status\`, the **top-level** \`app_url\` + \`brand_url\` (the links you hand the user for the in-app
2100
+ review in Step 3 and delivery in Step 5), AND the user's **\`creative_brief\`**, project
2101
+ **\`assets\`**, \`character_id\`, \`default_voice_id\` — these are the authoritative inputs the user
2102
+ chose in the composer (see Step 1.5). Do NOT discard them. Then \`export GW_PROJECT_ID=<project_id>\`.
2103
+ Keep its saved \`campaign_id\` and \`campaign_concept_id\` too. If the customer explicitly
2104
+ wants to attach this existing video to a known campaign/concept, verify the IDs with the
2105
+ campaign read and use \`video_project_upsert { brand_id, project_id, patch: {
2106
+ campaign_association: { campaign_id, campaign_concept_id? } } }\` as a separate, sole-field
2107
+ patch. Read back the same project and confirm the saved IDs. Linking never needs a new
2108
+ project, render, generation call or approval. A conflicting saved link stays intact; explain
2109
+ it instead of silently moving the video. Leave unlinked legacy videos alone unless asked.
1195
2110
 
1196
2111
  ### Step 1.5 — the project brief is AUTHORITATIVE (honor it; don't re-ask)
1197
2112
 
1198
2113
  The composer already collected the user's creative direction onto the project. **Read it and treat
1199
2114
  it as ground truth — it OVERRIDES the template recipe's defaults, and it REPLACES the clarifying
1200
- questions you would otherwise ask.** Only fall back to the recipe default (then, last, to asking)
1201
- for a field the brief leaves empty. Map the fields you WILL honor:
2115
+ questions you would otherwise ask.** For a field the brief leaves empty, use the Brain search's
2116
+ evidence brief first, then the recipe default, then (last) asking. Map the fields you WILL honor:
1202
2117
 
1203
2118
  - \`creative_brief.productName\` / \`.offer\` / \`.angle\` → the product, offer/code, and angle. Do
1204
2119
  **not** ask "which product / what offer / what angle" if these are set.
@@ -1210,49 +2125,300 @@ for a field the brief leaves empty. Map the fields you WILL honor:
1210
2125
  assets.
1211
2126
  - \`character_id\` → the avatar/creator to use. \`default_voice_id\` → the voice for any VO (put its
1212
2127
  NAME in the review \`subtitle\`). Use these instead of picking your own.
1213
- - \`creative_brief.ratio\` / \`.durationSeconds\` → target aspect ratio + length. Honor when the
1214
- format's render pipeline supports it; if the format physically can't (e.g. a fixed phone-mockup
1215
- aspect), keep the format's native value and note the constraint in the review rather than silently
1216
- ignoring the request.
2128
+ - \`creative_brief.durationSeconds\` → target length; honor it when the format allows.
2129
+ \`creative_brief.ratio\` → video ads are ALWAYS 9:16 (1080×1920). If the brief asks for another
2130
+ ratio, make 9:16 anyway and say so in the review; never export another size (a recipe's
2131
+ "also 1:1" option included).
1217
2132
  - \`polish_policy\` (\`standard\` | \`extra\`) → \`extra\` means spend the extra pass on QC/polish.
1218
- 2. \`get_ad_template { template_id: source_sample_id }\` → the source video: \`media_url\`,
1219
- \`recipe\`, \`format\` (e.g. "imessage"), \`extracted_script\`, \`how_to\`, \`remix_spec\`.
1220
- 3. Brand gate: \`get_brand_kit { brand_id }\`. If \`researchStatus\` is \`complete\`, REUSE it —
1221
- never re-research. If not, run brand research first (\`gooseworks fetch brand-research\`,
1222
- follow it, then \`finalize_brand_research { brand_id }\`) before continuing.
2133
+
2134
+ 2. Brand gate: \`brand_read { brand_id, sections: ["summary", "kit", "products", "learnings"] }\`
2135
+ (older clients: \`brand_get_context\` with the same sections). Ask for all four: the default
2136
+ leaves out the kit and the brand's saved rules, and a video made without them is off-brand.
2137
+ If the kit's \`researchStatus\` (or the brand's \`research_status\`) is \`complete\`, REUSE it —
2138
+ never re-research. A pending status alone does not require research; use verified stored
2139
+ facts and ask only for an actual gap. If the context is empty or the customer requests
2140
+ research, load \`catalog_fetch { type: "skill", slug:
2141
+ "brand-research" }\` and follow its stored-pack workflow. Only when that verified pack is
2142
+ saved in the supported research workspace, finalize with
2143
+ \`brand_update { brand_id, patch: { finalize_research: true } }\`, then read the brand back.
2144
+ Never send raw research JSON through \`kit_patch\`: the public tool accepts only the existing
2145
+ \`video_lab\` asset slot there. If there is no verified stored pack, submit researched facts
2146
+ through typed \`patch.knowledge\` / \`patch.kit\` as pending agent proposals; do not pretend
2147
+ research is finalized or its proposals are approved. Then do Step 1.7 with verified facts.
2148
+ 3. For a new plan, \`catalog_fetch { type: "template", slug: <source_sample_id> }\` → the source
2149
+ video: \`media_url\`, \`recipe\`, \`format\` (e.g. "podcast-skit", "imessage"),
2150
+ \`extracted_script\`, \`how_to\`, \`remix_spec\`. For an approved resume, restore the recorded
2151
+ package and dependencies instead of fetching today's recipe over the saved plan.
2152
+
2153
+ ### Step 1.6 — a remix of a FINISHED video (the project read has a \`remix\` block)
2154
+
2155
+ A project made from **Community videos** remakes another customer's finished video for THIS brand.
2156
+ Its \`video_project_read\` returns a top-level \`remix\` block
2157
+ (\`{ remix_of_project_id, instruction, direction }\`), and \`reference_video_url\` is that finished video.
2158
+
2159
+ - **Watch the reference video first** (download \`reference_video_url\`, pull frames + the transcript).
2160
+ It is the target: match its structure, beat order, pacing, framing, look, voice and tone.
2161
+ - **\`remix.direction\` is its approved review set** (scenes and lines, set/look notes, take prompts,
2162
+ captions, music, voice). Start your review set from it instead of the template's defaults; it
2163
+ already carries every change that customer made to the template.
2164
+ - **\`remix.direction.implementation\`, when present, is HOW it was made**: the pipeline and the model
2165
+ each step used, the style and negative prompts, the character and per-scene still/motion prompts
2166
+ with their guards, voice and music settings, the mix, and \`fixes\` (what went wrong and how it was
2167
+ fixed). Reuse the same models, style/negative prompts and guards, rewrite the prompts' subjects for
2168
+ this brand, and apply every \`fix\` up front so you don't repeat the same mistake.
2169
+ - **Rewrite everything for THIS brand.** "[source brand]", "[source product]", "[link]", "[email]"
2170
+ and "[code]" mark the other customer's details: never write them, and never reuse their claims,
2171
+ numbers, URLs, offers or CTA. Every product, claim, name, image and CTA comes from this brand's
2172
+ kit, products and media. Their footage (screen recordings, product shots) and their creator face
2173
+ are NOT carried over: use this brand's own assets and make a new creator from the description.
2174
+ - Precedence: this project's own \`creative_brief\` and assets (Step 1.5) > \`remix.direction\` >
2175
+ the template recipe's defaults.
2176
+ - In the Step 3 review, say it is a remix of that video and list what you kept vs. changed.
2177
+
2178
+ ### Step 1.7 — the brand rules file and the brand assets (every run, before any writing)
2179
+
2180
+ Before new writing, write \`working/brand-rules.json\` from the Step 1 brand read. Preserve an
2181
+ approved run's saved rules on resume; reconcile an intentional rule change through the existing
2182
+ review gate. Every later step reads THIS file, not your memory of the chat:
2183
+
2184
+ \`\`\`json
2185
+ {
2186
+ "name": "Acme",
2187
+ "pronunciations": [{ "term": "Acme", "say_as": "ak-mee", "learning_id": "…" }],
2188
+ "must_say": [{ "text": "…", "learning_id": "…" }],
2189
+ "never_say": [{ "text": "Never claim it cures insomnia", "learning_id": "…" }],
2190
+ "products": [{ "id": "…", "name": "…", "facts": ["size, flavour, price, ingredients as stored"], "images": ["…"] }],
2191
+ "logo": { "url": "…", "confidence": "<kit.logoConfidence>" },
2192
+ "fonts": ["…"],
2193
+ "palette": ["#…"]
2194
+ }
2195
+ \`\`\`
2196
+
2197
+ - **Video taste is direction, not dialogue.** Before mapping brand rules, extract entries
2198
+ prefixed \`Video preference:\` from both \`learnings\` and \`kit.instructions\` into the verified
2199
+ taste brief, including accepted proposals. They govern pacing, voices, captions, visuals and
2200
+ format choice; do not copy them into \`must_say\` / \`never_say\` or read them aloud. Carry the
2201
+ brief into the choices, scene planning and review.
2202
+ - **Sources.** \`learnings\` are the brand's saved rules (the user's past corrections among them):
2203
+ \`must\` / \`do\` → \`must_say\`, \`dont\` → \`never_say\`, and a \`must\` whose text reads
2204
+ \`Pronounce "<term>" as "<say_as>"\` (straight or curly quotes) → \`pronunciations\`. Add \`kit.instructions\` (free-text
2205
+ standing rules) only when they require actual spoken wording or prohibit a claim. Production
2206
+ directions stay in the brief. A required spoken line or prohibited claim remains its own
2207
+ ordinary \`must\` / \`dont\` rule.
2208
+ - **Which product.** The one the brief names (\`creative_brief.productName\`); with none, the row
2209
+ whose name matches the product the user asked for, or the brand itself for a one-product
2210
+ brand. Product lists often hold other brands' items or old ads saved as products: if more than
2211
+ one row could be it, ask in the choices round. Never mix facts across rows.
2212
+ - **Product facts** come from that row (name, description, variant, price) and, when the row is
2213
+ empty, from the kit (\`valueProps\`, \`description\`, \`tagline\`): nothing else. Write them to
2214
+ \`products[].facts\`; the script may only state what is there.
2215
+ - **Logo.** Download the kit's logo FILE (\`kit.logoUrl\`, else \`kit.logos[0]\`) to \`working/brand/\`,
2216
+ keeping its real extension (an SVG stays \`.svg\`; rasterise it to a 1024px-wide PNG with
2217
+ \`rsvg-convert\` or \`cairosvg\` when a renderer needs pixels). It is used as-is on every scene and
2218
+ end card that shows a logo: **never generate, redraw, re-letter or restyle a logo with an image
2219
+ model.** If the kit's \`logoConfidence\` says favicon-grade, or the file's long side is under
2220
+ 256 px (or it is under 40,000 px²), it is a site favicon, not a logo: do not upscale it. Ask the user for a real logo (or offer the brand name
2221
+ set as text in the brand font) in the SAME question round as the recipe's \`choices\`.
2222
+ - **Font.** \`kit.typography.heading\` when its \`source\` is \`user\` (the user chose it), else
2223
+ \`kit.fonts.heading\`. Download the font file (the kit's own, or the same family from Google
2224
+ Fonts) to \`working/brand/\` and use it for every on-screen line. With no match, use the closest
2225
+ free font and say so in the review.
2226
+ - **No wordmark file.** When a recipe wants a wordmark SVG and the kit has only a logo image, set
2227
+ the brand name as text in the brand font beside the logo file. Never generate one.
2228
+ - Record in \`brand-rules.json\` which logo the video will actually composite (\`logo.file\`), or
2229
+ \`"logo": { "mode": "text" }\` when it will show only the brand name set in the brand font.
2230
+ - **Product images.** Download this product's own images to \`working/brand/\`. Use only images of
2231
+ THIS product: never a catalogue image of another product, a mascot, a lifestyle photo of a
2232
+ person, or a stand-in. If the product has no usable image, ask in the choices round.
2233
+
2234
+ ### Brand corrections stick — save them to the brand the moment they are made
2235
+
2236
+ When the user corrects something about the BRAND in chat — how a name is said, a claim that may
2237
+ not be made, a product fact, a visual rule ("never use red", "the logo goes top-left") — save it
2238
+ in the SAME turn, before anything else:
2239
+
2240
+ \`brand_update { brand_id, knowledge_intent: "user_correction", user_statement: <the user's exact correction>, patch: { facts: [{ kind, text }] } }\`
2241
+
2242
+ | Correction | \`kind\` | \`text\` |
2243
+ |---|---|---|
2244
+ | Pronunciation | \`must\` | \`Pronounce "Acme" as "ak-mee"\` (exactly this form) |
2245
+ | A claim or word to avoid | \`dont\` | \`Never say or imply: <the claim>\` |
2246
+ | Something that must be said | \`must\` | \`<the rule>\` |
2247
+ | A product fact | \`must\` | \`<product name>: <the fact>\` |
2248
+ | A visual rule | \`do\` / \`dont\` | \`<the rule>\` |
2249
+
2250
+ - If it changes an EXISTING rule (a new pronunciation for the same term), update that rule by id
2251
+ (\`facts: [{ id: <learning_id>, text }]\`) instead of adding a second one.
2252
+ - Use the correction intent and user's statement when supported, as described below. Read
2253
+ \`brand_read\` learnings back and verify the rule and source before claiming it was saved.
2254
+ - Then tell the user in one line: "Saved to your brand for future videos." Update
2255
+ \`working/brand-rules.json\` and apply the rule to THIS video too.
2256
+ - A one-off note about this video ("make it shorter", "use the blue background here") is NOT a
2257
+ brand rule: don't save it.
2258
+
2259
+ ${DURABLE_BRAND_CAPTURE}
2260
+
2261
+ ${VIDEO_TASTE_CAPTURE}
1223
2262
 
1224
2263
  ## Step 2 — read the template's recipe (it carries everything; NO hardcoded format map)
1225
2264
 
1226
2265
  The ad format is a **template (data) in the ad_sample DB**, not a per-format skill.
1227
- \`get_ad_template(source_sample_id)\` returns the template's \`recipe\` — a self-contained brief you
2266
+ \`catalog_fetch { type: "template" }\` returns the template's \`recipe\` — a self-contained brief you
1228
2267
  read and execute. **Do NOT map \`format\` to a hardcoded recipe slug** (there is no such table):
1229
2268
 
1230
2269
  - \`recipe.format\` — the format label (e.g. \`vignette\`), for display only.
1231
- - \`recipe.atoms\` — the **capabilities** this template composes (e.g. \`create-video-seedance-2-fal\`,
1232
- \`create-image-gpt-image-fal\`, \`review-ugc-render\`, \`watch\`). \`gooseworks fetch <name>\` each — they
1233
- live in \`skills/ads/capabilities/\` and are reused across templates (so they cache).
2270
+ - \`recipe.atoms\` — the **capabilities** this template composes (e.g. \`create-vo-elevenlabs\`,
2271
+ \`create-image-gpt-image-fal\`, \`render-podcast-skit\`, \`review-ugc-render\`, \`watch\`). Fetch each
2272
+ with \`catalog_fetch { type: "skill", slug }\` (or \`gooseworks fetch <name>\` when the CLI is
2273
+ installed) — they are reused across templates.
1234
2274
  - \`recipe.instructions\` — the **playbook** to follow: \`instructions.inline\` prose, or
1235
2275
  \`instructions.doc_url\` (an S3 markdown doc — fetch it).
1236
2276
  - \`recipe.config\` — every param (prompts, layout, timings, palette, model choices).
1237
2277
  - \`recipe.inputs\` — the brand-asset contract (which product / logo / offer this template needs).
2278
+ - \`recipe.choices\` — the creative calls the USER makes (who is on screen, narrator, tone, setting,
2279
+ art style, music). Ask every unanswered one in ONE round before any paid step, with its options plus
2280
+ "you pick". Its \`reference\` is what the demo used: an example, never the default.
1238
2281
  - \`recipe.assets\` — reference material as S3 links (reference render, style guide, example frames) —
1239
2282
  fetch as needed.
1240
2283
 
1241
- Runtime: **read the recipe → \`gooseworks fetch\` each capability in \`recipe.atoms\` → follow
2284
+ Runtime: **read the recipe → fetch each capability in \`recipe.atoms\` → follow
1242
2285
  \`recipe.instructions\` with \`recipe.config\` + the brand's bound \`inputs\`.** The template IS the recipe;
1243
2286
  there is no \`format → recipe-slug\` table and no per-format skill to fetch.
1244
2287
 
1245
- Save each fetched capability's scripts + files under \`/tmp/gooseworks-scripts/<name>/\`. If a capability
1246
- is a Node package (a phone-mockup renderer), \`npm install\` in its folder so its \`generate.js\` +
1247
- Playwright resolve, and point the recorder's \`NODE_PATH\` at it.
2288
+ Save each fetched capability's content, scripts + files under \`/tmp/gooseworks-scripts/<name>/\`
2289
+ (layout in "Running in a GooseWorks sandbox"). If a capability is a Node package (a phone-mockup
2290
+ renderer), install its documented dependencies in the folder containing its \`package.json\`,
2291
+ and preserve the recorder's documented \`NODE_PATH\` — local machines only; in a sandbox that
2292
+ format stops (no Chromium).
2293
+
2294
+ **Selected browser readiness — before ANY paid ingredient:** identify the actual browser script
2295
+ from the fetched capability's instructions (including an HTML end-card renderer if used).
2296
+ For Node scripts using \`require('playwright')\` with default \`chromium.launch()\`, run
2297
+ \`gooseworks doctor --renderer-script "/absolute/path/to/the/fetched/scripts/record.js"\`.
2298
+ Substitute the actual script, and keep the same cwd and environment as the render, including
2299
+ \`NODE_PATH\` and \`PLAYWRIGHT_BROWSERS_PATH\`. It resolves Playwright relative to that script,
2300
+ launches and closes Chromium with the default settings and bounded waits, and downloads nothing.
2301
+ On failure, stop before spending, show its folder-specific repair and recheck after setup.
2302
+ For non-browser formats use \`doctor --no-browser\` plus their documented runtime checks;
2303
+ never combine \`--no-browser\` with \`--renderer-script\` or use it to bypass a browser renderer.
2304
+
2305
+ **No CLI or CLI without these flags:** run an equivalent free probe in a separate Node process: use
2306
+ \`require('node:module').createRequire(require('node:path').resolve(actualRendererScript))\`
2307
+ to load \`playwright\`; keep the render's cwd, environment and default launch settings. Await
2308
+ \`chromium.launch({ timeout: 15000 })\`, then await \`browser.close()\` (bound close to 3 seconds).
2309
+ Bound the whole process to 20 seconds and stop its own process tree on failure or timeout.
2310
+ Report the resolved module path/version and error without credentials. Missing module, executable,
2311
+ headless runtime or failed launch is a failed check; a cache folder, executablePath alone or
2312
+ another project's browser is not a pass. Other browser packages, Python renderers or custom
2313
+ launch settings need the same free launch/close check through their documented runtime and
2314
+ actual settings. Do not replace their browser/channel/flags to get a pass. Setup is a separate
2315
+ action under existing permissions; this check never silently installs or downloads anything.
1248
2316
 
1249
2317
  > **Migration note:** older phone-mockup formats (\`imessage\` / \`chatgpt\` / \`apple-notes\`) whose DB
1250
2318
  > recipe does not yet carry \`atoms\` / \`instructions\` still hold the legacy \`recipe.thread\` payload;
1251
2319
  > migrate them to this shape (capabilities + instructions in the DB) — do not reintroduce a CLI map.
1252
2320
 
1253
- ## Step 3 — assemble the review set, then get ONE approval in the app (before the expensive render)
1254
-
1255
- This is a **review-once** flow: put the whole review set in the app, get ONE approval, then run the
2321
+ ## Step 2.5 — plan words and visuals with \`write-video-ad-script\`
2322
+
2323
+ Every video concept gets creative strategy, including silent formats. Fetch
2324
+ **\`write-video-ad-script\`** through the skill catalogue and follow it before assembling
2325
+ review ingredients. It reuses or fetches **\`ad-angle-miner\`**, binds the researched promise
2326
+ to this template's full recipe, writes words and visuals together, and checks product
2327
+ claims and production fit. Its independent critic is a quality screen, not a forecast
2328
+ of ad performance. The existing Step 3 review and approval remain unchanged.
2329
+
2330
+ - **Carry the research into the writer.** Pass the brand and exact product, audience,
2331
+ objective, offer, CTA, selected template recipe, available assets and the full miner
2332
+ bank or its readable workspace pointer. A selected miner idea includes its angle id,
2333
+ evidence and proof plan, not only its hook. Reuse the shared video-angle-bank.v1 from
2334
+ the brand's video-scripts workspace or this run. Prepare angle-context.json with the
2335
+ writer's preparation script with \`--brief working/script/creative-brief.json\` and use
2336
+ its strict rule check before review. First run the fetched writer's free
2337
+ \`python3 <saved-writer-package>/scripts/verify_handoff.py --package-dir <saved-writer-package> --out working/script/writer-handoff-check.json\` check against those
2338
+ actual saved prepare/lint scripts. Retain the result and script hashes with the run.
2339
+ A provided writer package with a missing checker, failed check or unsupported flag
2340
+ is incompatible: stop this handoff and refresh the package on the same connection.
2341
+ Never use the provisional agent path after a provided package fails validation.
2342
+ Only an actual \`not_found\` for an optional writer permits the explicit provisional
2343
+ agent check below; a required writer remains blocked. Never silently remove the brief,
2344
+ strict check or new shape requirement to run an older parser.
2345
+ Save the sourced brief in the writer's documented
2346
+ shape: exact product/variant, buyer situation, supported mechanism, offer/CTA, constraints,
2347
+ delivery intent, source references, locked copy, applicable prior decisions and unknowns.
2348
+ New custom/template shapes set \`requires_creative_brief: true\`; both writer and critic
2349
+ receive the same angle-context. Carry that brief revision into the scene/tool plan and
2350
+ production manifest; missing evidence stays explicit rather than becoming a claim.
2351
+ - **Already decided stays decided.** The user's angle, hook, batch concept, recipe choice
2352
+ or remix direction is binding. Preserve the selected angle id. Compare executions or
2353
+ hooks inside it; do not reopen the angle choice. An incompatible promise needs a
2354
+ supported execution or an explicit format change before production.
2355
+ - **The recipe owns the production contract.** Its story mechanism, speakers, timing,
2356
+ text limits and visual capabilities win. Inspect the actual recipe and available assets;
2357
+ a catalogue card alone cannot establish fit. An impossible essential visual blocks
2358
+ the proposal until repaired. Do not force a testimonial into every format.
2359
+ - **Research only what is missing.** Fetching the miner does not spend or rerun research.
2360
+ Reuse current facts and prior evidence. The Brain search's evidence brief (citations, the
2361
+ state of each query and open gaps) is the first source for the brief's source references and
2362
+ prior decisions. If paid collection is needed, its permission
2363
+ rides in the existing choices round. A new product with no reviews can use verified
2364
+ facts and a feasible demo; do not require paid research merely to fill a quote quota.
2365
+ - **Choose eligible concepts only.** Take the strongest supported concept and validated
2366
+ hook into review, with up to two viable alternatives when the direction is open.
2367
+ Do not pad three concepts or select the highest-ranked rejected one. If all fail,
2368
+ repair within the brief and recipe before Step 3.
2369
+ - **Optional writer not found or unavailable research.** Only when the optional writer
2370
+ returns \`not_found\`, complete an explicit agent check of claim support, recipe limits,
2371
+ visual feasibility and hook payoff. Record missing provenance and say in the review when
2372
+ research is provisional. A provided incompatible writer or another fetch error stops
2373
+ the handoff. Never report a failed check as a pass.
2374
+ - **The user's exact lines** remain verbatim; use report-only checks and raise material
2375
+ timing, claim or format conflicts without silently rewriting them.
2376
+ - **No spoken words.** Still fit the visual promise, reveal, cards and CTA to the silent
2377
+ recipe. Skip speech writing and spoken checks, not the strategy step.
2378
+ - **Keep private provenance separate.** Store source quotes and links in the research
2379
+ workspace; put only a concise strategy explanation in remixable review data.
2380
+
2381
+ ## Shared editorial craft inside this template flow
2382
+
2383
+ Fetch \`video-production-harness\` and read its \`references/editorial-review.md\`,
2384
+ \`references/specialist-handoff.md\` and \`references/hook-compatibility.md\`, plus the
2385
+ review/edit/polish/promote/wrap steps when used. Verify every named file is returned as
2386
+ nonempty text before relying on that package. A version/content hash alone cannot establish
2387
+ that the required guides were published. Missing guides block new template creative work
2388
+ before project writes or paid previews; resolve the package on this same connection. Keep
2389
+ an unchanged approved resume on its recorded package and approvals.
2390
+ Save the fetched version/content hash with this run's capability records. These supply
2391
+ craft and evidence rules inside the template flow; this entry's existing review, paid
2392
+ approval, storage and two-repair limit still apply.
2393
+ Do not turn a template into a custom project or import the custom host's extra gates.
2394
+
2395
+ Before executing a fetched recipe atom, pass the exact brief/script revision, scene/beat IDs,
2396
+ timing, selected product/assets/source windows, performance choice and expected output roles.
2397
+ Check actual scripts/dependencies and free runtime readiness, then validate returned media
2398
+ against that handoff. An interrupted run reuses valid approved media with matching digest;
2399
+ a file merely existing does not prove validity. A missing package/required renderer blocks
2400
+ that route before paid generation, with a concrete supported alternative for review.
2401
+
2402
+ Rough review asks what the cut communicates and whether the proof/payoff serves the hook;
2403
+ fine review diagnoses performance, sequence and local execution; final review checks each
2404
+ actual export. Save the received message from the cut before comparing with the brief where
2405
+ possible. A technically clean incoherent cut returns to script/sequence work; a local caption
2406
+ defect gets a local repair. These internal questions share existing review surfaces.
2407
+
2408
+ Before a hook change, classify same-promise compatible opening versus new-promise/body recut.
2409
+ For example, an automatic report supports “stop copying campaign results”; it does not alone
2410
+ prove “know where to spend the next dollar.” Propose required script/body/coverage changes for
2411
+ the latter. Every restyled opening gets fresh image/action/audio/text review even when words
2412
+ are unchanged. Fetch the existing \`render-hook-replacement\` before selecting that route;
2413
+ a not-found/empty result stops it before generation. Use its actual preflight and preservation
2414
+ checks when available; never build a duplicate renderer or infer publication from source.
2415
+ Compare complete labeled candidates, retain source/body/ending and record the hypothesis.
2416
+ No quality score or recommendation is an observed performance winner.
2417
+
2418
+ ## Step 3 — assemble the review set, then get ONE approval (before the expensive render)
2419
+
2420
+ This is a **review-once** flow: save the whole review set to the project, get ONE approval in this
2421
+ chat, then run the
1256
2422
  expensive render + any remaining paid work end-to-end. Never spend on the expensive render before
1257
2423
  approval, and don't drip pieces out one at a time and re-pause.
1258
2424
 
@@ -1265,9 +2431,10 @@ every piece three ways by cost, NOT just "free vs paid":
1265
2431
  mirror the real asset. The few credits buy a real review: the user SEES the actual creator face
1266
2432
  and end card and HEARS the VO, instead of judging a prompt. **This OVERRIDES any recipe rule that
1267
2433
  says to gate ALL paid calls** — only the expensive render below is gated.
1268
- - **EXPENSIVE paid** — the video take / final AI render (hundreds of credits) → do NOT generate.
1269
- Put its **exact prompt/spec** (+ ref image URLs) in the tile. This is the ONE thing approved as a
1270
- prompt (you can't preview a hundreds-of-credits video for free); it's generated only in Step 4.
2434
+ - **EXPENSIVE paid** — the video take / final AI render / per-line lipsync clips (hundreds of
2435
+ credits) → do NOT generate. Put its **exact prompt/spec** (+ ref image URLs) in the tile. This is
2436
+ the ONE thing approved as a prompt (you can't preview a hundreds-of-credits video for free); it's
2437
+ generated only in Step 4.
1271
2438
 
1272
2439
  **The expensive render's exact prompt must be in the panel BEFORE you ask for approval** — so a
1273
2440
  single "go" runs it (plus any remaining paid work) without re-pausing mid-run.
@@ -1284,27 +2451,38 @@ render (GOOSE-2542). The full video is assembled ONLY in Step 4, after approval.
1284
2451
  ingredient here is only a genuinely separate SOURCE clip the format needs (e.g. supplied b-roll).
1285
2452
 
1286
2453
  1. **Assemble every piece the format needs — not just the script.** Read the recipe for the exact
1287
- list. For an iMessage video that's the **script** (bubble thread), the **conversation image(s)**,
1288
- and the **end card**; richer templates add a hook frame, background, product shots, music bed, a
1289
- creator/avatar, a voiceover… For each piece, decide FREE / CHEAP-paid / EXPENSIVE-paid (above):
1290
- - **FREE or CHEAP paid** (≤ ~100 credits — HTML mockups, a still, the creator frame, the end
1291
- card, a short VO/music bed) → generate it now and \`get_upload_url\` the asset to the project
1292
- folder \`agent-config/brands/<brand_slug>/projects/<project_id>/working/review/<name>\` (same
1293
- path-prefix rule as final publish — a bare \`working/review/<name>\` won't render in the panel);
1294
- set that piece's \`path\` in \`script_drafts\` to the project-relative \`working/review/<name>\`.
1295
- - **EXPENSIVE paid** (the video take / final render, hundreds of credits) → do NOT generate. Put
1296
- the **exact prompt/spec** (and any ref image URLs) in the tile's \`text\` / \`subtitle\` so the
1297
- user reviews what will be spent on. No \`path\` yet — it's generated in Step 4.
1298
- Include the **estimated cost in CREDITS** (never dollars) of the cheap pieces already generated +
1299
- the pending render, so the user approves knowing the total spend. **Answer clarifying questions
2454
+ list. For a podcast skit that's the **script** (both hosts' lines), the two **host stills**,
2455
+ one **voice sample per host**, and the **end card**; an iMessage video has the bubble thread,
2456
+ the conversation image(s) and the end card. For each piece, decide FREE / CHEAP-paid /
2457
+ EXPENSIVE-paid (above):
2458
+ - **FREE or CHEAP paid** (≤ ~100 credits) → generate it now and upload it with
2459
+ \`media_upload { brand_id, scope: "video_project", scope_id: project_id, kind, path:
2460
+ "working/review/<name>", ingredient_key, input_digest, source: { type: "file", filename:
2461
+ "<name>", content_type } }\` → PUT → \`media_confirm { brand_id, media_id: media.id }\`;
2462
+ after confirmation succeeds, set that
2463
+ piece's \`path\` (+ \`media_id\`, \`ingredient_key\`) in \`script_drafts\` to the project-relative
2464
+ \`working/review/<name>\`. First check "Save as you go" — a piece already saved with the same
2465
+ digest is downloaded, not regenerated.
2466
+ - **EXPENSIVE paid** → do NOT generate. Put the **exact prompt/spec** (and any ref image URLs)
2467
+ in the tile's \`text\` / \`subtitle\` so the user reviews what will be spent on. No \`path\` yet —
2468
+ it's generated in Step 4.
2469
+ Save **render_estimate.total_credits**: previews already spent + pending render + the 200 base fee.
2470
+ Use current server pricing, never a catalog range in place of the full total.
2471
+ **Brand check of the script, before it goes in the panel:** every line, caption and on-screen
2472
+ text is checked against \`working/brand-rules.json\`. Nothing in \`never_say\` appears, in words or
2473
+ in meaning (a paraphrase of a banned claim is still banned). Every product detail (name,
2474
+ flavour, size, price, ingredient, result) comes from \`products[]\` or the kit: anything else is
2475
+ cut, not invented. Add a \`note\` ingredient labelled "Brand rules applied" that lists the
2476
+ pronunciations used and the rules the script respects, so the user sees them.
2477
+ **Answer clarifying questions
1300
2478
  from the project brief FIRST (Step 1.5)** — only ask the user for a field (angle, which product,
1301
2479
  offer/code) the \`creative_brief\` leaves empty AND the recipe can't default. Do not re-ask for
1302
2480
  anything the composer already captured.
1303
- 2. **Mirror the whole ingredient set for review** — \`update_ad_project_script { project_id,
1304
- script_drafts, script }\`. \`script_drafts\` is a structured payload of **container-tagged
1305
- ingredients** so the app renders each piece the right way:
1306
- \`{ format, scenes?, ingredients: [{ container, label, subtitle?, path?, text? }] }\`. Each
1307
- ingredient's \`container\` tells the app HOW to show it:
2481
+ 2. **Mirror the whole ingredient set for review** — \`video_project_upsert { brand_id, project_id,
2482
+ patch: { script: { script_drafts, script } } }\`. \`script_drafts\` is a structured payload of
2483
+ **container-tagged ingredients** so the app renders each piece the right way:
2484
+ \`{ format, scenes?, ingredients: [{ container, label, subtitle?, path?, url?, text? }], render_estimate? }\`.
2485
+ Each ingredient's \`container\` tells the app HOW to show it:
1308
2486
  - \`image\` (a frame shown in the video), \`endcard\` (the end card), \`avatar\` (a character
1309
2487
  headshot), \`background\` → rendered as an image tile.
1310
2488
  - \`voice\` (a voiceover clip — put the voice NAME in \`subtitle\`), \`music\` (the bed),
@@ -1312,147 +2490,343 @@ ingredient here is only a genuinely separate SOURCE clip the format needs (e.g.
1312
2490
  - \`video\` (a clip) → a video player. \`text\` (a copy line like the CTA) → a text tile.
1313
2491
  - \`script\` / \`thread\` / \`note\` / \`conversation\` → the written script (or set \`scenes[]\`
1314
2492
  for the podcast shape, or pass the readable \`script\` string).
1315
- \`path\` = \`working/review/<name>\` (upload the preview asset first via \`get_upload_url\`); \`url\`
1316
- works too. **Label every ingredient** ("Hook image", "End card", "Voiceover", "Background
1317
- music", "HER"). The \`update_ad_project_script\` call itself writes no render and costs no credits
1318
- (the cheap pieces you already generated above have their own small cost) — it just populates the
1319
- review panel.
1320
- 3. **STOP — the review happens in the APP's review panel, NOT in this chat.** You've mirrored the
1321
- ingredients (3.2); now hand the user the project's \`app_url\` (from \`get_ad_project\`) and tell
1322
- them to review the pieces there and hit **"Approve & render"**. That button gives them a short
1323
- message to paste back into this session — THAT is your go-ahead. Do NOT paste the
1324
- script/ingredients into the chat for a thumbs-up, and do NOT render until that approval comes
1325
- back from the app. If they want changes (via the app's comments or here), regenerate the
1326
- affected ingredient, call \`update_ad_project_script\` again, tell them it's refreshed in the
1327
- app, and wait for a fresh approval. Only AFTER the app approval do Step 4. A single approval
1328
- authorises the WHOLE remaining chain — generate every paid piece, render, self-QC, publish —
1329
- with NO further pauses (that is exactly why every paid prompt must already be in the panel).
1330
-
1331
- ## Step 4 — render locally, report stages, publish
1332
-
1333
- 1. Now generate every PAID piece you showed as a prompt in Step 3 — the AI stills/video, voice,
1334
- music, the end-card render — through the media proxies (below), each from its approved prompt.
1335
- Then assemble per the recipe (Playwright record where needed → ffmpeg stitch → \`mix-master\`
1336
- audio).
1337
- 2. Open the row LAST: \`submit_render { project_id, kind: "full" }\` → keep \`render_id\`, then
1338
- \`update_render_status { render_id, status: "running" }\`. The render row tracks status only
1339
- (queued / running / complete / failed) — narrate fine-grained progress with
1340
- \`append_project_message\` instead.
1341
- 3. **MANDATORY final-video QC gate — YOU review EVERY finished master before \`set_final_render\`,
1342
- whatever the format (UGC or not).** This is your own automated quality check, separate from the
1343
- user's Step-3 approval — it does not go back to the user. The render row is already open (its
1344
- nominal credit spent, \`submit_render\` in 4.2);
1345
- this gate stands between a rendered master and PINNING/publishing it, so a bad render never gets
1346
- set as final. A master that looks fine on a still can still have a mis-voiced word, a caption
1347
- drifting off its line, a beat out of order, or a deformation — review the actual VIDEO, not
1348
- stills. Run the passes that APPLY to this format:
2493
+ **Label every ingredient** ("Hook image", "End card", "Voiceover", "Host A", "HER"). The upsert
2494
+ writes no render and costs no credits — it just populates the review panel.
2495
+ 3. **STOP for ONE approval, in this chat.** Only a complete current saved review set with
2496
+ render_estimate.total_credits can be approved. Widget: one line, no duplicate plan or app
2497
+ link; otherwise print card.text_summary. The choices are Approve, Change and Not now.
2498
+ “Not now” keeps the plan. In a coworker sandbox end the turn after saving the review set;
2499
+ the customer's approval arrives as the next message.
2500
+ Record the explicit yes with \`video_project_upsert { brand_id, project_id, patch: { approve:
2501
+ { user_quote: "<their exact words>", total_credits: <the saved total> } } }\` before rendering;
2502
+ its reply says what comes next. A single project requires recorded approval too; never treat an absent approval as permission.
2503
+ Changes require an updated saved review, clearing prior approval, and approval of the new
2504
+ total. One yes authorizes the remaining approved chain. On insufficient_balance start
2505
+ nothing; offer a shorter video or top-up. Remove batch concepts with patch.concepts remove:true,
2506
+ then show the changed total before approval.
2507
+
2508
+ ## Step 4 — render, report stages, publish
2509
+
2510
+ 1. **Open the render row FIRST** — right after recording the Step 3 approval, before any paid
2511
+ generation (if this returns \`approval_required\`, the approval is missing or was cleared: go
2512
+ back to Step 3 and ask in this chat, don't retry; if it returns \`coworker_makes_it\`, the video
2513
+ was handed to the GooseWorks coworker, so don't render it: the coworker is making it):
2514
+ \`${exports.RENDER_ROW_TOOL} { brand_id, project_id, ${exports.RENDER_OPEN_ARGS} }\` (no \`dry_run\`; returns
2515
+ \`render_id\`) → keep \`render_id\`, then mark it running:
2516
+ \`${exports.RENDER_ROW_TOOL} { brand_id, project_id, ${exports.RENDER_UPDATE_KEY}: { render_id, status: "running",
2517
+ workflow_stage: "preparing", progress_note: "starting", progress_percent: 5 } }\`. The user sees this
2518
+ live in the app and gets a WhatsApp "started" message automatically — don't message them yourself
2519
+ about start / blocked / complete.
2520
+ After EVERY progress callback inspect stop. If true, start no new paid step; record
2521
+ status:"stopped" with a plain note and report what is kept and credits used. SPEND_CAP_REACHED
2522
+ stops the same way. Never raise a video's budget on your own judgment. Raise it only when a paid
2523
+ step was refused with SPEND_CAP_REACHED or the progress card offers "Finish it" or a choice past the budget.
2524
+ Then read the project and say cost.raise_quote in one line: "Finishing needs up to <by_credits>
2525
+ more credits, so your budget goes from <from_credits> to <to_credits>. OK?" Only after the
2526
+ customer's yes to that number: patch.approve { scope:"raise_cap", total_credits: <to_credits>,
2527
+ user_quote: "<their exact words>" }. total_credits is the new budget itself, never credits to
2528
+ add; do not send raise_cap_credits for one video. If they pick a smaller choice past the
2529
+ budget, send that choice's new budget as total_credits (never above to_credits). Sending the same
2530
+ total_credits again changes nothing (already_raised: carry on). An earlier yes, or "I don't care
2531
+ about the cost", is not approval of a new budget. If cost.raise_quote is null because the plan was saved
2532
+ again since its approval (plan_status is not "approved"), show the plan with its total and
2533
+ approve it again; otherwise the approved budget already covers the work: carry on, do not ask.
2534
+ After a raise, open a new render that reuses the saved pieces. A refused raise changes nothing:
2535
+ cap_raise_not_needed means carry on within the budget, or, when it says the plan was saved again,
2536
+ show the plan with its total and approve the plan again; cap_raise_total_required means read the
2537
+ project again and send total_credits equal to cost.raise_quote.to_credits after the customer
2538
+ approves that number; cap_raise_changed or cap_raise_too_large mean read the project again and
2539
+ show the current cost.raise_quote. Custom videos never use raise_cap.
2540
+ Send render.steps with the same neutral names each time and a live count only in the
2541
+ current detail; use render.choices when blocked.
2542
+ 2. Now generate every PAID piece you showed as a prompt in Step 3 — the AI stills/video, lipsync
2543
+ clips, voice, music — through the media proxies (below), each from its approved prompt, with
2544
+ \`GW_PROJECT_ID\` exported. **Save as you go** (section above): skip any piece already saved
2545
+ with the same \`input_digest\` (download it), and upload each new piece with its
2546
+ \`ingredient_key\` + \`input_digest\` the moment it passes QC.
2547
+ Keep the approved script and review unchanged throughout production and QC; confirmed media
2548
+ and progress preserve resume state. Stop and replan/reapprove any material creative change.
2549
+ A voiceover made with \`data_post_provider\` (ElevenLabs \`…/with-timestamps\`) returns its
2550
+ \`alignment\` only in the reply: write it to \`working/vo/<scene>.timestamps.json\` at once
2551
+ (captions are timed from it) and record the returned \`media_id\` on the ingredient.
2552
+ **Brand pronunciations in every voiceover:** the text sent to the voice has each
2553
+ \`pronunciations[].term\` replaced by its \`say_as\` (\`create-vo-elevenlabs\`:
2554
+ \`gen_vo.py … --rules working/brand-rules.json\`; when a render atom calls the voice itself,
2555
+ swap the terms in the text you hand it). Captions, on-screen text and the review keep the
2556
+ written name. Write \`working/approved-script.txt\` (the Step 4.3 audio check) with the SPOKEN
2557
+ form. **Logo, font, product:** every logo is the file from Step 1.7 composited as-is; every
2558
+ product shot uses the Step 1.7 product images as its reference.
2559
+ Then assemble per the recipe (ffmpeg stitch; PIL captions / end card;
2560
+ Playwright record only where the format needs it and the host has Chromium → \`mix-master\` audio).
2561
+ **Report progress at each milestone** — about one update per milestone, never per poll:
2562
+ \`${exports.RENDER_ROW_TOOL} { brand_id, project_id, ${exports.RENDER_UPDATE_KEY}: { render_id, status: "running", workflow_stage, progress_note, progress_percent } }\`
2563
+ (\`progress_note\` = plain words the customer reads in the app, ≤200 chars; no model, tool or
2564
+ step names):
2565
+ - voiceovers done → \`"preparing"\`, \`"voices recorded"\`, 20
2566
+ - stills done → \`"preparing"\`, \`"scenes designed"\`, 35
2567
+ - each lipsync / video clip → \`"rendering"\`, e.g. \`"filming scene 5 of 8"\`, 35–75
2568
+ - assembly → \`"rendering"\`, \`"putting the video together"\`, 85
2569
+ - QC (4.3) → \`"final check"\`, 95
2570
+ **Hard stop that needs the user** → \`{ render_id, status: "running", workflow_stage: "blocked",
2571
+ error_message: "<what's wrong + what you need>" }\`; an unrecoverable failure → \`status: "failed"\`
2572
+ + \`error_message\`. Completing (4.4) sets the bar to 100.
2573
+ 3. **MANDATORY final-video QC gate — YOU review EVERY finished master before pinning it
2574
+ (\`patch.final_render_id\`), whatever the format (UGC or not).** This is your own automated quality
2575
+ check, separate from the user's Step-3 approval — it does not go back to the user. The render row
2576
+ is already open (4.1); this gate stands between a rendered master and PINNING/publishing it, so a
2577
+ bad render never gets set as final. A master that looks fine on a still can still have a
2578
+ mis-voiced word, a caption drifting off its line, a beat out of order, or a deformation — review
2579
+ the actual VIDEO, not stills. Run the passes that APPLY to this format:
1349
2580
  - **Audio ↔ script** — any master with SPEECH (VO or native/Seedance voice); **skip for
1350
2581
  music-only / no-speech formats.** \`review-ugc-render\` is format-agnostic despite the name —
1351
- a deterministic Whisper transcript-vs-script diff, not UGC-specific: persist the approved
1352
- spoken lines to \`working/approved-script.txt\`, then \`gooseworks fetch review-ugc-render\` and
1353
- run \`review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json
1354
- working/review-verdict.json\` (exit 0 PASS / 2 FAIL / 3 ERROR). It blocks a mis-voiced word
2582
+ a deterministic Whisper transcript-vs-script diff: persist the approved spoken lines to
2583
+ \`working/approved-script.txt\`, fetch \`review-ugc-render\` (\`catalog_fetch\`) and run
2584
+ \`review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json
2585
+ working/review-verdict.json\` (exit 0 PASS / 2 FAIL / 3 ERROR). For each brand pronunciation add
2586
+ \`--brand-term "<term>"\`, plus \`--brand-term\` for each word of \`say_as\` that is not an
2587
+ everyday word (the flag strips those tokens from the WHOLE diff, so never pass "a", "one",
2588
+ "works" alone): Whisper spells a respelled name back as the brand word ("Goose Works" heard
2589
+ as "Gooseworks"), so the diff must accept it. The proof of
2590
+ intended delivery is the text/settings you sent (keep them in the review); actual
2591
+ pronunciation/performance requires isolated and mixed-audio listening at normal speed.
2592
+ Preserve pauses/emphasis and final consonants; never mandate faster or louder delivery.
2593
+ Verify a transcript mismatch against the actual audio before calling it a defect. It flags a mis-voiced word
1355
2594
  (approved "human-vetted" → "human witted"), a dropped phrase, or silence. It routes Whisper
1356
- through the gooseworks proxy when \`OPENAI_BASE_URL\` is set; with no backend at all, run
1357
- \`fal-ai/whisper\` via \`fal-proxy\` (upload the audio, pass its \`get_download_url\` as \`audio_url\`)
2595
+ through the gooseworks proxy when \`OPENAI_BASE_URL\` is set (sandbox:
2596
+ \`$GW_WHISPER_PROXY_URL/v1\`); with no backend at all, run \`fal-ai/whisper\` via the FAL proxy
1358
2597
  and diff the transcript yourself.
1359
2598
  - **Captions / subtitles** — ANY captioned format (the most common non-UGC defect); **skip for
1360
- UGC/Seedance masters, which carry no subtitle track.** Concrete check: diff the caption file
1361
- you burned (SRT/ASS) against the SAME Whisper transcript + word timings from the audio pass —
1362
- every caption line must match the heard/scripted words and sit within ~0.3s of when they're
1363
- spoken; then in the visual pass below, OCR-read the burned caption off 4–5 sampled frames to
1364
- confirm it's actually on screen at that time and not colliding with a hyperframe or the end
1365
- card. Mismatched text or >0.3s drift fails the gate.
2599
+ UGC/Seedance masters, which carry no subtitle track.** Diff the caption file you burned
2600
+ (SRT/ASS/PNG cue list) against the SAME Whisper transcript + word timings — every caption line
2601
+ must match the heard/scripted words and sit within ~0.3s of when they're spoken; then in the
2602
+ visual pass below, inspect each cue and its start/end boundaries in actual frames, plus
2603
+ full-speed playback at destination size, to confirm reading time, product visibility,
2604
+ hierarchy and no duplicate overlay or end-card collision. Silent/text-led formats use
2605
+ approved visible wording and reading windows, not nonexistent speech timings. Mismatched text or >0.3s drift fails the gate.
1366
2606
  - **Visual + structure** — always: run the \`watch\` skill on the master — beat/scene order + SFX,
1367
- the brand's product (not the source's) is shown, the end card has the real wordmark + code, no
2607
+ the brand's product (not the source's) is shown, the end card has the brand's logo file (or its name set in the brand font) + code, no
1368
2608
  deformation/artifact, duration within ~20% of the source.
1369
- If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, rebuild captions)
1370
- and re-review — only a clean pass proceeds to \`set_final_render\`. **This gate is universal: it
1371
- runs from the master skill for every format, so a recipe never has to opt in.**
1372
- 4. Publish: \`get_upload_url { target: { type: "agent", agent_id: ADS_AGENT } }\` → PUT the master
1373
- and poster **under the project folder** (see Identity's path-prefix rule) — to
1374
- \`agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4\` and
1375
- \`.../working/final-thumb.jpg\`. **Always target ADS_AGENT AND use the full project-folder path**
1376
- — a bare \`working/final.mp4\`, even on the right agent, 404s in the app. Verify servable:
1377
- \`get_download_url { target: ADS_AGENT, path: "agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4" }\`
1378
- must return a non-empty URL (curl it for HTTP 200).
1379
- Then \`update_render_status { render_id, status: "complete", output_url, thumbnail_url }\` where
2609
+ - **Finished ad + brand fidelity** — always: fetch \`review-finished-ad\` (\`catalog_fetch\`,
2610
+ \`pip install --quiet numpy pillow\` if needed) and run
2611
+ \`review_finished_ad.py --video <master>.mp4 --json working/review/finished-ad.json
2612
+ --sheet working/review/finished-ad-sheet.png --logo <the logo file the video composites>
2613
+ --palette "<kit palette, comma-separated>" --product-images <the Step 1.7 images>
2614
+ --font <brand font file> --brand-name "<name>" --endcard-s <end card length>\`. Pass the
2615
+ file that is ACTUALLY on screen (the kit logo, or the recipe's wordmark file). When the video
2616
+ shows only the brand name as text (\`"logo": {"mode": "text"}\`), omit \`--logo\` and judge the
2617
+ text on the sheet. Add \`--no-speech\` for a format with no VO or dialogue, \`--logo-at <s>\` for
2618
+ each mid-video logo, and any gate flags the recipe's instructions name (a chat format's
2619
+ reading holds need a longer \`--max-freeze-s\`). Exit 0 PASS / 2 FAIL / 3 ERROR. It
2620
+ checks size, hook, pacing, dead air, black frames, the kit logo on the end card
2621
+ (a wrong, redrawn or favicon logo fails) and the palette. **Then open the sheet it writes
2622
+ and judge every line of its \`judge_on_sheet\`:** captions/CTA/logo outside the red safe
2623
+ zones, the brand font, every product shot matching the product images, the product the same
2624
+ in every scene, the logo unaltered. A failed eye check is a FAIL like any other.
2625
+ - **Output size** — always **1080×1920 (9:16)**. Video ads are only ever 9:16. Lipsync / video
2626
+ models often return 720p or odd sizes — scale (and pad if the aspect differs) every clip to
2627
+ 1080×1920 BEFORE the concat, never ship the model's native size.
2628
+ Save exact output checksum/version, first-to-last normal-speed playback and full-audio
2629
+ coverage. Frame sheets, transcripts and process success do not substitute for listening or
2630
+ continuous motion. Missing required capability leaves review incomplete/blocked. Each
2631
+ supported crop, duration, language or caption derivative needs its own actual-file review;
2632
+ unsupported destination requirements need a reviewed route, never a silent export change.
2633
+ Honor intended silence, loops and endings rather than forcing music or fades on every ad.
2634
+ If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, re-composite the
2635
+ end card from the real logo file, rebuild captions) and re-run the passes — only a clean pass
2636
+ proceeds to pinning. **At most 2 repair rounds.** If a pass still fails after them, do NOT pin
2637
+ and do NOT present the video as finished: upload it (4.4) and close the row with exactly
2638
+ \`video_render_run { brand_id, project_id, render: { render_id, status: "complete",
2639
+ workflow_stage: "blocked", quality_status: "blocked", repair_pass_count: 2, output_url,
2640
+ thumbnail_url, quality_report } }\` (the failing checks as \`fail\` in the report), and tell the user plainly in
2641
+ chat which checks failed, what you tried, and the choices (fix a specific thing, re-roll, or
2642
+ use it anyway). Pin it only if they say to use it anyway. The app shows a blocked render as
2643
+ "Needs attention". **This gate is universal: it runs from this skill for every format, so a
2644
+ recipe never has to opt in.**
2645
+ 4. **For a passing final, create the finished-video checkpoint first**, following "Finished video, interrupted saving"
2646
+ above. With the CLI, use its prepare/resume flow instead of duplicating the writes below; it
2647
+ saves the same already-opened render and pins only the checked output. Save the final review set
2648
+ and production manifest (Step 4.5/4.6) before preparing that checkpoint. Without it, follow the
2649
+ same durable protocol. A blocked version follows Step 4.3's existing upload/report-only path;
2650
+ never route it through the passing-final helper or pin it automatically.
2651
+ Publish: \`media_upload { brand_id, scope: "video_project", scope_id: project_id, kind: "render",
2652
+ path: "working/final.mp4", ingredient_key: "final", input_digest, source: { type: "file", filename: "final.mp4", content_type:
2653
+ "video/mp4" } }\` → PUT the master to \`upload.url\` with \`upload.required_headers\` →
2654
+ \`media_confirm { brand_id, media_id: media.id }\`. Same for the poster (\`kind: "thumbnail"\`, \`path: "working/final-thumb.jpg"\`, \`ingredient_key: "final-thumb"\`).
2655
+ Keep each \`upload.render_file_url\`. Verify the PUT returned 2xx and the file you uploaded is a
2656
+ real, non-empty MP4 (ffprobe it), and require both confirmations to succeed BEFORE marking
2657
+ the render complete.
2658
+ Then \`${exports.RENDER_ROW_TOOL} { brand_id, project_id, ${exports.RENDER_UPDATE_KEY}: { render_id, status: "complete", output_url, thumbnail_url } }\` (attach the Step 4.3 verdict as \`quality_status: "passed"\` (or
2659
+ \`"blocked"\` when the gate still fails after 2 repair rounds) + \`quality_report\` — ALWAYS
2660
+ attach it; a batch concept cannot complete without a passing one; exact shape, strict (no extra keys):
2661
+ \`{ version: 1, summary: string, checks: { source, brand, product, hook_and_scene_order,
2662
+ voice_and_script, captions, endcard_and_cta, duration_and_ratio, visual_artifacts }, detected_issues?:
2663
+ string[], repair_actions?: string[] }\` where EVERY check is \`{ status: "pass"|"fail"|"not_applicable",
2664
+ note?: string }\`). Fill each check from the Step 4.3 passes: \`brand\` ← logo_asset + logo +
2665
+ palette + font + logo unaltered; \`product\` ← product likeness + product consistency;
2666
+ \`hook_and_scene_order\` ← hook + pacing + the \`watch\` beat order; \`voice_and_script\` ← the
2667
+ Whisper diff (pronunciations included) + no \`never_say\` line; \`captions\` ← the caption diff +
2668
+ safe zones; \`duration_and_ratio\` ← ratio + duration; \`visual_artifacts\` ← black frames +
2669
+ frozen stretches + the \`watch\` pass; \`endcard_and_cta\` ← the end card holds the real logo +
2670
+ CTA. Put each failing check's note in \`detected_issues\` and each fix in \`repair_actions\`.
1380
2671
  **output_url MUST be the durable render-file URL**
1381
- \`/api/ads/projects/<project_id>/render-file?path=working/final.mp4\` (the app re-presigns it on
1382
- every view) — NEVER a raw proxy/CDN URL (those expire). Same for \`thumbnail_url\`.
1383
- 5. \`set_final_render { project_id, render_id }\` to pin it, then return the \`app_url\` +
1384
- \`brand_url\` (from the project/links) verbatim. Never end on just "done" or a file path.
1385
-
1386
- Narrate each long step in one line via \`append_project_message { project_id, role: "agent",
1387
- content }\` — never sit silent on a queue > 90s.
2672
+ (\`upload.render_file_url\`, i.e. \`/api/ads/projects/<project_id>/render-file?path=working/final.mp4\`
2673
+ — the app re-presigns it on every view) — NEVER a raw proxy/CDN/presigned URL (those expire).
2674
+ Same for \`thumbnail_url\`.
2675
+ 5. **Save the final review set BEFORE pinning** — it must describe the video you actually rendered.
2676
+ Only after all paid production, QC, repairs and pending provider work are finished and settled,
2677
+ save one final descriptive/provenance update: confirmed pieces, exact settings and repairs that
2678
+ preserve the approved creative choices. Material creative changes, including changed lines,
2679
+ look, timing, captions or music, require a changed complete plan, total and fresh approval BEFORE
2680
+ the changed production. Never relabel a changed plan as provenance or silently reuse approval.
2681
+ In the checkpoint flow, save the final review before Step 4.4; do not rewrite it afterward merely
2682
+ to follow the numbered order. Resume checks that this reviewed set stayed unchanged.
2683
+ When final provenance needs updating, upsert the review set once:
2684
+ \`video_project_upsert { brand_id, project_id, patch: { script: {
2685
+ script_drafts, script } } }\` with the final lines, final pieces (mark generated takes as done, not
2686
+ "not generated yet") and the settings you used. The project keeps this, not your chat: it is
2687
+ what the app shows, and what a Community remix of this video copies. Instructions that live only
2688
+ in this conversation are lost when it ends. **Keep the approved detail**: never shorten a piece to a
2689
+ summary (e.g. per-scene prompts or their "no shake / no letterbox" guards). Only update what changed.
2690
+ After this write, do not start new paid work. Save the production manifest, prepare the final
2691
+ checkpoint and complete the same render with its existing finishing allowance and guards.
2692
+ 6. **Save the production manifest** — HOW you made it, so the next run (or a remix) starts from what
2693
+ worked instead of rediscovering it: \`video_project_upsert { brand_id, project_id, patch: { production:
2694
+ { version: 1, pipeline: [{ step, model, purpose?, settings? }], style: { prompt, negative, notes? },
2695
+ characters: [{ name, prompt }], scenes: [{ id, line, still_prompt, motion_prompt, duration_s }],
2696
+ voice: { voice_id, name, model, settings }, music: { prompt, model, length_s }, assembly: { … },
2697
+ fixes: [{ problem, fix }], notes? } } }\`. Use the exact models and prompts you sent (full text, guards
2698
+ included), and list every fix you had to make (e.g. "VO ran 47s → tightened four lines, voice 1.08x";
2699
+ "hair drifted → restated the hair colour"). No URLs, keys or raw logs; it must stay under 48 KB.
2700
+ In the checkpoint flow, save this before Step 4.4 too and skip the duplicate write afterward.
2701
+ 7. Pin it — only a \`passed\` render (or a \`blocked\` one the user said to use anyway):
2702
+ If checkpoint recovery already verified selection of this render, skip this duplicate write.
2703
+ An unfinished checkpoint must use the guarded selection protocol above; never replace it
2704
+ with an unguarded pin.
2705
+ \`video_project_upsert { brand_id, project_id, patch: { final_render_id: render_id } }\`,
2706
+ then use the returned card: widget hosts get one line and no duplicate links; text hosts get
2707
+ card.text_summary and the returned delivery links verbatim. Never end with a local file path.
2708
+
2709
+ Narrate each long step in one line via \`video_project_upsert { brand_id, project_id, patch:
2710
+ { message: { role: "agent", content } } }\` — never sit silent on a queue > 90s. Write it for the
2711
+ customer ("Filming the scenes, about 5 more minutes"), per "How to talk to the customer" above.
1388
2712
 
1389
2713
  ## Media generation — the GooseWorks proxies (queue loop)
1390
2714
 
1391
- Media APIs go through GooseWorks proxies with your agent token; do NOT use an SDK's default host
1392
- (your token isn't a FAL/ElevenLabs token → 401). Base = \`<api_base>/api/internal/<proxy>\`; pass
1393
- \`?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>\` (agent_id bills the Ads agent;
1394
- \`project_id\` = the id of the project you're rendering — it attributes this generation's credits to
1395
- that ad project so the user sees per-project spend in the app. ALWAYS pass it). FAL = \`fal-proxy\`
1396
- (+ \`fal-storage-proxy\` to host a local image and get a CDN URL); ElevenLabs = \`elevenlabs-proxy\`
1397
- (VO / music bed).
2715
+ Media APIs go through GooseWorks proxies; do NOT use an SDK's default host (your token isn't a
2716
+ FAL/ElevenLabs token → 401, and in a sandbox a raw provider call is never allowed). Prefer the
2717
+ atoms' own \`media_proxy.py\` (\`fal_generate\`, \`fal_generate_video\`, \`eleven_music\`, …) — it already
2718
+ handles auth, the queue host-swap, project attribution (\`GW_PROJECT_ID\`) and failure logging.
2719
+
2720
+ - **Sandbox:** FAL base = \`$GW_FAL_PROXY_URL\`, ElevenLabs = \`$GW_ELEVENLABS_PROXY_URL\`, FAL
2721
+ storage = \`$GW_FAL_STORAGE_PROXY_URL\`; auth = \`?token=$GW_MEDIA_PROXY_TOKEN\` (or
2722
+ \`Authorization: Bearer\`), plus \`&project_id=$GW_PROJECT_ID\`. The token already names the agent.
2723
+ - **Local:** base = \`<api_base>/api/internal/<proxy>\` (\`fal-proxy\`, \`fal-storage-proxy\`,
2724
+ \`elevenlabs-proxy\`); pass \`?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>\`
2725
+ (agent_id bills the Ads agent; project_id attributes the spend to this ad project so the user
2726
+ sees per-project spend in the app. ALWAYS pass it).
1398
2727
 
1399
2728
  **FAL queue gotcha** (#1 waste of generations): submit returns \`status_url\`/\`response_url\` on
1400
2729
  \`queue.fal.run\` (the real host, not the proxy). Polling those 401s forever — rewrite their host
1401
- to the proxy base (keep the path), re-add \`?token=&agent_id=\`. Only the final \`*.fal.media\`
1402
- image is a real public URL. Helper:
2730
+ to the proxy base (keep the path), re-add the auth params. Only the final \`*.fal.media\`
2731
+ file is a real public URL. Helper (works in both modes):
1403
2732
 
1404
2733
  \`\`\`python
1405
2734
  import json, os, pathlib, time, requests
1406
2735
  from urllib.parse import urlparse
1407
2736
 
1408
2737
  def _cfg():
2738
+ """(fal_base, token, agent_id). Sandbox env first, then ~/.gooseworks/credentials.json."""
2739
+ tok = os.environ.get("GW_MEDIA_PROXY_TOKEN")
2740
+ if tok:
2741
+ base = os.environ.get("GW_FAL_PROXY_URL") or os.environ["GW_API_BASE"].rstrip("/") + "/api/internal/fal-proxy"
2742
+ return base.rstrip("/"), tok, None
1409
2743
  c = json.loads(pathlib.Path(os.path.expanduser("~/.gooseworks/credentials.json")).read_text())
1410
- return c["api_base"].rstrip("/"), c["api_key"], c.get("agent_id")
2744
+ return c["api_base"].rstrip("/") + "/api/internal/fal-proxy", c["api_key"], c.get("agent_id")
1411
2745
 
1412
- def _params(tok, agent, project_id=None):
2746
+ def _params(tok, agent):
1413
2747
  p = {"token": tok}
1414
2748
  if agent: p["agent_id"] = agent
1415
- if project_id: p["project_id"] = project_id # attributes the spend to this ad project
2749
+ pid = os.environ.get("GW_PROJECT_ID")
2750
+ if pid: p["project_id"] = pid # attributes the spend to this ad project
1416
2751
  return p
1417
2752
 
1418
- def fal_generate(model_path, payload, project_id=None, timeout_s=180, poll_s=3):
2753
+ def fal_generate(model_path, payload, timeout_s=180, poll_s=3):
1419
2754
  """model_path e.g. 'fal-ai/nano-banana-2/edit' (the recipe names the model).
1420
- Pass project_id = the ad project you're rendering so credits attribute to it.
1421
- Returns the result image URL (a public *.fal.media CDN URL)."""
1422
- api_base, tok, agent = _cfg()
1423
- base = api_base + "/api/internal/fal-proxy"
1424
- sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent, project_id), json=payload).json()
2755
+ Export GW_PROJECT_ID first. Returns the result image URL (a public *.fal.media URL)."""
2756
+ base, tok, agent = _cfg()
2757
+ sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent), json=payload).json()
1425
2758
  to_proxy = lambda u: base + urlparse(u).path
1426
2759
  status_url, response_url = to_proxy(sub["status_url"]), to_proxy(sub["response_url"])
1427
2760
  deadline = time.time() + timeout_s
1428
2761
  while time.time() < deadline:
1429
- st = requests.get(status_url, params=_params(tok, agent, project_id)).json()
2762
+ st = requests.get(status_url, params=_params(tok, agent)).json()
1430
2763
  if st.get("status") == "COMPLETED":
1431
- return requests.get(response_url, params=_params(tok, agent, project_id)).json()["images"][0]["url"]
2764
+ return requests.get(response_url, params=_params(tok, agent)).json()["images"][0]["url"]
1432
2765
  if st.get("status") in ("FAILED", "ERROR"):
1433
2766
  raise RuntimeError(f"FAL failed: {st}")
1434
2767
  time.sleep(poll_s)
1435
2768
  raise TimeoutError("FAL polling exceeded timeout")
1436
2769
  \`\`\`
1437
2770
 
1438
- ElevenLabs (VO / music) is the same shape against \`<api_base>/api/internal/elevenlabs-proxy\`
1439
- with \`?token=&agent_id=&project_id=\`. Feed FAL a local image by storing it (\`get_upload_url\`) and passing its
1440
- \`get_download_url\` presigned URL as an \`image_urls\` / \`audio_url\` entry — this is the reliable
1441
- path. (\`fal-storage-proxy\` may 404 depending on the install; don't block on it — prefer the
1442
- \`get_download_url\` presigned URL.)
2771
+ ElevenLabs (VO / music) is the same shape against the ElevenLabs proxy base. To feed FAL a local
2772
+ file (a product image, a VO track), it must be a PUBLIC URL: upload it with \`media_upload\`
2773
+ (\`scope: "video_project"\`, source \`file\`, no \`path\`) → PUT → \`media_confirm\` and use the returned
2774
+ \`media.url\` if it is a public https URL (curl it: HTTP 200 without auth), or host it through the
2775
+ FAL storage proxy. Never pass a \`render-file\` URL to a provider — it needs app auth.
2776
+
2777
+ ## Follow-up actions use the watched version
2778
+
2779
+ Keep original note text, exact source render/time and intended viewer effect. Record an
2780
+ acceptance condition and disposition in existing feedback/project artifacts; applying a
2781
+ command means changed, not verified. Consolidate duplicates without deleting their source
2782
+ IDs. Conflicting “calmer” and “more urgent” instructions need the authorized decision owner's
2783
+ recorded choice, even if they touch different files. Offer concrete cheap alternatives for
2784
+ “more premium” inside the approved scope before escalating taste.
2785
+
2786
+ Map changed copy/performance/shot/pause to affected picture, timings, captions, music/SFX,
2787
+ approvals and derivatives; preserve unrelated approved work. Rewatch the full candidate and
2788
+ verify the original effect before resolving a note. Accepted final, candidate and original
2789
+ note source remain distinct across selection and rollback. Keep editable script/timeline,
2790
+ source assets/takes, stems, captions, package versions and prior decisions in the existing
2791
+ production manifest/history; retrieve applicable rejections/pronunciation/delivery rules
2792
+ before a related brief. Project taste is not a standing brand rule. Record audience results
2793
+ only with actual variant/audience/placement/objective/window; otherwise unavailable.
2794
+
2795
+ - “Fix this”: use the render/project context attached to the message (or named version, else
2796
+ the final one). Save patch.fix { of_render_id, changes, total_credits }. Never replace a supplied
2797
+ watched render with the final. After explicit approval save patch.approve { user_quote,
2798
+ total_credits, scope:"fix" }, then video_render_run { kind:"partial", fix_of_render_id }.
2799
+ Re-make only changed scenes, keep earlier versions and charge no second base fee.
2800
+ - “Make 3 more like this”: video_project_upsert { brand_id, name, remix_of_project_id,
2801
+ remix_of_render_id, vary, concepts:3 }. Preserve a provided watched render; omit only when none is named.
2802
+ - “Test 1 of each”: approve only:"test"; “Make the other N”: only:"rest". “Stop the rest”:
2803
+ job_cancel { job_id:<batch_id> }. Finished videos stay saved.
2804
+ - Share for review uses patch.share_for_review { subject }; return the real link.
1443
2805
 
1444
2806
  ## Rules
1445
2807
 
1446
- - **MCP + ffmpeg + Playwright required** — run \`gooseworks doctor\` in Phase 0; stop with the
1447
- exact fix it prints if anything is ✗.
1448
- - **Assemble the whole review set first**, mirror it with \`update_ad_project_script\`, and get the
1449
- user's approval **in the app's review panel** (the "Approve & render" button) BEFORE the expensive
1450
- render — never ask for a thumbs-up in this chat (review-once, in-app).
2808
+ - **Canonical MCP tools first** (\`video_project_read\`, \`video_project_upsert\`, \`catalog_fetch\`,
2809
+ \`media_upload\` + \`media_confirm\`, the \`video_render_run\` render-row actions, \`account_whoami\`);
2810
+ legacy names only when the client lacks the canonical tool.
2811
+ - **A missing key is never a blocker.** Paid media goes through the proxy: a one-off image or clip
2812
+ is \`data_post_provider { provider: "fal", … }\` + \`job_get\` (see "Paid media over the MCP");
2813
+ never ask for FAL_KEY, never use \`photos_generate\` for anything but a physical product.
2814
+ - **The CLI and credentials.json are optional.** In a GooseWorks sandbox (\`GW_MEDIA_PROXY_TOKEN\`
2815
+ set) use the env proxies and \`catalog_fetch\`; never call a provider with a raw key.
2816
+ - **No Chromium in a sandbox** — a browser-rendered format stops there, before any spend, and says so.
2817
+ - **Toolchain before spend** — common checks use \`gooseworks doctor --no-browser\`; every selected
2818
+ browser renderer must also pass \`--renderer-script\` or its equivalent exact-runtime launch
2819
+ check in Step 2. Stop with the folder-specific fix if anything is missing.
2820
+ - **Assemble the whole review set first**, mirror it with \`video_project_upsert patch.script\`, and
2821
+ get ONE approval in this chat, recorded with \`patch.approve\`, BEFORE the expensive render
2822
+ (review-once). Never send the user to the app to approve; the app is only for reviewing.
1451
2823
  - **Show the REAL cheap pieces; PROMPT only the expensive render.** Generate the FREE + CHEAP-paid
1452
2824
  pieces (≤ ~100 credits — stills, creator frame, end card, short VO/music) and mirror the real
1453
2825
  assets; put ONLY the expensive video take/render in the panel as its exact prompt. That prompt
1454
2826
  must be in the panel before you ask to approve, so a single "go" runs the render + any remaining
1455
2827
  paid work (→ QC → publish) with no re-pausing.
2828
+ - **Operator may be absent (sandbox):** decide mechanical choices yourself; ask in chat only for a
2829
+ true taste call.
1456
2830
  - **Costs in CREDITS, never dollars.** credits ≈ round-up(provider-$ × 120) per generation + a flat
1457
2831
  200-credit base per video; never show a "$…" figure to the user.
1458
2832
  - **Never assemble the full video before approval.** The review shows the
@@ -1460,19 +2834,27 @@ path. (\`fal-storage-proxy\` may 404 depending on the install; don't block on it
1460
2834
  stitched/composited cut; do not add a "full cascade" / finished-video clip
1461
2835
  as a review ingredient (GOOSE-2542). The assembled video is produced only in
1462
2836
  Step 4.
1463
- - **submit_render only after the master is rendered** (Step 4.2), never on a guess; \`output_url\` =
1464
- the durable render-file URL, never a CDN URL.
1465
- - **Always pass \`project_id\` on media-proxy calls** (fal / ElevenLabs) so the credits attribute
1466
- to this ad project — that's what lets the user see per-project spend in the app.
2837
+ - **Open the render row at the start of Step 4** (4.1, after the approval) and keep its stage /
2838
+ progress current; mark it \`complete\` only after the master passes QC. \`output_url\` = the
2839
+ durable render-file URL, never a CDN URL.
2840
+ - **Always export \`GW_PROJECT_ID\`** (and pass \`project_id\` on hand-rolled proxy calls) so the
2841
+ credits attribute to this ad project.
1467
2842
  - **Verify a real, non-empty MP4** (watch it) before marking the render complete.
1468
2843
  - **Reuse the brand** when its research is complete; never re-research.
2844
+ - **Brand rules first:** load kit + products + learnings (Step 1), write \`working/brand-rules.json\`
2845
+ (Step 1.7), and follow it: pronunciations in every voiceover, nothing from \`never_say\`, product
2846
+ facts only from the product rows, the kit's logo FILE (never a generated or favicon logo), the
2847
+ brand font, this product's own images.
2848
+ - **Save brand corrections** from chat with \`brand_update patch.facts\` in the same turn, and say so.
2849
+ - **Finished-ad gate on every master** (\`review-finished-ad\` + the sheet); at most 2 repair
2850
+ rounds, then publish as \`blocked\` with the report and warn the user — never pass off a failing
2851
+ video as finished.
1469
2852
  - On a hard error (auth/quota/model/timeout) set the render \`failed\` with a short
1470
- \`error_message\` and stop — don't ship the source unchanged. **Also \`log\` it** (\`gooseworks log\`
1471
- / \`log_cli_event\`, \`--event-type api_failure|error\`) so we can see + fix it (see "Report problems").
1472
- - **Report blockers/bugs/confusing instructions via telemetry** (\`gooseworks log\` or the
1473
- \`log_cli_event\` MCP tool) — not just to the user. Set \`GW_RUN_ID\` once so events group.
1474
- - Always end a successful run with \`app_url\` + \`brand_url\`, verbatim.
1475
- `;
2853
+ \`error_message\` (\`${exports.RENDER_ROW_TOOL} { …, ${exports.RENDER_UPDATE_KEY}: { render_id, status: "failed", error_message } }\`) and stop — don't ship the source unchanged. **Also log
2854
+ it** (see "Report problems") so we can see + fix it.
2855
+ - Finish according to card.display_hint: widget hosts get one short line without duplicate links;
2856
+ text hosts get card.text_summary and the returned delivery links verbatim.
2857
+ ${STORED_FOOTAGE_GUIDANCE}`;
1476
2858
  }
1477
2859
  /**
1478
2860
  * Returns the goose-product-photos entry SKILL.md content.
@@ -1483,6 +2865,10 @@ path. (\`fal-storage-proxy\` may 404 depending on the install; don't block on it
1483
2865
  * never regenerated it and `install` / `update` / login-refresh never wrote or
1484
2866
  * refreshed it on a user's machine. Moving the body here makes the registry the
1485
2867
  * one source: one command emits all four entry skills.
2868
+ *
2869
+ * QA-26 / VWR14: chat apps fetch it with `catalog_fetch`, so it names only the
2870
+ * tools the connector lists (`photos_generate` / `photos_read` / `photos_update`,
2871
+ * `brand_read`, `brand_update`, `job_get`).
1486
2872
  */
1487
2873
  function getGooseProductPhotosSkillContent() {
1488
2874
  return `---
@@ -1498,7 +2884,7 @@ description: >
1498
2884
  references a product to photograph. Unlike goose-ads (ad creative) this produces clean PRODUCT
1499
2885
  photos that can then feed the ad workflow.
1500
2886
  category: ads
1501
- version: 0.2.0
2887
+ version: 0.3.0
1502
2888
  author: GooseWorks
1503
2889
  tags: [gooseworks, ads, product-photos, photoshoot, product, ecommerce, studio, lifestyle, on-model]
1504
2890
  ---
@@ -1506,26 +2892,31 @@ tags: [gooseworks, ads, product-photos, photoshoot, product, ecommerce, studio,
1506
2892
  # GooseWorks Product Photos — branded product photography
1507
2893
 
1508
2894
  The GooseWorks Product Photos skill. You **pick a brand + product and submit one generation**;
1509
- the **backend** runs the whole pipeline (compose the shot prompt → generate on \`gpt_image_2\` →
1510
- judge for product fidelity → auto-retry a few times for free) and stores the results. You do NOT
2895
+ the **backend** runs the whole pipeline (compose the shot prompt → generate → judge for product
2896
+ fidelity → auto-retry a few times for free) and stores the results. You do NOT
1511
2897
  generate images, call a model, or manage files — this is the exact same workflow the Product
1512
2898
  Photos studio uses, so the skill and the app can never drift. The point is to **enrich a brand's
1513
2899
  usable product imagery** — approved photos join the brand kit and can then feed the ad workflow
1514
2900
  (\`goose-ads\`).
1515
2901
 
1516
- ## Prerequisite — the GooseWorks MCP server is REQUIRED
2902
+ It shoots a **physical catalog product** (apparel, beauty, consumer goods). A software
2903
+ screenshot or an app mockup is not a product photo: that is an image edit, not this skill.
2904
+
2905
+ ${CUSTOMER_TALK}
1517
2906
 
1518
- Everything goes through the \`mcp__gooseworks__*\` tools. If they are not available, **stop and
1519
- tell the user to run \`gooseworks install --claude --mcp\`** (and restart Claude Code). There is no
1520
- HTTP/file fallback.
2907
+ ${CLI_TOOL_BRIDGE}
2908
+
2909
+ ${connectorPrerequisite('photos_generate')}
1521
2910
 
1522
2911
  ## Start from the brand context — don't re-ask what it already answers
1523
2912
 
1524
2913
  If the \`gooseworks\` router handed you brand context, USE IT. If you were invoked directly, call
1525
- \`brand_get_context\` yourself first. It answers most of the setup questions below, so **do not ask
1526
- the user for them**:
2914
+ \`brand_read { brand_id, sections: ["summary","kit","products","learnings"] }\` yourself first, then
2915
+ \`knowledge_search { brand_id, query: "<the shoot, in the user's words>" }\` for saved rules and past
2916
+ feedback (an archived photo's reason is saved as a \`dont\` rule). They answer most of the setup
2917
+ questions below, so **do not ask the user for them**:
1527
2918
 
1528
- - **Which product?** — the context's \`products[]\` are the real catalog entries. Offer them; never
2919
+ - **Which product?** — the context's \`products\` are the real catalog entries. Offer them; never
1529
2920
  invent a product or ask the user to describe one you can already see.
1530
2921
  - **What does it look like / what is it made of?** — grounded in the product's stored images and
1531
2922
  description. Never guess a material, colorway, or silhouette.
@@ -1537,83 +2928,98 @@ the user for them**:
1537
2928
  Ask only for the genuinely open choices: the shot \`category\`, how many photos, quality, and
1538
2929
  whether a human model is wanted (which needs explicit consent — see the rules).
1539
2930
 
1540
- ## Identity & credits
2931
+ ## Credits — state the total, then get a yes
1541
2932
 
1542
- - One agent-scoped token authenticates the tools; they resolve your org automatically. Never
1543
- print the token. (You may pass an optional \`target\` to operate on a specific agent/org, exactly
1544
- as the other GooseWorks tools; omit it to use your pinned scope.)
1545
- - **Credits are handled by the backend.** \`generate_product_photos\` reserves the estimated cost up
1546
- front and bills only the photos that pass the judge — **automatic retries are free**, and a photo
1547
- the judge can't get right (\`flagged\`) is shown but **never billed**. Call
1548
- \`estimate_product_photos\` first to quote the cost; \`get_ad_credits\` shows the balance.
2933
+ - One token authenticates the tools and resolves your org; never print it. Omit \`target\`.
2934
+ - **Quote first.** Call \`photos_generate\` with the exact arguments you will submit plus
2935
+ \`dry_run: true\`. It reserves nothing and returns \`creditsPerOutput\` and \`totalCredits\`. Pass
2936
+ \`count\` and \`quality\` explicitly in both calls so the quote matches the run.
2937
+ - **Nothing paid runs without the user's explicit yes in this chat, given after you state that
2938
+ credit total.** Then submit the same call without \`dry_run\`.
2939
+ - The submit reserves the quoted credits and bills only the photos that pass the judge:
2940
+ **automatic retries are free**, and a photo the judge can't get right (\`flagged\`) is shown but
2941
+ **never billed**. The balance is \`credits.available_credits\` from \`account_whoami\`. If the
2942
+ wallet is short, say so plainly with the total and the balance, and stop.
1549
2943
 
1550
2944
  ## The tools
1551
2945
 
1552
2946
  **Pick the brand + product**
1553
- - \`list_ad_brands\` — the user's ad brands (get a \`brand_id\`; also carries \`slug\`).
1554
- - \`list_brand_products { brand_id, search?, page?, page_size? }\` — the brand's imported products.
1555
- Pick a \`product_id\` to shoot. \`search\` matches name / type / variant / SKU.
1556
- - \`import_product { brand_id, kind, url, product_name? }\` — import a product if it isn't in the
1557
- catalog yet. \`kind\` is \`product_url\` (a single product page), \`shopify_store\` (a store URL →
1558
- imports the catalog), or \`image_url\` (a direct image; requires \`product_name\`). Returns an import
1559
- row with an \`id\`; if its \`status\` isn't \`complete\`, poll \`get_product_import\` until it is, then
1560
- \`list_brand_products\` to find the new product. (File uploads aren't available over MCP — use a URL.)
1561
- - \`get_product_import { import_id }\` — poll an import until \`status\` is \`complete\` or \`failed\`.
2947
+ - \`brand_read\` with no \`brand_id\` — the user's brands (each row's \`id\` is the \`brand_id\`).
2948
+ - \`brand_read { brand_id, sections: ["products"], products_query: "<name>" }\` — the brand's
2949
+ imported products in \`products.items\`; a product's \`id\` is the \`product_id\` to shoot.
2950
+ \`products_query\` matches name / type / variant / SKU / description.
2951
+ - Import a product that isn't in the catalog yet (free):
2952
+ \`brand_update { brand_id, patch: { products: [{ import_url, import_kind, name? }] } }\`.
2953
+ \`import_kind\` is \`product_url\` (a single product page), \`shopify_store\` (a store URL → imports
2954
+ the catalog), or \`image_url\` (a direct image; also needs \`name\`). It returns \`jobs[]\`: poll
2955
+ \`job_get { job_id, kind: "product_import" }\` until it finishes, then read the products again.
2956
+ To add a photo the user attached to an existing product:
2957
+ \`media_upload { brand_id, scope: "product", scope_id: <product_id>, kind: "reference", source: { type: "bytes", filename, content_base64 } }\`.
1562
2958
 
1563
2959
  **Generate**
1564
- - \`estimate_product_photos { count, quality? }\` — cost preview (per-photo + total credits). \`count\`
1565
- is 1, 2, 4, or 8; \`quality\` is \`low\` | \`medium\` | \`high\` (default \`medium\`). Reserves nothing.
1566
- - \`generate_product_photos { brand_id, product_id, variant_id?, category, controls?, prompt?,
1567
- count?, quality?, reference_image_urls?, attestation_accepted? }\` — **the one call that makes
1568
- photos.** \`category\` is \`apparel\` | \`beauty\` | \`cpg\` (seeds sensible scene/framing defaults).
1569
- Omit \`controls\` to use the category preset; pass \`prompt\` as free-text steering **added on top of**
1570
- the settings (it doesn't replace them). Returns a generation with an \`id\` **immediately** — poll
1571
- \`get_product_photo_generation\` until done, then read each \`outputs[].final_image_url\`.
2960
+ - \`photos_generate { brand_id, product_id, variant_id?, category, controls?, prompt?, count,
2961
+ quality, reference_image_urls?, attestation_accepted?, dry_run? }\` — **the one call that makes
2962
+ photos** (and, with \`dry_run: true\`, its free quote). \`category\` is \`apparel\` | \`beauty\` |
2963
+ \`cpg\` (seeds sensible scene/framing defaults). \`count\` is 1, 2, 4, or 8; \`quality\` is
2964
+ \`low\` | \`medium\` | \`high\`. Omit \`controls\` to use the category preset; pass \`prompt\` as
2965
+ free-text steering **added on top of** the settings (it doesn't replace them). Returns the
2966
+ generation with its \`id\` **immediately**.
1572
2967
  **If you request a human model** (\`controls.model.presence\` is not \`none\`) you MUST pass
1573
2968
  \`attestation_accepted: true\` to confirm the user has the rights for model imagery.
1574
- - \`get_product_photo_generation { generation_id }\` — poll until \`status\` is \`complete\`,
1575
- \`partial_failure\`, or \`failed\`. Each \`outputs[]\` entry has its own \`status\` and, once ready, a
2969
+ - \`photos_read { brand_id, generation_id }\` — poll until \`status\` is \`complete\`,
2970
+ \`partial_failure\`, or \`failed\` (\`job_get { job_id: <generation id>, kind: "photo_generation" }\`
2971
+ works too). Each \`outputs[]\` entry has its own \`id\`, \`status\` and, once ready, a
1576
2972
  \`final_image_url\`. A \`flagged\` output is the best attempt but wasn't billed.
1577
2973
 
1578
2974
  **Use the results**
1579
- - \`list_product_photos { brand_id, archived? }\` — the brand's generated photos (\`archived: false\`
1580
- = active, \`true\` = archived).
1581
- - \`approve_product_photo { output_id }\` — approve a photo: links it to the product and makes it
1582
- available in the **brand kit**, so \`goose-ads\` can use it. **Photos are not used anywhere until
1583
- approved.**
1584
- - \`archive_product_photo { output_id, reason? }\` — archive a photo; archived photos are **excluded**
1585
- from ad generation.
2975
+ - \`photos_read { brand_id, archived?, product_id?, status? }\` — the brand's generated photos
2976
+ (\`archived: false\` = active, \`true\` = archived).
2977
+ - \`photos_update { brand_id, output_id, action: "approve" }\` — approve a photo: links it to the
2978
+ product and makes it available in the **brand kit**, so \`goose-ads\` can use it. **Photos are
2979
+ not used anywhere until approved.**
2980
+ - \`photos_update { brand_id, output_id, action: "archive", reason? }\` — archive a photo; archived
2981
+ photos are **excluded** from ad generation, and the reason is saved as a \`dont\` rule.
1586
2982
 
1587
2983
  ## Workflow — shoot a product
1588
2984
 
1589
- 1. **Load the brand context** (\`brand_get_context\`, or reuse what the router passed you) and
1590
- **resolve the brand + product.** \`list_ad_brands\` → \`brand_id\`. \`list_brand_products\` → pick a
1591
- \`product_id\` from the catalog you already know about. If the product genuinely isn't there,
1592
- \`import_product\` (poll \`get_product_import\`).
1593
- 2. **Quote the cost.** \`estimate_product_photos { count, quality }\` → tell the user credits.
1594
- 3. **Generate.** \`generate_product_photos { brand_id, product_id, category, count, quality, prompt? }\`.
1595
- Build \`prompt\` from the brand's voice/positioning you already have — don't interview the user for it.
1596
- Returns a generation \`id\` right away.
1597
- 4. **Poll.** \`get_product_photo_generation { generation_id }\` until terminal; hand back each
2985
+ 1. **Load the brand context** (\`brand_read\` + \`knowledge_search\`, or reuse what the router passed
2986
+ you) and **resolve the brand + product.** Pick a \`product_id\` from the catalog you already
2987
+ know about. If the product genuinely isn't there, import it and poll the import.
2988
+ 2. **Quote the cost.** \`photos_generate\` with \`dry_run: true\` and the exact \`brand_id\`,
2989
+ \`product_id\`, \`category\`, \`count\` and \`quality\` → tell the user the credit total and wait
2990
+ for their explicit yes.
2991
+ 3. **Generate.** The same \`photos_generate\` call without \`dry_run\` (add \`prompt\` built from the
2992
+ brand's voice/positioning you already have — don't interview the user for it). Returns a
2993
+ generation \`id\` right away.
2994
+ 4. **Poll.** \`photos_read { brand_id, generation_id }\` every ~20-30s until terminal; show each
1598
2995
  \`final_image_url\`.
1599
- 5. **Approve the keepers.** Show the results and let the user pick; \`approve_product_photo\` the ones
1600
- they'd publish (that's what puts them in the brand kit for ads), \`archive_product_photo\` the rest.
2996
+ 5. **Approve the keepers.** Show the results and let the user pick; \`photos_update\` with
2997
+ \`action: "approve"\` the ones they'd publish (that's what puts them in the brand kit for ads),
2998
+ \`action: "archive"\` the rest.
1601
2999
 
1602
3000
  ## Rules
1603
3001
 
3002
+ - **Connector tools only** — a missing tool means the GooseWorks connection is stale: ask the
3003
+ user to reconnect or refresh GooseWorks. Never send a chat-app user to a terminal or a CLI
3004
+ install.
1604
3005
  - **Never invent product facts.** The backend grounds the shot on the product's real images; don't
1605
3006
  describe a product you can't see.
1606
3007
  - **Use the brand context instead of interviewing the user.** Product, audience, voice, positioning,
1607
- logo/colors/fonts all come from \`brand_get_context\` / the brand kit. Ask only for the shot
3008
+ logo/colors/fonts all come from \`brand_read\` / the brand kit. Ask only for the shot
1608
3009
  category, count, quality, and model consent.
1609
- - **Ask before spending.** Quote the estimate and confirm \`count\` / \`quality\` before
1610
- \`generate_product_photos\` — it reserves credits.
3010
+ - **Ask before spending.** State the dry-run credit total and get the user's explicit yes before
3011
+ the real \`photos_generate\` — it reserves credits.
1611
3012
  - **Poll, don't re-submit.** A generation that's still \`running\` is not stuck; re-submitting
1612
- double-bills. Only a \`failed\` generation should be retried.
3013
+ double-bills. A \`failed\` generation may still hold \`flagged\` photos (shown, never billed):
3014
+ show those first, and run again only after a new quote and the user's yes.
1613
3015
  - **Model imagery needs consent.** Only set a human model when the user asks, and pass
1614
3016
  \`attestation_accepted: true\`.
1615
3017
  - **Approval is the hand-off to ads.** Remind the user that only **approved** photos reach the brand
1616
3018
  kit / ad workflow; archived ones never do.
1617
3019
  `;
1618
3020
  }
3021
+ /** Thin GooseWorks connection to the catalog-published production harness. */
3022
+ function getMakeCustomVideoSkillContent() {
3023
+ return custom_video_skill_1.CUSTOM_VIDEO_ADAPTER_CONTENT.replace("\n# Agent version\n", `\n# Agent version\n\n${CUSTOMER_TALK}\n\n${CLI_TOOL_BRIDGE}\n\n${videoEntryPreparation('make-custom-video')}\n\n${ENVIRONMENT_IDENTITY}\n\n${ASSET_READINESS}\n`) + STORED_FOOTAGE_GUIDANCE;
3024
+ }
1619
3025
  //# sourceMappingURL=master-skill.js.map