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
@@ -2,26 +2,277 @@
2
2
  name: goose-video-local
3
3
  slug: goose-video-local
4
4
  description: >
5
- Render an EXISTING GooseWorks video ad project or video batch on this machine (Playwright +
6
- ffmpeg + GooseWorks media proxies) and save the finished MP4 back to the project over MCP. Use
7
- when the app's "copy for Claude" command names goose-video-local, for "make the video for
8
- project <id>" / "for video batch <id>", or to remix a video ad template locally. Needs a machine
9
- with network egress and ffmpeg (local Claude Code or the desktop app), not a hosted connector.
10
- To order a NEW video ad in chat, use goose-video instead.
5
+ Render an EXISTING GooseWorks video ad project or video batch with ffmpeg (plus Playwright for
6
+ phone-mockup formats) and the GooseWorks media proxies, then save the finished MP4 back to the
7
+ project over MCP. Runs on the user's own machine (Claude Code / desktop app, with or without the
8
+ gooseworks CLI) OR inside a GooseWorks workspace sandbox (canonical MCP tools + Bash, no CLI).
9
+ Use for a client-side format project (created by goose-video), a template-remix project or a
10
+ video batch. A copy-for-Claude command or project id must first be checked with
11
+ 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,
12
+ use goose-video first.
11
13
  category: ads
12
- version: 0.3.0
14
+ version: 0.7.0
13
15
  author: GooseWorks
14
- tags: [gooseworks, ads, video, remix, imessage, local-render, byoa]
16
+ tags: [gooseworks, ads, video, remix, imessage, podcast, ugc, local-render, sandbox, byoa]
15
17
  ---
16
18
 
17
19
  # GooseWorks Video Ads — local remix runtime
18
20
 
19
- You produce **video** ad creative on the user's OWN machine and sync the result back to the
20
- GooseWorks app over MCP. This document is the **runtime contract** (auth, credits, the media
21
- proxies, data I/O, the review gate). A separate **recipe skill** — fetched per format — tells you
22
- *what to make* (the pieces, prompts, models, order of assembly).
21
+ ## How to talk to the customer (applies to every message you send them)
23
22
 
24
- **Division of authority — read both, but when they disagree THIS doc wins on the environment AND the
23
+ The customer is a marketer or founder, not an engineer. Everything in this skill about tools,
24
+ fields, files, polling, models and pipeline steps is for YOU. Never pass it on to them.
25
+
26
+ - **Say what they get and what they need to decide, never how it's made.**
27
+ Bad: "Checking the media-proxy helpers for voiceover timestamps and lipsync." / "Generated with
28
+ gpt-image-2 at high quality, mouth closed, passes QC."
29
+ Good: "Recording the voices now." / "Here are your two hosts."
30
+ - **Never mention** tool, file or field names, ids, JSON, commands, scripts, installed software,
31
+ uploads, model or vendor names, timings in seconds or frames, retries, polling, internal statuses
32
+ or your own quality checks. Fix what you can quietly. If they ask how something works, explain it
33
+ simply.
34
+ - **Update them only at milestones they care about**: choices confirmed, script and voices done,
35
+ visuals done, ready to review, finished. One short line each, then stay quiet until the next
36
+ milestone. No update for setup, downloads, uploads or saving.
37
+ - **Money is in credits, with a number**: "the full video uses about N credits". Never "the cheap
38
+ pieces" or "the expensive render".
39
+ - **Problems:** say what it means for them and what they can do, in one or two sentences. No error
40
+ codes or stack traces. Only raise a problem that changes something for them.
41
+ - **Use their words**: script, voice, hook, scene, host, ending, logo, the product shot.
42
+ - **No filler**: no "Great question!", "Certainly!", "I hope this helps!", no restating what they
43
+ just said at length.
44
+
45
+ Example. Instead of a dozen lines about render tools, scripts, uploads and portraits, send:
46
+ "Got it: warm tone, home podcast studio, Brielle as the skeptic and Mark as the believer." then
47
+ "Script and voices are done (about 34 seconds)." then "Ready for you to review: the script, voices,
48
+ both hosts and the ending. You can also review your recipe ingredients in the app: <link>. Say go
49
+ here and I'll make the full video (about N credits)."
50
+
51
+ ## Use current instructions for new work
52
+
53
+ Before a new task in a terminal host, run `gooseworks skills status` once. It compares the
54
+ installed entry files, running CLI and published npm release. An older CLI needs a package
55
+ upgrade before `gooseworks update`; update alone only uses that CLI's bundled instructions.
56
+ Preserve local edits or unknown install provenance. Review/back up before explicitly replacing
57
+ modified files; never quietly reinstall over them. If the release check is unavailable, report
58
+ that freshness is unknown rather than claiming the local copy is latest.
59
+
60
+ For a new recipe run, fetch its package from the connected catalog. Retain the returned
61
+ `version` / `contentHash` and every dependency's hash with the saved package. When reusing a
62
+ saved fetch JSON, `gooseworks fetch <slug> --saved-package <file>` returns the current package
63
+ and a hash comparison without changing that file. With MCP, if the advertised `catalog_fetch`
64
+ schema accepts them, send `saved_content_hash` and `saved_dependency_hashes`; otherwise fetch
65
+ normally and compare the returned hashes yourself. Missing hashes mean unknown, not current.
66
+ The server cannot inspect a client's saved files; hash metadata does not certify later edits.
67
+ Fetch current packages into a new run directory and report stale saved instructions. Preserve
68
+ an existing approved run's recorded package; changing that harness requires a reviewed change
69
+ and approval before spending. Hosted installed snapshots use the existing Skills Update action.
70
+ Skill content and a host's cached MCP tool schemas are separate: refreshing one does not refresh
71
+ the other. Check the actual advertised tools before using new fields.
72
+
73
+ ## No GooseWorks tools in this session? Call them through the CLI
74
+
75
+ The GooseWorks tools named here (for example `account_whoami`, `brand_onboarding`,
76
+ `brand_read`) normally come from the GooseWorks connector. If they are not registered in this
77
+ session but you have a shell with the `gooseworks` CLI signed in (a cloud sandbox such as
78
+ ChatGPT agent, Meta AI or Grok), call the same tool from the shell:
79
+
80
+ ```bash
81
+ npx gooseworks tool <name> '<arguments as a JSON object>'
82
+ ```
83
+
84
+ - Run `npx gooseworks tool --list` once first. It prints the server's rules for using the tools
85
+ and every tool name; `npx gooseworks tool <name> --schema` shows one tool's arguments.
86
+ - The result prints as JSON. When a tool answers with a one-line summary, the fields you act on
87
+ (such as `next_step` and ids) follow under "Data:". It exits 1 when the tool reports an error.
88
+ - Nothing renders as a widget here, so tell the user the result in plain words.
89
+ - The same rules apply as over the connector: state the credit total and get the user's yes
90
+ before paid work.
91
+ - If it says you are not logged in, run `npx gooseworks login --device --no-wait`, show the
92
+ user the link and code it prints, and run `npx gooseworks login --device` once they approve.
93
+
94
+ When the tools are registered in this session, call them directly instead.
95
+
96
+ ## Prepare the video workflow and brand before creative work
97
+
98
+ **Installed entry files are bootstrap instructions.** For new work started from an installed
99
+ file or an old chat, fetch `catalog_fetch { type: "skill", slug: "goose-video-local" }` on the selected
100
+ connection and read its returned content and dependencies before continuing. Use the already
101
+ fetched body when this entry came from that connection in this run; do not recursively fetch
102
+ the same entry. A CLI freshness warning does not block using the current connected package.
103
+ Keep the fetched package and hashes in a new run folder; do not replace edited installed files.
104
+ If current instructions cannot be loaded, resolve the connection before format suggestions,
105
+ script writing or production. Do not continue from an old installed workflow or session notes.
106
+
107
+ **Follow the connector's full-guide requirement when available.** Fetch
108
+ `catalog_fetch { type: "skill", slug: "gooseworks-guide" }` when the connector requires it.
109
+ The only older-server exception is below; it never removes brand preparation or approvals.
110
+
111
+ | Returned guide/workflow state on the selected connection | Required action |
112
+ | --- | --- |
113
+ | 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. |
114
+ | 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. |
115
+
116
+ A missing custom entry or required custom tool/schema remains unavailable: never substitute
117
+ a template or an import to bypass it. Never hide other fetch errors or continue from cached
118
+ instructions merely because the guide was not found.
119
+
120
+ **For an existing project or batch, read `video_project_read` first.** Determine its actual
121
+ route and saved review state before fetching a recipe or doing local work. Resume an approved
122
+ run with its recorded packages, brand inputs, script and ingredients; do not silently replace
123
+ them with today's release or brand rules. Follow returned preparation requirements and use
124
+ the existing affected review/approval flow for an intentional change. Reads, free drafts,
125
+ imports and existing-job retrieval do not grant permission for new paid production.
126
+
127
+ **For every new plan, resolve the brand and call**
128
+ `brand_read { brand_id, sections: ["summary", "kit", "products", "learnings"] }` **before**
129
+ suggesting formats, mining video angles, writing any script, creating a production plan or
130
+ handing the work to another agent. Use `brand_get_context` with those same four sections only
131
+ when advertised. Reuse a complete read from this run on the same connection and brand.
132
+ Onboarding completion, a summary-only response and earlier session notes do not supply the
133
+ kit, selected product facts or saved rules. If the full read is unavailable, resolve it before
134
+ creative work. Use known kit facts while research is incomplete; do not restart research
135
+ solely because its status is pending. Never invent a product fact or accept a campaign to
136
+ bypass preparation. Then search the Brand Brain (next section) before suggesting formats,
137
+ choosing an angle or asking the user for a brand fact.
138
+
139
+ Carry that context through custom/template/idea routing and delegation. Before writing, make
140
+ the workflow's brand-rules file from those sources: pronunciations, required spoken copy,
141
+ prohibited claims, real logo, font, palette and the selected product's supported facts.
142
+ Entries prefixed `Video preference:` and visual/pacing instructions are creative direction,
143
+ not spoken lines. Use the kit's audience, offer and voice as defaults without an interview.
144
+ The rules file mirrors context; it is not proof of a read or permission to spend. Keep the
145
+ route's existing script, ingredient and budget approvals, including its allowed previews.
146
+
147
+ **Use the server's brand bundle when the selected API returns it.** Project create/read may
148
+ return top-level
149
+ `brand_context`: `{ version: 1, brand_id, digest, loaded_at, sections, brand, kit, products,
150
+ learnings }`. Its sections cover summary, kit, products and learnings.
151
+
152
+ | Returned brand/project response on the selected connection | Required action |
153
+ | --- | --- |
154
+ | 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. |
155
+ | 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. |
156
+
157
+ An incomplete/malformed bundle or missing brand section is a preparation failure, never the
158
+ older-API exception. Once this connection returns an authoritative bundle, use its binding
159
+ contract. `script_drafts.video_brand_context` is server-owned: do not author, replace or forge
160
+ it, and do not invent a read receipt or hash local brand facts into one. Keep the full bundle
161
+ out of client review payloads. If managed generation returns HTTP 409
162
+ `video_brand_context_required`, reload the project and bind the plan to its returned bundle
163
+ through the normal review flow before retrying. That refusal never permits the older-API
164
+ fallback, skipping sections or retrying paid calls blindly. Unchanged approved legacy resumes
165
+ retain their package, context and approvals; an upgrade alone needs no extra approval.
166
+
167
+ **To apply a deliberate brand correction to a new or revised plan**, make a fresh, unfiltered
168
+ `brand_read` of those four sections. When it returns the authoritative bundle, use its
169
+ returned `brand_context.digest` in the complete revised `script_drafts` through the existing
170
+ `video_project_upsert` `patch.script` save; the server verifies the current brand and replaces
171
+ its snapshot with the saved plan. On an older API with no bundle contract, apply the actual
172
+ four-section correction and save the revised draft through its existing flow without inventing
173
+ digest fields. If a connection that already returned a bundle cannot supply the fresh bundle,
174
+ resolve that failure before rebinding. Review and approve the affected script, ingredients and
175
+ budget through the normal flow. Never refresh an ongoing approved run automatically; its
176
+ recorded package and brand inputs stay pinned until an intentional change.
177
+
178
+ ## Search the Brand Brain, then propose — before any creative choice or question
179
+
180
+ The Brand Kit is a summary. The brand's saved knowledge (its Brain) holds what the Kit does not:
181
+ rules from past feedback, approved and rejected creatives, customer evidence, approved claims,
182
+ reports and documents. **After `brand_read`, and before you choose an angle, claim, hook,
183
+ product emphasis, format or source ad — and before you ask the user for any brand fact — call
184
+ `knowledge_search { brand_id, query }`** when it is registered. It is free and read-only: no
185
+ approval, no announcement to the customer and no questionnaire.
186
+
187
+ 1. **Search for this task, not the whole Brain.** Run one short query (under 500 characters) in
188
+ the user's own words plus the product (for example "ads for <product>: what worked, what to
189
+ avoid") and one with
190
+ `source_types: ["evidence", "claim", "learning", "creative", "document"]` for proof and past
191
+ creative results. Add a query only for a specific open question. Reuse results from this run.
192
+ 2. **Keep these states distinct** and record which one each query returned:
193
+
194
+ | Result | Means | Do |
195
+ | --- | --- | --- |
196
+ | `status: "ok"` with matches | Saved knowledge exists | Use it; keep each fact's citation in your working brief |
197
+ | `status: "empty"` | Nothing saved matches this query | Say "no saved evidence for <topic>", never "the brand has no proof" |
198
+ | `refresh_required` | The results shown are current; some changed sources were left out | Use them; search again shortly for anything missing |
199
+ | `building` | The index is not ready | Retry once shortly, then continue with the gap stated |
200
+ | 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 |
201
+ | An empty Kit field | Only that field is blank | Not a search result: still search before asking |
202
+
203
+ 3. **Let the findings change the plan.** A `dont`/`must` learning or a rejected creative rules
204
+ options out; an approved or well-rated past creative is a proven angle to lead with; a report
205
+ shows what worked. Claim-grade proof is a result with `approved_ad_claim: true` or the kit's
206
+ `approvedClaims` within their stated applicability; the kit's `proofPoints` are what the
207
+ backend allows an ad to state, used as written. Other Kit text, documents and performance
208
+ numbers are context, never public claims. Never invent an offer, price or result. Judge
209
+ relevance and skip results about another product or business.
210
+ 4. **Propose; don't interview.** Lead with one recommended direction (angle, product, source or
211
+ format) and a one-line reason naming what you found, with up to two alternatives. When several
212
+ directions fit (two audiences, products or campaigns), pick the one the evidence favours, such
213
+ as an active campaign or approved past creatives, and name the other as an alternative instead
214
+ of asking. Ask only for a decision the brand read and the search cannot settle, or for spend
215
+ approval. Never ask the user for a fact the Brain already answered.
216
+ Keep directions already chosen (the user's words, a project's creative brief, handed-off
217
+ defaults, approved plans): search only to fill empty fields and to apply saved rules. When a
218
+ returned card or picker presents the choice, your recommendation is its one acknowledgement
219
+ line and the saved plan's defaults, not a separate list.
220
+ 5. **Carry an evidence brief** into the routed skill, writer or plan: findings with citations,
221
+ the state of each query, what it ruled out and the open gaps. Tell the customer the findings
222
+ in plain words; the citations stay in the brief.
223
+
224
+ On an approved resume, keep the saved brief and evidence; search again only for a new decision.
225
+
226
+ ## Chat hosts and cards
227
+
228
+ **Who makes the video (one rule).** If you can run shell commands on the customer's own
229
+ computer, you make it there with `goose-video-local`: check the toolchain first and, if it
230
+ can't be fixed, hand it off. If you can't, hand it to the GooseWorks coworker with
231
+ `goose_run_task { brand_id, project_id, message: the customer's words }`; the one approval,
232
+ Stop and fixes stay in this chat. Inside the GooseWorks coworker's own sandbox you are the
233
+ coworker: make it yourself and never hand off. A hosted code sandbox is not the customer's
234
+ computer. Decide this yourself, never from the host's name; the server needs no flag for it.
235
+
236
+ First perform the mandatory route check below. Then, if you can't run shell commands on the
237
+ customer's computer, hand a verified template
238
+ project to `goose_run_task { brand_id, project_id, message }`, or a verified template batch to
239
+ `goose_run_task { brand_id, batch_id, message }` (never send both ids). Keep task_id and the
240
+ same project or batch. Generated custom children keep their separate make-custom-video flow.
241
+ Inside a coworker sandbox you are the renderer: never delegate recursively.
242
+ With card.display_hint:"widget", say at most one line and never duplicate its plan, total or
243
+ links. Otherwise print card.text_summary. Read again on customer input, not in a polling loop.
244
+ A free saved draft is not proof that planning started or a complete plan exists.
245
+
246
+ ## Mandatory route check before any local work or spend
247
+
248
+ For every existing `project_id` (including one supplied by the app's copy-for-Claude command),
249
+ call `video_project_read { brand_id, project_id }` **before** template lookup, toolchain setup,
250
+ media-proxy calls, or a review-set upload. For a batch, inspect each child project.
251
+
252
+ - **A server-rendered order** — the response has `creative_plan`,
253
+ `project.creative_spec_revision_id`, `order.creative_spec_revision_id`, a planning
254
+ `lifecycle`, or an `order` / `script_drafts.recipe` on a server format → **stop.**
255
+ Server video orders are paused, and there is **no vetted local node-execution API** for them:
256
+ do not rebuild one locally. Tell the customer this project was made for the server flow, which
257
+ is paused; offer to start the same ad on a client-side format (`goose-video`, fetched with
258
+ `catalog_fetch { type: "skill", slug: "goose-video" }`, older clients:
259
+ `fetch_skill("goose-video")`). An order already holding credits can be released with
260
+ `job_cancel`.
261
+ - **A client-side format or template remix** (a `source_sample_id` / `template_id` and none of
262
+ the above) → continue below.
263
+ - **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.
264
+ - **Unclear** → read again or ask; never guess and generate. A copy prompt that names this skill
265
+ is not proof of which kind the project is.
266
+
267
+ Continue below only for a verified client-side format or template remix.
268
+
269
+ For client-side formats and template remixes, you produce **video** ad creative wherever THIS agent runs and sync
270
+ the result back to the GooseWorks app over MCP. This document is the **runtime contract** (auth,
271
+ credits, the media proxies, data I/O, the review gate). A separate **recipe** — the template's
272
+ `recipe`, plus the capability skills it names — tells you *what to make* (the pieces, prompts,
273
+ models, order of assembly).
274
+
275
+ **Division of authority: read both, but when they disagree THIS doc wins on the environment AND the
25
276
  review/approval flow.** The recipe governs WHAT to make; this doc governs WHEN you pause, generate,
26
277
  and spend. In particular: a recipe may spell out a **multi-phase, multi-gate** flow — "generate the
27
278
  still [GATE] → approve → author the prompt [GATE] → approve → render [GATE] → approve", several
@@ -30,19 +281,176 @@ separate pauses. **Do NOT run it that way.** Collapse every one of those gates i
30
281
  and models; ignore its intermediate pauses. This is the exact contradiction that confused past runs
31
282
  (GOOSE-2542) — there is no ambiguity: review-once wins.
32
283
 
33
- You run inside the user's own Claude Code session (they pasted an instruction with a project
34
- id). The app NEVER runs you — it is the viewer + review surface; you are the renderer.
284
+ The app NEVER runs this skill by itself — it is the viewer + review surface; you are the renderer.
285
+
286
+ ## Keep the selected connection for the whole run
287
+
288
+ Call `account_whoami` on the connection that owns the brand/project before the first write.
289
+ Keep `environment.name` and the public `environment.api_origin` with this run. Respect the
290
+ user's selected production or staging connection. If multiple connections are available and
291
+ none was selected, resolve that once. Missing or unknown identity is uncertainty, not
292
+ permission to switch. Never infer the environment from credits, billing links or Node mode.
293
+
294
+ Reads, project creation, uploads, generation, polling and final updates all use that same
295
+ connection. If a production request fails, resume or report the failure on production;
296
+ never retry it on staging or recreate the project there. Before retrying a timed-out write,
297
+ read back the existing project or paid request on the selected connection.
298
+
299
+ Local/CLI proxy origin must match `environment.api_origin`. If it differs, use the MCP
300
+ relay on the selected connection (`GW_MEDIA_VIA=mcp`) before paid calls. Do not change
301
+ credentials or API origins to recover a failed write. An explicit user-requested move is
302
+ a separate operation, with the existing project and paid requests reconciled first.
303
+
304
+ ## Check assets for the selected format before spending
305
+
306
+ For a template, read its structured `asset_readiness` from
307
+ `video_catalog_list { kind: "formats", brand_id }`. `missing` names gaps;
308
+ `needs_review` means suitability is unverified, including older recipes without
309
+ structured requirements. `ready` describes assets only, not script or budget approval.
310
+ For a custom video, derive requirements from its actual approved scenes.
311
+
312
+ Inspect candidate files for the chosen product and format. A catalog photo can contain
313
+ multiple objects, other products or a person; its existence or approved status does not
314
+ make it a standalone image of the selected product. Check object count, framing, readable
315
+ print and real image bytes. A service conversation has no automatic packshot requirement.
316
+
317
+ Reuse a suitable approved image first. A free crop or cutout is a new file: keep the
318
+ original, inspect the result and include it in the normal ingredient review. When no
319
+ usable input exists, explain the gap before generation. Estimate any paid preparation
320
+ separately before spending. Record the selected asset and inspection in the project
321
+ review set; do not add a separate approval round.
322
+
323
+ ## Where am I running? (decide once, first)
324
+
325
+ With no shell on the customer's computer at all, you don't render here: hand the project off
326
+ (Chat hosts and cards above). Otherwise check in Bash, without printing any secret value:
327
+
328
+ ```bash
329
+ [ -n "$GW_MEDIA_PROXY_TOKEN" ] && echo sandbox || echo local
330
+ command -v gooseworks >/dev/null && echo cli || echo no-cli
331
+ ```
332
+
333
+ | Mode | How you know | Skills / atoms | Media-proxy auth |
334
+ |---|---|---|---|
335
+ | **GooseWorks sandbox** | `GW_MEDIA_PROXY_TOKEN` is set | `catalog_fetch { type: "skill" }` | env: `GW_MEDIA_PROXY_TOKEN` + `GW_*_PROXY_URL` |
336
+ | **Local, CLI installed** | `gooseworks` on PATH | `gooseworks fetch <slug>` or `catalog_fetch` | `~/.gooseworks/credentials.json` |
337
+ | **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) |
338
+
339
+ The `gooseworks` CLI and `~/.gooseworks/credentials.json` are **optional**. Everything this skill
340
+ needs from the app goes through the GooseWorks MCP tools below; the atoms' `media_proxy.py` reads
341
+ credentials.json when it exists and falls back to the `GW_MEDIA_PROXY_TOKEN` env otherwise.
342
+
343
+ ### Paid media over the MCP: no key, no CLI needed
344
+
345
+ Every paid generation goes through the GooseWorks media proxy and is billed to the project. **You
346
+ never need FAL_KEY, an ElevenLabs key or `fal_client`.** An atom, a recipe or an open-source
347
+ skill that lists `FAL_KEY` in its environment is describing a standalone setup; here the proxy
348
+ satisfies it. Never stop, and never ask anyone to set a key, because one is missing.
35
349
 
36
- ## CLI-free environments (cowork / headless)
350
+ **A one-off image or clip — call the MCP directly.** A frame placed in a laptop, a product cutout,
351
+ a creator still, a restyle, an animated shot, with any fal model (Nano Banana, GPT-image, Seedream,
352
+ Seedance, Kling). No atom script is needed:
37
353
 
38
- You may be running WITHOUT the `gooseworks` CLI binary (e.g. Anthropic cowork). The
39
- `mcp__gooseworks__*` tools work over the MCP connection regardless, so wherever this skill
40
- says to shell out, use the MCP equivalent:
354
+ 1. A local input (a frame pulled from a screen recording, a screenshot) must be a public URL first:
355
+ `media_upload { brand_id, scope: "video_project", scope_id: project_id, source: { type: "file" | "bytes", … } }`
356
+ (no `path`). For a file, PUT its bytes to `upload.url` with `upload.required_headers`,
357
+ then call `media_confirm { brand_id, media_id: media.id }`. Use the returned `media.url`.
358
+ 2. `data_post_provider { provider: "fal", path: <model id, e.g. "fal-ai/nano-banana/edit">, body: <model input>, project_id }`
359
+ returns `{ job_id: "fal:<request_id>" }`. Pass an `idempotency_key` so a retry isn't billed twice.
360
+ 3. Poll `job_get { job_id }` every few seconds until `complete`; the `*.fal.media` URLs are in
361
+ `result`. Download each and QC it (open the image) before using it.
41
362
 
42
- - `gooseworks fetch <slug>` → the **`fetch_skill`** MCP tool (returns the same content/scripts/
43
- files/dependencySkills). `gooseworks search <q>` → **`search_skills`**.
44
- - `gooseworks credits` → the **`get_ad_credits`** MCP tool.
45
- - `gooseworks doctor` → do the manual toolchain check in the preflight below.
363
+ For a set that must match (every laptop shot, every creator still), use ONE prompt and the same
364
+ model for all of them and change only the input image. Voice and music:
365
+ `data_post_provider { provider: "elevenlabs", path: "/v1/text-to-speech/{voice_id}" | "/v1/music", body, project_id }`
366
+ (synchronous; the audio lands in the project folder).
367
+
368
+ `photos_generate` is **not** a general image tool: it only photographs a physical catalog
369
+ product (apparel, beauty, CPG) and needs a `product_id`. A software screenshot or app mockup is a
370
+ fal image edit, above.
371
+
372
+ **Atom scripts — the MCP relay.** With neither `GW_MEDIA_PROXY_TOKEN` nor
373
+ `~/.gooseworks/credentials.json`, the atoms' `media_proxy.py` RELAYS each paid call through you
374
+ instead of calling the proxies over HTTP. Before running any atom,
375
+ `export GW_PROJECT_ID=<project_id> GW_BRAND_ID=<brand_id>` (every call is billed to that
376
+ project). When a script **exits with code 3** it wrote a request file under
377
+ `working/mcp-requests/`: make exactly that MCP call — fal:
378
+ `data_post_provider { provider: "fal", path, body, project_id }` then `job_get { job_id }` until
379
+ `complete`, saving `result.output`; ElevenLabs: `data_post_provider { provider: "elevenlabs", … }`,
380
+ saving the reply; a local file: `media_upload` with its bytes, saving `{"url": …}`. Write that JSON
381
+ to the request's `save_result_to` and **re-run the same command**; repeat until the script
382
+ finishes. Same server proxy and price as the CLI path. If the CLI is logged in to a DIFFERENT
383
+ environment than this MCP connector (prod vs staging), set `GW_MEDIA_VIA=mcp` so the spend lands
384
+ where the project lives.
385
+
386
+ ## MCP tools — canonical names (use these)
387
+
388
+ Use the canonical GooseWorks MCP tools. Legacy names are listed only as a fallback for an older
389
+ client that does not expose the canonical tool; never mix both for one step.
390
+
391
+ | Step | Canonical tool (use this) | Legacy fallback |
392
+ |---|---|---|
393
+ | 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` |
394
+ | Template recipe | `catalog_fetch { type: "template", slug: <source_sample_id> }` | `get_ad_template` |
395
+ | Capability skill (atom) + its scripts | `catalog_fetch { type: "skill", slug }` | `gooseworks fetch <slug>` / `fetch_skill` |
396
+ | Brand kit, products, rules | `brand_read { brand_id, sections: ["summary","kit","products","learnings"] }` | `brand_get_context` / `get_brand_kit` |
397
+ | 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 |
398
+ | Mirror the review set | `video_project_upsert { brand_id, project_id, patch: { script: { script_drafts, script } } }` | `update_ad_project_script` |
399
+ | Project assets | `video_project_upsert { …, patch: { assets: [...] } }` | `update_ad_project_asset` |
400
+ | Progress note | `video_project_upsert { …, patch: { message: { role: "agent", content } } }` | `append_project_message` |
401
+ | Batch status | `video_project_upsert { brand_id, batch_id, patch: { batch: { status } } }` | `update_ad_video_batch` |
402
+ | 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` |
403
+ | 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 |
404
+ | Open the render row | `video_render_run { brand_id, project_id, kind: "full" }` (no `dry_run`; returns `render_id`) | `submit_render { project_id, kind: "full" }` |
405
+ | Update the render row | `video_render_run { brand_id, project_id, render: { render_id, status, output_url?, thumbnail_url?, error_message?, quality_status?, quality_report? } }` | `update_render_status` |
406
+ | Pin the final render | `video_project_upsert { brand_id, project_id, patch: { final_render_id: render_id } }` | `set_final_render` |
407
+ | Credits / identity | `account_whoami` | `get_ad_credits` |
408
+
409
+ `list_accessible_scopes` and the `target: { type: "agent", agent_id }` dance are **not needed**:
410
+ `media_upload` with `scope: "video_project"` derives the right storage key (org-default Ads agent
411
+ + brand slug + project folder) on the server.
412
+
413
+ ## Running in a GooseWorks sandbox
414
+
415
+ You are an agent inside the user's GooseWorks workspace sandbox (Claude Code harness, Bash, the
416
+ GooseWorks MCP with canonical tools only). There is **no `gooseworks` CLI** and **no
417
+ `~/.gooseworks/credentials.json`**. The environment carries `GW_MEDIA_PROXY_TOKEN`, `GW_API_BASE`,
418
+ `GW_FAL_PROXY_URL`, `GW_FAL_STORAGE_PROXY_URL`, `GW_ELEVENLABS_PROXY_URL`, `GW_WHISPER_PROXY_URL`
419
+ and usually `GW_PROJECT_ID`.
420
+
421
+ - **Formats:** only formats that need **no Chromium** run here — podcast skits and UGC /
422
+ talking-head formats (ffmpeg + PIL assembly). If the template's recipe or an atom needs
423
+ Playwright/Chromium with no PIL fallback (iMessage / ChatGPT / Notes phone mockups,
424
+ hyperframes, HTML recorders), **stop before any spend** and say so plainly: "This format needs
425
+ a browser renderer that this workspace doesn't have. Run it in your own Claude Code with the
426
+ GooseWorks CLI (`goose-video-local`), or pick a podcast/UGC format." Do not half-render.
427
+ - **Toolchain:** check `ffmpeg -version` and `ffprobe -version`. Install Python deps only when an
428
+ atom needs them: `pip install --quiet pillow` when a render atom uses PIL (captions, end card),
429
+ `pip install --quiet requests` if `import requests` fails. Never `npx playwright install` here.
430
+ - **Atoms:** `catalog_fetch { type: "skill", slug }` returns `content`, `scripts`, `files` and
431
+ `dependencySkills`. Write `content` to `/tmp/gooseworks-scripts/<slug>/SKILL.md`, each
432
+ `scripts` entry to `/tmp/gooseworks-scripts/<slug>/scripts/<name>` and each `files` entry to
433
+ `/tmp/gooseworks-scripts/<slug>/<name>` (keep the key as the relative path; do the same for
434
+ each dependency skill). Run scripts from their `scripts/` folder so `import media_proxy` resolves.
435
+ - **Project attribution:** `export GW_PROJECT_ID=<project_id>` in the shell **before running any
436
+ atom** for that project (re-export per concept in a batch). The proxies bill the workspace
437
+ agent and attribute spend to that project.
438
+ - **Never call a provider with a raw key.** All FAL / ElevenLabs / Whisper traffic goes through
439
+ the `GW_*_PROXY_URL` bases with `GW_MEDIA_PROXY_TOKEN`. `FAL_KEY` / `ELEVENLABS_API_KEY` in
440
+ this environment are proxy tokens, not provider keys — never send them to `fal.run`,
441
+ `queue.fal.run` or `api.elevenlabs.io` directly. Never print any token.
442
+ - **Whisper for QC:** `export OPENAI_BASE_URL="$GW_WHISPER_PROXY_URL/v1"` before running
443
+ `review-ugc-render` / `watch` transcripts.
444
+ - **The operator may be absent.** Keep every quality gate and checkpoint below, but decide
445
+ defaults yourself from the project brief and the recipe, and record them in the review set.
446
+ Ask in the chat **only for a true taste call** (a creative choice the brief and recipe leave
447
+ open and that changes the ad), never for a mechanical or recoverable decision. The Step 3
448
+ approval still applies: ask for it in this chat and record the yes with
449
+ `video_project_upsert patch.approve`.
450
+ - **Outputs:** keep working files under `/tmp/gooseworks-video/<project_id>/` (local disk — never
451
+ the s3fs workspace mount, which is slow and can drop writes); anything the user must see goes to
452
+ the project via `media_upload` (never leave the result only in the sandbox). Every paid piece is
453
+ also SAVED to the project as soon as it passes QC — see "Save as you go — and resume".
46
454
 
47
455
  ## Report problems so we can fix them (telemetry — do this, don't skip it)
48
456
 
@@ -55,97 +463,288 @@ the skill. It's fire-and-forget, never counts against you, and never blocks your
55
463
  `export GW_RUN_ID="vid-<project_or_batch_id>"` (and `export GW_SKILL="<recipe-slug>"`) in the
56
464
  shell you render from. The media proxies read `GW_RUN_ID` automatically.
57
465
  - **CLI present →** `gooseworks log "<what happened>" --event-type <type> --level error --details '{"error":"...","step":"...","model":"..."}'`
58
- - **No CLI (cowork / headless) →** the **`log_cli_event`** MCP tool with the same fields (pass `run_id`).
59
- - `--event-type`: `api_failure` (a proxy/model call failed) · `missing_input` · `blocker` ·
466
+ - **No CLI (sandbox / cowork) →** from any atom's `scripts/` folder:
467
+ `python3 -c 'from media_proxy import gw_log; gw_log("<what happened>", event_type="blocker", level="error", details={"step": "..."})'`
468
+ (or the `log_cli_event` MCP tool if your client has it).
469
+ - Event types: `api_failure` (a proxy/model call failed) · `missing_input` · `blocker` ·
60
470
  `confusion` (unclear/contradictory instruction) · `error` (a bug) · `step`/`info` (progress notes).
61
- - Put the **real error text + the step you were on** in `--details`. Paid FAL/ElevenLabs calls
471
+ - Put the **real error text + the step you were on** in the details. Paid FAL/ElevenLabs calls
62
472
  ALREADY auto-log their own failures, so focus your manual logs on what the proxy can't see:
63
473
  missing inputs, confusing/contradictory recipe instructions, toolchain/setup failures, and bugs.
64
474
  - Logging is FOR US — it does not replace telling the user. When a problem blocks the run, still
65
- explain it to the user (and ask if you need a decision); just also `log` it so we can fix the skill.
475
+ explain it to the user (and ask if you need a decision); just also log it so we can fix the skill.
66
476
 
67
477
  ## Prerequisite — MCP + a render toolchain (Phase 0 preflight)
68
478
 
69
- - The `mcp__gooseworks__*` tools are REQUIRED. If they're unavailable, stop and tell the user
479
+ - The GooseWorks MCP tools are REQUIRED. If they're unavailable, stop and tell the user
70
480
  to connect the GooseWorks MCP server (or run `gooseworks install --claude --mcp` on the CLI)
71
481
  and restart. There is no REST fallback.
72
- - **The render runs wherever THIS agent runs, and it needs a real toolchain: `ffmpeg` +
73
- `ffprobe` + a Playwright **Chromium**.** Establish it in this priority order, and do NOT start
482
+ - **The render runs wherever THIS agent runs, and it needs a real toolchain:** `ffmpeg` +
483
+ `ffprobe` always, plus a Playwright **Chromium** for browser-rendered formats (phone mockups,
484
+ HTML end cards without a PIL fallback). Establish it in this priority order, and do NOT start
74
485
  rendering until one is confirmed:
75
- 1. **CLI present →** run `gooseworks doctor` (checks login, MCP, ffmpeg/ffprobe, Playwright
76
- Chromium in one shot). Fix any ✗ with the command it prints, then continue.
77
- 2. **No CLI →** check the toolchain yourself: `ffmpeg -version`, `ffprobe -version`, and a
78
- Playwright Chromium probe (`npx playwright --version` and, if needed, `npx playwright install
79
- chromium`). If all resolve, continue.
80
- 3. **Docker available →** this is the most reliable way to get the toolchain in a sandbox that
81
- lacks it: run the render steps inside the prebuilt image
486
+ 1. **Sandbox →** see "Running in a GooseWorks sandbox": ffmpeg + ffprobe (+ PIL on demand); no
487
+ Chromium, so browser formats stop there.
488
+ 2. **CLI present →** run `gooseworks doctor --no-browser` for login, MCP, Node 18+, ffmpeg
489
+ 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
490
+ capabilities in Step 2, check each browser renderer's actual launch before ANY paid
491
+ ingredient. An unscoped `gooseworks doctor` checks only the calling folder's browser;
492
+ it cannot certify a different fetched renderer.
493
+ 3. **No CLI →** check the toolchain yourself: `node --version` (18+), `ffmpeg -version`,
494
+ `ffprobe -version`, plus `ffmpeg -hide_banner -encoders` (libx264) and
495
+ `ffmpeg -hide_banner -filters` (ass). After fetching, use the exact-package free launch
496
+ check in Step 2; resolving a package or finding a cache folder does not prove its browser
497
+ can launch. The `watch` QC step later needs the same ffmpeg and,
498
+ for transcripts, a Whisper backend — without one it degrades to frames only.
499
+ 4. **Docker available →** the most reliable way to get the toolchain on a host that lacks it:
500
+ run the render steps inside the prebuilt image
82
501
  **`ghcr.io/gooseworks-ai/goose-video-render`** (ffmpeg + ffprobe + Playwright Chromium baked
83
- in), mounting the project working directory. Use Docker whenever the host is missing ffmpeg or
84
- Chromium and `docker` is on PATH. (Note: nested Docker is usually disabled inside managed
85
- sandboxes like cowork — treat this as an option, not a guarantee.)
86
- 4. **None of the above works →** STOP and tell the user plainly, e.g.: *"Video rendering needs
87
- ffmpeg + a Playwright Chromium (or Docker) on the machine running this agent. This environment
88
- doesn't have them and I can't install them here. Options: (a) enable/allow Docker so I can use
89
- the goose-video-render image, (b) install ffmpeg + `npx playwright install chromium`, or
90
- (c) run this skill locally in your own Claude Code where the toolchain is available."* Do not
502
+ in), mounting the project working directory. (Nested Docker is usually disabled inside
503
+ managed sandboxes — treat this as an option, not a guarantee.)
504
+ 5. **None of the above works →** on the customer's computer, hand the project to the GooseWorks
505
+ coworker with `goose_run_task` (a `needs_browser` format can't be: offer another). Inside the
506
+ coworker sandbox, or when the customer wants it made on their own machine, STOP and tell the
507
+ user plainly, e.g.: *"Video rendering needs
508
+ ffmpeg (and, for this format, a Playwright Chromium) on the machine running this agent. This
509
+ environment doesn't have them and I can't install them here. Options: (a) enable Docker so I
510
+ can use the goose-video-render image, (b) install ffmpeg + `npx playwright install chromium`,
511
+ or (c) run this skill in your own Claude Code where the toolchain is available."* Do not
91
512
  half-render or fake a result. Static image ads (the `goose-ads` skill) do NOT need any of this
92
513
  and work anywhere — offer that as the fallback if they just want an ad now.
93
514
 
94
515
  ## Identity, token, credits
95
516
 
96
- - Read `~/.gooseworks/credentials.json` → `api_key` (your agent token), `api_base`, `agent_id`.
97
- Never print the token.
98
- - **CRITICAL — target the org-default Ads agent on EVERY file op.** The app serves project files
99
- (the render-file route) from the org's DEFAULT agent, but MCP file writes default to your
100
- token's pinned agent — which can be a DIFFERENT agent, so a render written with the default
101
- scope is **invisible in the app**. First resolve the Ads agent: `list_accessible_scopes` → the
102
- scope with `is_org_default: true` (the ORG default — NOT the `is_default` / `default_agent_id`
103
- fields, which are the *user's* default agent and are often a DIFFERENT agent). Its `agent_id` is
104
- `ADS_AGENT` (name "Ads agent", slug `org-default`; usually also the `agent_id` in
105
- credentials.json). Then pass `target: { type: "agent", agent_id: ADS_AGENT }` on EVERY
106
- `get_upload_url` / `get_download_url` / `write_file` / `list_directory` / `read_file` — NEVER
107
- omit `target`.
108
- - **CRITICAL — publish under the PROJECT FOLDER, not the workspace root (the #1 "video renders but
109
- is invisible" bug).** `get_upload_url` stores at `<ADS_AGENT>/files/<path>` verbatim, but the
110
- render-file route reads from
111
- `<ADS_AGENT>/files/agent-config/brands/<brand_slug>/projects/<project_id>/<path>`
112
- (see backend `resolveProjectFileKey`). So EVERY publish/preview `path` MUST be prefixed with
113
- `agent-config/brands/<brand_slug>/projects/<project_id>/` — e.g. upload to
114
- `agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4`, NEVER bare
115
- `working/final.mp4`. A bare path 404s in the app even though the render row AND a bare-path
116
- `get_download_url` both "succeed" (they resolve the wrong key). The render `output_url` still
117
- stays the project-relative `...render-file?path=working/final.mp4` — the route re-prepends the
118
- prefix itself. Always verify with `get_download_url` on the FULL `agent-config/...` path (must
119
- be non-empty; curl it for HTTP 200) BEFORE marking the render complete.
517
+ - **Sandbox:** the token is `GW_MEDIA_PROXY_TOKEN` (already scoped to this workspace's agent and
518
+ org); the API base is `GW_API_BASE`. Never print either.
519
+ - **Local:** read `~/.gooseworks/credentials.json` → `api_key` (your agent token), `api_base`,
520
+ `agent_id`. Never print the token.
521
+ - **Uploads go through `media_upload` with `scope: "video_project"`** — pass `path` = the
522
+ project-relative path (`working/final.mp4`, `working/review/end-card.png`). The server stores it
523
+ in the project folder of the org-default Ads agent (where the app's render-file route reads) and
524
+ returns `upload.url` (presigned PUT), `upload.required_headers` and
525
+ `upload.render_file_url`. PUT the bytes with exactly those headers, check the PUT returned
526
+ 2xx, then call `media_confirm { brand_id, media_id: media.id }` and require success before
527
+ using the file in ingredients or completing a render. This applies to project-path and
528
+ path-less file uploads. Confirmation verifies the stored file; Goose performs this tool step
529
+ without asking the user for another approval. On failure, report or repair the upload before
530
+ continuing. Never hand-build storage paths or agent prefixes; a bare workspace upload is
531
+ invisible in the app.
120
532
  - Media generation (FAL / ElevenLabs) through the GooseWorks proxies is the **REAL spend** — billed
121
- to the agent per call as you generate (Step 4). `submit_render { kind: "full" }` additionally
122
- debits **1 nominal ad credit when the render ROW is opened** (a bookkeeping fee, NOT the render's
123
- true cost) — so open it only once you actually have a rendered master (Step 4.1/4.2), and never
124
- re-submit on a guess (that double-bills). The final-video QC gate (Step 4.3) then sits between
125
- that master and PINNING it. Call `get_ad_credits` first; the user can check `gooseworks credits`.
533
+ per call as you generate (Step 4). The render row (`video_render_run kind: "full"`) charges the flat
534
+ **video base fee once, when a full render is reported `complete`**. Open one row immediately
535
+ after recorded approval, BEFORE paid production (Step 4.1), and reuse that render_id for progress
536
+ 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
537
+ that master and PINNING it. Call `account_whoami` first to see the credit balance.
538
+
539
+ ## Save as you go — and resume (never pay twice for a piece)
540
+
541
+ A sandbox can die mid-run (timeout, restart, a new session picks the project up). Anything that
542
+ lives only in `/tmp` is then gone, and regenerating it pays again. So: **save every piece to the
543
+ project the moment it passes its QC, and start every run by loading what is already saved.**
544
+ Working files stay in `/tmp/gooseworks-video/<project_id>/` (never the s3fs workspace mount);
545
+ the project is the durable copy.
546
+
547
+ **Ingredient keys.** Give every planned piece a stable key before you generate it, the same on
548
+ every run: `vo/scene-03`, `vo/sample-her`, `still/her-base`, `still/scene-05`,
549
+ `clip/scene-05` (a lipsync / video clip), `music/bed`, `endcard`, `captions`, `final`,
550
+ `final-thumb`. Upload path = `working/<role>/<file>` (`working/vo/scene-03.mp3`,
551
+ `working/clip/scene-05.mp4`); the review set keeps its `working/review/<name>` paths.
552
+
553
+ **Input digest.** Name the exact inputs of each generation with `input_digest` from
554
+ `media_proxy` (in every media capability's `scripts/` folder):
555
+
556
+ ```python
557
+ from media_proxy import input_digest
558
+ digest = input_digest(model_path, args) # the model + the EXACT payload you send
559
+ ```
560
+
561
+ Hash only what decides the output (prompt, voice_id, model_id, seed, duration, aspect…). An input
562
+ that is a presigned or proxy URL changes every run, so swap it for that input's own identity
563
+ before hashing, e.g. `{**args, "image_url": {"ingredient": "still/her-base", "digest": her_digest}}`
564
+ — then a changed still correctly invalidates every clip made from it. For a piece you build
565
+ locally (ffmpeg stitch, PIL end card, captions) use `input_digest("local/<step>", {params,
566
+ inputs: {key: digest, …}})`.
567
+
568
+ **1. At the START of every run (first run, resume, new sandbox), load what exists — one call:**
569
+ `media_list { brand_id, scope: "video_project", scope_id: project_id, ingredient_key_prefix: "",
570
+ limit: 100 }`. It returns ONE compact row per `ingredient_key` (the newest):
571
+ `{ id, ingredient_key, input_digest, kind, status, mime, bytes, url, path, created_at }`. Every
572
+ status except archived is included. Project-path uploads remain `pending` until
573
+ `media_confirm` succeeds. Confirm a pending project-path upload before reusing it; if
574
+ confirmation fails, repair the upload before treating it as a verified save.
575
+ 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
576
+ `video_project_read`: it records which pieces were already approved in the review.
577
+
578
+ **2. For each planned piece:** compute its digest from the args you WOULD send now. If a saved row
579
+ has the same `ingredient_key` AND the same `input_digest`, **download it instead of
580
+ generating**:
581
+
582
+ ```bash
583
+ curl -fsSL "$URL" -o /tmp/gooseworks-video/<project_id>/<path> # URL = that row's `url`
584
+ ```
585
+
586
+ That `url` is a short-lived (~15 min) presigned S3 GET the server signs for you, so it needs no
587
+ auth header — download right after listing (list again if it expired). **Never fetch the
588
+ `/api/ads/projects/<id>/render-file?path=…` route from the sandbox:** it needs the app's
589
+ browser session and answers 401 to a token. Check the file is non-empty and plays (ffprobe for
590
+ audio/video, open the image); if the download fails or the file is broken, regenerate the piece.
591
+ Only generate what is missing or whose digest changed — a changed digest means the inputs changed,
592
+ so the old file is stale.
593
+
594
+ **Pass the digest to the proxy too:** `fal_generate(..., input_digest=digest)` (and
595
+ `fal_generate_video` / `fal_whisper`). A piece that was generated but never saved (the sandbox
596
+ died between the fal result and the upload) is then handed back by the proxy instead of paid for
597
+ again, even though its input URLs changed. Only pass `new_take=True` when the user wants a
598
+ different take of the same inputs.
599
+
600
+ **3. After EACH piece is generated AND passes its own QC, upload it right away** — don't batch
601
+ the uploads to the end:
602
+ `media_upload { brand_id, scope: "video_project", scope_id: project_id, kind, path:
603
+ "working/<role>/<file>", ingredient_key, input_digest, source: { type: "file", filename,
604
+ content_type } }` → PUT the bytes to `upload.url` with `upload.required_headers` →
605
+ `media_confirm { brand_id, media_id: media.id }`. Require a successful confirmation.
606
+ Kind: `audio` (VO), `music`, `image` (a still),
607
+ `video` (a clip), `endcard`, `document` (captions / a JSON sidecar), `render` (the master),
608
+ `thumbnail`. Re-uploading the same key is fine — the newest wins. A piece that FAILED QC is never
609
+ uploaded under its key. **Save a piece's sidecars with it** under `<key>.<name>` — e.g. the VO's
610
+ char-level timestamps as `vo/scene-03.timestamps` (`kind: "document"`, same digest). Captions are
611
+ built from them; without them a resumed run has to fall back to Whisper timings, which mis-case
612
+ brand names.
613
+
614
+ **4. Keep the approved review unchanged during production.** Record each confirmed piece's
615
+ `media_id`, `path`, `ingredient_key` and `input_digest` locally; the confirmed media rows
616
+ and render progress are the durable resume record. Before approval, mirror the draft ingredients
617
+ as part of Step 3. After approval, do NOT write `patch.script` or `script_drafts` during any
618
+ paid production, QC or repair step, or while provider work is pending. A review-set write clears
619
+ approval and can stop the next paid step, including a batch concept. Save the final descriptive
620
+ review once all paid work is finished and settled, as Step 4.5 requires.
621
+
622
+ Any material creative change or ANY change the user asked for in this chat must stop production:
623
+ save the changed complete plan and full credit total, then obtain fresh approval before continuing.
624
+ Never reuse the earlier yes for a changed plan. A repair that restores the approved choices can
625
+ continue within the existing allowance and Stop guards without rewriting the approved review.
626
+
627
+ The `final` master and `final-thumb` poster (Step 4.4) carry `ingredient_key` too, so a
628
+ resumed run that finds a passing `final` with the same digest only needs to publish.
629
+
630
+ ## Finished video, interrupted saving — resume the same version
631
+
632
+ If the final already passed QC and the customer says **"finish saving this video"**, start here.
633
+ Do not restart the recipe, choose another format, open another render, generate a take, or run a
634
+ paid transcript check again. Saving uses the **same render_id**. The ordinary first-completion
635
+ video fee can still apply; its existing same-render idempotency prevents a duplicate fee. Never
636
+ promise that all saving is free.
637
+
638
+ **Before the first final upload**, save the final review set and production manifest from Step
639
+ 4.5/4.6 (including any QC repairs) only after all paid work is finished and settled. Preserve the
640
+ approved creative choices; a material change requires the changed-plan approval flow first.
641
+ After that final save, start no new provider or render work: prepare the checkpoint and finish
642
+ the same opened render using its existing finishing allowance and guards. A released allowance
643
+ or stopped/capped render requires normal recovery; never reopen or bypass approval.
644
+ Write a durable local checkpoint outside the fetched-scripts cache. Keep the actual
645
+ final, JPEG poster, structured passing quality report, and review evidence files alongside it in
646
+ a retained local working folder. Record their SHA-256 **at the time those exact bytes pass QC**;
647
+ never attach an old verdict to a newly hashed replacement. A sandbox's local disk can disappear:
648
+ this checkpoint recovers connection interruptions, not lost storage. Save ingredients remotely
649
+ as above and tell the customer if the checked local output is no longer available.
650
+
651
+ With the current CLI and its normal login to the selected environment, write a manifest like:
652
+
653
+ ```json
654
+ {
655
+ "brand_id": "<owning brand>", "project_id": "<same project>", "render_id": "<already opened render>",
656
+ "input_digest": "<assembly input digest>",
657
+ "final_path": "/absolute/retained/working/final.mp4",
658
+ "poster_path": "/absolute/retained/working/final-thumb.jpg",
659
+ "qc": {
660
+ "final_sha256": "<64 lowercase hex characters recorded during final QC>",
661
+ "poster_sha256": "<64 lowercase hex characters recorded during poster review>",
662
+ "report_path": "/absolute/retained/working/quality-report.json",
663
+ "evidence_paths": ["/absolute/retained/working/review/finished-ad.json"]
664
+ }
665
+ }
666
+ ```
667
+
668
+ Run `gooseworks video-save prepare --manifest <manifest.json> --checkpoint <retained/save-<render_id>.json>`
669
+ before any upload, then `gooseworks video-save resume --checkpoint <same checkpoint>` for both
670
+ normal saving and recovery. The helper uses the saved login and canonical MCP connection; it does
671
+ not copy credentials or use a raw Ads REST fallback. Its atomic private checkpoint binds the
672
+ environment, account, project owner, project/render, final/poster bytes, assembly digest, passing
673
+ quality report, review set, evidence files and stage receipts. Preserve it until delivery. Never
674
+ edit the checkpoint to clear a failed check. Keep checkpoint and manifest files out of commits.
675
+
676
+ The helper reads remote state before writes. It keeps `final` / `final-thumb` ingredient keys
677
+ and writes per-render paths `working/final-<render_id>.mp4` / `working/final-<render_id>-thumb.jpg`
678
+ so saving this version cannot overwrite an older one. It confirms pending uploads, checks remote
679
+ bytes against the QC hashes, and reuses confirmed media. A request with missing stored bytes can
680
+ get a fresh upload URL at that same path. A lost completion reply is unknown until project read
681
+ confirms the same render's output and passing report; then only final selection remains.
682
+
683
+ **No CLI / a host-selected MCP connection:** keep the same protocol with the host's canonical
684
+ tools. Atomically write versioned JSON locally (write a private same-directory temporary file,
685
+ flush/fsync, rename, then fsync the directory) before any upload and between each upload request,
686
+ PUT, confirmation, completion and final selection. Persist only public environment identity,
687
+ account/owner/project/render IDs, file paths/sizes/hashes, input digest, exact quality report and
688
+ evidence fingerprints, saved-review digest, media IDs and stage states; **no tokens, session IDs,
689
+ signed URLs or auth headers**. On reconnect call `account_whoami`,
690
+ `video_project_read { brand_id, project_id, include: ["renders"] }` and
691
+ `media_list { brand_id, scope: "video_project", scope_id: project_id, ingredient_key, input_digest }`
692
+ on the original connection. Compare identities, local/evidence hashes and saved review; verify
693
+ confirmed remote bytes too. Treat a lost reply as unknown and read back before retrying. Use only
694
+ the missing `media_upload` → PUT → `media_confirm`, same-render `video_render_run { render: … }`
695
+ callback and `video_project_upsert` with only `patch.final_render_id` and
696
+ `patch.final_selection_guard: { expected_final_render_id, expected_review_digest }`; never a
697
+ render-open call. The expected final is the last read's pin, including explicit `null`; the
698
+ review digest is SHA-256 of recursively sorted-key JSON
699
+ `{script: project.script ?? null, script_drafts: project.script_drafts ?? null}` saved at checkpoint
700
+ creation. First confirm the selected server advertises this guard in `tools/list`. If absent,
701
+ stop and request the normal server update/reconnect; an unguarded pin is unsafe. A
702
+ `final_selection_conflict` means the choice or review changed: keep the checkpoint and explain
703
+ that saving stopped without overwriting it. Retain the
704
+ strict Step 4.3/4.4 report and Step 4.5 review-set requirements.
705
+
706
+ If authorization expired, reconnect/sign in through the normal host/CLI flow on the original
707
+ connection, then resume. Missing tools are a reconnect requirement, not permission to switch
708
+ environments. Missing/changed files, stale/missing QC, changed account/owner/review, another final
709
+ selection, or a stopped/capped/failed/blocked render require diagnosis; do not reopen it or
710
+ regenerate automatically. If saving stops, say: **"Your video is finished locally, but saving to
711
+ Goose was interrupted."** Link the actual local file and preserve its checkpoint. Claim saved
712
+ delivery only after a fresh project read verifies completion and selection of that exact render.
126
713
 
127
714
  ## Step 0 — project id, or video BATCH id? (fan out before anything else)
128
715
 
129
- The handoff you were pasted is EITHER a single `project <id>` OR a `video batch <id>`. A batch is
716
+ The handoff is EITHER a single `project <id>` OR a `video batch <id>`. A batch is
130
717
  the app's "N concepts" flow: one composer submission fans out into **N independent concept projects**
131
718
  (the user picked a concept count, default 3), and the app expects EACH to be rendered. **Handle both:**
132
719
 
133
720
  - **`project <id>`** → you have one project. Treat it as a batch of one and continue to Step 1.
134
- - **`video batch <id>`** → call `get_ad_video_batch { video_batch_id }`. It returns every child
721
+ - **`video batch <id>`** → call `video_project_read { brand_id, batch_id }`. It returns every child
135
722
  concept under `projects[]` — each is a normal project with its own `id`, `variant_index`
136
723
  (Concept 1..N), and its own `creative_brief` (the per-concept angle/hook/offer/message). **You
137
724
  MUST process every concept, not just the first** — dropping concepts 2..N is the #1 batch bug.
138
725
 
139
- **Loop shape (one agent, sequential, ONE approval for the whole batch):**
140
- 1. Run **Step 1 + Step 1.5 + Step 2 + Step 3-assemble** for EACH concept project (each has its own
141
- `project_id`, brief, and `working/` folder — never cross-write between concepts).
142
- 2. Mirror EVERY concept's review set (Step 3's `update_ad_project_script` per project), then stop
143
- for **ONE** approval that covers all concepts — show the per-concept credit estimate and the
144
- batch total. Set the batch to `review` (`update_ad_video_batch { status: "review" }`).
145
- 3. On approval, set the batch to `rendering` and run **Step 4 (the expensive render)** for each
146
- concept **sequentially** (finish Concept 1's master before starting Concept 2 — one machine can't
147
- render them in parallel). Deliver each (Step 5). When all concepts are pinned, set the batch to
148
- `complete`.
726
+ **Loop shape (ONE approval for the batch, isolated work per concept):**
727
+ 1. Run **Step 1 + Step 1.5 + Step 2 + Step 2.5 + Step 3-assemble** for EACH concept project (each
728
+ has its own `project_id`, brief, `GW_PROJECT_ID` and `working/` folder — never cross-write
729
+ between concepts). The brand read (Step 1 item 2) and `brand-rules.json` (Step 1.7) are per
730
+ BRAND: do them once for the batch and copy the file into each concept's `working/`. The read is
731
+ ~90K characters. Step 2.5's angle bank is shared per brand AND product: load or build it once, and
732
+ give every concept whose angle is `auto` a DIFFERENT angle from that list, so the batch is N
733
+ different ads, not one ad N times.
734
+ 2. Mirror EVERY concept's review set (Step 3's `video_project_upsert patch.script` per project),
735
+ then stop for **ONE** approval in this chat that covers all concepts. Save each render_estimate.total_credits
736
+ (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
737
+ { brand_id, batch_id, patch: { batch: { status: "review" } } }`).
738
+ 3. On an explicit yes, record it ONCE for the whole batch: `video_project_upsert { brand_id, batch_id,
739
+ patch: { approve: { user_quote: "<their exact words>", total_credits: <the saved total> } } }`. Check its `not_ready` list is
740
+ empty (a concept listed there has no saved review set: save it, show it, ask again). If they
741
+ approve only some concepts ("1 and 3 are good, redo 2"), record each approved one with its
742
+ `project_id` instead, and redo the rest. Then set the batch to `rendering` and run **Step 4 (the expensive render)** for each
743
+ concept with up to 8 isolated workers when the machine can sustain them. Retry
744
+ 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
745
+ `complete`. A concept the Step 4.3 gate leaves `blocked` cannot be pinned (a batch concept
746
+ needs `passed`): finish the others, set the batch to `blocked`, and tell the user which
747
+ concepts passed and which are blocked, with each one's failing checks.
149
748
 
150
749
  If a single concept fails, keep going with the rest, mark that concept blocked, and report which
151
750
  ones shipped — never abort the whole batch on one bad concept. Everything below (Steps 1–5) is
@@ -153,18 +752,25 @@ written per-project; a batch just runs it N times with the shared approval gate
153
752
 
154
753
  ## Step 1 — resolve the project, source, brand
155
754
 
156
- 1. `get_ad_project { project_id }` → keep `brand_id`, `source_sample_id`, `name`, `status`, the
157
- **top-level** `app_url` + `brand_url` (returned alongside `project`, NOT inside it — the links
158
- you hand the user for the in-app review in Step 3 and delivery in Step 5), AND the user's
159
- **`creative_brief`**, project **`assets`**, `character_id`, `default_voice_id` — these are the
160
- authoritative inputs the user chose in the composer (see Step 1.5). Do NOT discard them.
755
+ 1. `video_project_read { brand_id, project_id }` → keep `brand_id`, `source_sample_id`, `name`,
756
+ `status`, the **top-level** `app_url` + `brand_url` (the links you hand the user for the in-app
757
+ review in Step 3 and delivery in Step 5), AND the user's **`creative_brief`**, project
758
+ **`assets`**, `character_id`, `default_voice_id` — these are the authoritative inputs the user
759
+ chose in the composer (see Step 1.5). Do NOT discard them. Then `export GW_PROJECT_ID=<project_id>`.
760
+ Keep its saved `campaign_id` and `campaign_concept_id` too. If the customer explicitly
761
+ wants to attach this existing video to a known campaign/concept, verify the IDs with the
762
+ campaign read and use `video_project_upsert { brand_id, project_id, patch: {
763
+ campaign_association: { campaign_id, campaign_concept_id? } } }` as a separate, sole-field
764
+ patch. Read back the same project and confirm the saved IDs. Linking never needs a new
765
+ project, render, generation call or approval. A conflicting saved link stays intact; explain
766
+ it instead of silently moving the video. Leave unlinked legacy videos alone unless asked.
161
767
 
162
768
  ### Step 1.5 — the project brief is AUTHORITATIVE (honor it; don't re-ask)
163
769
 
164
770
  The composer already collected the user's creative direction onto the project. **Read it and treat
165
771
  it as ground truth — it OVERRIDES the template recipe's defaults, and it REPLACES the clarifying
166
- questions you would otherwise ask.** Only fall back to the recipe default (then, last, to asking)
167
- for a field the brief leaves empty. Map the fields you WILL honor:
772
+ questions you would otherwise ask.** For a field the brief leaves empty, use the Brain search's
773
+ evidence brief first, then the recipe default, then (last) asking. Map the fields you WILL honor:
168
774
 
169
775
  - `creative_brief.productName` / `.offer` / `.angle` → the product, offer/code, and angle. Do
170
776
  **not** ask "which product / what offer / what angle" if these are set.
@@ -176,49 +782,392 @@ for a field the brief leaves empty. Map the fields you WILL honor:
176
782
  assets.
177
783
  - `character_id` → the avatar/creator to use. `default_voice_id` → the voice for any VO (put its
178
784
  NAME in the review `subtitle`). Use these instead of picking your own.
179
- - `creative_brief.ratio` / `.durationSeconds` → target aspect ratio + length. Honor when the
180
- format's render pipeline supports it; if the format physically can't (e.g. a fixed phone-mockup
181
- aspect), keep the format's native value and note the constraint in the review rather than silently
182
- ignoring the request.
785
+ - `creative_brief.durationSeconds` → target length; honor it when the format allows.
786
+ `creative_brief.ratio` → video ads are ALWAYS 9:16 (1080×1920). If the brief asks for another
787
+ ratio, make 9:16 anyway and say so in the review; never export another size (a recipe's
788
+ "also 1:1" option included).
183
789
  - `polish_policy` (`standard` | `extra`) → `extra` means spend the extra pass on QC/polish.
184
- 2. `get_ad_template { template_id: source_sample_id }` → the source video: `media_url`,
185
- `recipe`, `format` (e.g. "imessage"), `extracted_script`, `how_to`, `remix_spec`.
186
- 3. Brand gate: `get_brand_kit { brand_id }`. If `researchStatus` is `complete`, REUSE it —
187
- never re-research. If not, run brand research first (`gooseworks fetch brand-research`,
188
- follow it, then `finalize_brand_research { brand_id }`) before continuing.
790
+
791
+ 2. Brand gate: `brand_read { brand_id, sections: ["summary", "kit", "products", "learnings"] }`
792
+ (older clients: `brand_get_context` with the same sections). Ask for all four: the default
793
+ leaves out the kit and the brand's saved rules, and a video made without them is off-brand.
794
+ If the kit's `researchStatus` (or the brand's `research_status`) is `complete`, REUSE it —
795
+ never re-research. A pending status alone does not require research; use verified stored
796
+ facts and ask only for an actual gap. If the context is empty or the customer requests
797
+ research, load `catalog_fetch { type: "skill", slug:
798
+ "brand-research" }` and follow its stored-pack workflow. Only when that verified pack is
799
+ saved in the supported research workspace, finalize with
800
+ `brand_update { brand_id, patch: { finalize_research: true } }`, then read the brand back.
801
+ Never send raw research JSON through `kit_patch`: the public tool accepts only the existing
802
+ `video_lab` asset slot there. If there is no verified stored pack, submit researched facts
803
+ through typed `patch.knowledge` / `patch.kit` as pending agent proposals; do not pretend
804
+ research is finalized or its proposals are approved. Then do Step 1.7 with verified facts.
805
+ 3. For a new plan, `catalog_fetch { type: "template", slug: <source_sample_id> }` → the source
806
+ video: `media_url`, `recipe`, `format` (e.g. "podcast-skit", "imessage"),
807
+ `extracted_script`, `how_to`, `remix_spec`. For an approved resume, restore the recorded
808
+ package and dependencies instead of fetching today's recipe over the saved plan.
809
+
810
+ ### Step 1.6 — a remix of a FINISHED video (the project read has a `remix` block)
811
+
812
+ A project made from **Community videos** remakes another customer's finished video for THIS brand.
813
+ Its `video_project_read` returns a top-level `remix` block
814
+ (`{ remix_of_project_id, instruction, direction }`), and `reference_video_url` is that finished video.
815
+
816
+ - **Watch the reference video first** (download `reference_video_url`, pull frames + the transcript).
817
+ It is the target: match its structure, beat order, pacing, framing, look, voice and tone.
818
+ - **`remix.direction` is its approved review set** (scenes and lines, set/look notes, take prompts,
819
+ captions, music, voice). Start your review set from it instead of the template's defaults; it
820
+ already carries every change that customer made to the template.
821
+ - **`remix.direction.implementation`, when present, is HOW it was made**: the pipeline and the model
822
+ each step used, the style and negative prompts, the character and per-scene still/motion prompts
823
+ with their guards, voice and music settings, the mix, and `fixes` (what went wrong and how it was
824
+ fixed). Reuse the same models, style/negative prompts and guards, rewrite the prompts' subjects for
825
+ this brand, and apply every `fix` up front so you don't repeat the same mistake.
826
+ - **Rewrite everything for THIS brand.** "[source brand]", "[source product]", "[link]", "[email]"
827
+ and "[code]" mark the other customer's details: never write them, and never reuse their claims,
828
+ numbers, URLs, offers or CTA. Every product, claim, name, image and CTA comes from this brand's
829
+ kit, products and media. Their footage (screen recordings, product shots) and their creator face
830
+ are NOT carried over: use this brand's own assets and make a new creator from the description.
831
+ - Precedence: this project's own `creative_brief` and assets (Step 1.5) > `remix.direction` >
832
+ the template recipe's defaults.
833
+ - In the Step 3 review, say it is a remix of that video and list what you kept vs. changed.
834
+
835
+ ### Step 1.7 — the brand rules file and the brand assets (every run, before any writing)
836
+
837
+ Before new writing, write `working/brand-rules.json` from the Step 1 brand read. Preserve an
838
+ approved run's saved rules on resume; reconcile an intentional rule change through the existing
839
+ review gate. Every later step reads THIS file, not your memory of the chat:
840
+
841
+ ```json
842
+ {
843
+ "name": "Acme",
844
+ "pronunciations": [{ "term": "Acme", "say_as": "ak-mee", "learning_id": "…" }],
845
+ "must_say": [{ "text": "…", "learning_id": "…" }],
846
+ "never_say": [{ "text": "Never claim it cures insomnia", "learning_id": "…" }],
847
+ "products": [{ "id": "…", "name": "…", "facts": ["size, flavour, price, ingredients as stored"], "images": ["…"] }],
848
+ "logo": { "url": "…", "confidence": "<kit.logoConfidence>" },
849
+ "fonts": ["…"],
850
+ "palette": ["#…"]
851
+ }
852
+ ```
853
+
854
+ - **Video taste is direction, not dialogue.** Before mapping brand rules, extract entries
855
+ prefixed `Video preference:` from both `learnings` and `kit.instructions` into the verified
856
+ taste brief, including accepted proposals. They govern pacing, voices, captions, visuals and
857
+ format choice; do not copy them into `must_say` / `never_say` or read them aloud. Carry the
858
+ brief into the choices, scene planning and review.
859
+ - **Sources.** `learnings` are the brand's saved rules (the user's past corrections among them):
860
+ `must` / `do` → `must_say`, `dont` → `never_say`, and a `must` whose text reads
861
+ `Pronounce "<term>" as "<say_as>"` (straight or curly quotes) → `pronunciations`. Add `kit.instructions` (free-text
862
+ standing rules) only when they require actual spoken wording or prohibit a claim. Production
863
+ directions stay in the brief. A required spoken line or prohibited claim remains its own
864
+ ordinary `must` / `dont` rule.
865
+ - **Which product.** The one the brief names (`creative_brief.productName`); with none, the row
866
+ whose name matches the product the user asked for, or the brand itself for a one-product
867
+ brand. Product lists often hold other brands' items or old ads saved as products: if more than
868
+ one row could be it, ask in the choices round. Never mix facts across rows.
869
+ - **Product facts** come from that row (name, description, variant, price) and, when the row is
870
+ empty, from the kit (`valueProps`, `description`, `tagline`): nothing else. Write them to
871
+ `products[].facts`; the script may only state what is there.
872
+ - **Logo.** Download the kit's logo FILE (`kit.logoUrl`, else `kit.logos[0]`) to `working/brand/`,
873
+ keeping its real extension (an SVG stays `.svg`; rasterise it to a 1024px-wide PNG with
874
+ `rsvg-convert` or `cairosvg` when a renderer needs pixels). It is used as-is on every scene and
875
+ end card that shows a logo: **never generate, redraw, re-letter or restyle a logo with an image
876
+ model.** If the kit's `logoConfidence` says favicon-grade, or the file's long side is under
877
+ 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
878
+ set as text in the brand font) in the SAME question round as the recipe's `choices`.
879
+ - **Font.** `kit.typography.heading` when its `source` is `user` (the user chose it), else
880
+ `kit.fonts.heading`. Download the font file (the kit's own, or the same family from Google
881
+ Fonts) to `working/brand/` and use it for every on-screen line. With no match, use the closest
882
+ free font and say so in the review.
883
+ - **No wordmark file.** When a recipe wants a wordmark SVG and the kit has only a logo image, set
884
+ the brand name as text in the brand font beside the logo file. Never generate one.
885
+ - Record in `brand-rules.json` which logo the video will actually composite (`logo.file`), or
886
+ `"logo": { "mode": "text" }` when it will show only the brand name set in the brand font.
887
+ - **Product images.** Download this product's own images to `working/brand/`. Use only images of
888
+ THIS product: never a catalogue image of another product, a mascot, a lifestyle photo of a
889
+ person, or a stand-in. If the product has no usable image, ask in the choices round.
890
+
891
+ ### Brand corrections stick — save them to the brand the moment they are made
892
+
893
+ When the user corrects something about the BRAND in chat — how a name is said, a claim that may
894
+ not be made, a product fact, a visual rule ("never use red", "the logo goes top-left") — save it
895
+ in the SAME turn, before anything else:
896
+
897
+ `brand_update { brand_id, knowledge_intent: "user_correction", user_statement: <the user's exact correction>, patch: { facts: [{ kind, text }] } }`
898
+
899
+ | Correction | `kind` | `text` |
900
+ |---|---|---|
901
+ | Pronunciation | `must` | `Pronounce "Acme" as "ak-mee"` (exactly this form) |
902
+ | A claim or word to avoid | `dont` | `Never say or imply: <the claim>` |
903
+ | Something that must be said | `must` | `<the rule>` |
904
+ | A product fact | `must` | `<product name>: <the fact>` |
905
+ | A visual rule | `do` / `dont` | `<the rule>` |
906
+
907
+ - If it changes an EXISTING rule (a new pronunciation for the same term), update that rule by id
908
+ (`facts: [{ id: <learning_id>, text }]`) instead of adding a second one.
909
+ - Use the correction intent and user's statement when supported, as described below. Read
910
+ `brand_read` learnings back and verify the rule and source before claiming it was saved.
911
+ - Then tell the user in one line: "Saved to your brand for future videos." Update
912
+ `working/brand-rules.json` and apply the rule to THIS video too.
913
+ - A one-off note about this video ("make it shorter", "use the blue background here") is NOT a
914
+ brand rule: don't save it.
915
+
916
+ ## Save durable brand answers, then verify them
917
+
918
+ Read the selected brand with `brand_read { brand_id, sections: ["summary", "kit", "products", "learnings", "onboarding"] }`
919
+ (older clients: `brand_get_context` with the same sections, only when that tool is advertised).
920
+ Keep founder answers, user corrections, research and your own hypotheses distinct. Reuse
921
+ matching saved answers; ask only about gaps.
922
+
923
+ When the user asks to remember a rule, answers a brand interview, or explicitly corrects a
924
+ standing fact, save that answer in the same turn. The capture request authorizes those answers;
925
+ do not ask for approval again. A direction for this one video stays in its brief. If the scope
926
+ is genuinely ambiguous, ask whether it applies to future videos before saving a standing rule.
927
+
928
+ Use the **live registered schema**. Where supported, call `brand_update` with
929
+ `knowledge_intent: "user_correction"` and `user_statement` containing the user's exact,
930
+ verbatim answer, not your paraphrase or researched text.
931
+ For an inference or suggested improvement, use `knowledge_intent: "agent_proposal"`. Show the
932
+ before/after change from your prior read and proposed value; retain the returned proposal IDs
933
+ and say the user must accept it in the app. Link only a review surface actually returned by a
934
+ tool; the compact `knowledge_updates` response does not itself contain a diff or URL.
935
+ A pending proposal is not a saved fact. Never call an unavailable
936
+ tool or silently relabel research or your inference as something the user said.
937
+
938
+ The safe structured shape is `patch: { knowledge: { positioning?, audience?, voice?,
939
+ instructions?, brandType?, tagline?, valueProps? } }`, using only fields present in the live
940
+ schema. Inferred rules/taste go in an `instructions` proposal with a rationale, never in
941
+ `patch.facts`. Prefix every video-only preference in that proposed text with "Video preference:"
942
+ so it remains production direction after acceptance. Preserve unrelated instructions when
943
+ proposing a merged replacement.
944
+
945
+ | User answer | Canonical write |
946
+ | --- | --- |
947
+ | 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. |
948
+ | 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. |
949
+ | Required wording or pronunciation | `patch.facts: [{ kind: "must", text }]`; pronunciation is exactly `Pronounce "<term>" as "<say_as>"`. |
950
+ | Forbidden claim, word or visual | `patch.facts: [{ kind: "dont", text }]`. |
951
+ | 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:". |
952
+
953
+ Facts are existing `ad_brand_learning` rows with user provenance; they are not a second profile.
954
+ Update a matching rule by its returned `id` instead of adding duplicates. Preserve unrelated
955
+ rules and the user's exact meaning. Only use the legacy facts shape for explicitly user-authored
956
+ answers when the live schema lacks intent fields; agent suggestions still need a proposal path.
957
+
958
+ **Claims and plans have separate gates.** A founder assertion or proof point is not an approved,
959
+ evidence-backed claim or consent to quote a customer. Use the existing evidence/claims and
960
+ operating-plan tools only if registered, following their proposal, evidence and confirmation
961
+ requirements. Never encode a spend cap, approver or emergency stop as a learning. If that write
962
+ path is missing, report the specific unsaved item and keep it pending for the supported review
963
+ surface; do not claim it was saved or create a parallel local profile.
964
+
965
+ After every write, **read back before saying saved**: use `brand_read` with `kit`, `learnings`
966
+ or `onboarding` as appropriate, or `brand_onboarding { action: "status", brand_id }` after an
967
+ onboarding answer. Verify the intended field/rule, its source and the absence of a conflicting
968
+ duplicate. A generic success response, pending proposal, ignored key or truncated result is not
969
+ proof. Report partial saves honestly. Carry the verified rules into the current task and the
970
+ routed skill; claims still pass their own safety gate.
971
+
972
+ ## Video taste — reuse examples and preferences
973
+
974
+ When asked to capture video taste, or when the user volunteers a durable video preference,
975
+ first read the saved learnings and `media_list { brand_id, scope: "brand", scope_id: brand_id,
976
+ tags: ["video-taste"], limit: 100 }`. Follow `next_cursor` before deciding an example is absent.
977
+ Do not force a taste interview before an unrelated task or ask again for an existing preference.
978
+
979
+ Save what the user has already supplied first. Then ask only the missing useful question, for
980
+ example: "What do you like about this video—its pace, voice, captions, or look?" An inaccessible
981
+ link can still be saved as a link with the user's explanation; do not pretend you watched it.
982
+
983
+ - **Direct clip or video file:** register with `media_upload { brand_id, scope: "brand",
984
+ scope_id: brand_id, kind: "video", source: { type: "url", url }, tags: ["video-taste", "reference-only"],
985
+ metadata: { purpose: "video_taste", source_url: url, provenance: "user", captured_at: <ISO timestamp>,
986
+ preference: <the user's explanation> } }`. For a file use the live file/bytes upload flow and
987
+ `media_confirm` after a presigned upload. Registration of a URL does not copy or inspect it.
988
+ - **Instagram/post/page link:** the same registration with `kind: "document"`; it is a link
989
+ bookmark, not downloadable footage or an indexed transcript. Do not fabricate a direct clip URL.
990
+ - **Preferences:** save the user's reasons, likes and dislikes through the facts mapping above.
991
+ Read and preserve existing facts before updating one. Do not invent `video_preferences` or
992
+ new `video_lab` keys; the live kit patch accepts only its documented asset fields.
993
+ - **Deduplicate:** reuse a matching returned media row, then `media_update` its title/tags/metadata
994
+ if needed; preserve existing metadata and tags. Do not create another row for the same example.
995
+ Read back with `media_list` and `brand_read` learnings before claiming it was saved.
996
+ Check the returned row belongs to this brand: URL deduplication may return another brand's
997
+ existing row. Do not relabel that row or claim success unless the current brand's scoped read
998
+ actually returns it. Report an unsaved association if no supported attach path is available.
999
+
1000
+ Never put third-party taste examples into kit reference images: `kind: "reference"` at brand
1001
+ scope writes there. The tags and metadata above record purpose and provenance; **they do not
1002
+ grant or enforce usage rights**. Study the structure, pacing and look only. Never use the example's
1003
+ footage, face, product, testimonial or claims in a new ad without independently verified permission.
1004
+
1005
+ Read video-only entries prefixed `Video preference:` from both saved learnings and
1006
+ `kit.instructions`, including accepted proposals. Keep them out of required or forbidden
1007
+ dialogue. Build a brief from the verified readback: preferred pace, voice, caption treatment, visual style,
1008
+ formats to favour/avoid, reference links and the user's reasons. Say what is still unknown.
1009
+ Pass it with the brand rules into the existing video workflow. A one-video request overrides a
1010
+ default for that project; it does not silently rewrite the brand's standing preference.
189
1011
 
190
1012
  ## Step 2 — read the template's recipe (it carries everything; NO hardcoded format map)
191
1013
 
192
1014
  The ad format is a **template (data) in the ad_sample DB**, not a per-format skill.
193
- `get_ad_template(source_sample_id)` returns the template's `recipe` — a self-contained brief you
1015
+ `catalog_fetch { type: "template" }` returns the template's `recipe` — a self-contained brief you
194
1016
  read and execute. **Do NOT map `format` to a hardcoded recipe slug** (there is no such table):
195
1017
 
196
1018
  - `recipe.format` — the format label (e.g. `vignette`), for display only.
197
- - `recipe.atoms` — the **capabilities** this template composes (e.g. `create-video-seedance-2-fal`,
198
- `create-image-gpt-image-fal`, `review-ugc-render`, `watch`). `gooseworks fetch <name>` each — they
199
- live in `skills/ads/capabilities/` and are reused across templates (so they cache).
1019
+ - `recipe.atoms` — the **capabilities** this template composes (e.g. `create-vo-elevenlabs`,
1020
+ `create-image-gpt-image-fal`, `render-podcast-skit`, `review-ugc-render`, `watch`). Fetch each
1021
+ with `catalog_fetch { type: "skill", slug }` (or `gooseworks fetch <name>` when the CLI is
1022
+ installed) — they are reused across templates.
200
1023
  - `recipe.instructions` — the **playbook** to follow: `instructions.inline` prose, or
201
1024
  `instructions.doc_url` (an S3 markdown doc — fetch it).
202
1025
  - `recipe.config` — every param (prompts, layout, timings, palette, model choices).
203
1026
  - `recipe.inputs` — the brand-asset contract (which product / logo / offer this template needs).
1027
+ - `recipe.choices` — the creative calls the USER makes (who is on screen, narrator, tone, setting,
1028
+ art style, music). Ask every unanswered one in ONE round before any paid step, with its options plus
1029
+ "you pick". Its `reference` is what the demo used: an example, never the default.
204
1030
  - `recipe.assets` — reference material as S3 links (reference render, style guide, example frames) —
205
1031
  fetch as needed.
206
1032
 
207
- Runtime: **read the recipe → `gooseworks fetch` each capability in `recipe.atoms` → follow
1033
+ Runtime: **read the recipe → fetch each capability in `recipe.atoms` → follow
208
1034
  `recipe.instructions` with `recipe.config` + the brand's bound `inputs`.** The template IS the recipe;
209
1035
  there is no `format → recipe-slug` table and no per-format skill to fetch.
210
1036
 
211
- Save each fetched capability's scripts + files under `/tmp/gooseworks-scripts/<name>/`. If a capability
212
- is a Node package (a phone-mockup renderer), `npm install` in its folder so its `generate.js` +
213
- Playwright resolve, and point the recorder's `NODE_PATH` at it.
1037
+ Save each fetched capability's content, scripts + files under `/tmp/gooseworks-scripts/<name>/`
1038
+ (layout in "Running in a GooseWorks sandbox"). If a capability is a Node package (a phone-mockup
1039
+ renderer), install its documented dependencies in the folder containing its `package.json`,
1040
+ and preserve the recorder's documented `NODE_PATH` — local machines only; in a sandbox that
1041
+ format stops (no Chromium).
1042
+
1043
+ **Selected browser readiness — before ANY paid ingredient:** identify the actual browser script
1044
+ from the fetched capability's instructions (including an HTML end-card renderer if used).
1045
+ For Node scripts using `require('playwright')` with default `chromium.launch()`, run
1046
+ `gooseworks doctor --renderer-script "/absolute/path/to/the/fetched/scripts/record.js"`.
1047
+ Substitute the actual script, and keep the same cwd and environment as the render, including
1048
+ `NODE_PATH` and `PLAYWRIGHT_BROWSERS_PATH`. It resolves Playwright relative to that script,
1049
+ launches and closes Chromium with the default settings and bounded waits, and downloads nothing.
1050
+ On failure, stop before spending, show its folder-specific repair and recheck after setup.
1051
+ For non-browser formats use `doctor --no-browser` plus their documented runtime checks;
1052
+ never combine `--no-browser` with `--renderer-script` or use it to bypass a browser renderer.
1053
+
1054
+ **No CLI or CLI without these flags:** run an equivalent free probe in a separate Node process: use
1055
+ `require('node:module').createRequire(require('node:path').resolve(actualRendererScript))`
1056
+ to load `playwright`; keep the render's cwd, environment and default launch settings. Await
1057
+ `chromium.launch({ timeout: 15000 })`, then await `browser.close()` (bound close to 3 seconds).
1058
+ Bound the whole process to 20 seconds and stop its own process tree on failure or timeout.
1059
+ Report the resolved module path/version and error without credentials. Missing module, executable,
1060
+ headless runtime or failed launch is a failed check; a cache folder, executablePath alone or
1061
+ another project's browser is not a pass. Other browser packages, Python renderers or custom
1062
+ launch settings need the same free launch/close check through their documented runtime and
1063
+ actual settings. Do not replace their browser/channel/flags to get a pass. Setup is a separate
1064
+ action under existing permissions; this check never silently installs or downloads anything.
214
1065
 
215
1066
  > **Migration note:** older phone-mockup formats (`imessage` / `chatgpt` / `apple-notes`) whose DB
216
1067
  > recipe does not yet carry `atoms` / `instructions` still hold the legacy `recipe.thread` payload;
217
1068
  > migrate them to this shape (capabilities + instructions in the DB) — do not reintroduce a CLI map.
218
1069
 
219
- ## Step 3 — assemble the review set, then get ONE approval in the app (before the expensive render)
1070
+ ## Step 2.5 — plan words and visuals with `write-video-ad-script`
1071
+
1072
+ Every video concept gets creative strategy, including silent formats. Fetch
1073
+ **`write-video-ad-script`** through the skill catalogue and follow it before assembling
1074
+ review ingredients. It reuses or fetches **`ad-angle-miner`**, binds the researched promise
1075
+ to this template's full recipe, writes words and visuals together, and checks product
1076
+ claims and production fit. Its independent critic is a quality screen, not a forecast
1077
+ of ad performance. The existing Step 3 review and approval remain unchanged.
1078
+
1079
+ - **Carry the research into the writer.** Pass the brand and exact product, audience,
1080
+ objective, offer, CTA, selected template recipe, available assets and the full miner
1081
+ bank or its readable workspace pointer. A selected miner idea includes its angle id,
1082
+ evidence and proof plan, not only its hook. Reuse the shared video-angle-bank.v1 from
1083
+ the brand's video-scripts workspace or this run. Prepare angle-context.json with the
1084
+ writer's preparation script with `--brief working/script/creative-brief.json` and use
1085
+ its strict rule check before review. First run the fetched writer's free
1086
+ `python3 <saved-writer-package>/scripts/verify_handoff.py --package-dir <saved-writer-package> --out working/script/writer-handoff-check.json` check against those
1087
+ actual saved prepare/lint scripts. Retain the result and script hashes with the run.
1088
+ A provided writer package with a missing checker, failed check or unsupported flag
1089
+ is incompatible: stop this handoff and refresh the package on the same connection.
1090
+ Never use the provisional agent path after a provided package fails validation.
1091
+ Only an actual `not_found` for an optional writer permits the explicit provisional
1092
+ agent check below; a required writer remains blocked. Never silently remove the brief,
1093
+ strict check or new shape requirement to run an older parser.
1094
+ Save the sourced brief in the writer's documented
1095
+ shape: exact product/variant, buyer situation, supported mechanism, offer/CTA, constraints,
1096
+ delivery intent, source references, locked copy, applicable prior decisions and unknowns.
1097
+ New custom/template shapes set `requires_creative_brief: true`; both writer and critic
1098
+ receive the same angle-context. Carry that brief revision into the scene/tool plan and
1099
+ production manifest; missing evidence stays explicit rather than becoming a claim.
1100
+ - **Already decided stays decided.** The user's angle, hook, batch concept, recipe choice
1101
+ or remix direction is binding. Preserve the selected angle id. Compare executions or
1102
+ hooks inside it; do not reopen the angle choice. An incompatible promise needs a
1103
+ supported execution or an explicit format change before production.
1104
+ - **The recipe owns the production contract.** Its story mechanism, speakers, timing,
1105
+ text limits and visual capabilities win. Inspect the actual recipe and available assets;
1106
+ a catalogue card alone cannot establish fit. An impossible essential visual blocks
1107
+ the proposal until repaired. Do not force a testimonial into every format.
1108
+ - **Research only what is missing.** Fetching the miner does not spend or rerun research.
1109
+ Reuse current facts and prior evidence. The Brain search's evidence brief (citations, the
1110
+ state of each query and open gaps) is the first source for the brief's source references and
1111
+ prior decisions. If paid collection is needed, its permission
1112
+ rides in the existing choices round. A new product with no reviews can use verified
1113
+ facts and a feasible demo; do not require paid research merely to fill a quote quota.
1114
+ - **Choose eligible concepts only.** Take the strongest supported concept and validated
1115
+ hook into review, with up to two viable alternatives when the direction is open.
1116
+ Do not pad three concepts or select the highest-ranked rejected one. If all fail,
1117
+ repair within the brief and recipe before Step 3.
1118
+ - **Optional writer not found or unavailable research.** Only when the optional writer
1119
+ returns `not_found`, complete an explicit agent check of claim support, recipe limits,
1120
+ visual feasibility and hook payoff. Record missing provenance and say in the review when
1121
+ research is provisional. A provided incompatible writer or another fetch error stops
1122
+ the handoff. Never report a failed check as a pass.
1123
+ - **The user's exact lines** remain verbatim; use report-only checks and raise material
1124
+ timing, claim or format conflicts without silently rewriting them.
1125
+ - **No spoken words.** Still fit the visual promise, reveal, cards and CTA to the silent
1126
+ recipe. Skip speech writing and spoken checks, not the strategy step.
1127
+ - **Keep private provenance separate.** Store source quotes and links in the research
1128
+ workspace; put only a concise strategy explanation in remixable review data.
220
1129
 
221
- This is a **review-once** flow: put the whole review set in the app, get ONE approval, then run the
1130
+ ## Shared editorial craft inside this template flow
1131
+
1132
+ Fetch `video-production-harness` and read its `references/editorial-review.md`,
1133
+ `references/specialist-handoff.md` and `references/hook-compatibility.md`, plus the
1134
+ review/edit/polish/promote/wrap steps when used. Verify every named file is returned as
1135
+ nonempty text before relying on that package. A version/content hash alone cannot establish
1136
+ that the required guides were published. Missing guides block new template creative work
1137
+ before project writes or paid previews; resolve the package on this same connection. Keep
1138
+ an unchanged approved resume on its recorded package and approvals.
1139
+ Save the fetched version/content hash with this run's capability records. These supply
1140
+ craft and evidence rules inside the template flow; this entry's existing review, paid
1141
+ approval, storage and two-repair limit still apply.
1142
+ Do not turn a template into a custom project or import the custom host's extra gates.
1143
+
1144
+ Before executing a fetched recipe atom, pass the exact brief/script revision, scene/beat IDs,
1145
+ timing, selected product/assets/source windows, performance choice and expected output roles.
1146
+ Check actual scripts/dependencies and free runtime readiness, then validate returned media
1147
+ against that handoff. An interrupted run reuses valid approved media with matching digest;
1148
+ a file merely existing does not prove validity. A missing package/required renderer blocks
1149
+ that route before paid generation, with a concrete supported alternative for review.
1150
+
1151
+ Rough review asks what the cut communicates and whether the proof/payoff serves the hook;
1152
+ fine review diagnoses performance, sequence and local execution; final review checks each
1153
+ actual export. Save the received message from the cut before comparing with the brief where
1154
+ possible. A technically clean incoherent cut returns to script/sequence work; a local caption
1155
+ defect gets a local repair. These internal questions share existing review surfaces.
1156
+
1157
+ Before a hook change, classify same-promise compatible opening versus new-promise/body recut.
1158
+ For example, an automatic report supports “stop copying campaign results”; it does not alone
1159
+ prove “know where to spend the next dollar.” Propose required script/body/coverage changes for
1160
+ the latter. Every restyled opening gets fresh image/action/audio/text review even when words
1161
+ are unchanged. Fetch the existing `render-hook-replacement` before selecting that route;
1162
+ a not-found/empty result stops it before generation. Use its actual preflight and preservation
1163
+ checks when available; never build a duplicate renderer or infer publication from source.
1164
+ Compare complete labeled candidates, retain source/body/ending and record the hypothesis.
1165
+ No quality score or recommendation is an observed performance winner.
1166
+
1167
+ ## Step 3 — assemble the review set, then get ONE approval (before the expensive render)
1168
+
1169
+ This is a **review-once** flow: save the whole review set to the project, get ONE approval in this
1170
+ chat, then run the
222
1171
  expensive render + any remaining paid work end-to-end. Never spend on the expensive render before
223
1172
  approval, and don't drip pieces out one at a time and re-pause.
224
1173
 
@@ -231,9 +1180,10 @@ every piece three ways by cost, NOT just "free vs paid":
231
1180
  mirror the real asset. The few credits buy a real review: the user SEES the actual creator face
232
1181
  and end card and HEARS the VO, instead of judging a prompt. **This OVERRIDES any recipe rule that
233
1182
  says to gate ALL paid calls** — only the expensive render below is gated.
234
- - **EXPENSIVE paid** — the video take / final AI render (hundreds of credits) → do NOT generate.
235
- Put its **exact prompt/spec** (+ ref image URLs) in the tile. This is the ONE thing approved as a
236
- prompt (you can't preview a hundreds-of-credits video for free); it's generated only in Step 4.
1183
+ - **EXPENSIVE paid** — the video take / final AI render / per-line lipsync clips (hundreds of
1184
+ credits) → do NOT generate. Put its **exact prompt/spec** (+ ref image URLs) in the tile. This is
1185
+ the ONE thing approved as a prompt (you can't preview a hundreds-of-credits video for free); it's
1186
+ generated only in Step 4.
237
1187
 
238
1188
  **The expensive render's exact prompt must be in the panel BEFORE you ask for approval** — so a
239
1189
  single "go" runs it (plus any remaining paid work) without re-pausing mid-run.
@@ -250,27 +1200,38 @@ render (GOOSE-2542). The full video is assembled ONLY in Step 4, after approval.
250
1200
  ingredient here is only a genuinely separate SOURCE clip the format needs (e.g. supplied b-roll).
251
1201
 
252
1202
  1. **Assemble every piece the format needs — not just the script.** Read the recipe for the exact
253
- list. For an iMessage video that's the **script** (bubble thread), the **conversation image(s)**,
254
- and the **end card**; richer templates add a hook frame, background, product shots, music bed, a
255
- creator/avatar, a voiceover… For each piece, decide FREE / CHEAP-paid / EXPENSIVE-paid (above):
256
- - **FREE or CHEAP paid** (≤ ~100 credits — HTML mockups, a still, the creator frame, the end
257
- card, a short VO/music bed) → generate it now and `get_upload_url` the asset to the project
258
- folder `agent-config/brands/<brand_slug>/projects/<project_id>/working/review/<name>` (same
259
- path-prefix rule as final publish — a bare `working/review/<name>` won't render in the panel);
260
- set that piece's `path` in `script_drafts` to the project-relative `working/review/<name>`.
261
- - **EXPENSIVE paid** (the video take / final render, hundreds of credits) → do NOT generate. Put
262
- the **exact prompt/spec** (and any ref image URLs) in the tile's `text` / `subtitle` so the
263
- user reviews what will be spent on. No `path` yet — it's generated in Step 4.
264
- Include the **estimated cost in CREDITS** (never dollars) of the cheap pieces already generated +
265
- the pending render, so the user approves knowing the total spend. **Answer clarifying questions
1203
+ list. For a podcast skit that's the **script** (both hosts' lines), the two **host stills**,
1204
+ one **voice sample per host**, and the **end card**; an iMessage video has the bubble thread,
1205
+ the conversation image(s) and the end card. For each piece, decide FREE / CHEAP-paid /
1206
+ EXPENSIVE-paid (above):
1207
+ - **FREE or CHEAP paid** (≤ ~100 credits) → generate it now and upload it with
1208
+ `media_upload { brand_id, scope: "video_project", scope_id: project_id, kind, path:
1209
+ "working/review/<name>", ingredient_key, input_digest, source: { type: "file", filename:
1210
+ "<name>", content_type } }` → PUT → `media_confirm { brand_id, media_id: media.id }`;
1211
+ after confirmation succeeds, set that
1212
+ piece's `path` (+ `media_id`, `ingredient_key`) in `script_drafts` to the project-relative
1213
+ `working/review/<name>`. First check "Save as you go" — a piece already saved with the same
1214
+ digest is downloaded, not regenerated.
1215
+ - **EXPENSIVE paid** → do NOT generate. Put the **exact prompt/spec** (and any ref image URLs)
1216
+ in the tile's `text` / `subtitle` so the user reviews what will be spent on. No `path` yet —
1217
+ it's generated in Step 4.
1218
+ Save **render_estimate.total_credits**: previews already spent + pending render + the 200 base fee.
1219
+ Use current server pricing, never a catalog range in place of the full total.
1220
+ **Brand check of the script, before it goes in the panel:** every line, caption and on-screen
1221
+ text is checked against `working/brand-rules.json`. Nothing in `never_say` appears, in words or
1222
+ in meaning (a paraphrase of a banned claim is still banned). Every product detail (name,
1223
+ flavour, size, price, ingredient, result) comes from `products[]` or the kit: anything else is
1224
+ cut, not invented. Add a `note` ingredient labelled "Brand rules applied" that lists the
1225
+ pronunciations used and the rules the script respects, so the user sees them.
1226
+ **Answer clarifying questions
266
1227
  from the project brief FIRST (Step 1.5)** — only ask the user for a field (angle, which product,
267
1228
  offer/code) the `creative_brief` leaves empty AND the recipe can't default. Do not re-ask for
268
1229
  anything the composer already captured.
269
- 2. **Mirror the whole ingredient set for review** — `update_ad_project_script { project_id,
270
- script_drafts, script }`. `script_drafts` is a structured payload of **container-tagged
271
- ingredients** so the app renders each piece the right way:
272
- `{ format, scenes?, ingredients: [{ container, label, subtitle?, path?, text? }] }`. Each
273
- ingredient's `container` tells the app HOW to show it:
1230
+ 2. **Mirror the whole ingredient set for review** — `video_project_upsert { brand_id, project_id,
1231
+ patch: { script: { script_drafts, script } } }`. `script_drafts` is a structured payload of
1232
+ **container-tagged ingredients** so the app renders each piece the right way:
1233
+ `{ format, scenes?, ingredients: [{ container, label, subtitle?, path?, url?, text? }], render_estimate? }`.
1234
+ Each ingredient's `container` tells the app HOW to show it:
274
1235
  - `image` (a frame shown in the video), `endcard` (the end card), `avatar` (a character
275
1236
  headshot), `background` → rendered as an image tile.
276
1237
  - `voice` (a voiceover clip — put the voice NAME in `subtitle`), `music` (the bed),
@@ -278,147 +1239,343 @@ ingredient here is only a genuinely separate SOURCE clip the format needs (e.g.
278
1239
  - `video` (a clip) → a video player. `text` (a copy line like the CTA) → a text tile.
279
1240
  - `script` / `thread` / `note` / `conversation` → the written script (or set `scenes[]`
280
1241
  for the podcast shape, or pass the readable `script` string).
281
- `path` = `working/review/<name>` (upload the preview asset first via `get_upload_url`); `url`
282
- works too. **Label every ingredient** ("Hook image", "End card", "Voiceover", "Background
283
- music", "HER"). The `update_ad_project_script` call itself writes no render and costs no credits
284
- (the cheap pieces you already generated above have their own small cost) — it just populates the
285
- review panel.
286
- 3. **STOP — the review happens in the APP's review panel, NOT in this chat.** You've mirrored the
287
- ingredients (3.2); now hand the user the project's `app_url` (from `get_ad_project`) and tell
288
- them to review the pieces there and hit **"Approve & render"**. That button gives them a short
289
- message to paste back into this session — THAT is your go-ahead. Do NOT paste the
290
- script/ingredients into the chat for a thumbs-up, and do NOT render until that approval comes
291
- back from the app. If they want changes (via the app's comments or here), regenerate the
292
- affected ingredient, call `update_ad_project_script` again, tell them it's refreshed in the
293
- app, and wait for a fresh approval. Only AFTER the app approval do Step 4. A single approval
294
- authorises the WHOLE remaining chain — generate every paid piece, render, self-QC, publish —
295
- with NO further pauses (that is exactly why every paid prompt must already be in the panel).
296
-
297
- ## Step 4 — render locally, report stages, publish
298
-
299
- 1. Now generate every PAID piece you showed as a prompt in Step 3 — the AI stills/video, voice,
300
- music, the end-card render — through the media proxies (below), each from its approved prompt.
301
- Then assemble per the recipe (Playwright record where needed → ffmpeg stitch → `mix-master`
302
- audio).
303
- 2. Open the row LAST: `submit_render { project_id, kind: "full" }` → keep `render_id`, then
304
- `update_render_status { render_id, status: "running" }`. The render row tracks status only
305
- (queued / running / complete / failed) — narrate fine-grained progress with
306
- `append_project_message` instead.
307
- 3. **MANDATORY final-video QC gate — YOU review EVERY finished master before `set_final_render`,
308
- whatever the format (UGC or not).** This is your own automated quality check, separate from the
309
- user's Step-3 approval — it does not go back to the user. The render row is already open (its
310
- nominal credit spent, `submit_render` in 4.2);
311
- this gate stands between a rendered master and PINNING/publishing it, so a bad render never gets
312
- set as final. A master that looks fine on a still can still have a mis-voiced word, a caption
313
- drifting off its line, a beat out of order, or a deformation — review the actual VIDEO, not
314
- stills. Run the passes that APPLY to this format:
1242
+ **Label every ingredient** ("Hook image", "End card", "Voiceover", "Host A", "HER"). The upsert
1243
+ writes no render and costs no credits — it just populates the review panel.
1244
+ 3. **STOP for ONE approval, in this chat.** Only a complete current saved review set with
1245
+ render_estimate.total_credits can be approved. Widget: one line, no duplicate plan or app
1246
+ link; otherwise print card.text_summary. The choices are Approve, Change and Not now.
1247
+ “Not now” keeps the plan. In a coworker sandbox end the turn after saving the review set;
1248
+ the customer's approval arrives as the next message.
1249
+ Record the explicit yes with `video_project_upsert { brand_id, project_id, patch: { approve:
1250
+ { user_quote: "<their exact words>", total_credits: <the saved total> } } }` before rendering;
1251
+ its reply says what comes next. A single project requires recorded approval too; never treat an absent approval as permission.
1252
+ Changes require an updated saved review, clearing prior approval, and approval of the new
1253
+ total. One yes authorizes the remaining approved chain. On insufficient_balance start
1254
+ nothing; offer a shorter video or top-up. Remove batch concepts with patch.concepts remove:true,
1255
+ then show the changed total before approval.
1256
+
1257
+ ## Step 4 — render, report stages, publish
1258
+
1259
+ 1. **Open the render row FIRST** — right after recording the Step 3 approval, before any paid
1260
+ generation (if this returns `approval_required`, the approval is missing or was cleared: go
1261
+ back to Step 3 and ask in this chat, don't retry; if it returns `coworker_makes_it`, the video
1262
+ was handed to the GooseWorks coworker, so don't render it: the coworker is making it):
1263
+ `video_render_run { brand_id, project_id, kind: "full" }` (no `dry_run`; returns
1264
+ `render_id`) → keep `render_id`, then mark it running:
1265
+ `video_render_run { brand_id, project_id, render: { render_id, status: "running",
1266
+ workflow_stage: "preparing", progress_note: "starting", progress_percent: 5 } }`. The user sees this
1267
+ live in the app and gets a WhatsApp "started" message automatically — don't message them yourself
1268
+ about start / blocked / complete.
1269
+ After EVERY progress callback inspect stop. If true, start no new paid step; record
1270
+ status:"stopped" with a plain note and report what is kept and credits used. SPEND_CAP_REACHED
1271
+ stops the same way. Never raise a video's budget on your own judgment. Raise it only when a paid
1272
+ step was refused with SPEND_CAP_REACHED or the progress card offers "Finish it" or a choice past the budget.
1273
+ Then read the project and say cost.raise_quote in one line: "Finishing needs up to <by_credits>
1274
+ more credits, so your budget goes from <from_credits> to <to_credits>. OK?" Only after the
1275
+ customer's yes to that number: patch.approve { scope:"raise_cap", total_credits: <to_credits>,
1276
+ user_quote: "<their exact words>" }. total_credits is the new budget itself, never credits to
1277
+ add; do not send raise_cap_credits for one video. If they pick a smaller choice past the
1278
+ budget, send that choice's new budget as total_credits (never above to_credits). Sending the same
1279
+ total_credits again changes nothing (already_raised: carry on). An earlier yes, or "I don't care
1280
+ about the cost", is not approval of a new budget. If cost.raise_quote is null because the plan was saved
1281
+ again since its approval (plan_status is not "approved"), show the plan with its total and
1282
+ approve it again; otherwise the approved budget already covers the work: carry on, do not ask.
1283
+ After a raise, open a new render that reuses the saved pieces. A refused raise changes nothing:
1284
+ cap_raise_not_needed means carry on within the budget, or, when it says the plan was saved again,
1285
+ show the plan with its total and approve the plan again; cap_raise_total_required means read the
1286
+ project again and send total_credits equal to cost.raise_quote.to_credits after the customer
1287
+ approves that number; cap_raise_changed or cap_raise_too_large mean read the project again and
1288
+ show the current cost.raise_quote. Custom videos never use raise_cap.
1289
+ Send render.steps with the same neutral names each time and a live count only in the
1290
+ current detail; use render.choices when blocked.
1291
+ 2. Now generate every PAID piece you showed as a prompt in Step 3 — the AI stills/video, lipsync
1292
+ clips, voice, music — through the media proxies (below), each from its approved prompt, with
1293
+ `GW_PROJECT_ID` exported. **Save as you go** (section above): skip any piece already saved
1294
+ with the same `input_digest` (download it), and upload each new piece with its
1295
+ `ingredient_key` + `input_digest` the moment it passes QC.
1296
+ Keep the approved script and review unchanged throughout production and QC; confirmed media
1297
+ and progress preserve resume state. Stop and replan/reapprove any material creative change.
1298
+ A voiceover made with `data_post_provider` (ElevenLabs `…/with-timestamps`) returns its
1299
+ `alignment` only in the reply: write it to `working/vo/<scene>.timestamps.json` at once
1300
+ (captions are timed from it) and record the returned `media_id` on the ingredient.
1301
+ **Brand pronunciations in every voiceover:** the text sent to the voice has each
1302
+ `pronunciations[].term` replaced by its `say_as` (`create-vo-elevenlabs`:
1303
+ `gen_vo.py … --rules working/brand-rules.json`; when a render atom calls the voice itself,
1304
+ swap the terms in the text you hand it). Captions, on-screen text and the review keep the
1305
+ written name. Write `working/approved-script.txt` (the Step 4.3 audio check) with the SPOKEN
1306
+ form. **Logo, font, product:** every logo is the file from Step 1.7 composited as-is; every
1307
+ product shot uses the Step 1.7 product images as its reference.
1308
+ Then assemble per the recipe (ffmpeg stitch; PIL captions / end card;
1309
+ Playwright record only where the format needs it and the host has Chromium → `mix-master` audio).
1310
+ **Report progress at each milestone** — about one update per milestone, never per poll:
1311
+ `video_render_run { brand_id, project_id, render: { render_id, status: "running", workflow_stage, progress_note, progress_percent } }`
1312
+ (`progress_note` = plain words the customer reads in the app, ≤200 chars; no model, tool or
1313
+ step names):
1314
+ - voiceovers done → `"preparing"`, `"voices recorded"`, 20
1315
+ - stills done → `"preparing"`, `"scenes designed"`, 35
1316
+ - each lipsync / video clip → `"rendering"`, e.g. `"filming scene 5 of 8"`, 35–75
1317
+ - assembly → `"rendering"`, `"putting the video together"`, 85
1318
+ - QC (4.3) → `"final check"`, 95
1319
+ **Hard stop that needs the user** → `{ render_id, status: "running", workflow_stage: "blocked",
1320
+ error_message: "<what's wrong + what you need>" }`; an unrecoverable failure → `status: "failed"`
1321
+ + `error_message`. Completing (4.4) sets the bar to 100.
1322
+ 3. **MANDATORY final-video QC gate — YOU review EVERY finished master before pinning it
1323
+ (`patch.final_render_id`), whatever the format (UGC or not).** This is your own automated quality
1324
+ check, separate from the user's Step-3 approval — it does not go back to the user. The render row
1325
+ is already open (4.1); this gate stands between a rendered master and PINNING/publishing it, so a
1326
+ bad render never gets set as final. A master that looks fine on a still can still have a
1327
+ mis-voiced word, a caption drifting off its line, a beat out of order, or a deformation — review
1328
+ the actual VIDEO, not stills. Run the passes that APPLY to this format:
315
1329
  - **Audio ↔ script** — any master with SPEECH (VO or native/Seedance voice); **skip for
316
1330
  music-only / no-speech formats.** `review-ugc-render` is format-agnostic despite the name —
317
- a deterministic Whisper transcript-vs-script diff, not UGC-specific: persist the approved
318
- spoken lines to `working/approved-script.txt`, then `gooseworks fetch review-ugc-render` and
319
- run `review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json
320
- working/review-verdict.json` (exit 0 PASS / 2 FAIL / 3 ERROR). It blocks a mis-voiced word
1331
+ a deterministic Whisper transcript-vs-script diff: persist the approved spoken lines to
1332
+ `working/approved-script.txt`, fetch `review-ugc-render` (`catalog_fetch`) and run
1333
+ `review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json
1334
+ working/review-verdict.json` (exit 0 PASS / 2 FAIL / 3 ERROR). For each brand pronunciation add
1335
+ `--brand-term "<term>"`, plus `--brand-term` for each word of `say_as` that is not an
1336
+ everyday word (the flag strips those tokens from the WHOLE diff, so never pass "a", "one",
1337
+ "works" alone): Whisper spells a respelled name back as the brand word ("Goose Works" heard
1338
+ as "Gooseworks"), so the diff must accept it. The proof of
1339
+ intended delivery is the text/settings you sent (keep them in the review); actual
1340
+ pronunciation/performance requires isolated and mixed-audio listening at normal speed.
1341
+ Preserve pauses/emphasis and final consonants; never mandate faster or louder delivery.
1342
+ Verify a transcript mismatch against the actual audio before calling it a defect. It flags a mis-voiced word
321
1343
  (approved "human-vetted" → "human witted"), a dropped phrase, or silence. It routes Whisper
322
- through the gooseworks proxy when `OPENAI_BASE_URL` is set; with no backend at all, run
323
- `fal-ai/whisper` via `fal-proxy` (upload the audio, pass its `get_download_url` as `audio_url`)
1344
+ through the gooseworks proxy when `OPENAI_BASE_URL` is set (sandbox:
1345
+ `$GW_WHISPER_PROXY_URL/v1`); with no backend at all, run `fal-ai/whisper` via the FAL proxy
324
1346
  and diff the transcript yourself.
325
1347
  - **Captions / subtitles** — ANY captioned format (the most common non-UGC defect); **skip for
326
- UGC/Seedance masters, which carry no subtitle track.** Concrete check: diff the caption file
327
- you burned (SRT/ASS) against the SAME Whisper transcript + word timings from the audio pass —
328
- every caption line must match the heard/scripted words and sit within ~0.3s of when they're
329
- spoken; then in the visual pass below, OCR-read the burned caption off 4–5 sampled frames to
330
- confirm it's actually on screen at that time and not colliding with a hyperframe or the end
331
- card. Mismatched text or >0.3s drift fails the gate.
1348
+ UGC/Seedance masters, which carry no subtitle track.** Diff the caption file you burned
1349
+ (SRT/ASS/PNG cue list) against the SAME Whisper transcript + word timings — every caption line
1350
+ must match the heard/scripted words and sit within ~0.3s of when they're spoken; then in the
1351
+ visual pass below, inspect each cue and its start/end boundaries in actual frames, plus
1352
+ full-speed playback at destination size, to confirm reading time, product visibility,
1353
+ hierarchy and no duplicate overlay or end-card collision. Silent/text-led formats use
1354
+ approved visible wording and reading windows, not nonexistent speech timings. Mismatched text or >0.3s drift fails the gate.
332
1355
  - **Visual + structure** — always: run the `watch` skill on the master — beat/scene order + SFX,
333
- the brand's product (not the source's) is shown, the end card has the real wordmark + code, no
1356
+ 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
334
1357
  deformation/artifact, duration within ~20% of the source.
335
- If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, rebuild captions)
336
- and re-review — only a clean pass proceeds to `set_final_render`. **This gate is universal: it
337
- runs from the master skill for every format, so a recipe never has to opt in.**
338
- 4. Publish: `get_upload_url { target: { type: "agent", agent_id: ADS_AGENT } }` → PUT the master
339
- and poster **under the project folder** (see Identity's path-prefix rule) — to
340
- `agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4` and
341
- `.../working/final-thumb.jpg`. **Always target ADS_AGENT AND use the full project-folder path**
342
- — a bare `working/final.mp4`, even on the right agent, 404s in the app. Verify servable:
343
- `get_download_url { target: ADS_AGENT, path: "agent-config/brands/<brand_slug>/projects/<project_id>/working/final.mp4" }`
344
- must return a non-empty URL (curl it for HTTP 200).
345
- Then `update_render_status { render_id, status: "complete", output_url, thumbnail_url }` where
1358
+ - **Finished ad + brand fidelity** — always: fetch `review-finished-ad` (`catalog_fetch`,
1359
+ `pip install --quiet numpy pillow` if needed) and run
1360
+ `review_finished_ad.py --video <master>.mp4 --json working/review/finished-ad.json
1361
+ --sheet working/review/finished-ad-sheet.png --logo <the logo file the video composites>
1362
+ --palette "<kit palette, comma-separated>" --product-images <the Step 1.7 images>
1363
+ --font <brand font file> --brand-name "<name>" --endcard-s <end card length>`. Pass the
1364
+ file that is ACTUALLY on screen (the kit logo, or the recipe's wordmark file). When the video
1365
+ shows only the brand name as text (`"logo": {"mode": "text"}`), omit `--logo` and judge the
1366
+ text on the sheet. Add `--no-speech` for a format with no VO or dialogue, `--logo-at <s>` for
1367
+ each mid-video logo, and any gate flags the recipe's instructions name (a chat format's
1368
+ reading holds need a longer `--max-freeze-s`). Exit 0 PASS / 2 FAIL / 3 ERROR. It
1369
+ checks size, hook, pacing, dead air, black frames, the kit logo on the end card
1370
+ (a wrong, redrawn or favicon logo fails) and the palette. **Then open the sheet it writes
1371
+ and judge every line of its `judge_on_sheet`:** captions/CTA/logo outside the red safe
1372
+ zones, the brand font, every product shot matching the product images, the product the same
1373
+ in every scene, the logo unaltered. A failed eye check is a FAIL like any other.
1374
+ - **Output size** — always **1080×1920 (9:16)**. Video ads are only ever 9:16. Lipsync / video
1375
+ models often return 720p or odd sizes — scale (and pad if the aspect differs) every clip to
1376
+ 1080×1920 BEFORE the concat, never ship the model's native size.
1377
+ Save exact output checksum/version, first-to-last normal-speed playback and full-audio
1378
+ coverage. Frame sheets, transcripts and process success do not substitute for listening or
1379
+ continuous motion. Missing required capability leaves review incomplete/blocked. Each
1380
+ supported crop, duration, language or caption derivative needs its own actual-file review;
1381
+ unsupported destination requirements need a reviewed route, never a silent export change.
1382
+ Honor intended silence, loops and endings rather than forcing music or fades on every ad.
1383
+ If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, re-composite the
1384
+ end card from the real logo file, rebuild captions) and re-run the passes — only a clean pass
1385
+ proceeds to pinning. **At most 2 repair rounds.** If a pass still fails after them, do NOT pin
1386
+ and do NOT present the video as finished: upload it (4.4) and close the row with exactly
1387
+ `video_render_run { brand_id, project_id, render: { render_id, status: "complete",
1388
+ workflow_stage: "blocked", quality_status: "blocked", repair_pass_count: 2, output_url,
1389
+ thumbnail_url, quality_report } }` (the failing checks as `fail` in the report), and tell the user plainly in
1390
+ chat which checks failed, what you tried, and the choices (fix a specific thing, re-roll, or
1391
+ use it anyway). Pin it only if they say to use it anyway. The app shows a blocked render as
1392
+ "Needs attention". **This gate is universal: it runs from this skill for every format, so a
1393
+ recipe never has to opt in.**
1394
+ 4. **For a passing final, create the finished-video checkpoint first**, following "Finished video, interrupted saving"
1395
+ above. With the CLI, use its prepare/resume flow instead of duplicating the writes below; it
1396
+ saves the same already-opened render and pins only the checked output. Save the final review set
1397
+ and production manifest (Step 4.5/4.6) before preparing that checkpoint. Without it, follow the
1398
+ same durable protocol. A blocked version follows Step 4.3's existing upload/report-only path;
1399
+ never route it through the passing-final helper or pin it automatically.
1400
+ Publish: `media_upload { brand_id, scope: "video_project", scope_id: project_id, kind: "render",
1401
+ path: "working/final.mp4", ingredient_key: "final", input_digest, source: { type: "file", filename: "final.mp4", content_type:
1402
+ "video/mp4" } }` → PUT the master to `upload.url` with `upload.required_headers` →
1403
+ `media_confirm { brand_id, media_id: media.id }`. Same for the poster (`kind: "thumbnail"`, `path: "working/final-thumb.jpg"`, `ingredient_key: "final-thumb"`).
1404
+ Keep each `upload.render_file_url`. Verify the PUT returned 2xx and the file you uploaded is a
1405
+ real, non-empty MP4 (ffprobe it), and require both confirmations to succeed BEFORE marking
1406
+ the render complete.
1407
+ Then `video_render_run { brand_id, project_id, render: { render_id, status: "complete", output_url, thumbnail_url } }` (attach the Step 4.3 verdict as `quality_status: "passed"` (or
1408
+ `"blocked"` when the gate still fails after 2 repair rounds) + `quality_report` — ALWAYS
1409
+ attach it; a batch concept cannot complete without a passing one; exact shape, strict (no extra keys):
1410
+ `{ version: 1, summary: string, checks: { source, brand, product, hook_and_scene_order,
1411
+ voice_and_script, captions, endcard_and_cta, duration_and_ratio, visual_artifacts }, detected_issues?:
1412
+ string[], repair_actions?: string[] }` where EVERY check is `{ status: "pass"|"fail"|"not_applicable",
1413
+ note?: string }`). Fill each check from the Step 4.3 passes: `brand` ← logo_asset + logo +
1414
+ palette + font + logo unaltered; `product` ← product likeness + product consistency;
1415
+ `hook_and_scene_order` ← hook + pacing + the `watch` beat order; `voice_and_script` ← the
1416
+ Whisper diff (pronunciations included) + no `never_say` line; `captions` ← the caption diff +
1417
+ safe zones; `duration_and_ratio` ← ratio + duration; `visual_artifacts` ← black frames +
1418
+ frozen stretches + the `watch` pass; `endcard_and_cta` ← the end card holds the real logo +
1419
+ CTA. Put each failing check's note in `detected_issues` and each fix in `repair_actions`.
346
1420
  **output_url MUST be the durable render-file URL**
347
- `/api/ads/projects/<project_id>/render-file?path=working/final.mp4` (the app re-presigns it on
348
- every view) — NEVER a raw proxy/CDN URL (those expire). Same for `thumbnail_url`.
349
- 5. `set_final_render { project_id, render_id }` to pin it, then return the `app_url` +
350
- `brand_url` (from the project/links) verbatim. Never end on just "done" or a file path.
1421
+ (`upload.render_file_url`, i.e. `/api/ads/projects/<project_id>/render-file?path=working/final.mp4`
1422
+ — the app re-presigns it on every view) — NEVER a raw proxy/CDN/presigned URL (those expire).
1423
+ Same for `thumbnail_url`.
1424
+ 5. **Save the final review set BEFORE pinning** — it must describe the video you actually rendered.
1425
+ Only after all paid production, QC, repairs and pending provider work are finished and settled,
1426
+ save one final descriptive/provenance update: confirmed pieces, exact settings and repairs that
1427
+ preserve the approved creative choices. Material creative changes, including changed lines,
1428
+ look, timing, captions or music, require a changed complete plan, total and fresh approval BEFORE
1429
+ the changed production. Never relabel a changed plan as provenance or silently reuse approval.
1430
+ In the checkpoint flow, save the final review before Step 4.4; do not rewrite it afterward merely
1431
+ to follow the numbered order. Resume checks that this reviewed set stayed unchanged.
1432
+ When final provenance needs updating, upsert the review set once:
1433
+ `video_project_upsert { brand_id, project_id, patch: { script: {
1434
+ script_drafts, script } } }` with the final lines, final pieces (mark generated takes as done, not
1435
+ "not generated yet") and the settings you used. The project keeps this, not your chat: it is
1436
+ what the app shows, and what a Community remix of this video copies. Instructions that live only
1437
+ in this conversation are lost when it ends. **Keep the approved detail**: never shorten a piece to a
1438
+ summary (e.g. per-scene prompts or their "no shake / no letterbox" guards). Only update what changed.
1439
+ After this write, do not start new paid work. Save the production manifest, prepare the final
1440
+ checkpoint and complete the same render with its existing finishing allowance and guards.
1441
+ 6. **Save the production manifest** — HOW you made it, so the next run (or a remix) starts from what
1442
+ worked instead of rediscovering it: `video_project_upsert { brand_id, project_id, patch: { production:
1443
+ { version: 1, pipeline: [{ step, model, purpose?, settings? }], style: { prompt, negative, notes? },
1444
+ characters: [{ name, prompt }], scenes: [{ id, line, still_prompt, motion_prompt, duration_s }],
1445
+ voice: { voice_id, name, model, settings }, music: { prompt, model, length_s }, assembly: { … },
1446
+ fixes: [{ problem, fix }], notes? } } }`. Use the exact models and prompts you sent (full text, guards
1447
+ included), and list every fix you had to make (e.g. "VO ran 47s → tightened four lines, voice 1.08x";
1448
+ "hair drifted → restated the hair colour"). No URLs, keys or raw logs; it must stay under 48 KB.
1449
+ In the checkpoint flow, save this before Step 4.4 too and skip the duplicate write afterward.
1450
+ 7. Pin it — only a `passed` render (or a `blocked` one the user said to use anyway):
1451
+ If checkpoint recovery already verified selection of this render, skip this duplicate write.
1452
+ An unfinished checkpoint must use the guarded selection protocol above; never replace it
1453
+ with an unguarded pin.
1454
+ `video_project_upsert { brand_id, project_id, patch: { final_render_id: render_id } }`,
1455
+ then use the returned card: widget hosts get one line and no duplicate links; text hosts get
1456
+ card.text_summary and the returned delivery links verbatim. Never end with a local file path.
351
1457
 
352
- Narrate each long step in one line via `append_project_message { project_id, role: "agent",
353
- content }` — never sit silent on a queue > 90s.
1458
+ Narrate each long step in one line via `video_project_upsert { brand_id, project_id, patch:
1459
+ { message: { role: "agent", content } } }` — never sit silent on a queue > 90s. Write it for the
1460
+ customer ("Filming the scenes, about 5 more minutes"), per "How to talk to the customer" above.
354
1461
 
355
1462
  ## Media generation — the GooseWorks proxies (queue loop)
356
1463
 
357
- Media APIs go through GooseWorks proxies with your agent token; do NOT use an SDK's default host
358
- (your token isn't a FAL/ElevenLabs token → 401). Base = `<api_base>/api/internal/<proxy>`; pass
359
- `?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>` (agent_id bills the Ads agent;
360
- `project_id` = the id of the project you're rendering — it attributes this generation's credits to
361
- that ad project so the user sees per-project spend in the app. ALWAYS pass it). FAL = `fal-proxy`
362
- (+ `fal-storage-proxy` to host a local image and get a CDN URL); ElevenLabs = `elevenlabs-proxy`
363
- (VO / music bed).
1464
+ Media APIs go through GooseWorks proxies; do NOT use an SDK's default host (your token isn't a
1465
+ FAL/ElevenLabs token → 401, and in a sandbox a raw provider call is never allowed). Prefer the
1466
+ atoms' own `media_proxy.py` (`fal_generate`, `fal_generate_video`, `eleven_music`, …) — it already
1467
+ handles auth, the queue host-swap, project attribution (`GW_PROJECT_ID`) and failure logging.
1468
+
1469
+ - **Sandbox:** FAL base = `$GW_FAL_PROXY_URL`, ElevenLabs = `$GW_ELEVENLABS_PROXY_URL`, FAL
1470
+ storage = `$GW_FAL_STORAGE_PROXY_URL`; auth = `?token=$GW_MEDIA_PROXY_TOKEN` (or
1471
+ `Authorization: Bearer`), plus `&project_id=$GW_PROJECT_ID`. The token already names the agent.
1472
+ - **Local:** base = `<api_base>/api/internal/<proxy>` (`fal-proxy`, `fal-storage-proxy`,
1473
+ `elevenlabs-proxy`); pass `?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>`
1474
+ (agent_id bills the Ads agent; project_id attributes the spend to this ad project so the user
1475
+ sees per-project spend in the app. ALWAYS pass it).
364
1476
 
365
1477
  **FAL queue gotcha** (#1 waste of generations): submit returns `status_url`/`response_url` on
366
1478
  `queue.fal.run` (the real host, not the proxy). Polling those 401s forever — rewrite their host
367
- to the proxy base (keep the path), re-add `?token=&agent_id=`. Only the final `*.fal.media`
368
- image is a real public URL. Helper:
1479
+ to the proxy base (keep the path), re-add the auth params. Only the final `*.fal.media`
1480
+ file is a real public URL. Helper (works in both modes):
369
1481
 
370
1482
  ```python
371
1483
  import json, os, pathlib, time, requests
372
1484
  from urllib.parse import urlparse
373
1485
 
374
1486
  def _cfg():
1487
+ """(fal_base, token, agent_id). Sandbox env first, then ~/.gooseworks/credentials.json."""
1488
+ tok = os.environ.get("GW_MEDIA_PROXY_TOKEN")
1489
+ if tok:
1490
+ base = os.environ.get("GW_FAL_PROXY_URL") or os.environ["GW_API_BASE"].rstrip("/") + "/api/internal/fal-proxy"
1491
+ return base.rstrip("/"), tok, None
375
1492
  c = json.loads(pathlib.Path(os.path.expanduser("~/.gooseworks/credentials.json")).read_text())
376
- return c["api_base"].rstrip("/"), c["api_key"], c.get("agent_id")
1493
+ return c["api_base"].rstrip("/") + "/api/internal/fal-proxy", c["api_key"], c.get("agent_id")
377
1494
 
378
- def _params(tok, agent, project_id=None):
1495
+ def _params(tok, agent):
379
1496
  p = {"token": tok}
380
1497
  if agent: p["agent_id"] = agent
381
- if project_id: p["project_id"] = project_id # attributes the spend to this ad project
1498
+ pid = os.environ.get("GW_PROJECT_ID")
1499
+ if pid: p["project_id"] = pid # attributes the spend to this ad project
382
1500
  return p
383
1501
 
384
- def fal_generate(model_path, payload, project_id=None, timeout_s=180, poll_s=3):
1502
+ def fal_generate(model_path, payload, timeout_s=180, poll_s=3):
385
1503
  """model_path e.g. 'fal-ai/nano-banana-2/edit' (the recipe names the model).
386
- Pass project_id = the ad project you're rendering so credits attribute to it.
387
- Returns the result image URL (a public *.fal.media CDN URL)."""
388
- api_base, tok, agent = _cfg()
389
- base = api_base + "/api/internal/fal-proxy"
390
- sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent, project_id), json=payload).json()
1504
+ Export GW_PROJECT_ID first. Returns the result image URL (a public *.fal.media URL)."""
1505
+ base, tok, agent = _cfg()
1506
+ sub = requests.post(f"{base}/{model_path}", params=_params(tok, agent), json=payload).json()
391
1507
  to_proxy = lambda u: base + urlparse(u).path
392
1508
  status_url, response_url = to_proxy(sub["status_url"]), to_proxy(sub["response_url"])
393
1509
  deadline = time.time() + timeout_s
394
1510
  while time.time() < deadline:
395
- st = requests.get(status_url, params=_params(tok, agent, project_id)).json()
1511
+ st = requests.get(status_url, params=_params(tok, agent)).json()
396
1512
  if st.get("status") == "COMPLETED":
397
- return requests.get(response_url, params=_params(tok, agent, project_id)).json()["images"][0]["url"]
1513
+ return requests.get(response_url, params=_params(tok, agent)).json()["images"][0]["url"]
398
1514
  if st.get("status") in ("FAILED", "ERROR"):
399
1515
  raise RuntimeError(f"FAL failed: {st}")
400
1516
  time.sleep(poll_s)
401
1517
  raise TimeoutError("FAL polling exceeded timeout")
402
1518
  ```
403
1519
 
404
- ElevenLabs (VO / music) is the same shape against `<api_base>/api/internal/elevenlabs-proxy`
405
- with `?token=&agent_id=&project_id=`. Feed FAL a local image by storing it (`get_upload_url`) and passing its
406
- `get_download_url` presigned URL as an `image_urls` / `audio_url` entry — this is the reliable
407
- path. (`fal-storage-proxy` may 404 depending on the install; don't block on it — prefer the
408
- `get_download_url` presigned URL.)
1520
+ ElevenLabs (VO / music) is the same shape against the ElevenLabs proxy base. To feed FAL a local
1521
+ file (a product image, a VO track), it must be a PUBLIC URL: upload it with `media_upload`
1522
+ (`scope: "video_project"`, source `file`, no `path`) → PUT → `media_confirm` and use the returned
1523
+ `media.url` if it is a public https URL (curl it: HTTP 200 without auth), or host it through the
1524
+ FAL storage proxy. Never pass a `render-file` URL to a provider — it needs app auth.
1525
+
1526
+ ## Follow-up actions use the watched version
1527
+
1528
+ Keep original note text, exact source render/time and intended viewer effect. Record an
1529
+ acceptance condition and disposition in existing feedback/project artifacts; applying a
1530
+ command means changed, not verified. Consolidate duplicates without deleting their source
1531
+ IDs. Conflicting “calmer” and “more urgent” instructions need the authorized decision owner's
1532
+ recorded choice, even if they touch different files. Offer concrete cheap alternatives for
1533
+ “more premium” inside the approved scope before escalating taste.
1534
+
1535
+ Map changed copy/performance/shot/pause to affected picture, timings, captions, music/SFX,
1536
+ approvals and derivatives; preserve unrelated approved work. Rewatch the full candidate and
1537
+ verify the original effect before resolving a note. Accepted final, candidate and original
1538
+ note source remain distinct across selection and rollback. Keep editable script/timeline,
1539
+ source assets/takes, stems, captions, package versions and prior decisions in the existing
1540
+ production manifest/history; retrieve applicable rejections/pronunciation/delivery rules
1541
+ before a related brief. Project taste is not a standing brand rule. Record audience results
1542
+ only with actual variant/audience/placement/objective/window; otherwise unavailable.
1543
+
1544
+ - “Fix this”: use the render/project context attached to the message (or named version, else
1545
+ the final one). Save patch.fix { of_render_id, changes, total_credits }. Never replace a supplied
1546
+ watched render with the final. After explicit approval save patch.approve { user_quote,
1547
+ total_credits, scope:"fix" }, then video_render_run { kind:"partial", fix_of_render_id }.
1548
+ Re-make only changed scenes, keep earlier versions and charge no second base fee.
1549
+ - “Make 3 more like this”: video_project_upsert { brand_id, name, remix_of_project_id,
1550
+ remix_of_render_id, vary, concepts:3 }. Preserve a provided watched render; omit only when none is named.
1551
+ - “Test 1 of each”: approve only:"test"; “Make the other N”: only:"rest". “Stop the rest”:
1552
+ job_cancel { job_id:<batch_id> }. Finished videos stay saved.
1553
+ - Share for review uses patch.share_for_review { subject }; return the real link.
409
1554
 
410
1555
  ## Rules
411
1556
 
412
- - **MCP + ffmpeg + Playwright required** — run `gooseworks doctor` in Phase 0; stop with the
413
- exact fix it prints if anything is ✗.
414
- - **Assemble the whole review set first**, mirror it with `update_ad_project_script`, and get the
415
- user's approval **in the app's review panel** (the "Approve & render" button) BEFORE the expensive
416
- render — never ask for a thumbs-up in this chat (review-once, in-app).
1557
+ - **Canonical MCP tools first** (`video_project_read`, `video_project_upsert`, `catalog_fetch`,
1558
+ `media_upload` + `media_confirm`, the `video_render_run` render-row actions, `account_whoami`);
1559
+ legacy names only when the client lacks the canonical tool.
1560
+ - **A missing key is never a blocker.** Paid media goes through the proxy: a one-off image or clip
1561
+ is `data_post_provider { provider: "fal", … }` + `job_get` (see "Paid media over the MCP");
1562
+ never ask for FAL_KEY, never use `photos_generate` for anything but a physical product.
1563
+ - **The CLI and credentials.json are optional.** In a GooseWorks sandbox (`GW_MEDIA_PROXY_TOKEN`
1564
+ set) use the env proxies and `catalog_fetch`; never call a provider with a raw key.
1565
+ - **No Chromium in a sandbox** — a browser-rendered format stops there, before any spend, and says so.
1566
+ - **Toolchain before spend** — common checks use `gooseworks doctor --no-browser`; every selected
1567
+ browser renderer must also pass `--renderer-script` or its equivalent exact-runtime launch
1568
+ check in Step 2. Stop with the folder-specific fix if anything is missing.
1569
+ - **Assemble the whole review set first**, mirror it with `video_project_upsert patch.script`, and
1570
+ get ONE approval in this chat, recorded with `patch.approve`, BEFORE the expensive render
1571
+ (review-once). Never send the user to the app to approve; the app is only for reviewing.
417
1572
  - **Show the REAL cheap pieces; PROMPT only the expensive render.** Generate the FREE + CHEAP-paid
418
1573
  pieces (≤ ~100 credits — stills, creator frame, end card, short VO/music) and mirror the real
419
1574
  assets; put ONLY the expensive video take/render in the panel as its exact prompt. That prompt
420
1575
  must be in the panel before you ask to approve, so a single "go" runs the render + any remaining
421
1576
  paid work (→ QC → publish) with no re-pausing.
1577
+ - **Operator may be absent (sandbox):** decide mechanical choices yourself; ask in chat only for a
1578
+ true taste call.
422
1579
  - **Costs in CREDITS, never dollars.** credits ≈ round-up(provider-$ × 120) per generation + a flat
423
1580
  200-credit base per video; never show a "$…" figure to the user.
424
1581
  - **Never assemble the full video before approval.** The review shows the
@@ -426,15 +1583,31 @@ path. (`fal-storage-proxy` may 404 depending on the install; don't block on it
426
1583
  stitched/composited cut; do not add a "full cascade" / finished-video clip
427
1584
  as a review ingredient (GOOSE-2542). The assembled video is produced only in
428
1585
  Step 4.
429
- - **submit_render only after the master is rendered** (Step 4.2), never on a guess; `output_url` =
430
- the durable render-file URL, never a CDN URL.
431
- - **Always pass `project_id` on media-proxy calls** (fal / ElevenLabs) so the credits attribute
432
- to this ad project — that's what lets the user see per-project spend in the app.
1586
+ - **Open the render row at the start of Step 4** (4.1, after the approval) and keep its stage /
1587
+ progress current; mark it `complete` only after the master passes QC. `output_url` = the
1588
+ durable render-file URL, never a CDN URL.
1589
+ - **Always export `GW_PROJECT_ID`** (and pass `project_id` on hand-rolled proxy calls) so the
1590
+ credits attribute to this ad project.
433
1591
  - **Verify a real, non-empty MP4** (watch it) before marking the render complete.
434
1592
  - **Reuse the brand** when its research is complete; never re-research.
1593
+ - **Brand rules first:** load kit + products + learnings (Step 1), write `working/brand-rules.json`
1594
+ (Step 1.7), and follow it: pronunciations in every voiceover, nothing from `never_say`, product
1595
+ facts only from the product rows, the kit's logo FILE (never a generated or favicon logo), the
1596
+ brand font, this product's own images.
1597
+ - **Save brand corrections** from chat with `brand_update patch.facts` in the same turn, and say so.
1598
+ - **Finished-ad gate on every master** (`review-finished-ad` + the sheet); at most 2 repair
1599
+ rounds, then publish as `blocked` with the report and warn the user — never pass off a failing
1600
+ video as finished.
435
1601
  - On a hard error (auth/quota/model/timeout) set the render `failed` with a short
436
- `error_message` and stop — don't ship the source unchanged. **Also `log` it** (`gooseworks log`
437
- / `log_cli_event`, `--event-type api_failure|error`) so we can see + fix it (see "Report problems").
438
- - **Report blockers/bugs/confusing instructions via telemetry** (`gooseworks log` or the
439
- `log_cli_event` MCP tool) — not just to the user. Set `GW_RUN_ID` once so events group.
440
- - Always end a successful run with `app_url` + `brand_url`, verbatim.
1602
+ `error_message` (`video_render_run { …, render: { render_id, status: "failed", error_message } }`) and stop — don't ship the source unchanged. **Also log
1603
+ it** (see "Report problems") so we can see + fix it.
1604
+ - Finish according to card.display_hint: widget hosts get one short line without duplicate links;
1605
+ text hosts get card.text_summary and the returned delivery links verbatim.
1606
+
1607
+ ## Use uploaded footage
1608
+
1609
+ When the customer asks to use existing footage, check the actual advertised schemas. Use `media_search` with purpose `production`, kind `video`, the brand and current project when needed. Omit query spend for free retrieval. Read facts/scenes with `media_analyze`, then use `media_excerpt` action `inspect` to review actual bounded frames and timed transcript before selecting. A description or thumbnail URL alone is not visual review.
1610
+
1611
+ Freeze `source_excerpt:{asset_id,analysis_revision,scene_id,start_ms,end_ms,audio_mode}`. Revision is the original-byte SHA, scene may be null for a known user trim, bounds use integer milliseconds, and audio is original or muted. Call `media_excerpt` action `attach` with that exact selection and a stable idempotency key. The returned project remains unfinished and unapproved. Open its existing Studio review; preserve user locks and requested format, and keep footage separate from image packshot indexes. An incompatible format requires an explicit choice before changing it.
1612
+
1613
+ For custom production, fetch the current shared `video-production-harness` and follow script/ingredient gates. Download the verified original and trim its exact selected window at normal speed with the approved audio. Recheck current revision and production permission before consumption and final upload/completion. Save excerpt lineage in plan and ingredient readback. Missing optional semantics does not require another upload; known user-selected stored footage remains usable. Research and competitor references never become production footage. Selected originals require no paid generation; only newly generated/replaced ingredients consume the approved budget.