gooseworks 0.5.8 → 0.5.9-dev.56.eca9486d

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 (188) hide show
  1. package/dist/agents/claude-mcp.d.ts +7 -1
  2. package/dist/agents/claude-mcp.d.ts.map +1 -1
  3. package/dist/agents/claude-mcp.js +43 -10
  4. package/dist/agents/claude-mcp.js.map +1 -1
  5. package/dist/auth/credentials.d.ts +8 -0
  6. package/dist/auth/credentials.d.ts.map +1 -1
  7. package/dist/auth/credentials.js +35 -1
  8. package/dist/auth/credentials.js.map +1 -1
  9. package/dist/auth/device-flow.d.ts +4 -1
  10. package/dist/auth/device-flow.d.ts.map +1 -1
  11. package/dist/auth/device-flow.js +8 -3
  12. package/dist/auth/device-flow.js.map +1 -1
  13. package/dist/auth/oauth-server.d.ts +9 -1
  14. package/dist/auth/oauth-server.d.ts.map +1 -1
  15. package/dist/auth/oauth-server.js +19 -11
  16. package/dist/auth/oauth-server.js.map +1 -1
  17. package/dist/auth/revoke.d.ts +10 -0
  18. package/dist/auth/revoke.d.ts.map +1 -0
  19. package/dist/auth/revoke.js +31 -0
  20. package/dist/auth/revoke.js.map +1 -0
  21. package/dist/commands/login.d.ts +15 -1
  22. package/dist/commands/login.d.ts.map +1 -1
  23. package/dist/commands/login.js +34 -10
  24. package/dist/commands/login.js.map +1 -1
  25. package/dist/commands/logout.d.ts.map +1 -1
  26. package/dist/commands/logout.js +29 -11
  27. package/dist/commands/logout.js.map +1 -1
  28. package/dist/commands/video.d.ts +35 -0
  29. package/dist/commands/video.d.ts.map +1 -1
  30. package/dist/commands/video.js +475 -39
  31. package/dist/commands/video.js.map +1 -1
  32. package/dist/index.js +2 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/kit/core/canonical.d.ts +3 -1
  35. package/dist/kit/core/canonical.d.ts.map +1 -1
  36. package/dist/kit/core/canonical.js +1 -1
  37. package/dist/kit/core/canonical.js.map +1 -1
  38. package/dist/kit/core/check.d.ts +2 -0
  39. package/dist/kit/core/check.d.ts.map +1 -1
  40. package/dist/kit/core/check.js +12 -2
  41. package/dist/kit/core/check.js.map +1 -1
  42. package/dist/kit/core/detach.d.ts +11 -0
  43. package/dist/kit/core/detach.d.ts.map +1 -0
  44. package/dist/kit/core/detach.js +72 -0
  45. package/dist/kit/core/detach.js.map +1 -0
  46. package/dist/kit/core/diff.d.ts +5 -0
  47. package/dist/kit/core/diff.d.ts.map +1 -0
  48. package/dist/kit/core/diff.js +132 -0
  49. package/dist/kit/core/diff.js.map +1 -0
  50. package/dist/kit/core/errors.d.ts +7 -1
  51. package/dist/kit/core/errors.d.ts.map +1 -1
  52. package/dist/kit/core/errors.js +11 -1
  53. package/dist/kit/core/errors.js.map +1 -1
  54. package/dist/kit/core/eyes.d.ts +80 -0
  55. package/dist/kit/core/eyes.d.ts.map +1 -0
  56. package/dist/kit/core/eyes.js +366 -0
  57. package/dist/kit/core/eyes.js.map +1 -0
  58. package/dist/kit/core/files.js +2 -2
  59. package/dist/kit/core/files.js.map +1 -1
  60. package/dist/kit/core/lock.d.ts +4 -0
  61. package/dist/kit/core/lock.d.ts.map +1 -0
  62. package/dist/kit/core/lock.js +13 -0
  63. package/dist/kit/core/lock.js.map +1 -0
  64. package/dist/kit/core/log.d.ts +38 -2
  65. package/dist/kit/core/log.d.ts.map +1 -1
  66. package/dist/kit/core/log.js +38 -5
  67. package/dist/kit/core/log.js.map +1 -1
  68. package/dist/kit/core/outcome.d.ts +114 -0
  69. package/dist/kit/core/outcome.d.ts.map +1 -0
  70. package/dist/kit/core/outcome.js +246 -0
  71. package/dist/kit/core/outcome.js.map +1 -0
  72. package/dist/kit/core/patch.d.ts +64 -0
  73. package/dist/kit/core/patch.d.ts.map +1 -0
  74. package/dist/kit/core/patch.js +289 -0
  75. package/dist/kit/core/patch.js.map +1 -0
  76. package/dist/kit/core/paths.d.ts +17 -0
  77. package/dist/kit/core/paths.d.ts.map +1 -1
  78. package/dist/kit/core/paths.js +21 -0
  79. package/dist/kit/core/paths.js.map +1 -1
  80. package/dist/kit/core/pieces.d.ts +16 -4
  81. package/dist/kit/core/pieces.d.ts.map +1 -1
  82. package/dist/kit/core/pieces.js +84 -27
  83. package/dist/kit/core/pieces.js.map +1 -1
  84. package/dist/kit/core/progress-reporter.d.ts +21 -4
  85. package/dist/kit/core/progress-reporter.d.ts.map +1 -1
  86. package/dist/kit/core/progress-reporter.js +74 -12
  87. package/dist/kit/core/progress-reporter.js.map +1 -1
  88. package/dist/kit/core/progress.d.ts +41 -4
  89. package/dist/kit/core/progress.d.ts.map +1 -1
  90. package/dist/kit/core/progress.js +105 -18
  91. package/dist/kit/core/progress.js.map +1 -1
  92. package/dist/kit/core/refusal-log.d.ts +7 -0
  93. package/dist/kit/core/refusal-log.d.ts.map +1 -0
  94. package/dist/kit/core/refusal-log.js +54 -0
  95. package/dist/kit/core/refusal-log.js.map +1 -0
  96. package/dist/kit/core/run.d.ts +39 -5
  97. package/dist/kit/core/run.d.ts.map +1 -1
  98. package/dist/kit/core/run.js +514 -83
  99. package/dist/kit/core/run.js.map +1 -1
  100. package/dist/kit/core/save.d.ts +7 -3
  101. package/dist/kit/core/save.d.ts.map +1 -1
  102. package/dist/kit/core/save.js +57 -20
  103. package/dist/kit/core/save.js.map +1 -1
  104. package/dist/kit/core/style.d.ts +1 -0
  105. package/dist/kit/core/style.d.ts.map +1 -1
  106. package/dist/kit/core/style.js +1 -1
  107. package/dist/kit/core/style.js.map +1 -1
  108. package/dist/kit/line/client.d.ts +14 -2
  109. package/dist/kit/line/client.d.ts.map +1 -1
  110. package/dist/kit/line/client.js +40 -20
  111. package/dist/kit/line/client.js.map +1 -1
  112. package/dist/kit/line/types.d.ts +33 -1
  113. package/dist/kit/line/types.d.ts.map +1 -1
  114. package/dist/kit/maker/cues.d.ts +67 -0
  115. package/dist/kit/maker/cues.d.ts.map +1 -0
  116. package/dist/kit/maker/cues.js +204 -0
  117. package/dist/kit/maker/cues.js.map +1 -0
  118. package/dist/kit/maker/encode.d.ts +5 -0
  119. package/dist/kit/maker/encode.d.ts.map +1 -1
  120. package/dist/kit/maker/encode.js +7 -1
  121. package/dist/kit/maker/encode.js.map +1 -1
  122. package/dist/kit/maker/facts.d.ts +46 -0
  123. package/dist/kit/maker/facts.d.ts.map +1 -0
  124. package/dist/kit/maker/facts.js +159 -0
  125. package/dist/kit/maker/facts.js.map +1 -0
  126. package/dist/kit/maker/inputs.d.ts +48 -3
  127. package/dist/kit/maker/inputs.d.ts.map +1 -1
  128. package/dist/kit/maker/inputs.js +151 -22
  129. package/dist/kit/maker/inputs.js.map +1 -1
  130. package/dist/kit/maker/make.d.ts +20 -1
  131. package/dist/kit/maker/make.d.ts.map +1 -1
  132. package/dist/kit/maker/make.js +129 -21
  133. package/dist/kit/maker/make.js.map +1 -1
  134. package/dist/kit/maker/page.d.ts.map +1 -1
  135. package/dist/kit/maker/page.js +49 -1
  136. package/dist/kit/maker/page.js.map +1 -1
  137. package/dist/kit/maker/parts/html-frames/1.2.1/part.json +1083 -0
  138. package/dist/kit/maker/parts/html-frames/src/part.d.ts.map +1 -1
  139. package/dist/kit/maker/parts/html-frames/src/part.js +4 -1
  140. package/dist/kit/maker/parts/html-frames/src/part.js.map +1 -1
  141. package/dist/kit/maker/runtime.d.ts.map +1 -1
  142. package/dist/kit/maker/runtime.js +54 -7
  143. package/dist/kit/maker/runtime.js.map +1 -1
  144. package/dist/kit/maker/timing.d.ts +74 -0
  145. package/dist/kit/maker/timing.d.ts.map +1 -0
  146. package/dist/kit/maker/timing.js +152 -0
  147. package/dist/kit/maker/timing.js.map +1 -0
  148. package/dist/kit/maker/under.d.ts +126 -0
  149. package/dist/kit/maker/under.d.ts.map +1 -0
  150. package/dist/kit/maker/under.js +299 -0
  151. package/dist/kit/maker/under.js.map +1 -0
  152. package/dist/kit/parts/catalog.d.ts +3 -20
  153. package/dist/kit/parts/catalog.d.ts.map +1 -1
  154. package/dist/kit/parts/catalog.js +6 -41
  155. package/dist/kit/parts/catalog.js.map +1 -1
  156. package/dist/kit/parts/loader.d.ts +7 -2
  157. package/dist/kit/parts/loader.d.ts.map +1 -1
  158. package/dist/kit/parts/loader.js +88 -23
  159. package/dist/kit/parts/loader.js.map +1 -1
  160. package/dist/skills/master-skill.d.ts +4 -6
  161. package/dist/skills/master-skill.d.ts.map +1 -1
  162. package/dist/skills/master-skill.js +28 -28
  163. package/dist/skills/master-skill.js.map +1 -1
  164. package/dist/skills/routes.js +5 -5
  165. package/dist/skills/routes.js.map +1 -1
  166. package/dist/skills/server-entry-skills.d.ts +6 -6
  167. package/dist/skills/server-entry-skills.d.ts.map +1 -1
  168. package/dist/skills/server-entry-skills.js +89 -60
  169. package/dist/skills/server-entry-skills.js.map +1 -1
  170. package/dist/utils/browser-preflight.d.ts +3 -0
  171. package/dist/utils/browser-preflight.d.ts.map +1 -1
  172. package/dist/utils/browser-preflight.js +27 -6
  173. package/dist/utils/browser-preflight.js.map +1 -1
  174. package/dist/utils/exit.d.ts +6 -0
  175. package/dist/utils/exit.d.ts.map +1 -0
  176. package/dist/utils/exit.js +37 -0
  177. package/dist/utils/exit.js.map +1 -0
  178. package/package.json +14 -3
  179. package/skills/goose-ads/SKILL.md +8 -8
  180. package/skills/goose-product-photos/SKILL.md +8 -8
  181. package/skills/goose-video/SKILL.md +29 -595
  182. package/skills/goose-video-local/SKILL.md +3 -1572
  183. package/skills/gooseworks/SKILL.md +10 -12
  184. package/skills/make-custom-video/SKILL.md +25 -672
  185. package/skills/manifest.json +7 -7
  186. package/skills/routes.json +3 -3
  187. package/dist/kit/maker/parts/html-frames/1.1.0/part.json +0 -199
  188. package/dist/skills/production-video-skills.json +0 -5
@@ -1,5 +0,0 @@
1
- {
2
- "goose-video": "---\nname: goose-video\nslug: goose-video\ndescription: >\n Start a video ad in the same chat. Resolve the brand and suggest supported formats with a\n picker or the returned text choices. An agent with a verified shell makes the video with\n goose-video-local; a chat host hands the same project to the GooseWorks coworker. Review\n one complete template plan and total credits before production. Custom videos retain two authenticated review gates in this chat.\ncategory: ads\nversion: 4.1.1\nauthor: GooseWorks\ntags: [gooseworks, ads, video, local-render, coworker, chat]\n---\n\n# GooseWorks Video Ads \u2014 choose, review, make\n\n## How to talk to the customer (applies to every message you send them)\n\nThe customer is a marketer or founder, not an engineer. Everything in this skill about tools,\nfields, files, polling, models and pipeline steps is for YOU. Never pass it on to them.\n\n- **Say what they get and what they need to decide, never how it's made.**\n Bad: \"Checking the media-proxy helpers for voiceover timestamps and lipsync.\" / \"Generated with\n gpt-image-2 at high quality, mouth closed, passes QC.\"\n Good: \"Recording the voices now.\" / \"Here are your two hosts.\"\n- **Never mention** tool, file or field names, ids, JSON, commands, scripts, installed software,\n uploads, model or vendor names, timings in seconds or frames, retries, polling, internal statuses\n or your own quality checks. Fix what you can quietly. If they ask how something works, explain it\n simply.\n- **Update them only at milestones they care about**: choices confirmed, script and voices done,\n visuals done, ready to review, finished. One short line each, then stay quiet until the next\n milestone. No update for setup, downloads, uploads or saving.\n- **Money is in credits, with a number**: \"the full video uses about N credits\". Never \"the cheap\n pieces\" or \"the expensive render\".\n- **Problems:** say what it means for them and what they can do, in one or two sentences. No error\n codes or stack traces. Only raise a problem that changes something for them.\n- **Use their words**: script, voice, hook, scene, host, ending, logo, the product shot.\n- **No filler**: no \"Great question!\", \"Certainly!\", \"I hope this helps!\", no restating what they\n just said at length.\n\nExample. Instead of a dozen lines about render tools, scripts, uploads and portraits, send:\n\"Got it: warm tone, home podcast studio, Brielle as the skeptic and Mark as the believer.\" then\n\"Script and voices are done (about 34 seconds).\" then \"Ready for you to review: the script, voices,\nboth hosts and the ending. You can also review your recipe ingredients in the app: <link>. Say go\nhere and I'll make the full video (about N credits).\"\n\n## Use current instructions for new work\n\nBefore a new task in a terminal host, run `gooseworks skills status` once. It compares the\ninstalled entry files, running CLI and published npm release. An older CLI needs a package\nupgrade before `gooseworks update`; update alone only uses that CLI's bundled instructions.\nPreserve local edits or unknown install provenance. Review/back up before explicitly replacing\nmodified files; never quietly reinstall over them. If the release check is unavailable, report\nthat freshness is unknown rather than claiming the local copy is latest.\n\nFor a new recipe run, fetch its package from the connected catalog. Retain the returned\n`version` / `contentHash` and every dependency's hash with the saved package. When reusing a\nsaved fetch JSON, `gooseworks fetch <slug> --saved-package <file>` returns the current package\nand a hash comparison without changing that file. With MCP, if the advertised `catalog_fetch`\nschema accepts them, send `saved_content_hash` and `saved_dependency_hashes`; otherwise fetch\nnormally and compare the returned hashes yourself. Missing hashes mean unknown, not current.\nThe server cannot inspect a client's saved files; hash metadata does not certify later edits.\nFetch current packages into a new run directory and report stale saved instructions. Preserve\nan existing approved run's recorded package; changing that harness requires a reviewed change\nand approval before spending. Hosted installed snapshots use the existing Skills Update action.\nSkill content and a host's cached MCP tool schemas are separate: refreshing one does not refresh\nthe other. Check the actual advertised tools before using new fields.\n\n## Purpose\n\nResolve the brand, show supported formats and create one saved project. An agent with a verified\nshell follows **`goose-video-local`**. A chat host delegates that same project to the GooseWorks\ncoworker, which renders in its sandbox. This is agent execution, not a recipe server-order API.\n\n**Cards and execution are separate capabilities.** A terminal may render without drawing a\npicker; a chat host may draw a picker while the coworker renders. Follow `card.display_hint`\nand `available_here`, never a guess from the host's name. Template-remix review stays in this\nchat. Custom videos use separate authenticated script and ingredient approvals in this same chat.\nStudio is an optional review surface.\n\n## Prepare the video workflow and brand before creative work\n\n**Installed entry files are bootstrap instructions.** For new work started from an installed\nfile or an old chat, fetch `catalog_fetch { type: \"skill\", slug: \"goose-video\" }` on the selected\nconnection and read its returned content and dependencies before continuing. Use the already\nfetched body when this entry came from that connection in this run; do not recursively fetch\nthe same entry. A CLI freshness warning does not block using the current connected package.\nKeep the fetched package and hashes in a new run folder; do not replace edited installed files.\nIf current instructions cannot be loaded, resolve the connection before format suggestions,\nscript writing or production. Do not continue from an old installed workflow or session notes.\n\n**Follow the connector's full-guide requirement when available.** Fetch\n`catalog_fetch { type: \"skill\", slug: \"gooseworks-guide\" }` when the connector requires it.\nThe only older-server exception is below; it never removes brand preparation or approvals.\n\n| Returned guide/workflow state on the selected connection | Required action |\n| --- | --- |\n| 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. |\n| 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. |\n\nA missing custom entry or required custom tool/schema remains unavailable: never substitute\na template or an import to bypass it. Never hide other fetch errors or continue from cached\ninstructions merely because the guide was not found.\n\n**For an existing project or batch, read `video_project_read` first.** Determine its actual\nroute and saved review state before fetching a recipe or doing local work. Resume an approved\nrun with its recorded packages, brand inputs, script and ingredients; do not silently replace\nthem with today's release or brand rules. Follow returned preparation requirements and use\nthe existing affected review/approval flow for an intentional change. Reads, free drafts,\nimports and existing-job retrieval do not grant permission for new paid production.\n\n**For every new plan, resolve the brand and call**\n`brand_read { brand_id, sections: [\"summary\", \"kit\", \"products\", \"learnings\"] }` **before**\nsuggesting formats, mining video angles, writing any script, creating a production plan or\nhanding the work to another agent. Use `brand_get_context` with those same four sections only\nwhen advertised. Reuse a complete read from this run on the same connection and brand.\nOnboarding completion, a summary-only response and earlier session notes do not supply the\nkit, selected product facts or saved rules. If the full read is unavailable, resolve it before\ncreative work. Use known kit facts while research is incomplete; do not restart research\nsolely because its status is pending. Never invent a product fact or accept a campaign to\nbypass preparation. Then search the Brand Brain (next section) before suggesting formats,\nchoosing an angle or asking the user for a brand fact.\n\nCarry that context through custom/template/idea routing and delegation. Before writing, make\nthe workflow's brand-rules file from those sources: pronunciations, required spoken copy,\nprohibited claims, real logo, font, palette and the selected product's supported facts.\nEntries prefixed `Video preference:` and visual/pacing instructions are creative direction,\nnot spoken lines. Use the kit's audience, offer and voice as defaults without an interview.\nThe rules file mirrors context; it is not proof of a read or permission to spend. Keep the\nroute's existing script, ingredient and budget approvals, including its allowed previews.\n\n**Use the server's brand bundle when the selected API returns it.** Project create/read may\nreturn top-level\n`brand_context`: `{ version: 1, brand_id, digest, loaded_at, sections, brand, kit, products,\nlearnings }`. Its sections cover summary, kit, products and learnings.\n\n| Returned brand/project response on the selected connection | Required action |\n| --- | --- |\n| 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. |\n| 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. |\n\nAn incomplete/malformed bundle or missing brand section is a preparation failure, never the\nolder-API exception. Once this connection returns an authoritative bundle, use its binding\ncontract. `script_drafts.video_brand_context` is server-owned: do not author, replace or forge\nit, and do not invent a read receipt or hash local brand facts into one. Keep the full bundle\nout of client review payloads. If managed generation returns HTTP 409\n`video_brand_context_required`, reload the project and bind the plan to its returned bundle\nthrough the normal review flow before retrying. That refusal never permits the older-API\nfallback, skipping sections or retrying paid calls blindly. Unchanged approved legacy resumes\nretain their package, context and approvals; an upgrade alone needs no extra approval.\n\n**To apply a deliberate brand correction to a new or revised plan**, make a fresh, unfiltered\n`brand_read` of those four sections. When it returns the authoritative bundle, use its\nreturned `brand_context.digest` in the complete revised `script_drafts` through the existing\n`video_project_upsert` `patch.script` save; the server verifies the current brand and replaces\nits snapshot with the saved plan. On an older API with no bundle contract, apply the actual\nfour-section correction and save the revised draft through its existing flow without inventing\ndigest fields. If a connection that already returned a bundle cannot supply the fresh bundle,\nresolve that failure before rebinding. Review and approve the affected script, ingredients and\nbudget through the normal flow. Never refresh an ongoing approved run automatically; its\nrecorded package and brand inputs stay pinned until an intentional change.\n\n## Search the Brand Brain, then propose \u2014 before any creative choice or question\n\nThe Brand Kit is a summary. The brand's saved knowledge (its Brain) holds what the Kit does not:\nrules from past feedback, approved and rejected creatives, customer evidence, approved claims,\nreports and documents. **After `brand_read`, and before you choose an angle, claim, hook,\nproduct emphasis, format or source ad \u2014 and before you ask the user for any brand fact \u2014 call\n`knowledge_search { brand_id, query }`** when it is registered. It is free and read-only: no\napproval, no announcement to the customer and no questionnaire.\n\n1. **Search for this task, not the whole Brain.** Run one short query (under 500 characters) in\n the user's own words plus the product (for example \"ads for <product>: what worked, what to\n avoid\") and one with\n `source_types: [\"evidence\", \"claim\", \"learning\", \"creative\", \"document\"]` for proof and past\n creative results. Add a query only for a specific open question. Reuse results from this run.\n2. **Keep these states distinct** and record which one each query returned:\n\n| Result | Means | Do |\n| --- | --- | --- |\n| `status: \"ok\"` with matches | Saved knowledge exists | Use it; keep each fact's citation in your working brief |\n| `status: \"empty\"` | Nothing saved matches this query | Say \"no saved evidence for <topic>\", never \"the brand has no proof\" |\n| `refresh_required` | The results shown are current; some changed sources were left out | Use them; search again shortly for anything missing |\n| `building` | The index is not ready | Retry once shortly, then continue with the gap stated |\n| 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 |\n| An empty Kit field | Only that field is blank | Not a search result: still search before asking |\n\n3. **Let the findings change the plan.** A `dont`/`must` learning or a rejected creative rules\n options out; an approved or well-rated past creative is a proven angle to lead with; a report\n shows what worked. Claim-grade proof is a result with `approved_ad_claim: true` or the kit's\n `approvedClaims` within their stated applicability; the kit's `proofPoints` are what the\n backend allows an ad to state, used as written. Other Kit text, documents and performance\n numbers are context, never public claims. Never invent an offer, price or result. Judge\n relevance and skip results about another product or business.\n4. **Propose; don't interview.** Lead with one recommended direction (angle, product, source or\n format) and a one-line reason naming what you found, with up to two alternatives. When several\n directions fit (two audiences, products or campaigns), pick the one the evidence favours, such\n as an active campaign or approved past creatives, and name the other as an alternative instead\n of asking. Ask only for a decision the brand read and the search cannot settle, or for spend\n approval. Never ask the user for a fact the Brain already answered.\n Keep directions already chosen (the user's words, a project's creative brief, handed-off\n defaults, approved plans): search only to fill empty fields and to apply saved rules. When a\n returned card or picker presents the choice, your recommendation is its one acknowledgement\n line and the saved plan's defaults, not a separate list.\n5. **Carry an evidence brief** into the routed skill, writer or plan: findings with citations,\n the state of each query, what it ruled out and the open gaps. Tell the customer the findings\n in plain words; the citations stay in the brief.\n\nOn an approved resume, keep the saved brief and evidence; search again only for a new decision.\n\n## Custom videos: check formats first\n\nFirst check the catalog: when the brief names or implies a listed format (for example a street interview, testimonial, podcast or chat video), show that format and its fit through the format flow below, including its support status. Go custom only when no format fits and the customer chooses custom after hearing why; custom keeps that format's hard constraints. For an original brief without a reference template, an Instagram reel/post URL or a direct video URL to study, fetch `catalog_fetch { type: \"skill\", slug: \"make-custom-video\" }` and follow it in this same session. It creates format:\"custom\", custom_mode:\"generate\" with the brief and optional reference_url. Growth executes in its managed sandbox; connected agents use their shell. Script and actual ingredients are reviewed and separately approved in the same chat before paid production; Studio is optional. Do not force a template that does not fit or that the customer declined, and do not import the reference as a finished video.\n\n## Run the format's route check before proposing it\n\nA catalog row says what a format is best for and ranks it for the brand. It does not say whether\nthe format can make the setup the customer asked for (how many people, mic only, no product).\nSome renderers declare a route selector that decides that; today that is the street interview\n(`render-street-interview`). Before you propose, script or create a street-interview project,\ntemplate or custom, check the request against its routes:\n\n| Street route | On screen | Needs | Makes |\n| --- | --- | --- | --- |\n| Guessing (`product-guess`) | Interviewer and up to four people | A physical product to hand over, photographed on its own | A finished video |\n| Conversation (`mic-only`, `product-sample`, `concept-challenge`) | Interviewer and one person | No product photo, phone, screen or UI | A script and prompt preview only, no finished video yet |\n| Street testimonial (its own format) | One person talking to camera, no interviewer | The creator still its recipe prescribes | A finished video, when the catalog lists it |\n\nNeither interview route takes a photo of a person: people are described in text, so never\npropose putting a founder's or creator's face in one. Keep the customer's named setup: never\nchange the interaction, mode or number of people just to make a check pass.\n\n**Never offer guessing to software or a service.** The guessing route needs a physical product\nto hand over. When what the brand sells is software, an app or a service (nothing a person can\nhold), never offer or script guessing, not even with a laptop, phone or other device showing the\napp as the prop: the device is a prop, so `offering_type` stays `digital` or `service` and the\nselector says unsupported. A brand that sells a physical product, including a device or\nhardware that comes with an app, can still use guessing with that product. Offer the table's\nreal alternatives instead: a one-person conversation (a script and prompt preview only, no\nfinished video yet), the street testimonial if the catalog lists it and does not rule it out,\nor custom. Asking again never unlocks a route the table or the selector rules out.\n\nWhen they asked for a street interview, the catalog may mark (newer servers do) the\nstreet-interview rows `fit.ok: false` for such a brand: they can't make a finished video for it.\nThat verdict is about the finished video, so still offer the one-person conversation, saying\nplainly that it is a script and prompt preview only. If the brand read shows the brand sells the\ndevice itself but the catalog rules guessing out, tell the customer the catalog flagged it and\nconfirm what they sell before offering guessing with that device.\n\n- **Without a shell** (a chat host), decide from this table. Do not fetch the renderer: its\n package is far too large for a chat.\n- **With a shell** (the customer's computer or the coworker sandbox), also run its free\n selector, which makes no paid call. If the fetch or the run fails, decide from the table.\n 1. Fetch the renderer to a file, not into the conversation: its inline package is over a\n million characters. Either save the JSON from `gooseworks fetch render-street-interview > <file.json>`,\n then write each `scripts` entry as\n `/tmp/gooseworks-scripts/render-street-interview/scripts/<name>` and each `files` entry at\n `/tmp/gooseworks-scripts/render-street-interview/<path>`. Or call\n `catalog_fetch { type: \"skill\", slug: \"render-street-interview\", delivery: \"archive\" }`,\n extract the ZIP, check each file against its `manifest.json` hashes and copy the contents of its\n `agent-config/skills/render-street-interview/` folder into\n `/tmp/gooseworks-scripts/render-street-interview/`. Either way the selector reads\n `references/street-reference-library.json` from that folder.\n 2. Write a brief JSON. `mode` and `interaction_type` go in pairs: `product-guess` with\n `product-guess`, or `conversation` with `mic-only`, `product-sample` or `concept-challenge`.\n `offering_type` is exactly `physical`, `service` or `digital` (software, SaaS and apps are\n `digital`). `participants` is a whole number: people interviewed on screen, not counting\n the interviewer. Leave it out when the customer named no count; the selector then uses the\n route's usual cast. When the customer named no interaction, run each mode that could fit.\n 3. Run `python3 /tmp/gooseworks-scripts/render-street-interview/scripts/prepare_script_context.py --brief <brief.json> --out <context.json>`.\n Read `status` and `brief_gaps` in the output file, not the exit code: it exits 2 for every\n status except `ready-for-writing`. A Python traceback is not a verdict: the brief or the\n saved files are wrong (for example the reference library in the wrong folder). Fix them and\n run it again.\n 4. A run with any `brief_gaps` has not checked the route. Fix every gap and run it again.\n\n**An unsupported setup is a stop.** `unsupported-route`, or a request the table rules out,\nmeans this setup can't be made. Tell the customer plainly, in one line, for example: \"A\nthree-person mic-only street interview isn't something we can make yet: that version takes one\nperson, and the three-person version needs a physical product to hand over.\" Then offer the\nclosest supported options in plain words, each with how it differs. With a shell, these are the\nselector's `alternatives`. Without one, take them from the table:\n\n- when only the number of people was the problem, the same setup within its limit: guessing\n (a physical product only) with up to four people, a finished video; or a conversation with\n one person, a script preview only;\n- for something people can't hold (software, a service), a one-person conversation instead\n of guessing;\n- one person talking to camera with no interviewer, if the catalog lists that format;\n- a custom video, untested, that keeps the street format's limits.\n\nNever use route names or the script's wording. Do not create the project or write a script for\nit. Go custom only if the customer picks it; a custom video keeps the street format's hard\nconstraints, which make-custom-video lists.\n\n**A supported setup.** With empty `brief_gaps`, any other status means the route exists.\n`needs-reference` means no observed reference ad matched yet: it does not block proposing the\nformat; carry it into the handoff so the script step resolves or reports it before script\napproval. Carry the route too: which one, how many people, finished video or preview only, and\nno person photos. A preview-only route makes a script and prompt preview, not a finished video:\nsay so before the customer chooses it.\n\n## Route first: is this a new video?\n\nHand off to **`goose-video-local`** now, and stop following this skill, for:\n\n- an existing **project** id or a **video batch** id;\n- the app's copy-for-Claude command (it names `goose-video-local`);\n- \"remix this video ad template\" for a specific app template.\n\nLoad the current `goose-video-local` entry with\n`catalog_fetch { type: \"skill\", slug: \"goose-video-local\" }` on the GooseWorks MCP (older clients:\n`fetch_skill(\"goose-video-local\")`), unless already fetched on this connection in this run.\nIt reads the project first and retains an approved run's recorded recipe packages.\n\nWhen they ask **what** to make (\"give me video ad ideas\", \"what angles should I use?\", \"what's\nworking for my competitors?\"), fetch **`ad-angle-miner`** (`catalog_fetch { type: \"skill\", slug: \"ad-angle-miner\" }`,\nusing its advertised tools) and run it with the **video** output. It returns ranked\nvideo ideas mapped to formats and hands the picked ones to `goose-video-local`.\n\nEverything else, including \"make me a video ad for <brand>\", starts at step 1 below.\n\n## Keep the selected connection for the whole run\n\nCall `account_whoami` on the connection that owns the brand/project before the first write.\nKeep `environment.name` and the public `environment.api_origin` with this run. Respect the\nuser's selected production or staging connection. If multiple connections are available and\nnone was selected, resolve that once. Missing or unknown identity is uncertainty, not\npermission to switch. Never infer the environment from credits, billing links or Node mode.\n\nReads, project creation, uploads, generation, polling and final updates all use that same\nconnection. If a production request fails, resume or report the failure on production;\nnever retry it on staging or recreate the project there. Before retrying a timed-out write,\nread back the existing project or paid request on the selected connection.\n\nLocal/CLI proxy origin must match `environment.api_origin`. If it differs, use the MCP\nrelay on the selected connection (`GW_MEDIA_VIA=mcp`) before paid calls. Do not change\ncredentials or API origins to recover a failed write. An explicit user-requested move is\na separate operation, with the existing project and paid requests reconciled first.\n\n## Check assets for the selected format before spending\n\nFor a template, read its structured `asset_readiness` from\n`video_catalog_list { kind: \"formats\", brand_id }`. `missing` names gaps;\n`needs_review` means suitability is unverified, including older recipes without\nstructured requirements. `ready` describes assets only, not script or budget approval.\nFor a custom video, derive requirements from its actual approved scenes.\n\nInspect candidate files for the chosen product and format. A catalog photo can contain\nmultiple objects, other products or a person; its existence or approved status does not\nmake it a standalone image of the selected product. Check object count, framing, readable\nprint and real image bytes. A service conversation has no automatic packshot requirement.\n\nReuse a suitable approved image first. A free crop or cutout is a new file: keep the\noriginal, inspect the result and include it in the normal ingredient review. When no\nusable input exists, explain the gap before generation. Estimate any paid preparation\nseparately before spending. Record the selected asset and inspection in the project\nreview set; do not add a separate approval round.\n\n## Inputs\n\n- A brand, usually named in the request. Resolve it from the accessible brands; one brand needs no question.\n- The customer's goal, occasion, audience, selected angle and constraints when supplied; otherwise propose defaults from the verified kit.\n- The selected format's required assets. Presence does not prove suitability; uncertain assets remain \u201cneeds review.\u201d\n\n## Save durable brand answers, then verify them\n\nRead the selected brand with `brand_read { brand_id, sections: [\"summary\", \"kit\", \"products\", \"learnings\", \"onboarding\"] }`\n(older clients: `brand_get_context` with the same sections, only when that tool is advertised).\nKeep founder answers, user corrections, research and your own hypotheses distinct. Reuse\nmatching saved answers; ask only about gaps.\n\nWhen the user asks to remember a rule, answers a brand interview, or explicitly corrects a\nstanding fact, save that answer in the same turn. The capture request authorizes those answers;\ndo not ask for approval again. A direction for this one video stays in its brief. If the scope\nis genuinely ambiguous, ask whether it applies to future videos before saving a standing rule.\n\nUse the **live registered schema**. Where supported, call `brand_update` with\n`knowledge_intent: \"user_correction\"` and `user_statement` containing the user's exact,\nverbatim answer, not your paraphrase or researched text.\nFor an inference or suggested improvement, use `knowledge_intent: \"agent_proposal\"`. Show the\nbefore/after change from your prior read and proposed value; retain the returned proposal IDs\nand say the user must accept it in the app. Link only a review surface actually returned by a\ntool; the compact `knowledge_updates` response does not itself contain a diff or URL.\nA pending proposal is not a saved fact. Never call an unavailable\ntool or silently relabel research or your inference as something the user said.\n\nThe safe structured shape is `patch: { knowledge: { positioning?, audience?, voice?,\ninstructions?, brandType?, tagline?, valueProps? } }`, using only fields present in the live\nschema. Inferred rules/taste go in an `instructions` proposal with a rationale, never in\n`patch.facts`. Prefix every video-only preference in that proposed text with \"Video preference:\"\nso it remains production direction after acceptance. Preserve unrelated instructions when\nproposing a merged replacement.\n\n| User answer | Canonical write |\n| --- | --- |\n| 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. |\n| 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: \u2026\" rather than turn a belief into a verified result. |\n| Required wording or pronunciation | `patch.facts: [{ kind: \"must\", text }]`; pronunciation is exactly `Pronounce \"<term>\" as \"<say_as>\"`. |\n| Forbidden claim, word or visual | `patch.facts: [{ kind: \"dont\", text }]`. |\n| 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:\". |\n\nFacts are existing `ad_brand_learning` rows with user provenance; they are not a second profile.\nUpdate a matching rule by its returned `id` instead of adding duplicates. Preserve unrelated\nrules and the user's exact meaning. Only use the legacy facts shape for explicitly user-authored\nanswers when the live schema lacks intent fields; agent suggestions still need a proposal path.\n\n**Claims and plans have separate gates.** A founder assertion or proof point is not an approved,\nevidence-backed claim or consent to quote a customer. Use the existing evidence/claims and\noperating-plan tools only if registered, following their proposal, evidence and confirmation\nrequirements. Never encode a spend cap, approver or emergency stop as a learning. If that write\npath is missing, report the specific unsaved item and keep it pending for the supported review\nsurface; do not claim it was saved or create a parallel local profile.\n\nAfter every write, **read back before saying saved**: use `brand_read` with `kit`, `learnings`\nor `onboarding` as appropriate, or `brand_onboarding { action: \"status\", brand_id }` after an\nonboarding answer. Verify the intended field/rule, its source and the absence of a conflicting\nduplicate. A generic success response, pending proposal, ignored key or truncated result is not\nproof. Report partial saves honestly. Carry the verified rules into the current task and the\nrouted skill; claims still pass their own safety gate.\n\n## Video taste \u2014 reuse examples and preferences\n\nWhen asked to capture video taste, or when the user volunteers a durable video preference,\nfirst read the saved learnings and `media_list { brand_id, scope: \"brand\", scope_id: brand_id,\ntags: [\"video-taste\"], limit: 100 }`. Follow `next_cursor` before deciding an example is absent.\nDo not force a taste interview before an unrelated task or ask again for an existing preference.\n\nSave what the user has already supplied first. Then ask only the missing useful question, for\nexample: \"What do you like about this video\u2014its pace, voice, captions, or look?\" An inaccessible\nlink can still be saved as a link with the user's explanation; do not pretend you watched it.\n\n- **Direct clip or video file:** register with `media_upload { brand_id, scope: \"brand\",\n scope_id: brand_id, kind: \"video\", source: { type: \"url\", url }, tags: [\"video-taste\", \"reference-only\"],\n metadata: { purpose: \"video_taste\", source_url: url, provenance: \"user\", captured_at: <ISO timestamp>,\n preference: <the user's explanation> } }`. For a file use the live file/bytes upload flow and\n `media_confirm` after a presigned upload. Registration of a URL does not copy or inspect it.\n- **Instagram/post/page link:** the same registration with `kind: \"document\"`; it is a link\n bookmark, not downloadable footage or an indexed transcript. Do not fabricate a direct clip URL.\n- **Preferences:** save the user's reasons, likes and dislikes through the facts mapping above.\n Read and preserve existing facts before updating one. Do not invent `video_preferences` or\n new `video_lab` keys; the live kit patch accepts only its documented asset fields.\n- **Deduplicate:** reuse a matching returned media row, then `media_update` its title/tags/metadata\n if needed; preserve existing metadata and tags. Do not create another row for the same example.\n Read back with `media_list` and `brand_read` learnings before claiming it was saved.\n Check the returned row belongs to this brand: URL deduplication may return another brand's\n existing row. Do not relabel that row or claim success unless the current brand's scoped read\n actually returns it. Report an unsaved association if no supported attach path is available.\n\nNever put third-party taste examples into kit reference images: `kind: \"reference\"` at brand\nscope writes there. The tags and metadata above record purpose and provenance; **they do not\ngrant or enforce usage rights**. Study the structure, pacing and look only. Never use the example's\nfootage, face, product, testimonial or claims in a new ad without independently verified permission.\n\nRead video-only entries prefixed `Video preference:` from both saved learnings and\n`kit.instructions`, including accepted proposals. Keep them out of required or forbidden\ndialogue. Build a brief from the verified readback: preferred pace, voice, caption treatment, visual style,\nformats to favour/avoid, reference links and the user's reasons. Say what is still unknown.\nPass it with the brand rules into the existing video workflow. A one-video request overrides a\ndefault for that project; it does not silently rewrite the brand's standing preference.\n\n## Composed Atoms\n\nUse the live canonical schemas: brand_read, brand_create, video_catalog_list,\nvideo_project_upsert, video_project_read, catalog_fetch, goose_run_task and job_cancel.\nAn old tool name in a recipe is not grounds for terminal-update advice in a chat host.\n\n## Paid media: images, clips, voice\n\n**No FAL_KEY, ElevenLabs key or `fal_client` is ever needed.** Use\n`data_post_provider { provider: \"fal\", path: <model id, e.g. \"fal-ai/nano-banana/edit\">, body: <model input>, project_id }`\nthen `job_get { job_id }`; voice/music use provider:\"elevenlabs\". Upload local inputs through\nmedia_upload first. **A missing key is never a blocker.** `photos_generate` is **not** a general\nimage tool: it photographs physical catalog products only. Use goose-video-local for the\nruntime, paid approval, asset saving and quality checks.\n\n## Workflow\n\n### 1. Resolve the brand, quietly when you can\n\nCall brand_read without brand_id to list accessible brands. Resolve the customer's name from\nactual returned names. One brand, or exactly one match: state it and continue. Several possible\nbrands: show names and websites and ask once. No match: show accessible brands and offer to add\none only when the customer asks and supplies its website; never guess the URL.\n\nRead `brand_read { brand_id, sections: [\"summary\",\"kit\",\"products\",\"learnings\"] }` before\nchoosing a format. Reconcile explicit brand corrections through the durable capture rules.\nA direct video request does not require first-campaign acceptance. Follow any returned\nrequest-specific setup requirement; never mark research complete or accept a campaign to\nbypass setup. Keep the original request.\n\n### 2. Keep the goal and propose defaults\n\nNever ask what the ad is for before showing formats. Keep the customer's exact direction.\nOtherwise propose defaults from the verified kit's products, audience, offer and voice and the\nBrain search's evidence brief (proven angles, saved rules, approved claims). Show those defaults\nin the saved plan so any can be changed. Never invent proof or product facts.\nIdea requests still follow ad-angle-miner with the video output.\n\n### 3. Show the picker or its text fallback\n\nCall `video_catalog_list { kind: \"formats\", brand_id }`. On the customer's computer, when you\ncan actually execute shell commands, include `client: { shell: true }`; in a chat host omit\nthat claim. Inside a coworker sandbox follow its reported capabilities, never claim to be the\ncustomer's computer. Respect requested limit and next_cursor for more item pages; the card may\ncontain a full picker independently of the item page.\n\n- **card.display_hint:\"widget\"**: the picker replaces a format table. Write at most one short\n acknowledgement, print no table or list of the formats, and wait for the selection.\n- **Otherwise**: print card.text_summary as returned. Without a card, show returned rows with\n names, faithful descriptions, needs and demo links, respecting the requested page size.\n Write \"no demo yet\" when absent. Never put links only in a question control.\n- A format whose card contradicts what they asked for is never Suggested. Never offer\n available_here:false as an executable choice. Relay not_available_here in one short line.\n- A row with `fit.ok: false` is ruled out for this brand. Never propose it unless the customer,\n after hearing why, still asks for it. For a street interview, insisting never unlocks a route\n the route check rules out; when they asked for one, the one-person conversation preview is the\n one exception (see the route check). When they ask about a ruled-out row, tell them why in\n plain words (its `fit.reason`), never the field name.\n- `suggested` ranks a row for the brand; it never means the row makes the setup they named.\n Never tell the customer a row is \"exactly\" what they asked for beyond what its card says. A\n street interview still needs the route check above first.\n- Missing required assets are missing; unknown suitability is \u201cneeds review.\u201d Inspect the\n selected format's candidates before spending. Never promise an unverified asset is ready.\n- Template formats use **one plan, one approval with the total in credits**, not approval for\n each paid step. Custom videos retain separate authenticated script/ingredient/budget gates in this chat.\n\n### 4. Check this machine can render it\n\n- **Chat host without a shell:** skip local checks and delegate an available format in Step 5.\n Never tell a chat host it needs Claude Code to start. Browser-only formats remain unavailable\n here; give computer setup guidance only if asked how to make one.\n- **Local shell:** run `gooseworks doctor --no-browser` for common setup (auth/MCP, Node 18+,\n ffmpeg with libx264 + libass, ffprobe). Then fetch the selected template and its capabilities,\n and install documented dependencies in its fetched folder. Do not guess a renderer from a\n format name. For each Node renderer using default Playwright Chromium, run\n `gooseworks doctor --renderer-script \"/absolute/path/to/the/fetched/scripts/record.js\"` with\n the exact environment used to render, including NODE_PATH and PLAYWRIGHT_BROWSERS_PATH.\n Use the actual script path. Custom browser launch settings need their equivalent exact-runtime\n check. Non-browser capabilities need only their documented runtime checks.\n Never create paid ingredients before the selected renderer passes. Repair and recheck under\n existing setup permissions. If it cannot be fixed, re-list with client:{shell:false} and\n delegate only a format available to the coworker; never silently change the selected format.\n- **Coworker sandbox:** you are the renderer. Follow goose-video-local's sandbox checks; never\n hand off recursively or claim browser capability that the sandbox lacks.\n\n### 5. Create the project and hand it off, in this session\n\n1. `video_project_upsert { brand_id, name, format: <template_id> }` with no brief (a brief creates\n a concept batch). A street interview is created only after its route check found a supported\n setup. Include client:{shell:true} only for real local execution. Default\n creation_intent:\"format\" keeps the style with this brand's content; source_remix requires an\n explicit choice to use source content.\n If they already chose a campaign/concept, include its verified campaign_id and optional\n campaign_concept_id. Read the IDs from that brand's saved campaign; the concept must belong\n to it. Omit unknown IDs and never infer a link or create a campaign solely to file a video.\n2. **Verified local shell or coworker sandbox:** load\n `catalog_fetch { type: \"skill\", slug: \"goose-video-local\" }` and follow it on the same project_id.\n Carry the customer's words, verified defaults, campaign, selected angle and a street\n interview's route check result into its brief.\n3. **Chat host:** `goose_run_task { brand_id, project_id, message }` with the request, selected\n format, defaults and a street interview's route check result. Keep task_id. Continue questions/edits with the same task and project.\n A saved free draft is not a started worker or a complete plan. Follow actual saved state.\n4. When follow.card_follows is true, the card follows progress: do not re-read it in a loop.\n Read again on a customer reply, card action or requested update. A failure or missing saved\n plan needs the returned recovery action; never claim completion or approve an empty plan.\n Approval requires the current complete plan and total. Insufficient balance: offer a shorter\n video or top-up. \u201cNot now\u201d keeps the saved plan.\n\nDo not hand the customer a command to paste somewhere else.\n\n## Decision Rules\n\n- One sentence is a complete request. Resolve the brand and show formats without a goal interview.\n- One brand in the org: never ask which brand. Cards decide presentation; checks decide execution.\n- Use canonical tools. Preserve campaign context, selected script angles and custom authenticated review gates.\n- Never fabricate readiness, completion, approval or a second project.\n\n## Output\n\nOne project, one current saved plan in this chat, then the finished video. With a widget write one\nline and do not add app_url, brand_url or duplicate video links. A text host gets text_summary\nand its usable delivery link.\n\n## Quality Checks\n\n- No forced goal question, duplicate table or unsupported shell claim.\n- The exact renderer passed before local paid ingredients; uncertain assets stay uncertain.\n- Selected campaign and angle survive; a single template project has no brief.\n- Current complete plan and total precede approval. Custom script and ingredient gates remain independent; Studio is optional.\n\n## Failure Modes\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| Picker plus table | Ignored display_hint | Widget: one line; otherwise text_summary |\n| Terminal loses formats | Confused cards with execution | Declare real shell capability and check renderer |\n| Chat told it cannot start | Only local execution considered | Create once and delegate to coworker |\n| Worker ends without a plan | Assumed completion means ready | Follow actual failure/recovery state |\n| Two projects for one video | Recreated instead of resuming | Continue same project and task |\n\n## Use uploaded footage\n\nWhen the customer asks to use existing footage, check the actual advertised schemas. Use `media_search` with purpose `production`, kind `video`, the brand and current project when needed. Omit query spend for free retrieval. Read facts/scenes with `media_analyze`, then use `media_excerpt` action `inspect` to review actual bounded frames and timed transcript before selecting. A description or thumbnail URL alone is not visual review.\n\nFreeze `source_excerpt:{asset_id,analysis_revision,scene_id,start_ms,end_ms,audio_mode}`. Revision is the original-byte SHA, scene may be null for a known user trim, bounds use integer milliseconds, and audio is original or muted. Call `media_excerpt` action `attach` with that exact selection and a stable idempotency key. The returned project remains unfinished and unapproved. Open its existing Studio review; preserve user locks and requested format, and keep footage separate from image packshot indexes. An incompatible format requires an explicit choice before changing it.\n\nFor custom production, fetch the current shared `video-production-harness` and follow script/ingredient gates. Download the verified original and trim its exact selected window at normal speed with the approved audio. Recheck current revision and production permission before consumption and final upload/completion. Save excerpt lineage in plan and ingredient readback. Missing optional semantics does not require another upload; known user-selected stored footage remains usable. Research and competitor references never become production footage. Selected originals require no paid generation; only newly generated/replaced ingredients consume the approved budget.\n",
3
- "make-custom-video": "---\nname: make-custom-video\nslug: make-custom-video\ndescription: Connect the shared video production harness to GooseWorks projects, script and ingredient approvals, managed media generation, budgets and final delivery. Use for original briefs, Instagram/video references and resumed custom projects.\ncategory: ads\nversion: 3.1.1\nauthor: GooseWorks\nrequires_skills: [video-production-harness]\nharness_binding: gooseworks/v2\n---\n\n# Human version\n\nThis adapter connects the production playbook and specialist skill catalog to GooseWorks. It matches each scene to existing creator, footage, graphics, audio and review tools. Customers review and approve their script, actual ingredients and budget in the same chat. Studio is an optional review surface. The agent saves the checked final video to the same project.\n\n---\n\n# Agent version\n\n## How to talk to the customer (applies to every message you send them)\n\nThe customer is a marketer or founder, not an engineer. Everything in this skill about tools,\nfields, files, polling, models and pipeline steps is for YOU. Never pass it on to them.\n\n- **Say what they get and what they need to decide, never how it's made.**\n Bad: \"Checking the media-proxy helpers for voiceover timestamps and lipsync.\" / \"Generated with\n gpt-image-2 at high quality, mouth closed, passes QC.\"\n Good: \"Recording the voices now.\" / \"Here are your two hosts.\"\n- **Never mention** tool, file or field names, ids, JSON, commands, scripts, installed software,\n uploads, model or vendor names, timings in seconds or frames, retries, polling, internal statuses\n or your own quality checks. Fix what you can quietly. If they ask how something works, explain it\n simply.\n- **Update them only at milestones they care about**: choices confirmed, script and voices done,\n visuals done, ready to review, finished. One short line each, then stay quiet until the next\n milestone. No update for setup, downloads, uploads or saving.\n- **Money is in credits, with a number**: \"the full video uses about N credits\". Never \"the cheap\n pieces\" or \"the expensive render\".\n- **Problems:** say what it means for them and what they can do, in one or two sentences. No error\n codes or stack traces. Only raise a problem that changes something for them.\n- **Use their words**: script, voice, hook, scene, host, ending, logo, the product shot.\n- **No filler**: no \"Great question!\", \"Certainly!\", \"I hope this helps!\", no restating what they\n just said at length.\n\nExample. Instead of a dozen lines about render tools, scripts, uploads and portraits, send:\n\"Got it: warm tone, home podcast studio, Brielle as the skeptic and Mark as the believer.\" then\n\"Script and voices are done (about 34 seconds).\" then \"Ready for you to review: the script, voices,\nboth hosts and the ending. You can also review your recipe ingredients in the app: <link>. Say go\nhere and I'll make the full video (about N credits).\"\n\n## Use current instructions for new work\n\nBefore a new task in a terminal host, run `gooseworks skills status` once. It compares the\ninstalled entry files, running CLI and published npm release. An older CLI needs a package\nupgrade before `gooseworks update`; update alone only uses that CLI's bundled instructions.\nPreserve local edits or unknown install provenance. Review/back up before explicitly replacing\nmodified files; never quietly reinstall over them. If the release check is unavailable, report\nthat freshness is unknown rather than claiming the local copy is latest.\n\nFor a new recipe run, fetch its package from the connected catalog. Retain the returned\n`version` / `contentHash` and every dependency's hash with the saved package. When reusing a\nsaved fetch JSON, `gooseworks fetch <slug> --saved-package <file>` returns the current package\nand a hash comparison without changing that file. With MCP, if the advertised `catalog_fetch`\nschema accepts them, send `saved_content_hash` and `saved_dependency_hashes`; otherwise fetch\nnormally and compare the returned hashes yourself. Missing hashes mean unknown, not current.\nThe server cannot inspect a client's saved files; hash metadata does not certify later edits.\nFetch current packages into a new run directory and report stale saved instructions. Preserve\nan existing approved run's recorded package; changing that harness requires a reviewed change\nand approval before spending. Hosted installed snapshots use the existing Skills Update action.\nSkill content and a host's cached MCP tool schemas are separate: refreshing one does not refresh\nthe other. Check the actual advertised tools before using new fields.\n\n## Prepare the video workflow and brand before creative work\n\n**Installed entry files are bootstrap instructions.** For new work started from an installed\nfile or an old chat, fetch `catalog_fetch { type: \"skill\", slug: \"make-custom-video\" }` on the selected\nconnection and read its returned content and dependencies before continuing. Use the already\nfetched body when this entry came from that connection in this run; do not recursively fetch\nthe same entry. A CLI freshness warning does not block using the current connected package.\nKeep the fetched package and hashes in a new run folder; do not replace edited installed files.\nIf current instructions cannot be loaded, resolve the connection before format suggestions,\nscript writing or production. Do not continue from an old installed workflow or session notes.\n\n**Follow the connector's full-guide requirement when available.** Fetch\n`catalog_fetch { type: \"skill\", slug: \"gooseworks-guide\" }` when the connector requires it.\nThe only older-server exception is below; it never removes brand preparation or approvals.\n\n| Returned guide/workflow state on the selected connection | Required action |\n| --- | --- |\n| 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. |\n| 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. |\n\nA missing custom entry or required custom tool/schema remains unavailable: never substitute\na template or an import to bypass it. Never hide other fetch errors or continue from cached\ninstructions merely because the guide was not found.\n\n**For an existing project or batch, read `video_project_read` first.** Determine its actual\nroute and saved review state before fetching a recipe or doing local work. Resume an approved\nrun with its recorded packages, brand inputs, script and ingredients; do not silently replace\nthem with today's release or brand rules. Follow returned preparation requirements and use\nthe existing affected review/approval flow for an intentional change. Reads, free drafts,\nimports and existing-job retrieval do not grant permission for new paid production.\n\n**For every new plan, resolve the brand and call**\n`brand_read { brand_id, sections: [\"summary\", \"kit\", \"products\", \"learnings\"] }` **before**\nsuggesting formats, mining video angles, writing any script, creating a production plan or\nhanding the work to another agent. Use `brand_get_context` with those same four sections only\nwhen advertised. Reuse a complete read from this run on the same connection and brand.\nOnboarding completion, a summary-only response and earlier session notes do not supply the\nkit, selected product facts or saved rules. If the full read is unavailable, resolve it before\ncreative work. Use known kit facts while research is incomplete; do not restart research\nsolely because its status is pending. Never invent a product fact or accept a campaign to\nbypass preparation. Then search the Brand Brain (next section) before suggesting formats,\nchoosing an angle or asking the user for a brand fact.\n\nCarry that context through custom/template/idea routing and delegation. Before writing, make\nthe workflow's brand-rules file from those sources: pronunciations, required spoken copy,\nprohibited claims, real logo, font, palette and the selected product's supported facts.\nEntries prefixed `Video preference:` and visual/pacing instructions are creative direction,\nnot spoken lines. Use the kit's audience, offer and voice as defaults without an interview.\nThe rules file mirrors context; it is not proof of a read or permission to spend. Keep the\nroute's existing script, ingredient and budget approvals, including its allowed previews.\n\n**Use the server's brand bundle when the selected API returns it.** Project create/read may\nreturn top-level\n`brand_context`: `{ version: 1, brand_id, digest, loaded_at, sections, brand, kit, products,\nlearnings }`. Its sections cover summary, kit, products and learnings.\n\n| Returned brand/project response on the selected connection | Required action |\n| --- | --- |\n| 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. |\n| 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. |\n\nAn incomplete/malformed bundle or missing brand section is a preparation failure, never the\nolder-API exception. Once this connection returns an authoritative bundle, use its binding\ncontract. `script_drafts.video_brand_context` is server-owned: do not author, replace or forge\nit, and do not invent a read receipt or hash local brand facts into one. Keep the full bundle\nout of client review payloads. If managed generation returns HTTP 409\n`video_brand_context_required`, reload the project and bind the plan to its returned bundle\nthrough the normal review flow before retrying. That refusal never permits the older-API\nfallback, skipping sections or retrying paid calls blindly. Unchanged approved legacy resumes\nretain their package, context and approvals; an upgrade alone needs no extra approval.\n\n**To apply a deliberate brand correction to a new or revised plan**, make a fresh, unfiltered\n`brand_read` of those four sections. When it returns the authoritative bundle, use its\nreturned `brand_context.digest` in the complete revised `script_drafts` through the existing\n`video_project_upsert` `patch.script` save; the server verifies the current brand and replaces\nits snapshot with the saved plan. On an older API with no bundle contract, apply the actual\nfour-section correction and save the revised draft through its existing flow without inventing\ndigest fields. If a connection that already returned a bundle cannot supply the fresh bundle,\nresolve that failure before rebinding. Review and approve the affected script, ingredients and\nbudget through the normal flow. Never refresh an ongoing approved run automatically; its\nrecorded package and brand inputs stay pinned until an intentional change.\n\n## Search the Brand Brain, then propose \u2014 before any creative choice or question\n\nThe Brand Kit is a summary. The brand's saved knowledge (its Brain) holds what the Kit does not:\nrules from past feedback, approved and rejected creatives, customer evidence, approved claims,\nreports and documents. **After `brand_read`, and before you choose an angle, claim, hook,\nproduct emphasis, format or source ad \u2014 and before you ask the user for any brand fact \u2014 call\n`knowledge_search { brand_id, query }`** when it is registered. It is free and read-only: no\napproval, no announcement to the customer and no questionnaire.\n\n1. **Search for this task, not the whole Brain.** Run one short query (under 500 characters) in\n the user's own words plus the product (for example \"ads for <product>: what worked, what to\n avoid\") and one with\n `source_types: [\"evidence\", \"claim\", \"learning\", \"creative\", \"document\"]` for proof and past\n creative results. Add a query only for a specific open question. Reuse results from this run.\n2. **Keep these states distinct** and record which one each query returned:\n\n| Result | Means | Do |\n| --- | --- | --- |\n| `status: \"ok\"` with matches | Saved knowledge exists | Use it; keep each fact's citation in your working brief |\n| `status: \"empty\"` | Nothing saved matches this query | Say \"no saved evidence for <topic>\", never \"the brand has no proof\" |\n| `refresh_required` | The results shown are current; some changed sources were left out | Use them; search again shortly for anything missing |\n| `building` | The index is not ready | Retry once shortly, then continue with the gap stated |\n| 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 |\n| An empty Kit field | Only that field is blank | Not a search result: still search before asking |\n\n3. **Let the findings change the plan.** A `dont`/`must` learning or a rejected creative rules\n options out; an approved or well-rated past creative is a proven angle to lead with; a report\n shows what worked. Claim-grade proof is a result with `approved_ad_claim: true` or the kit's\n `approvedClaims` within their stated applicability; the kit's `proofPoints` are what the\n backend allows an ad to state, used as written. Other Kit text, documents and performance\n numbers are context, never public claims. Never invent an offer, price or result. Judge\n relevance and skip results about another product or business.\n4. **Propose; don't interview.** Lead with one recommended direction (angle, product, source or\n format) and a one-line reason naming what you found, with up to two alternatives. When several\n directions fit (two audiences, products or campaigns), pick the one the evidence favours, such\n as an active campaign or approved past creatives, and name the other as an alternative instead\n of asking. Ask only for a decision the brand read and the search cannot settle, or for spend\n approval. Never ask the user for a fact the Brain already answered.\n Keep directions already chosen (the user's words, a project's creative brief, handed-off\n defaults, approved plans): search only to fill empty fields and to apply saved rules. When a\n returned card or picker presents the choice, your recommendation is its one acknowledgement\n line and the saved plan's defaults, not a separate list.\n5. **Carry an evidence brief** into the routed skill, writer or plan: findings with citations,\n the state of each query, what it ruled out and the open gaps. Tell the customer the findings\n in plain words; the citations stay in the brief.\n\nOn an approved resume, keep the saved brief and evidence; search again only for a new decision.\n\n## Keep the selected connection for the whole run\n\nCall `account_whoami` on the connection that owns the brand/project before the first write.\nKeep `environment.name` and the public `environment.api_origin` with this run. Respect the\nuser's selected production or staging connection. If multiple connections are available and\nnone was selected, resolve that once. Missing or unknown identity is uncertainty, not\npermission to switch. Never infer the environment from credits, billing links or Node mode.\n\nReads, project creation, uploads, generation, polling and final updates all use that same\nconnection. If a production request fails, resume or report the failure on production;\nnever retry it on staging or recreate the project there. Before retrying a timed-out write,\nread back the existing project or paid request on the selected connection.\n\nLocal/CLI proxy origin must match `environment.api_origin`. If it differs, use the MCP\nrelay on the selected connection (`GW_MEDIA_VIA=mcp`) before paid calls. Do not change\ncredentials or API origins to recover a failed write. An explicit user-requested move is\na separate operation, with the existing project and paid requests reconciled first.\n\n## Check assets for the selected format before spending\n\nFor a template, read its structured `asset_readiness` from\n`video_catalog_list { kind: \"formats\", brand_id }`. `missing` names gaps;\n`needs_review` means suitability is unverified, including older recipes without\nstructured requirements. `ready` describes assets only, not script or budget approval.\nFor a custom video, derive requirements from its actual approved scenes.\n\nInspect candidate files for the chosen product and format. A catalog photo can contain\nmultiple objects, other products or a person; its existence or approved status does not\nmake it a standalone image of the selected product. Check object count, framing, readable\nprint and real image bytes. A service conversation has no automatic packshot requirement.\n\nReuse a suitable approved image first. A free crop or cutout is a new file: keep the\noriginal, inspect the result and include it in the normal ingredient review. When no\nusable input exists, explain the gap before generation. Estimate any paid preparation\nseparately before spending. Record the selected asset and inspection in the project\nreview set; do not add a separate approval round.\n\n## Make Custom Video\n\nThe generic production phases run in the Growth sandbox or a connected agent with a shell. GooseWorks owns the project, approvals, budget, uploads and final selection. This adapter replaces Studio desktop files and Tauri approval events with the product contract below.\n\n## Check the connected tool contract first\n\nInspect the tool parameters exposed by the selected connection before creating or changing a custom project. catalog_fetch returns skill packages, not tool schemas. Use the host's registered tool definitions (MCP tools/list when available); do not assume that fetching a newer skill updates a connector's saved schema.\n\nvideo_project_upsert must expose custom_mode and idempotency_key for creation, reference_url when a reference is requested, and patch.script.expected_plan_revision for review saves. Before approvals, require patch.approve fields user_quote, total_credits, phase and review_token, plus custom_review.approval_quote in video_project_read. Before later writes, check that the connected tool exposes every field required by that action, including production clip checks and final quality evidence. If a required field is missing, stop before project writes or paid calls and report that the connected tools and custom-video skill are out of sync. Keep any proposed script in chat and label it as an unsaved, unapproved proposal. Do not drop required fields, switch environments, import the reference as a finished file, or create an HTML review page to work around the mismatch.\n\nThe environment owner must confirm the app-MCP service rollout and refresh the connector's discovered tool schemas. Resume only when the required parameters are visible on the same selected connection. Review and approval stay in the current chat; the Studio creative page is optional. Custom generation requires separate authenticated script and ingredient approvals with their current review tokens and cumulative budgets. Template skills' one-approval instructions do not replace either custom gate. A missing approval field is a tool rollout mismatch, not a reason to require Chrome.\n\n## Match a named format before going custom\n\nBefore creating a custom project, when the brief changes, and before any paid preview, check whether the request names or implies a catalog format: street interview / vox pop / man-on-the-street, street testimonial, podcast, iMessage or chat, split-screen creator, screen insert, listicle, and so on. Call video_catalog_list {kind:\"formats\", brand_id}, then fetch that format's renderer or recipe skill and run its own selector or checks. A street row marked fit.ok: false means no finished street video for this brand; when they asked for a street interview, the one-person conversation (preview only) still fits.\n\nFor a street interview, fetch render-street-interview, read its street-script-writing guide, write a brief JSON with mode, offering_type (physical, service or digital), interaction_type and participants (people interviewed on screen, not counting the interviewer), and run python3 scripts/prepare_script_context.py --brief <brief.json> --out <context.json>. It exits 2 whenever the status is not ready-for-writing. Its route output is binding: route.support (\"render\" or \"preview-only\"), route.person_reference and route.max_participants. A status of unsupported-route lists route_gaps and alternatives; it is a stop, not permission to go custom: show the alternatives and go custom only if the customer picks it. For a software, app or service brand, a laptop, phone or other device showing the software is a prop, not their product: offering_type stays digital or service, so guessing stays unsupported. A brand that sells the device or hardware itself is physical and keeps guessing (product-guess) with that device.\n\n| Street route | On screen | Person image reference | Status |\n| --- | --- | --- | --- |\n| render-street-interview product-guess (physical product only) | Interviewer and up to four participants in one take | Forbidden: people are written in the prompt; the only reference is a standalone physical product photo | Renders |\n| render-street-interview conversation (mic-only, product-sample, concept-challenge) | Interviewer and one participant | Forbidden: no reference images, no phone, screen or UI | Preview only in this format: script and prompt dry run, no paid render |\n| ugc-street-testimonial format | One person, no interviewer | Required: the creator still that format's recipe prescribes | Template format |\n\nIf the request fits a route, use that format; a fitting template format is not a custom video. If nothing fits, tell the customer in plain words what is supported and what is preview only, and name the closest supported options (the selector's alternatives) and how each differs. Or propose custom and say why. Go custom only when the customer chooses it. A custom video built on a named format still keeps that format's hard constraints. For a street interview, those are:\n\n- product-guess only with a physical product the brand sells; never a phone, laptop or screen showing software (no phone, screen or UI demonstration);\n- people described in text, never as uploaded person photos;\n- one generation per interview take;\n- deep focus;\n- the first take at 720p;\n- every caption and letter drawn locally.\n\nA selector result of needs-reference or unsupported-route, a provisional creative review or an unobserved reference is unresolved. Report it and ask before script approval; never continue past it silently.\n\n## Load the shared production harness first\n\nThis entry contains only the GooseWorks connection. Production sequencing, creative craft, reviews and repair loops are maintained in video-production-harness, published from the existing Studio harness. Do not invent a shorter local workflow.\n\nFor a new run, use the video-production-harness dependency returned by catalog_fetch or fetch_skill. If absent, fetch catalog_fetch {type:\"skill\",slug:\"video-production-harness\"}; CLI users can use gooseworks fetch video-production-harness. Require nonempty content, scripts, files, version and contentHash. Require the returned files references/editorial-review.md, references/specialist-handoff.md and references/hook-compatibility.md as nonempty text too; a coherent older release or version label does not supply missing guides. If unavailable, stop before new creative work, project writes or generation; never fall back to a vendored playbook. An unchanged approved resume keeps its recorded package and approvals.\n\nRead the returned content, orchestrator.md, capabilities.md and every detailed step invoked by the orchestrator. Materialize every returned script and file at its package-relative path in a run-specific video-production-harness folder. scripts are relative to its scripts directory; files are relative to the package root. Reject paths outside the package. Save the exact bundle only in the run's persistent workspace. Store only {slug,version,content_hash,package_path} as script_drafts.harness in the saved project; never inline the package content, files or scripts into the review draft. The review payload must stay below its 256,000-character limit. Keep the saved package in this run's durable folder, not a shared mutable installation.\n\nOn resume, read video_project_read and use the saved package matching script_drafts.harness.content_hash. Do not refetch latest or overwrite this run's package. If the package is missing, stop and restore that version before continuing; fetching today's release does not restore it. New projects load the current catalog release. This is an agent-held saved package, not a server-enforced immutable registry.\n\n## Discover specialists for the actual scenes\n\nCustom means composing available capabilities around the brief. Before the design/tool plan,\nmatch each scene and finishing step to the current skill catalog. Repeat this check when a\nnew requirement or repair appears, before writing a helper or making a provider call. The\nshared harness owns the sequence; specialist skills supply implementations inside its phases.\n\nUse the known routes below as starting points. Fetch the relevant skill, read its instructions\nand failure modes, and inspect its returned scripts/files/dependencies before choosing it.\nFor an unmatched need, use the advertised catalog_search {type:\"skill\",query:\"<capability>\"},\nor search_skills on a connection exposing that alias; CLI: gooseworks search \"<capability>\".\nUse short separate queries such as \"creator\", \"footage\", \"motion\" or \"captions\": search words\nare ANDed. Do not restrict discovery to category \"ads\"; video helpers also live under\n\"general\" and \"content\". Follow returned pagination when relevant results are not on the\nfirst page. Fetch by returned slug with catalog_fetch {type:\"skill\",slug:\"<slug>\"}, fetch_skill,\nor gooseworks fetch <slug>. Inspect the selected connection's advertised schemas first.\n\n| Scene or operation | Specialist to fetch | How it fits custom production |\n| --- | --- | --- |\n| Evidence-backed hook, script or script rewrite | [[composes::write-video-ad-script]] | Supply the actual custom scene/tool plan and brand evidence in place of a fixed template recipe. Keep the harness script review and lock. |\n| Generated creator saying an exact script across takes | [[composes::create-creator-takes-h3]] | Plan on line boundaries, preserve approved face/room/dialogue, review the first take alone and carry its native voice to later takes. |\n| Real product footage, screen recordings or screenshots | [[composes::footage-cutlist]] | Inspect source windows, map each proof shot to its line, review the cut list and render the product layer. |\n| Creator beside, over or between product footage | [[composes::compose-creator-layer]] | Combine the inspected creator track and product layer per beat. Preserve the creator's dialogue as the master audio. |\n| Native multi-cut performance or general generated B-roll | [[composes::create-video-seedance-2-fal]] or [[composes::create-video-fal]] | Choose from the actual supported endpoint/schema and quoted operations; use H3 when a continuous exact-script creator track is the better fit. |\n| Street interview, vox pop or man-on-the-street | [[composes::render-street-interview]] | Run its selector first (see \"Match a named format before going custom\"). Keep its route contract: people in text, one take per interview, 720p first take, local captions, end card and ambience bed. Do not call its one-shot driver or create a second project. |\n| A generated person in any still or as a video reference | [[composes::create-creator-takes-h3]] (scripts/make_character.py) | Follow \"People in generated images\" below. The selected route decides whether a person still is allowed at all. |\n| Grounded product edits or scene stills without a person | [[composes::create-image-fal]] or [[composes::create-image-gpt-image-fal]] | Reuse approved product references. Check preview eligibility and bounded pricing for the exact endpoint. |\n| Separate narrator or voice audition | [[composes::create-vo-elevenlabs]] | Use the host-approved voice and locked copy. Do not add a second narration over native creator speech. |\n| Music bed or song | [[composes::create-music-elevenlabs]] | Include its actual quote in the phase budget; preserve the approved audio strategy and lyric/beat timing. |\n| Branded text cards, graphics or end card | [[composes::goose-graphics]] | Start from a fitting layout/style and real logo. It produces graphics; animation still requires an actual local renderer. |\n| Narration-led zoom/pan on an existing demo | [[composes::video-polish]] | Use its measured zoom targets only with the installed Remotion/transcription capabilities required by this route. |\n| Conventional clip stitching or overlays | [[composes::stitch-videos-ffmpeg]] | Use its local implementation when it fits the edit; the shared harness assembly helper remains available for ordinary concat/mix. |\n| Word timing and final caption treatment | [[composes::caption-burn]] | Inspect the actual transcript against locked copy. Caption only after picture and mix polish, then repeat final review. |\n| Replace a defective silent B-roll window in a UGC master | [[composes::ugc-fixloop]] | Keep the original continuous dialogue; this repair does not fix a talking shot's lips or voice. |\n| Complete-video observation and finished-ad checks | [[composes::watch]] and [[composes::review-finished-ad]] | Supply real frames/audio, brand assets and timestamps. Their results support the shared final QC; they do not grant approval. |\n\nFor format-specific motion, chat, podcast, street-interview, product or music layouts, search the catalog for\nthe matching render-* capability and inspect its config and inputs. Reuse a compatible\nrenderer or phase implementation without changing the custom project into a fixed template.\nDo not call a template's one-shot driver, create a second project, or replace the shared\nproduction sequence just to use one of its helpers. The table is a starting map, not an\nexhaustive catalog or a promise that every published skill is executable on this connection.\n\n## People in generated images\n\nApply this whenever a generated image or video shows a person: creator, presenter, interviewee or any face in focus. Customers reject smooth, airbrushed, AI-looking people and blurred backgrounds.\n\n- The selected route decides first. When it forbids person references (every render-street-interview route), describe people in the video prompt and make no person stills. When it requires a person reference, make it with the builder that route prescribes. The ugc-street-testimonial recipe prescribes its own creator still.\n- Otherwise, every generated person comes from create-creator-takes-h3's scripts/make_character.py. Fetch create-creator-takes-h3 and create-image-fal together, because the builder calls create-image-fal's gen_image.py. Pass the customer's six choices (age, gender, ethnicity, hair, wardrobe and scene) and keep its defaults: fal-ai/nano-banana-pro, 4K, phone capture with deep focus, head and upper chest, face filling 45 to 55 percent. Run it with --dry-run --payload-out <file> first: the file holds {model, body}, the exact request. Quote that body through data_post_provider with path set to its model and query:{quote_only:true}, as in \"Saved review schema\". Once the still is approved and quoted, run make_character.py again without --dry-run and with GW_PROJECT_ID set to this project, so the same body goes through the managed proxy.\n- Never hand-write a person prompt, fill the generic avatar template by hand, or use fal-ai/flux/dev, fal-ai/flux/schnell or another Flux route for a face. A hand-written generic prompt is not a substitute for the builder. If the builder's request cannot be quoted, report a pricing blocker.\n- Never send a generated photoreal person still as a reference image to bytedance/seedance-2.0/reference-to-video; its likeness gate refuses uploaded images of people. For an exact-script creator from an approved still, use create-creator-takes-h3's takes.\n- Before showing ingredients, crop each face, enlarge it 2\u00d7 and look. Fix it or report it when skin is waxy or glossy, pores are missing, the background is blurred or bokeh, hands or lettering are garbled, or the face is under about 300 px tall. Show the customer only stills that pass.\n\n## Bind and retain the selected specialists\n\nA fetched package proves that instructions exist; it does not prove that its provider,\npricing, dependencies or local renderer are available. Check each selected route before\nspending. Materialize all returned scripts, files and dependency packages in separate,\nrun-specific package folders, preserving relative paths and rejecting paths outside the\npackage. Do not save several skills into one scripts folder: helpers with the same filename\nwould overwrite each other. Read the installed media-proxy dependency before running any\npaid helper. Every paid submit must propagate this project's project_id / GW_PROJECT_ID\nthrough the managed transport on the selected connection and pass the normal quote and\napproval checks. A helper that cannot carry that context is unavailable for paid custom work.\nNever run its direct-key path or substitute a guessed endpoint or price.\n\nUse specialist craft and executable helpers inside the corresponding shared phase. This\nadapter's project state, supported providers, authenticated script/ingredient gates and\nbudget override a specialist's legacy setup, dollar estimate or standalone approval flow.\nCaptions remain last even if a renderer normally burns them earlier: choose its uncaptioned\noutput or use another compatible implementation. Verify the returned scripts implement any\nrequested setting; prose alone is not an executable capability.\n\nFor H3, the creator still is a script-approved image preview, while the first native-speaking\ntake is a video and requires ingredient approval and its quoted budget. Review that take's\nvoice before the remaining takes; do not spend on it as an image/voice audition. Native H3\nspeech is not a selected ElevenLabs voice: do not invent a voice_id or replace its audio with\nseparate TTS. Save the actual audio strategy and take/reference provenance in extra draft\nfields. Use selected voice roles and playable auditions when the route actually uses them.\n\nSave a compact script_drafts.capability_plan with each selected operation's purpose,\nscene numbers (or \"final\"), skill_slug, version, content_hash, package_path, dependency\nidentities, inputs, expected outputs and supported execution route. Keep full packages in the\nrun's durable workspace, outside the 256 KB review payload. On resume, reuse the recorded\npackages and existing jobs; do not silently fetch latest over an approved run. A missing\npackage must be restored, and a changed route/model/input must be quoted and reviewed through\nthe affected custom gate before spending. If a required capability is unavailable, record\nthe limitation and propose a supported revision instead of improvising a bypass.\n\n## Carry the sourced brief and verify the handoff\n\nBefore script writing, save the writer's documented creative-brief.json from the current brand/product evidence (the Brain search's evidence brief: cited findings, the state of each query and open gaps) and project directions. Include exact product/variant, buyer situation, supported mechanism/claims, offer/CTA, constraints, delivery intent, locked copy, applicable prior decisions and explicit unknowns with source references. Before using the writer, run its free python3 <saved-writer-package>/scripts/verify_handoff.py --package-dir <saved-writer-package> --out working/script/writer-handoff-check.json check against the actual fetched prepare/lint scripts and retain the result and script hashes. A missing checker, failed check or unsupported --brief command blocks the required writer route before saving or approving a new plan; keep any chat proposal unsaved and unapproved until the compatible package is available on this same connection. Do not remove the brief, strict check or requires_creative_brief requirement to run an older parser. Use the fetched write-video-ad-script preparation command with --brief working/script/creative-brief.json and its documented bank/brand/product/template arguments; set the new shape.requires_creative_brief to true. The writer and critic consume that same angle-context. Save only the concise brief/revision and workspace reference in script_drafts extra fields, never full research/package contents. A missing required brief fails the script check; do not rerun known research or rewrite user-locked wording silently.\n\nRead references/specialist-handoff.md from the saved shared harness. For each selected UGC, product/demo or motion operation, extend the existing script_drafts.capability_plan entry with the exact approved brief/script revisions, mapped scene/beat numbers and timing, selected product/reference/source IDs and excerpt ranges, audio/performance choice, package/version/hash and expected returned artifact roles. These are existing extra draft/project artifacts, not new API parameters. Preserve the distinction between numeric script_drafts.scenes[].scene and string production scene IDs.\n\nCheck the actual returned package before executing: H3 plan_takes uses beats id/start/end/vo and character image/identity/environment/delivery; footage-cutlist uses sources and per-beat source/in/out/fit/why; goose-graphics returns static composition/PNG and still needs an actual motion renderer. Do not replace an unsupported helper with a guessed tool name. Preflight the real binary/import/browser and a free dry run, not just its presence. Existing-file skip behavior is not validity: probe saved media and verify fingerprints/approval/job results first, then reuse approved media on resume without new generation.\n\nValidate returned media and provenance against the handoff before clip checks/assembly: exact scene/source revision, words/product/variant, visible proof, measured duration/ratio, selected voice/native speech and neighboring continuity. All speech requires actual isolated performance listening and full mixed-output listening; transcript/word timing only supports that judgment. Audition representative unresolved material and reuse valid approved takes. No mandatory faster delivery or louder climax.\n\n## Hook changes and editorial review\n\nRead references/hook-compatibility.md and references/editorial-review.md from this run's saved harness. Resolve the exact watched source/render and classify same-promise hook versus a new promise requiring different body/proof/payoff. Every changed opening gets new image/action/audio/text review, including visual-only restyles with unchanged words. Shortlist cheap ideas before media spend; keep a broader recut distinct from an isolated opening test.\n\nFor an isolated hook replacement, fetch the existing render-hook-replacement package and inspect its real preflight/config contract. A not-found/empty catalog response blocks that route before paid generation; an open source change is not proof of served availability. Preserve source/body/ending using its own verification, then play/listen to the complete candidates and register each separately. Recommendations are creative judgments, never unmeasured performance winners.\n\nRun rough/fine/final review questions through the current Studio surfaces. First save what the actual cut communicates, then compare with the brief; concept review has no watched-cut verdict. Route unsupported promise to script/concept, missing proof to production, sequence problems to edit and local defects to repair. Unavailable required playback/listening is incomplete and cannot become quality_status passed.\n\nKeep exact feedback text/source/time, intended effect and acceptance condition. Consolidate duplicate notes and record conflicting intentions plus the authorized owner's decision. Preserve changed versus verified dispositions in existing feedback/production metadata or linked project artifacts; executing a fix does not resolve it. Invalidate only affected script/assets/timings/captions/mix/approvals/derivatives, retain valid unrelated work, and rewatch each actual delivered file. Preserve editable timeline, stems, caption sources, selected/candidate versions, rejected options with reasons and scoped pronunciation/delivery history for the next run. Approval and model scores are not audience performance.\n\n## GooseWorks capability binding\n\nRead capabilities.md to bind shared instructions. GooseWorks is authoritative for project state and approvals; local working artifacts grant no spending permission.\n\n- Brand facts and product assets: brand_get_context plus the saved brief/reference analysis.\n- Durable state, concept/design/script/scene/continuity records and feedback: video_project_read and video_project_upsert. Save shared artifact roles in script_drafts extra fields and production metadata; translate spoken lines, captions and scenes into the review schema below.\n- Human gates: discuss free creative choices in chat. Persist concept/design/script before authenticated script approval. Selected storyboard, playable voice auditions, characters, worlds and end card become actual ingredients. Stop for authenticated ingredient approval before any paid clip or preview video. Record the customer's explicit chat approval through patch.approve with the current phase, review token and cumulative budget; a reply alone, local artifact or shared auto mode does not record approval. A handed-off coworker cannot approve its own work. Changes require renewed approval.\n- Provider access, bounded pricing and spend: managed fal/ElevenLabs tools and currently supported endpoints only. Do not switch to direct provider keys, Higgsfield or an unpriced fallback. A missing capability blocks its state until a supported equivalent is reviewed and quoted.\n- Storage: media_upload and media_confirm with exact project ownership; use durable previews in ingredients.\n- Assembly, probes, frame extraction, transcript comparison and continuous inspection: shared package scripts, FFmpeg/FFprobe/Python and available media review tools. A thumbnail or text description cannot satisfy video review.\n- Render attempts, QC, history and promotion: video_render_run and video_project_upsert using the backend contract below. Shared QC still applies; the backend checklist is a minimum.\n\nGrowth and connected coding agents supply local execution. A terminal-free MCP client can prepare/review with connected tools, but must stop or continue through the project's Growth session when required local capabilities are absent. Never claim a render ran without execution.\n\n## Start and resume\n\nAfter the connected tool contract passes the checks above, for a new brief call video_project_upsert with brand_id, name, format:\"custom\", custom_mode:\"generate\", brief:{prompt, audience, objective, product_name, cta, ratio, duration_seconds}, optional reference_url and a stable idempotency_key. Use only brief fields present in the schema. Instagram references have a metered public-data lookup; direct files are downloaded, probed and stored. generated_video_url and media_id mean finished-file imports, never references. Do not use import mode to bypass a generation project's review gates.\n\nRead video_project_read on every resume. When it returns remix.direction for this generated custom project, use remix.direction.implementation as the production starting point: keep the pipeline, full prompts and guards, timings, assembly settings and fixes. Watch the reference. Rewrite all spoken lines and bind products, claims, images and CTA to the target brand. Stay on this same custom project; obtain fresh approvals for its own script/ingredients and budget before any paid call. A source approval or source voice recording never authorizes this new run.\n\nRead video_project_read on every resume. Its project and custom_review are authoritative. Growth creates and retains origin_session_id from trusted chat context. Show the saved review in this conversation and record the customer's explicit approval through the authenticated customer-facing connection as described below. Studio remains available when the customer wants it; never require Chrome to approve. Continue in Growth resumes the bound conversation. A handed-off production coworker prepares the review, ends its turn and waits for the customer-facing connection to record approval before it resumes the same project.\n\nIf reference preparation failed, explain custom_review.reference.error. Continue from the brief if requested, or retry with patch:{retry_reference:true}. A live preparation claim is protected for twenty minutes; after interruption an explicit retry reuses a saved resolved URL when possible. Never invent a reference analysis when no reference was viewed.\n\n## Saved review schema\n\nSave through video_project_upsert {brand_id,project_id,patch:{script:{script_drafts,script,expected_plan_revision}}}. script_drafts contains format:\"custom\", title, duration_target_sec (<=180), aspect_ratio, scenes:[{scene:1,time:\"0\u20135s\",text,caption,direction}], preview_estimate:{total_credits,basis,operations:[\"Describe each priced operation as a string\"]}, optional render_estimate in the same shape, voices:{narrator:{voice_id,name}} and ingredients:[{container,label,subtitle,url,text,note,pending}]. script_drafts.scenes[].scene is a positive integer such as 1; production scene IDs use the separate string shape described below. The plain script is joined spoken copy. Read project.plan_revision and pass it as expected_plan_revision on every existing-draft save; a stale write is refused. Store concept, reference_analysis, continuity, planned operations and job/asset provenance as extra draft fields; keep the review set under 256 KB.\n\nEach estimate requires total_credits (a nonnegative number, at most 3,000), basis (a nonempty string) and operations (1\u2013100 nonempty strings, each at most 500 characters). Do not put operation objects in either estimate. Keep detailed model/settings/cost rows in a separate extra field such as planned_operations. voices is an optional object keyed by role, never an array; each selected role is {voice_id, name?}. Keep unselected audition candidates in a separate extra field. Copy actual catalog voice IDs into the selected roles; every selected role later needs a matching playable voice ingredient.\n\nFor example, a narrator selection is voices:{narrator:{voice_id:\"<actual catalog ID>\",name:\"<catalog name>\"}}. A preview estimate is preview_estimate:{total_credits:100,basis:\"One presenter image and two voice auditions\",operations:[\"Presenter image: fixed-price supported model\",\"Two voice auditions: the saved script\"]}. These values illustrate the shape; quote the actual supported operations for the run.\n\nBefore saving either preview_estimate or render_estimate, quote each remaining paid media operation through data_post_provider with its exact supported path, body and project_id plus query:{quote_only:true}. Before sending this flag, read a fresh data_post_provider tool description from the selected connection. It must explicitly advertise query:{quote_only:true}, maximum_credits and no generation, reservation or debit. Alternatively, require explicit environment-owner evidence that this same selected app-MCP runtime has deployed free quote support. An extensible query schema or a newer fetched skill is not evidence. If the description is old and no verified runtime evidence exists, stop before the call and request the backend rollout and connector refresh; an older server can treat this flag as a paid generation request. The quote requires no approval and generates no media. This mode is MCP-only; do not send quote_only to an HTTP generation proxy.\n\n```json\n{\n \"provider\": \"fal\",\n \"path\": \"fal-ai/kling-video/v3/standard/image-to-video\",\n \"body\": {\n \"image_url\": \"https://example.com/approved-anchor.png\",\n \"duration\": \"5\",\n \"generate_audio\": false\n },\n \"project_id\": \"<this project ID>\",\n \"query\": {\n \"quote_only\": true\n }\n}\n```\n\nRequire quote_only:true, a finite positive maximum_credits and cost.credits:0 in the result. If the server returns a job, audio, a charge or any non-quote result, stop and report the tool/runtime mismatch; never retry it as a quote. Unknown or unbounded prices block planning. Do not substitute public unit prices or guessed billed discounts for maximum_credits: the custom reservation includes conservative quantity, size, audio and preset bounds, while actual settlement can be lower.\n\nSum maximum_credits for all remaining operations in the phase into that estimate's total_credits, including every concurrently planned job. Keep local free assembly/QC operations at zero. The approved project cap includes custom_review.committed_credits plus the remaining quoted estimate; committed includes both settled spend and outstanding reservations. Read the current review again before approval, avoid counting an already reserved job twice, and quote any changed model/body before seeking revised approval. Quotes are not approval tokens or locked prices; real submission rechecks its current conservative bound and the approved budget.\n\nBefore script approval, only plan/save free text. preview_estimate quotes future storyboard images and voice auditions. custom_review.script_approved permits priced image previews on supported fal image endpoints and ElevenLabs text-to-speech auditions. Unknown endpoints require ingredient approval. Use catalog voice previews when possible; do not silently select a paid model outside the preview allowlist.\n\nUpload actual previews with media_upload/media_confirm. Put public/durable URLs on image/avatar/background/endcard/voice/audio/video ingredients. Text containers can carry inline text; a voice or image description cannot replace its preview. Save a render_estimate for remaining clips/assembly and show the complete ingredients. Stop until custom_review.ingredients_approved. Script and ingredient tokens are independent; do not infer approval from project.status.\n\n## Record each approval in this chat\n\nThere are two independent gates. Script approval permits the quoted image/voice previews. Ingredient approval permits the quoted clips and finishing work after the actual review set is ready. Keep both gates even when the customer prefers chat to Studio.\n\n1. Read video_project_read. Show the current saved script or actual playable ingredients, the changes since any previous approval and the cumulative credit limit in custom_review.approval_quote. Explain that the limit includes custom_review.committed_credits plus the remaining quoted operations. An old estimate, approval from a previous revision or remaining-only amount is not the current budget.\n2. Ask for the customer's explicit approval of that phase and amount in this conversation. Copy their actual words as user_quote; never invent consent or interpret a pricing-error complaint as approval of a changed creative plan or larger budget.\n3. Read video_project_read again before recording the yes. Require the same review token, phase and total shown to the customer. If any changed, show the changed review and ask for approval of it; never silently attach the earlier yes to a newer token or amount.\n4. Through the authenticated customer-facing connection call video_project_upsert {brand_id,project_id,patch:{approve:{user_quote:\"<the customer's actual words>\",phase:custom_review.approval_quote.phase,review_token:custom_review.approval_quote.review_token,total_credits:custom_review.approval_quote.total_credits}}}. This is the custom contract; template approvals without phase and review_token cannot authorize custom production. Do not combine this approval with a script edit.\n5. Read back custom_review. For phase:\"script\", require script_approved:true and the matching script_token. For phase:\"ingredients\", require ingredients_approved:true and the matching ingredient_token. Check the approved max_credits covers the current cumulative total before dispatch. A tool error or merely sending patch.approve is not approval.\n\nThe approval_quote.phase is \"script\" or \"ingredients\". Its review_token is the matching custom_review.script_token or custom_review.ingredient_token from that saved snapshot. If approval_quote is missing or the requested phase's review is not ready, finish the free preparation or resolve the returned blocker; never guess a token, reuse the other phase's token or change the budget to force approval.\n\nOnly the authenticated customer-facing connection can record the customer's approval. A handed-off coworker with a production task prepares and displays the review, ends its turn, and waits. It cannot call patch.approve to approve its own work, forward a fabricated user_quote or treat the original brief as approval. After the customer-facing connection records and verifies approval, resume that same task/project with goose_run_task when required. Studio can record the same authenticated approvals if the customer chooses it; do not require Chrome, browser unlocks or Studio access just to approve in chat.\n\n## Paid execution and resume\n\nEvery managed media call carries project_id (GW_PROJECT_ID in scripts) and a stable input fingerprint. Use the managed fal/ElevenLabs endpoints and credentials supplied by Growth; never request customer provider keys. Do not expose those credentials in logs. Recheck custom_review before dispatch. The backend reserves estimated outstanding costs atomically, enforces the approved total (maximum 3,000 credits) and prevents duplicate submissions. At most a 20% overrun can be newly approved; this is not automatic permission to spend above the customer's selected limit. A custom video's budget grows only through a renewed approval. On SPEND_CAP_REACHED the open render is asked to stop: report it \"stopped\" with what was kept (see \"Render run states\") and tell the customer what was made and spent. Then save a revised estimate, show the new custom_review.approval_quote with its current total, record the customer's approval of that total, and open a new render that reuses the saved pieces.\n\nPreview eligibility and price support are separate checks. Use an exact endpoint permitted by the backend preview policy and confirm its current request schema and server quote. Model discovery and provider pricing can change: use the selected connection's actual quote, including supported provider estimates, instead of a skill-maintained priced-model list. A model-family name does not make every route eligible for script previews. Do not move a preview to another phase to bypass its approval gate. If pricing cannot be established, keep the saved request and report a system pricing blocker; do not ask the customer to choose when their choices are already approved. Final clip models require ingredient approval. Existing-job polls remain permitted after edits; retrieval does not authorize new media.\n\nSave provider request IDs and actual uploaded URLs promptly. Custom operations with known rejection can retry safely; ambiguous timeouts keep their authorization until reconciled. Media spend is metered through the shared gateway. Custom videos have no extra template base render fee. Imported finished files still file for zero credits.\n\n## Clips, assembly and quality\n\nUse the selected packages in script_drafts.capability_plan for each scene and operation. Recheck specialist discovery for new requirements or repairs before improvising a helper. Keep anchors and exact lines fixed. Use the Python assembly helper returned in the shared harness scripts for actual existing clips. It needs only Python, FFmpeg and FFprobe; it does not call a provider or require Chromium. Inspect generated clips before assembly, and watch the final export after assembly/captions.\n\nOpen a render with video_render_run {brand_id,project_id,kind:\"full\"}; executor:\"cloud_agent\" for Growth when exposed by the current schema. Report live workflow_stage/progress_note/progress_percent while producing it. Upload the final file to scope:\"video_project\", scope_id:project_id. The tool opens/reports a client-executed render; fixed server orders remain paused.\n\n### Render run states\n\nReport the open render's state with video_render_run {brand_id,project_id,render:{render_id,...}}. Use the row that matches:\n\n| Situation | status | workflow_stage | quality_status | Also send |\n| --- | --- | --- | --- | --- |\n| Producing, checking or repairing | \"running\" | preparing, rendering, checking, repairing or rechecking | \"pending\" | progress_note, progress_percent, steps |\n| Waiting for the customer: they must approve a candidate, choose or give feedback, with or without an uploaded candidate | \"running\" | \"blocked\" | \"blocked\" | output_url when an uploaded candidate exists, error_message saying in plain words what you need from them, and choices (at most 4 buttons; put credits on any paid choice). A quality_report is optional while waiting; when sent, it carries the current findings |\n| Stop requested: a progress callback returned stop:true because the customer stopped the video or a paid step hit the spending limit | \"stopped\" | omit | omit | progress_note saying in plain words what was kept. Start no new paid step; let a call already running finish. |\n| Real failure: no usable candidate can be produced (provider refusal with no approved alternative, or a system blocker you cannot recover) | \"failed\" | omit (the server sets blocked) | \"blocked\" | error_message in plain words |\n| Done | \"complete\" | omit (the server sets ready) | \"passed\" | output_url and the full quality_report, only after the final recipe is saved and read back as described below |\n\nWaiting for the customer is not a failure, and neither is a stop. Never report \"failed\" because you are waiting for approval, a choice or feedback, or because the customer or the spending limit stopped the video. When stop:true comes with stop_reason \"superseded\", report nothing further on that render. The app shows a waiting render's candidate as \"Needs your approval\" and keeps it for up to 14 days; after that the server ends it as failed with failure_code review_expired. The uploaded file stays in the project either way.\n\nA waiting state is valid only while the approval it rests on is current. A render can complete only under the approval it was opened with. After any script or ingredient edit (yours or the customer's), or after review_expired, the old render cannot be completed: read video_project_read {brand_id,project_id,include:[\"assets\",\"renders\"]}, show the changed review and the current custom_review.approval_quote in chat, record the customer's approval of each phase it asks for, then open a new render. Opening a new render stops the older open render of this video with stop_reason \"superseded\" (\"Replaced by a newer version.\"). Report nothing more on a superseded render and start no paid step for it; continue on the new one. To deliver an unchanged candidate on the new render, follow \"Finalize an existing candidate after re-approval\" below.\n\nWhen the customer approves a waiting candidate and nothing in the review changed, finish the shared final review and complete that same render; a candidate needs no separate approval phase. When they ask for changes, save the feedback and follow the normal edit and approval gates.\n\nOn resume, treat a render left at \"running\" with workflow_stage \"blocked\" as a pending customer decision. Read custom_review and custom_review.feedback before doing anything else. If your own render shows stop_reason \"superseded\", another run replaced it: read video_project_read {brand_id,project_id,include:[\"assets\",\"renders\"]} and continue on the newer render; do not open another one just to resume.\n\nAfter each accepted generation or deterministic edit, update patch.production with the steps and settings that actually ran. Save and read back the actual candidate recipe before showing its preview or pausing for review, so an interrupted or waiting run does not leave only an early plan in the backend. Partial progress is allowed; pending creative approval or QC must stay explicit and must never be converted into passing clip evidence.\n\nBefore completing or pinning a candidate, replace the early planning manifest with the actual production recipe. Record every current script scene using its string scene number, exact current spoken line, measured duration and full generation prompts or deterministic rendering instructions. Record the actual pipeline/model settings, assembly/captions/audio settings, and repairs in fixes. Never leave blocked/pending planning statuses in a completed recipe. Preserve full prompts and guards; do not reduce them to summaries. Save partial progress while working, but an unfinished recipe is not a reusable final.\n\nSave the final recipe through patch.production, then read video_project_read and compare production_manifest with what you just saved, including all scene IDs and spoken lines. If the save fails or the read-back is stale/incomplete, repair it before completion; do not describe the run as finished or remixable. Final completion freezes the actual recipe and reviewed script together for the next agent.\n\nSave patch.production as a production manifest with version:1 and a nonempty pipeline array. Each pipeline item requires step and model; purpose and settings are optional. production.characters is an optional array, never a record/object: each entry requires both name and prompt strings. Store anchor media IDs, URLs, actual appearance and other extra provenance in script_drafts; character entries accept name and the full design prompt.\n\nproduction.scenes[].id is a string containing the corresponding script scene number, for example \"1\". script_drafts.scenes[].scene and production.clips[].scene remain positive integers, for example 1. Map production scene IDs with String(scriptScene.scene); do not convert clip.scene or script scene IDs to strings. Each scene can also carry line, still_prompt, motion_prompt, duration_s and notes. style is an object; voice and music are objects when present; assembly is an object; fixes is an array of {problem,fix}.\n\nEach clip requires a numeric scene, a stored URL and exactly one of each named check: visual_artifacts, brand, product, voice_and_script, duration_and_ratio. Each clip check is {check,status:\"pass\"|\"fail\",note?}; not_applicable is not valid for clip checks. Save exactly one checked clip per approved script scene before final completion, with every check passing. Before clips exist, omit clips from an ingredient-preparation save.\n\nThe following is a one-scene JSON shape example. Replace all placeholders with actual saved values. Its failed QC statuses are deliberately unapproved; never copy a passing status without performing the shared review.\n\n```json\n{\n \"patch\": {\n \"production\": {\n \"version\": 1,\n \"pipeline\": [\n {\n \"step\": \"Scene clips\",\n \"model\": \"<actual supported model>\",\n \"settings\": {}\n }\n ],\n \"style\": {\n \"prompt\": \"<shared visual direction>\",\n \"negative\": \"<shared guards>\"\n },\n \"characters\": [\n {\n \"name\": \"Presenter\",\n \"prompt\": \"<exact full character design prompt>\"\n }\n ],\n \"scenes\": [\n {\n \"id\": \"1\",\n \"line\": \"<approved spoken line>\",\n \"still_prompt\": \"<exact full still prompt>\",\n \"motion_prompt\": \"<exact full motion prompt and guards>\",\n \"duration_s\": 5\n }\n ],\n \"voice\": {\n \"voice_id\": \"<selected catalog voice ID>\",\n \"name\": \"<selected voice name>\",\n \"model\": \"<actual voice model>\",\n \"settings\": {}\n },\n \"assembly\": {\n \"ratio\": \"9:16\",\n \"fps\": 30\n },\n \"fixes\": [],\n \"clips\": [\n {\n \"scene\": 1,\n \"url\": \"https://example.com/confirmed-scene-1.mp4\",\n \"checks\": [\n {\n \"check\": \"visual_artifacts\",\n \"status\": \"fail\",\n \"note\": \"<actual observation; pass only after inspection>\"\n },\n {\n \"check\": \"brand\",\n \"status\": \"fail\",\n \"note\": \"<actual observation; pass only after inspection>\"\n },\n {\n \"check\": \"product\",\n \"status\": \"fail\",\n \"note\": \"<actual observation; pass only after inspection>\"\n },\n {\n \"check\": \"voice_and_script\",\n \"status\": \"fail\",\n \"note\": \"<actual observation; pass only after inspection>\"\n },\n {\n \"check\": \"duration_and_ratio\",\n \"status\": \"fail\",\n \"note\": \"<actual observation; pass only after inspection>\"\n }\n ]\n }\n ]\n }\n }\n}\n```\n\nComplete with video_render_run {brand_id,project_id,render:{render_id,status:\"complete\",output_url,quality_status:\"passed\",quality_report:{version:1,summary,checks:{source,brand,product,hook_and_scene_order,voice_and_script,captions,endcard_and_cta,duration_and_ratio,visual_artifacts},detected_issues:[],repair_actions:[],checked_at}}}. Each final check is {status:\"pass\"|\"fail\"|\"not_applicable\",note}; brand/product/scene order/duration/visual artifacts must pass. A candidate that fails QC is never complete: keep it \"running\" with workflow_stage \"repairing\" while you fix it, report the waiting state above when the customer must decide, and use \"failed\" only for a real failure. Final completion binds approval revisions, output and quality evidence to the render; pin with patch.final_render_id only after passing.\n\nAll nine named final checks above are required, including source, voice_and_script, captions and endcard_and_cta when they are not applicable. A final check uses status:\"pass\"|\"fail\"|\"not_applicable\" and an optional note. The five mandatory passing checks are brand, product, hook_and_scene_order, duration_and_ratio and visual_artifacts; no final check may fail and detected_issues must be empty to complete. Record why a permitted check is not_applicable. Keep actual repair_actions and use the actual ISO 8601 checked_at time. The backend report is a minimum; it does not replace the full shared watch, claim verification or craft review.\n\nThis waiting-for-the-customer report illustrates every required field. Replace the URL, reason, choices, findings and example timestamp with actual evidence. Only after repairs and the shared final review pass may the render be reported complete with quality_status:\"passed\", its confirmed output_url and no unresolved issues.\n\n```json\n{\n \"render\": {\n \"render_id\": \"<existing render ID>\",\n \"status\": \"running\",\n \"workflow_stage\": \"blocked\",\n \"quality_status\": \"blocked\",\n \"output_url\": \"https://example.com/confirmed-candidate-in-this-project.mp4\",\n \"error_message\": \"<what you need from the customer, in plain words>\",\n \"choices\": [\n {\n \"label\": \"Approve this version\",\n \"message\": \"I approve this version\"\n },\n {\n \"label\": \"Change something\",\n \"message\": \"I want to change something in this version\"\n }\n ],\n \"quality_report\": {\n \"version\": 1,\n \"summary\": \"<actual full-video review summary>\",\n \"checks\": {\n \"source\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"brand\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"product\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"hook_and_scene_order\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"voice_and_script\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"captions\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"endcard_and_cta\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"duration_and_ratio\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n },\n \"visual_artifacts\": {\n \"status\": \"fail\",\n \"note\": \"<actual finding from the shared review>\"\n }\n },\n \"detected_issues\": [\n \"<actual unresolved issue>\"\n ],\n \"repair_actions\": [],\n \"checked_at\": \"2026-10-03T00:00:00Z\"\n }\n }\n}\n```\n\n### Finalize an existing candidate after re-approval\n\nUse this when an uploaded candidate (a confirmed video in this project) already shows and says exactly what the current script says, but its render can no longer complete: the script was amended to match the take, or the wait ended as review_expired. If any spoken line, caption, scene or ingredient differs from the current script, this route does not apply: repair or regenerate under the normal gates.\n\n1. If the script changed, save it and record the customer's script approval first, as in \"Record each approval in this chat\". Then save the candidate as a video ingredient (its confirmed URL) in the review set, show the candidate and the current custom_review.approval_quote in chat, and record the ingredient approval. The earlier render was approved against the old review, and the server will not complete it.\n2. Open a new render with video_render_run {brand_id,project_id,kind:\"full\"}. Start no provider call. The older waiting render stops as superseded.\n3. Run the shared final review on the existing file against the current script. voice_and_script and captions must match the current lines.\n4. Save and read back the final production recipe with the current lines and one checked clip per scene, as described in \"Clips, assembly and quality\". Reuse the clip records from the candidate's saved recipe, updated to the current lines.\n5. Complete the new render with status:\"complete\", output_url set to the existing candidate's confirmed URL, quality_status:\"passed\" and the full quality_report. Do not regenerate or re-upload. No credits are spent.\n6. Pin it with video_project_upsert patch.final_render_id (alias set_final_render).\n\n## Feedback and delivery\n\nSave customer script/ingredient/video feedback through the connected project tools before continuation; affected creative edits invalidate their approvals. Read custom_review.feedback on every turn, preserving the original watched render/time and text when provided. A selected final change never relocates an earlier note. Resolve only after the candidate satisfies the original intended effect and full-output recheck; keep incomplete or conflicting notes open. Replace only the affected assets; save revised estimates and show the changed review and cumulative budget in the same chat before recording renewed approval. Deliver the playable current_render_url with render_artifact kind:\"video\" in Growth and actual project spend. Include the Studio app_url when a text host or the customer needs it; do not require it for approval. Keep final history; never create another project just because an agent restarted.\n\n## Reviewed snapshot, voices and supported pricing\n\ncustom_review returns script_drafts and plan_revision from the same saved snapshot as its tokens. Show that snapshot in chat; Studio can also render it. Use the revision for edits and the current approval_quote for each approval. Spoken copy (text) and on-screen copy (caption) are separate fields. Voice audition ingredients carry role and voice_id; script_drafts.voices maps each role to its selected voice_id/name. Every selected role needs an actual matching playable preview before approval.\n\nA pending/processing/failed reference blocks script approval. If the customer explicitly chooses to continue from the brief without the reference, save that choice through the connected reference-review tool when exposed, or offer Studio's Continue from brief action when needed. This is a reference decision, not a requirement to approve in Chrome. Do not silently replace a requested reference with brief-only production.\n\nCustom provider submits require a positive server quote, including quantity, supported dimensions/audio and any provider estimate. The server must establish source duration for audio/video-driven routes from authorized media metadata or a bounded probe; do not add invented duration fields to the provider body to force a quote. A provider estimate permits production within the approved budget even when exact final billing is unavailable. Missing source duration or an unavailable provider estimate is a system blocker to resolve on the saved project, not a model allowlist or a new creative choice. A conservative reservation is released to actual charged spend when the provider result settles. Final output must be a confirmed, stored video_asset in this exact organization/project, and completion binds its media_id to the render. External hotlinks cannot be pinned as generated custom finals. The portable helper checks FFmpeg's libass capability before local captions, rejects clips shorter than their timeline and validates video-stream duration.\n\n## Use uploaded footage\n\nWhen the customer asks to use existing footage, check the actual advertised schemas. Use `media_search` with purpose `production`, kind `video`, the brand and current project when needed. Omit query spend for free retrieval. Read facts/scenes with `media_analyze`, then use `media_excerpt` action `inspect` to review actual bounded frames and timed transcript before selecting. A description or thumbnail URL alone is not visual review.\n\nFreeze `source_excerpt:{asset_id,analysis_revision,scene_id,start_ms,end_ms,audio_mode}`. Revision is the original-byte SHA, scene may be null for a known user trim, bounds use integer milliseconds, and audio is original or muted. Call `media_excerpt` action `attach` with that exact selection and a stable idempotency key. The returned project remains unfinished and unapproved. Open its existing Studio review; preserve user locks and requested format, and keep footage separate from image packshot indexes. An incompatible format requires an explicit choice before changing it.\n\nFor custom production, fetch the current shared `video-production-harness` and follow script/ingredient gates. Download the verified original and trim its exact selected window at normal speed with the approved audio. Recheck current revision and production permission before consumption and final upload/completion. Save excerpt lineage in plan and ingredient readback. Missing optional semantics does not require another upload; known user-selected stored footage remains usable. Research and competitor references never become production footage. Selected originals require no paid generation; only newly generated/replaced ingredients consume the approved budget.\n",
4
- "goose-video-local": "---\nname: goose-video-local\nslug: goose-video-local\ndescription: >\n Render an EXISTING GooseWorks video ad project or video batch with ffmpeg (plus Playwright for\n phone-mockup formats) and the GooseWorks media proxies, then save the finished MP4 back to the\n project over MCP. Runs on the user's own machine (Claude Code / desktop app, with or without the\n gooseworks CLI) OR inside a GooseWorks workspace sandbox (canonical MCP tools + Bash, no CLI).\n Use for a client-side format project (created by goose-video), a template-remix project or a\n video batch. A copy-for-Claude command or project id must first be checked with\n video_project_read. A hosted connector with no shell hands this same project to the GooseWorks coworker. To start a NEW video ad in chat,\n use goose-video first.\ncategory: ads\nversion: 1.0.1\nauthor: GooseWorks\ntags: [gooseworks, ads, video, remix, imessage, podcast, ugc, local-render, sandbox, byoa]\n---\n\n# GooseWorks Video Ads \u2014 local remix runtime\n\n## How to talk to the customer (applies to every message you send them)\n\nThe customer is a marketer or founder, not an engineer. Everything in this skill about tools,\nfields, files, polling, models and pipeline steps is for YOU. Never pass it on to them.\n\n- **Say what they get and what they need to decide, never how it's made.**\n Bad: \"Checking the media-proxy helpers for voiceover timestamps and lipsync.\" / \"Generated with\n gpt-image-2 at high quality, mouth closed, passes QC.\"\n Good: \"Recording the voices now.\" / \"Here are your two hosts.\"\n- **Never mention** tool, file or field names, ids, JSON, commands, scripts, installed software,\n uploads, model or vendor names, timings in seconds or frames, retries, polling, internal statuses\n or your own quality checks. Fix what you can quietly. If they ask how something works, explain it\n simply.\n- **Update them only at milestones they care about**: choices confirmed, script and voices done,\n visuals done, ready to review, finished. One short line each, then stay quiet until the next\n milestone. No update for setup, downloads, uploads or saving.\n- **Money is in credits, with a number**: \"the full video uses about N credits\". Never \"the cheap\n pieces\" or \"the expensive render\".\n- **Problems:** say what it means for them and what they can do, in one or two sentences. No error\n codes or stack traces. Only raise a problem that changes something for them.\n- **Use their words**: script, voice, hook, scene, host, ending, logo, the product shot.\n- **No filler**: no \"Great question!\", \"Certainly!\", \"I hope this helps!\", no restating what they\n just said at length.\n\nExample. Instead of a dozen lines about render tools, scripts, uploads and portraits, send:\n\"Got it: warm tone, home podcast studio, Brielle as the skeptic and Mark as the believer.\" then\n\"Script and voices are done (about 34 seconds).\" then \"Ready for you to review: the script, voices,\nboth hosts and the ending. You can also review your recipe ingredients in the app: <link>. Say go\nhere and I'll make the full video (about N credits).\"\n\n## Use current instructions for new work\n\nBefore a new task in a terminal host, run `gooseworks skills status` once. It compares the\ninstalled entry files, running CLI and published npm release. An older CLI needs a package\nupgrade before `gooseworks update`; update alone only uses that CLI's bundled instructions.\nPreserve local edits or unknown install provenance. Review/back up before explicitly replacing\nmodified files; never quietly reinstall over them. If the release check is unavailable, report\nthat freshness is unknown rather than claiming the local copy is latest.\n\nFor a new recipe run, fetch its package from the connected catalog. Retain the returned\n`version` / `contentHash` and every dependency's hash with the saved package. When reusing a\nsaved fetch JSON, `gooseworks fetch <slug> --saved-package <file>` returns the current package\nand a hash comparison without changing that file. With MCP, if the advertised `catalog_fetch`\nschema accepts them, send `saved_content_hash` and `saved_dependency_hashes`; otherwise fetch\nnormally and compare the returned hashes yourself. Missing hashes mean unknown, not current.\nThe server cannot inspect a client's saved files; hash metadata does not certify later edits.\nFetch current packages into a new run directory and report stale saved instructions. Preserve\nan existing approved run's recorded package; changing that harness requires a reviewed change\nand approval before spending. Hosted installed snapshots use the existing Skills Update action.\nSkill content and a host's cached MCP tool schemas are separate: refreshing one does not refresh\nthe other. Check the actual advertised tools before using new fields.\n\n## Prepare the video workflow and brand before creative work\n\n**Installed entry files are bootstrap instructions.** For new work started from an installed\nfile or an old chat, fetch `catalog_fetch { type: \"skill\", slug: \"goose-video-local\" }` on the selected\nconnection and read its returned content and dependencies before continuing. Use the already\nfetched body when this entry came from that connection in this run; do not recursively fetch\nthe same entry. A CLI freshness warning does not block using the current connected package.\nKeep the fetched package and hashes in a new run folder; do not replace edited installed files.\nIf current instructions cannot be loaded, resolve the connection before format suggestions,\nscript writing or production. Do not continue from an old installed workflow or session notes.\n\n**Follow the connector's full-guide requirement when available.** Fetch\n`catalog_fetch { type: \"skill\", slug: \"gooseworks-guide\" }` when the connector requires it.\nThe only older-server exception is below; it never removes brand preparation or approvals.\n\n| Returned guide/workflow state on the selected connection | Required action |\n| --- | --- |\n| 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. |\n| 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. |\n\nA missing custom entry or required custom tool/schema remains unavailable: never substitute\na template or an import to bypass it. Never hide other fetch errors or continue from cached\ninstructions merely because the guide was not found.\n\n**For an existing project or batch, read `video_project_read` first.** Determine its actual\nroute and saved review state before fetching a recipe or doing local work. Resume an approved\nrun with its recorded packages, brand inputs, script and ingredients; do not silently replace\nthem with today's release or brand rules. Follow returned preparation requirements and use\nthe existing affected review/approval flow for an intentional change. Reads, free drafts,\nimports and existing-job retrieval do not grant permission for new paid production.\n\n**For every new plan, resolve the brand and call**\n`brand_read { brand_id, sections: [\"summary\", \"kit\", \"products\", \"learnings\"] }` **before**\nsuggesting formats, mining video angles, writing any script, creating a production plan or\nhanding the work to another agent. Use `brand_get_context` with those same four sections only\nwhen advertised. Reuse a complete read from this run on the same connection and brand.\nOnboarding completion, a summary-only response and earlier session notes do not supply the\nkit, selected product facts or saved rules. If the full read is unavailable, resolve it before\ncreative work. Use known kit facts while research is incomplete; do not restart research\nsolely because its status is pending. Never invent a product fact or accept a campaign to\nbypass preparation. Then search the Brand Brain (next section) before suggesting formats,\nchoosing an angle or asking the user for a brand fact.\n\nCarry that context through custom/template/idea routing and delegation. Before writing, make\nthe workflow's brand-rules file from those sources: pronunciations, required spoken copy,\nprohibited claims, real logo, font, palette and the selected product's supported facts.\nEntries prefixed `Video preference:` and visual/pacing instructions are creative direction,\nnot spoken lines. Use the kit's audience, offer and voice as defaults without an interview.\nThe rules file mirrors context; it is not proof of a read or permission to spend. Keep the\nroute's existing script, ingredient and budget approvals, including its allowed previews.\n\n**Use the server's brand bundle when the selected API returns it.** Project create/read may\nreturn top-level\n`brand_context`: `{ version: 1, brand_id, digest, loaded_at, sections, brand, kit, products,\nlearnings }`. Its sections cover summary, kit, products and learnings.\n\n| Returned brand/project response on the selected connection | Required action |\n| --- | --- |\n| 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. |\n| 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. |\n\nAn incomplete/malformed bundle or missing brand section is a preparation failure, never the\nolder-API exception. Once this connection returns an authoritative bundle, use its binding\ncontract. `script_drafts.video_brand_context` is server-owned: do not author, replace or forge\nit, and do not invent a read receipt or hash local brand facts into one. Keep the full bundle\nout of client review payloads. If managed generation returns HTTP 409\n`video_brand_context_required`, reload the project and bind the plan to its returned bundle\nthrough the normal review flow before retrying. That refusal never permits the older-API\nfallback, skipping sections or retrying paid calls blindly. Unchanged approved legacy resumes\nretain their package, context and approvals; an upgrade alone needs no extra approval.\n\n**To apply a deliberate brand correction to a new or revised plan**, make a fresh, unfiltered\n`brand_read` of those four sections. When it returns the authoritative bundle, use its\nreturned `brand_context.digest` in the complete revised `script_drafts` through the existing\n`video_project_upsert` `patch.script` save; the server verifies the current brand and replaces\nits snapshot with the saved plan. On an older API with no bundle contract, apply the actual\nfour-section correction and save the revised draft through its existing flow without inventing\ndigest fields. If a connection that already returned a bundle cannot supply the fresh bundle,\nresolve that failure before rebinding. Review and approve the affected script, ingredients and\nbudget through the normal flow. Never refresh an ongoing approved run automatically; its\nrecorded package and brand inputs stay pinned until an intentional change.\n\n## Search the Brand Brain, then propose \u2014 before any creative choice or question\n\nThe Brand Kit is a summary. The brand's saved knowledge (its Brain) holds what the Kit does not:\nrules from past feedback, approved and rejected creatives, customer evidence, approved claims,\nreports and documents. **After `brand_read`, and before you choose an angle, claim, hook,\nproduct emphasis, format or source ad \u2014 and before you ask the user for any brand fact \u2014 call\n`knowledge_search { brand_id, query }`** when it is registered. It is free and read-only: no\napproval, no announcement to the customer and no questionnaire.\n\n1. **Search for this task, not the whole Brain.** Run one short query (under 500 characters) in\n the user's own words plus the product (for example \"ads for <product>: what worked, what to\n avoid\") and one with\n `source_types: [\"evidence\", \"claim\", \"learning\", \"creative\", \"document\"]` for proof and past\n creative results. Add a query only for a specific open question. Reuse results from this run.\n2. **Keep these states distinct** and record which one each query returned:\n\n| Result | Means | Do |\n| --- | --- | --- |\n| `status: \"ok\"` with matches | Saved knowledge exists | Use it; keep each fact's citation in your working brief |\n| `status: \"empty\"` | Nothing saved matches this query | Say \"no saved evidence for <topic>\", never \"the brand has no proof\" |\n| `refresh_required` | The results shown are current; some changed sources were left out | Use them; search again shortly for anything missing |\n| `building` | The index is not ready | Retry once shortly, then continue with the gap stated |\n| 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 |\n| An empty Kit field | Only that field is blank | Not a search result: still search before asking |\n\n3. **Let the findings change the plan.** A `dont`/`must` learning or a rejected creative rules\n options out; an approved or well-rated past creative is a proven angle to lead with; a report\n shows what worked. Claim-grade proof is a result with `approved_ad_claim: true` or the kit's\n `approvedClaims` within their stated applicability; the kit's `proofPoints` are what the\n backend allows an ad to state, used as written. Other Kit text, documents and performance\n numbers are context, never public claims. Never invent an offer, price or result. Judge\n relevance and skip results about another product or business.\n4. **Propose; don't interview.** Lead with one recommended direction (angle, product, source or\n format) and a one-line reason naming what you found, with up to two alternatives. When several\n directions fit (two audiences, products or campaigns), pick the one the evidence favours, such\n as an active campaign or approved past creatives, and name the other as an alternative instead\n of asking. Ask only for a decision the brand read and the search cannot settle, or for spend\n approval. Never ask the user for a fact the Brain already answered.\n Keep directions already chosen (the user's words, a project's creative brief, handed-off\n defaults, approved plans): search only to fill empty fields and to apply saved rules. When a\n returned card or picker presents the choice, your recommendation is its one acknowledgement\n line and the saved plan's defaults, not a separate list.\n5. **Carry an evidence brief** into the routed skill, writer or plan: findings with citations,\n the state of each query, what it ruled out and the open gaps. Tell the customer the findings\n in plain words; the citations stay in the brief.\n\nOn an approved resume, keep the saved brief and evidence; search again only for a new decision.\n\n## Chat hosts and cards\n\nFirst perform the mandatory route check below. Then, without a shell, hand a verified template\nproject to `goose_run_task { brand_id, project_id, message }`, or a verified template batch to\n`goose_run_task { brand_id, batch_id, message }` (never send both ids). Keep task_id and the\nsame project or batch. Generated custom children keep their separate make-custom-video flow.\nInside a coworker sandbox you are the renderer: never delegate recursively.\nWith card.display_hint:\"widget\", say at most one line and never duplicate its plan, total or\nlinks. Otherwise print card.text_summary. Read again on customer input, not in a polling loop.\nA free saved draft is not proof that planning started or a complete plan exists.\n\n## Mandatory route check before any local work or spend\n\nFor every existing `project_id` (including one supplied by the app's copy-for-Claude command),\ncall `video_project_read { brand_id, project_id }` **before** template lookup, toolchain setup,\nmedia-proxy calls, or a review-set upload. For a batch, inspect each child project.\n\n- **A server-rendered order** \u2014 the response has `creative_plan`,\n `project.creative_spec_revision_id`, `order.creative_spec_revision_id`, a planning\n `lifecycle`, or an `order` / `script_drafts.recipe` on a server format \u2192 **stop.**\n Server video orders are paused, and there is **no vetted local node-execution API** for them:\n do not rebuild one locally. Tell the customer this project was made for the server flow, which\n is paused; offer to start the same ad on a client-side format (`goose-video`, fetched with\n `catalog_fetch { type: \"skill\", slug: \"goose-video\" }`, older clients:\n `fetch_skill(\"goose-video\")`). An order already holding credits can be released with\n `job_cancel`.\n- **A client-side format or template remix** (a `source_sample_id` / `template_id` and none of\n the above) \u2192 continue below.\n- **Generated custom video** (`project.custom_video_state.mode === \"generate\"`) \u2192 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.\n- **Unclear** \u2192 read again or ask; never guess and generate. A copy prompt that names this skill\n is not proof of which kind the project is.\n\nContinue below only for a verified client-side format or template remix.\n\nFor client-side formats and template remixes, you produce **video** ad creative wherever THIS agent runs and sync\nthe result back to the GooseWorks app over MCP. This document is the **runtime contract** (auth,\ncredits, the media proxies, data I/O, the review gate). A separate **recipe** \u2014 the template's\n`recipe`, plus the capability skills it names \u2014 tells you *what to make* (the pieces, prompts,\nmodels, order of assembly).\n\n**Division of authority: read both, but when they disagree THIS doc wins on the environment AND the\nreview/approval flow.** The recipe governs WHAT to make; this doc governs WHEN you pause, generate,\nand spend. In particular: a recipe may spell out a **multi-phase, multi-gate** flow \u2014 \"generate the\nstill [GATE] \u2192 approve \u2192 author the prompt [GATE] \u2192 approve \u2192 render [GATE] \u2192 approve\", several\nseparate pauses. **Do NOT run it that way.** Collapse every one of those gates into the single\n**review-once** flow below: one review set, one approval (Step 3). Take the recipe's pieces, prompts\nand models; ignore its intermediate pauses. This is the exact contradiction that confused past runs\n(GOOSE-2542) \u2014 there is no ambiguity: review-once wins.\n\nThe app NEVER runs this skill by itself \u2014 it is the viewer + review surface; you are the renderer.\n\n## Keep the selected connection for the whole run\n\nCall `account_whoami` on the connection that owns the brand/project before the first write.\nKeep `environment.name` and the public `environment.api_origin` with this run. Respect the\nuser's selected production or staging connection. If multiple connections are available and\nnone was selected, resolve that once. Missing or unknown identity is uncertainty, not\npermission to switch. Never infer the environment from credits, billing links or Node mode.\n\nReads, project creation, uploads, generation, polling and final updates all use that same\nconnection. If a production request fails, resume or report the failure on production;\nnever retry it on staging or recreate the project there. Before retrying a timed-out write,\nread back the existing project or paid request on the selected connection.\n\nLocal/CLI proxy origin must match `environment.api_origin`. If it differs, use the MCP\nrelay on the selected connection (`GW_MEDIA_VIA=mcp`) before paid calls. Do not change\ncredentials or API origins to recover a failed write. An explicit user-requested move is\na separate operation, with the existing project and paid requests reconciled first.\n\n## Check assets for the selected format before spending\n\nFor a template, read its structured `asset_readiness` from\n`video_catalog_list { kind: \"formats\", brand_id }`. `missing` names gaps;\n`needs_review` means suitability is unverified, including older recipes without\nstructured requirements. `ready` describes assets only, not script or budget approval.\nFor a custom video, derive requirements from its actual approved scenes.\n\nInspect candidate files for the chosen product and format. A catalog photo can contain\nmultiple objects, other products or a person; its existence or approved status does not\nmake it a standalone image of the selected product. Check object count, framing, readable\nprint and real image bytes. A service conversation has no automatic packshot requirement.\n\nReuse a suitable approved image first. A free crop or cutout is a new file: keep the\noriginal, inspect the result and include it in the normal ingredient review. When no\nusable input exists, explain the gap before generation. Estimate any paid preparation\nseparately before spending. Record the selected asset and inspection in the project\nreview set; do not add a separate approval round.\n\n## Where am I running? (decide once, first)\n\nCheck in Bash, without printing any secret value:\n\n```bash\n[ -n \"$GW_MEDIA_PROXY_TOKEN\" ] && echo sandbox || echo local\ncommand -v gooseworks >/dev/null && echo cli || echo no-cli\n```\n\n| Mode | How you know | Skills / atoms | Media-proxy auth |\n|---|---|---|---|\n| **GooseWorks sandbox** | `GW_MEDIA_PROXY_TOKEN` is set | `catalog_fetch { type: \"skill\" }` | env: `GW_MEDIA_PROXY_TOKEN` + `GW_*_PROXY_URL` |\n| **Local, CLI installed** | `gooseworks` on PATH | `gooseworks fetch <slug>` or `catalog_fetch` | `~/.gooseworks/credentials.json` |\n| **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) |\n\nThe `gooseworks` CLI and `~/.gooseworks/credentials.json` are **optional**. Everything this skill\nneeds from the app goes through the GooseWorks MCP tools below; the atoms' `media_proxy.py` reads\ncredentials.json when it exists and falls back to the `GW_MEDIA_PROXY_TOKEN` env otherwise.\n\n### Paid media over the MCP: no key, no CLI needed\n\nEvery paid generation goes through the GooseWorks media proxy and is billed to the project. **You\nnever need FAL_KEY, an ElevenLabs key or `fal_client`.** An atom, a recipe or an open-source\nskill that lists `FAL_KEY` in its environment is describing a standalone setup; here the proxy\nsatisfies it. Never stop, and never ask anyone to set a key, because one is missing.\n\n**A one-off image or clip \u2014 call the MCP directly.** A frame placed in a laptop, a product cutout,\na creator still, a restyle, an animated shot, with any fal model (Nano Banana, GPT-image, Seedream,\nSeedance, Kling). No atom script is needed:\n\n1. A local input (a frame pulled from a screen recording, a screenshot) must be a public URL first:\n `media_upload { brand_id, scope: \"video_project\", scope_id: project_id, source: { type: \"file\" | \"bytes\", \u2026 } }`\n (no `path`). For a file, PUT its bytes to `upload.url` with `upload.required_headers`,\n then call `media_confirm { brand_id, media_id: media.id }`. Use the returned `media.url`.\n2. `data_post_provider { provider: \"fal\", path: <model id, e.g. \"fal-ai/nano-banana/edit\">, body: <model input>, project_id }`\n returns `{ job_id: \"fal:<request_id>\" }`. Pass an `idempotency_key` so a retry isn't billed twice.\n3. Poll `job_get { job_id }` every few seconds until `complete`; the `*.fal.media` URLs are in\n `result`. Download each and QC it (open the image) before using it.\n\nFor a set that must match (every laptop shot, every creator still), use ONE prompt and the same\nmodel for all of them and change only the input image. Voice and music:\n`data_post_provider { provider: \"elevenlabs\", path: \"/v1/text-to-speech/{voice_id}\" | \"/v1/music\", body, project_id }`\n(synchronous; the audio lands in the project folder).\n\n`photos_generate` is **not** a general image tool: it only photographs a physical catalog\nproduct (apparel, beauty, CPG) and needs a `product_id`. A software screenshot or app mockup is a\nfal image edit, above.\n\n**Atom scripts \u2014 the MCP relay.** With neither `GW_MEDIA_PROXY_TOKEN` nor\n`~/.gooseworks/credentials.json`, the atoms' `media_proxy.py` RELAYS each paid call through you\ninstead of calling the proxies over HTTP. Before running any atom,\n`export GW_PROJECT_ID=<project_id> GW_BRAND_ID=<brand_id>` (every call is billed to that\nproject). When a script **exits with code 3** it wrote a request file under\n`working/mcp-requests/`: make exactly that MCP call \u2014 fal:\n`data_post_provider { provider: \"fal\", path, body, project_id }` then `job_get { job_id }` until\n`complete`, saving `result.output`; ElevenLabs: `data_post_provider { provider: \"elevenlabs\", \u2026 }`,\nsaving the reply; a local file: `media_upload` with its bytes, saving `{\"url\": \u2026}`. Write that JSON\nto the request's `save_result_to` and **re-run the same command**; repeat until the script\nfinishes. Same server proxy and price as the CLI path. If the CLI is logged in to a DIFFERENT\nenvironment than this MCP connector (prod vs staging), set `GW_MEDIA_VIA=mcp` so the spend lands\nwhere the project lives.\n\n## MCP tools \u2014 canonical names (use these)\n\nUse the canonical GooseWorks MCP tools. Legacy names are listed only as a fallback for an older\nclient that does not expose the canonical tool; never mix both for one step.\n\n| Step | Canonical tool (use this) | Legacy fallback |\n|---|---|---|\n| 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` |\n| Template recipe | `catalog_fetch { type: \"template\", slug: <source_sample_id> }` | `get_ad_template` |\n| Capability skill (atom) + its scripts | `catalog_fetch { type: \"skill\", slug }` | `gooseworks fetch <slug>` / `fetch_skill` |\n| Brand kit, products, rules | `brand_read { brand_id, sections: [\"summary\",\"kit\",\"products\",\"learnings\"] }` | `brand_get_context` / `get_brand_kit` |\n| 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 |\n| Mirror the review set | `video_project_upsert { brand_id, project_id, patch: { script: { script_drafts, script } } }` | `update_ad_project_script` |\n| Project assets | `video_project_upsert { \u2026, patch: { assets: [...] } }` | `update_ad_project_asset` |\n| Progress note | `video_project_upsert { \u2026, patch: { message: { role: \"agent\", content } } }` | `append_project_message` |\n| Batch status | `video_project_upsert { brand_id, batch_id, patch: { batch: { status } } }` | `update_ad_video_batch` |\n| 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 } }` \u2192 PUT \u2192 `media_confirm { brand_id, media_id: media.id }` | `get_upload_url` / `get_ad_upload_url` |\n| Save / find a finished piece (resume) | `media_upload { \u2026, 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 |\n| 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\" }` |\n| 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` |\n| Pin the final render | `video_project_upsert { brand_id, project_id, patch: { final_render_id: render_id } }` | `set_final_render` |\n| Credits / identity | `account_whoami` | `get_ad_credits` |\n\n`list_accessible_scopes` and the `target: { type: \"agent\", agent_id }` dance are **not needed**:\n`media_upload` with `scope: \"video_project\"` derives the right storage key (org-default Ads agent\n+ brand slug + project folder) on the server.\n\n## Running in a GooseWorks sandbox\n\nYou are an agent inside the user's GooseWorks workspace sandbox (Claude Code harness, Bash, the\nGooseWorks MCP with canonical tools only). There is **no `gooseworks` CLI** and **no\n`~/.gooseworks/credentials.json`**. The environment carries `GW_MEDIA_PROXY_TOKEN`, `GW_API_BASE`,\n`GW_FAL_PROXY_URL`, `GW_FAL_STORAGE_PROXY_URL`, `GW_ELEVENLABS_PROXY_URL`, `GW_WHISPER_PROXY_URL`\nand usually `GW_PROJECT_ID`.\n\n- **Formats:** only formats that need **no Chromium** run here \u2014 podcast skits and UGC /\n talking-head formats (ffmpeg + PIL assembly). If the template's recipe or an atom needs\n Playwright/Chromium with no PIL fallback (iMessage / ChatGPT / Notes phone mockups,\n hyperframes, HTML recorders), **stop before any spend** and say so plainly: \"This format needs\n a browser renderer that this workspace doesn't have. Run it in your own Claude Code with the\n GooseWorks CLI (`goose-video-local`), or pick a podcast/UGC format.\" Do not half-render.\n- **Toolchain:** check `ffmpeg -version` and `ffprobe -version`. Install Python deps only when an\n atom needs them: `pip install --quiet pillow` when a render atom uses PIL (captions, end card),\n `pip install --quiet requests` if `import requests` fails. Never `npx playwright install` here.\n- **Atoms:** `catalog_fetch { type: \"skill\", slug }` returns `content`, `scripts`, `files` and\n `dependencySkills`. Write `content` to `/tmp/gooseworks-scripts/<slug>/SKILL.md`, each\n `scripts` entry to `/tmp/gooseworks-scripts/<slug>/scripts/<name>` and each `files` entry to\n `/tmp/gooseworks-scripts/<slug>/<name>` (keep the key as the relative path; do the same for\n each dependency skill). Run scripts from their `scripts/` folder so `import media_proxy` resolves.\n- **Project attribution:** `export GW_PROJECT_ID=<project_id>` in the shell **before running any\n atom** for that project (re-export per concept in a batch). The proxies bill the workspace\n agent and attribute spend to that project.\n- **Never call a provider with a raw key.** All FAL / ElevenLabs / Whisper traffic goes through\n the `GW_*_PROXY_URL` bases with `GW_MEDIA_PROXY_TOKEN`. `FAL_KEY` / `ELEVENLABS_API_KEY` in\n this environment are proxy tokens, not provider keys \u2014 never send them to `fal.run`,\n `queue.fal.run` or `api.elevenlabs.io` directly. Never print any token.\n- **Whisper for QC:** `export OPENAI_BASE_URL=\"$GW_WHISPER_PROXY_URL/v1\"` before running\n `review-ugc-render` / `watch` transcripts.\n- **The operator may be absent.** Keep every quality gate and checkpoint below, but decide\n defaults yourself from the project brief and the recipe, and record them in the review set.\n Ask in the chat **only for a true taste call** (a creative choice the brief and recipe leave\n open and that changes the ad), never for a mechanical or recoverable decision. The Step 3\n approval still applies: ask for it in this chat and record the yes with\n `video_project_upsert patch.approve`.\n- **Outputs:** keep working files under `/tmp/gooseworks-video/<project_id>/` (local disk \u2014 never\n the s3fs workspace mount, which is slow and can drop writes); anything the user must see goes to\n the project via `media_upload` (never leave the result only in the sandbox). Every paid piece is\n also SAVED to the project as soon as it passes QC \u2014 see \"Save as you go \u2014 and resume\".\n\n## Report problems so we can fix them (telemetry \u2014 do this, don't skip it)\n\nIf anything blocks or degrades this run \u2014 a media/proxy call fails or errors, a required input or\nasset is missing, a recipe instruction is ambiguous or contradictory, the render toolchain won't set\nup, or you hit a bug you can't work around \u2014 **report it** so the team gets visibility and can fix\nthe skill. It's fire-and-forget, never counts against you, and never blocks your work.\n\n- **First, set a stable run id** so every event (yours + the auto-logged media calls) groups together:\n `export GW_RUN_ID=\"vid-<project_or_batch_id>\"` (and `export GW_SKILL=\"<recipe-slug>\"`) in the\n shell you render from. The media proxies read `GW_RUN_ID` automatically.\n- **CLI present \u2192** `gooseworks log \"<what happened>\" --event-type <type> --level error --details '{\"error\":\"...\",\"step\":\"...\",\"model\":\"...\"}'`\n- **No CLI (sandbox / cowork) \u2192** from any atom's `scripts/` folder:\n `python3 -c 'from media_proxy import gw_log; gw_log(\"<what happened>\", event_type=\"blocker\", level=\"error\", details={\"step\": \"...\"})'`\n (or the `log_cli_event` MCP tool if your client has it).\n- Event types: `api_failure` (a proxy/model call failed) \u00b7 `missing_input` \u00b7 `blocker` \u00b7\n `confusion` (unclear/contradictory instruction) \u00b7 `error` (a bug) \u00b7 `step`/`info` (progress notes).\n- Put the **real error text + the step you were on** in the details. Paid FAL/ElevenLabs calls\n ALREADY auto-log their own failures, so focus your manual logs on what the proxy can't see:\n missing inputs, confusing/contradictory recipe instructions, toolchain/setup failures, and bugs.\n- Logging is FOR US \u2014 it does not replace telling the user. When a problem blocks the run, still\n explain it to the user (and ask if you need a decision); just also log it so we can fix the skill.\n\n## Prerequisite \u2014 MCP + a render toolchain (Phase 0 preflight)\n\n- The GooseWorks MCP tools are REQUIRED. If they're unavailable, stop and tell the user\n to connect the GooseWorks MCP server (or run `gooseworks install --claude --mcp` on the CLI)\n and restart. There is no REST fallback.\n- **The render runs wherever THIS agent runs, and it needs a real toolchain:** `ffmpeg` +\n `ffprobe` always, plus a Playwright **Chromium** for browser-rendered formats (phone mockups,\n HTML end cards without a PIL fallback). Establish it in this priority order, and do NOT start\n rendering until one is confirmed:\n 1. **Sandbox \u2192** see \"Running in a GooseWorks sandbox\": ffmpeg + ffprobe (+ PIL on demand); no\n Chromium, so browser formats stop there.\n 2. **CLI present \u2192** run `gooseworks doctor --no-browser` for login, MCP, Node 18+, ffmpeg\n with libx264 + libass, and ffprobe. This is common setup only. After fetching the selected\n capabilities in Step 2, check each browser renderer's actual launch before ANY paid\n ingredient. An unscoped `gooseworks doctor` checks only the calling folder's browser;\n it cannot certify a different fetched renderer.\n 3. **No CLI \u2192** check the toolchain yourself: `node --version` (18+), `ffmpeg -version`,\n `ffprobe -version`, plus `ffmpeg -hide_banner -encoders` (libx264) and\n `ffmpeg -hide_banner -filters` (ass). After fetching, use the exact-package free launch\n check in Step 2; resolving a package or finding a cache folder does not prove its browser\n can launch. The `watch` QC step later needs the same ffmpeg and,\n for transcripts, a Whisper backend \u2014 without one it degrades to frames only.\n 4. **Docker available \u2192** the most reliable way to get the toolchain on a host that lacks it:\n run the render steps inside the prebuilt image\n **`ghcr.io/gooseworks-ai/goose-video-render`** (ffmpeg + ffprobe + Playwright Chromium baked\n in), mounting the project working directory. (Nested Docker is usually disabled inside\n managed sandboxes \u2014 treat this as an option, not a guarantee.)\n 5. **None of the above works \u2192** STOP and tell the user plainly, e.g.: *\"Video rendering needs\n ffmpeg (and, for this format, a Playwright Chromium) on the machine running this agent. This\n environment doesn't have them and I can't install them here. Options: (a) enable Docker so I\n can use the goose-video-render image, (b) install ffmpeg + `npx playwright install chromium`,\n or (c) run this skill in your own Claude Code where the toolchain is available.\"* Do not\n half-render or fake a result. Static image ads (the `goose-ads` skill) do NOT need any of this\n and work anywhere \u2014 offer that as the fallback if they just want an ad now.\n\n## Identity, token, credits\n\n- **Sandbox:** the token is `GW_MEDIA_PROXY_TOKEN` (already scoped to this workspace's agent and\n org); the API base is `GW_API_BASE`. Never print either.\n- **Local:** read `~/.gooseworks/credentials.json` \u2192 `api_key` (your agent token), `api_base`,\n `agent_id`. Never print the token.\n- **Uploads go through `media_upload` with `scope: \"video_project\"`** \u2014 pass `path` = the\n project-relative path (`working/final.mp4`, `working/review/end-card.png`). The server stores it\n in the project folder of the org-default Ads agent (where the app's render-file route reads) and\n returns `upload.url` (presigned PUT), `upload.required_headers` and\n `upload.render_file_url`. PUT the bytes with exactly those headers, check the PUT returned\n 2xx, then call `media_confirm { brand_id, media_id: media.id }` and require success before\n using the file in ingredients or completing a render. This applies to project-path and\n path-less file uploads. Confirmation verifies the stored file; Goose performs this tool step\n without asking the user for another approval. On failure, report or repair the upload before\n continuing. Never hand-build storage paths or agent prefixes; a bare workspace upload is\n invisible in the app.\n- Media generation (FAL / ElevenLabs) through the GooseWorks proxies is the **REAL spend** \u2014 billed\n per call as you generate (Step 4). The render row (`video_render_run kind: \"full\"`) charges the flat\n **video base fee once, when a full render is reported `complete`**. Open one row immediately\n after recorded approval, BEFORE paid production (Step 4.1), and reuse that render_id for progress\n 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\n that master and PINNING it. Call `account_whoami` first to see the credit balance.\n\n## Save as you go \u2014 and resume (never pay twice for a piece)\n\nA sandbox can die mid-run (timeout, restart, a new session picks the project up). Anything that\nlives only in `/tmp` is then gone, and regenerating it pays again. So: **save every piece to the\nproject the moment it passes its QC, and start every run by loading what is already saved.**\nWorking files stay in `/tmp/gooseworks-video/<project_id>/` (never the s3fs workspace mount);\nthe project is the durable copy.\n\n**Ingredient keys.** Give every planned piece a stable key before you generate it, the same on\nevery run: `vo/scene-03`, `vo/sample-her`, `still/her-base`, `still/scene-05`,\n`clip/scene-05` (a lipsync / video clip), `music/bed`, `endcard`, `captions`, `final`,\n`final-thumb`. Upload path = `working/<role>/<file>` (`working/vo/scene-03.mp3`,\n`working/clip/scene-05.mp4`); the review set keeps its `working/review/<name>` paths.\n\n**Input digest.** Name the exact inputs of each generation with `input_digest` from\n`media_proxy` (in every media capability's `scripts/` folder):\n\n```python\nfrom media_proxy import input_digest\ndigest = input_digest(model_path, args) # the model + the EXACT payload you send\n```\n\nHash only what decides the output (prompt, voice_id, model_id, seed, duration, aspect\u2026). An input\nthat is a presigned or proxy URL changes every run, so swap it for that input's own identity\nbefore hashing, e.g. `{**args, \"image_url\": {\"ingredient\": \"still/her-base\", \"digest\": her_digest}}`\n\u2014 then a changed still correctly invalidates every clip made from it. For a piece you build\nlocally (ffmpeg stitch, PIL end card, captions) use `input_digest(\"local/<step>\", {params,\ninputs: {key: digest, \u2026}})`.\n\n**1. At the START of every run (first run, resume, new sandbox), load what exists \u2014 one call:**\n`media_list { brand_id, scope: \"video_project\", scope_id: project_id, ingredient_key_prefix: \"\",\nlimit: 100 }`. It returns ONE compact row per `ingredient_key` (the newest):\n`{ id, ingredient_key, input_digest, kind, status, mime, bytes, url, path, created_at }`. Every\nstatus except archived is included. Project-path uploads remain `pending` until\n`media_confirm` succeeds. Confirm a pending project-path upload before reusing it; if\nconfirmation fails, repair the upload before treating it as a verified save.\nPage with `cursor` if `next_cursor` is set (keep the first row you see per key \u2014 it is the newest). Also read `script_drafts.ingredients` from\n`video_project_read`: it records which pieces were already approved in the review.\n\n**2. For each planned piece:** compute its digest from the args you WOULD send now. If a saved row\nhas the same `ingredient_key` AND the same `input_digest`, **download it instead of\ngenerating**:\n\n```bash\ncurl -fsSL \"$URL\" -o /tmp/gooseworks-video/<project_id>/<path> # URL = that row's `url`\n```\n\nThat `url` is a short-lived (~15 min) presigned S3 GET the server signs for you, so it needs no\nauth header \u2014 download right after listing (list again if it expired). **Never fetch the\n`/api/ads/projects/<id>/render-file?path=\u2026` route from the sandbox:** it needs the app's\nbrowser session and answers 401 to a token. Check the file is non-empty and plays (ffprobe for\naudio/video, open the image); if the download fails or the file is broken, regenerate the piece.\nOnly generate what is missing or whose digest changed \u2014 a changed digest means the inputs changed,\nso the old file is stale.\n\n**Pass the digest to the proxy too:** `fal_generate(..., input_digest=digest)` (and\n`fal_generate_video` / `fal_whisper`). A piece that was generated but never saved (the sandbox\ndied between the fal result and the upload) is then handed back by the proxy instead of paid for\nagain, even though its input URLs changed. Only pass `new_take=True` when the user wants a\ndifferent take of the same inputs.\n\n**3. After EACH piece is generated AND passes its own QC, upload it right away** \u2014 don't batch\nthe uploads to the end:\n`media_upload { brand_id, scope: \"video_project\", scope_id: project_id, kind, path:\n\"working/<role>/<file>\", ingredient_key, input_digest, source: { type: \"file\", filename,\ncontent_type } }` \u2192 PUT the bytes to `upload.url` with `upload.required_headers` \u2192\n`media_confirm { brand_id, media_id: media.id }`. Require a successful confirmation.\nKind: `audio` (VO), `music`, `image` (a still),\n`video` (a clip), `endcard`, `document` (captions / a JSON sidecar), `render` (the master),\n`thumbnail`. Re-uploading the same key is fine \u2014 the newest wins. A piece that FAILED QC is never\nuploaded under its key. **Save a piece's sidecars with it** under `<key>.<name>` \u2014 e.g. the VO's\nchar-level timestamps as `vo/scene-03.timestamps` (`kind: \"document\"`, same digest). Captions are\nbuilt from them; without them a resumed run has to fall back to Whisper timings, which mis-case\nbrand names.\n\n**4. Keep the approved review unchanged during production.** Record each confirmed piece's\n`media_id`, `path`, `ingredient_key` and `input_digest` locally; the confirmed media rows\nand render progress are the durable resume record. Before approval, mirror the draft ingredients\nas part of Step 3. After approval, do NOT write `patch.script` or `script_drafts` during any\npaid production, QC or repair step, or while provider work is pending. A review-set write clears\napproval and can stop the next paid step, including a batch concept. Save the final descriptive\nreview once all paid work is finished and settled, as Step 4.5 requires.\n\nAny material creative change or ANY change the user asked for in this chat must stop production:\nsave the changed complete plan and full credit total, then obtain fresh approval before continuing.\nNever reuse the earlier yes for a changed plan. A repair that restores the approved choices can\ncontinue within the existing allowance and Stop guards without rewriting the approved review.\n\nThe `final` master and `final-thumb` poster (Step 4.4) carry `ingredient_key` too, so a\nresumed run that finds a passing `final` with the same digest only needs to publish.\n\n## Finished video, interrupted saving \u2014 resume the same version\n\nIf the final already passed QC and the customer says **\"finish saving this video\"**, start here.\nDo not restart the recipe, choose another format, open another render, generate a take, or run a\npaid transcript check again. Saving uses the **same render_id**. The ordinary first-completion\nvideo fee can still apply; its existing same-render idempotency prevents a duplicate fee. Never\npromise that all saving is free.\n\n**Before the first final upload**, save the final review set and production manifest from Step\n4.5/4.6 (including any QC repairs) only after all paid work is finished and settled. Preserve the\napproved creative choices; a material change requires the changed-plan approval flow first.\nAfter that final save, start no new provider or render work: prepare the checkpoint and finish\nthe same opened render using its existing finishing allowance and guards. A released allowance\nor stopped/capped render requires normal recovery; never reopen or bypass approval.\nWrite a durable local checkpoint outside the fetched-scripts cache. Keep the actual\nfinal, JPEG poster, structured passing quality report, and review evidence files alongside it in\na retained local working folder. Record their SHA-256 **at the time those exact bytes pass QC**;\nnever attach an old verdict to a newly hashed replacement. A sandbox's local disk can disappear:\nthis checkpoint recovers connection interruptions, not lost storage. Save ingredients remotely\nas above and tell the customer if the checked local output is no longer available.\n\nWith the current CLI and its normal login to the selected environment, write a manifest like:\n\n```json\n{\n \"brand_id\": \"<owning brand>\", \"project_id\": \"<same project>\", \"render_id\": \"<already opened render>\",\n \"input_digest\": \"<assembly input digest>\",\n \"final_path\": \"/absolute/retained/working/final.mp4\",\n \"poster_path\": \"/absolute/retained/working/final-thumb.jpg\",\n \"qc\": {\n \"final_sha256\": \"<64 lowercase hex characters recorded during final QC>\",\n \"poster_sha256\": \"<64 lowercase hex characters recorded during poster review>\",\n \"report_path\": \"/absolute/retained/working/quality-report.json\",\n \"evidence_paths\": [\"/absolute/retained/working/review/finished-ad.json\"]\n }\n}\n```\n\nRun `gooseworks video-save prepare --manifest <manifest.json> --checkpoint <retained/save-<render_id>.json>`\nbefore any upload, then `gooseworks video-save resume --checkpoint <same checkpoint>` for both\nnormal saving and recovery. The helper uses the saved login and canonical MCP connection; it does\nnot copy credentials or use a raw Ads REST fallback. Its atomic private checkpoint binds the\nenvironment, account, project owner, project/render, final/poster bytes, assembly digest, passing\nquality report, review set, evidence files and stage receipts. Preserve it until delivery. Never\nedit the checkpoint to clear a failed check. Keep checkpoint and manifest files out of commits.\n\nThe helper reads remote state before writes. It keeps `final` / `final-thumb` ingredient keys\nand writes per-render paths `working/final-<render_id>.mp4` / `working/final-<render_id>-thumb.jpg`\nso saving this version cannot overwrite an older one. It confirms pending uploads, checks remote\nbytes against the QC hashes, and reuses confirmed media. A request with missing stored bytes can\nget a fresh upload URL at that same path. A lost completion reply is unknown until project read\nconfirms the same render's output and passing report; then only final selection remains.\n\n**No CLI / a host-selected MCP connection:** keep the same protocol with the host's canonical\ntools. Atomically write versioned JSON locally (write a private same-directory temporary file,\nflush/fsync, rename, then fsync the directory) before any upload and between each upload request,\nPUT, confirmation, completion and final selection. Persist only public environment identity,\naccount/owner/project/render IDs, file paths/sizes/hashes, input digest, exact quality report and\nevidence fingerprints, saved-review digest, media IDs and stage states; **no tokens, session IDs,\nsigned URLs or auth headers**. On reconnect call `account_whoami`,\n`video_project_read { brand_id, project_id, include: [\"renders\"] }` and\n`media_list { brand_id, scope: \"video_project\", scope_id: project_id, ingredient_key, input_digest }`\non the original connection. Compare identities, local/evidence hashes and saved review; verify\nconfirmed remote bytes too. Treat a lost reply as unknown and read back before retrying. Use only\nthe missing `media_upload` \u2192 PUT \u2192 `media_confirm`, same-render `video_render_run { render: \u2026 }`\ncallback and `video_project_upsert` with only `patch.final_render_id` and\n`patch.final_selection_guard: { expected_final_render_id, expected_review_digest }`; never a\nrender-open call. The expected final is the last read's pin, including explicit `null`; the\nreview digest is SHA-256 of recursively sorted-key JSON\n`{script: project.script ?? null, script_drafts: project.script_drafts ?? null}` saved at checkpoint\ncreation. First confirm the selected server advertises this guard in `tools/list`. If absent,\nstop and request the normal server update/reconnect; an unguarded pin is unsafe. A\n`final_selection_conflict` means the choice or review changed: keep the checkpoint and explain\nthat saving stopped without overwriting it. Retain the\nstrict Step 4.3/4.4 report and Step 4.5 review-set requirements.\n\nIf authorization expired, reconnect/sign in through the normal host/CLI flow on the original\nconnection, then resume. Missing tools are a reconnect requirement, not permission to switch\nenvironments. Missing/changed files, stale/missing QC, changed account/owner/review, another final\nselection, or a stopped/capped/failed/blocked render require diagnosis; do not reopen it or\nregenerate automatically. If saving stops, say: **\"Your video is finished locally, but saving to\nGoose was interrupted.\"** Link the actual local file and preserve its checkpoint. Claim saved\ndelivery only after a fresh project read verifies completion and selection of that exact render.\n\n## Step 0 \u2014 project id, or video BATCH id? (fan out before anything else)\n\nThe handoff is EITHER a single `project <id>` OR a `video batch <id>`. A batch is\nthe app's \"N concepts\" flow: one composer submission fans out into **N independent concept projects**\n(the user picked a concept count, default 3), and the app expects EACH to be rendered. **Handle both:**\n\n- **`project <id>`** \u2192 you have one project. Treat it as a batch of one and continue to Step 1.\n- **`video batch <id>`** \u2192 call `video_project_read { brand_id, batch_id }`. It returns every child\n concept under `projects[]` \u2014 each is a normal project with its own `id`, `variant_index`\n (Concept 1..N), and its own `creative_brief` (the per-concept angle/hook/offer/message). **You\n MUST process every concept, not just the first** \u2014 dropping concepts 2..N is the #1 batch bug.\n\n**Loop shape (ONE approval for the batch, isolated work per concept):**\n1. Run **Step 1 + Step 1.5 + Step 2 + Step 2.5 + Step 3-assemble** for EACH concept project (each\n has its own `project_id`, brief, `GW_PROJECT_ID` and `working/` folder \u2014 never cross-write\n between concepts). The brand read (Step 1 item 2) and `brand-rules.json` (Step 1.7) are per\n BRAND: do them once for the batch and copy the file into each concept's `working/`. The read is\n ~90K characters. Step 2.5's angle bank is shared per brand AND product: load or build it once, and\n give every concept whose angle is `auto` a DIFFERENT angle from that list, so the batch is N\n different ads, not one ad N times.\n2. Mirror EVERY concept's review set (Step 3's `video_project_upsert patch.script` per project),\n then stop for **ONE** approval in this chat that covers all concepts. Save each render_estimate.total_credits\n (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\n { brand_id, batch_id, patch: { batch: { status: \"review\" } } }`).\n3. On an explicit yes, record it ONCE for the whole batch: `video_project_upsert { brand_id, batch_id,\n patch: { approve: { user_quote: \"<their exact words>\", total_credits: <the saved total> } } }`. Check its `not_ready` list is\n empty (a concept listed there has no saved review set: save it, show it, ask again). If they\n approve only some concepts (\"1 and 3 are good, redo 2\"), record each approved one with its\n `project_id` instead, and redo the rest. Then set the batch to `rendering` and run **Step 4 (the expensive render)** for each\n concept with up to 8 isolated workers when the machine can sustain them. Retry\n 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\n `complete`. A concept the Step 4.3 gate leaves `blocked` cannot be pinned (a batch concept\n needs `passed`): finish the others, set the batch to `blocked`, and tell the user which\n concepts passed and which are blocked, with each one's failing checks.\n\nIf a single concept fails, keep going with the rest, mark that concept blocked, and report which\nones shipped \u2014 never abort the whole batch on one bad concept. Everything below (Steps 1\u20135) is\nwritten per-project; a batch just runs it N times with the shared approval gate above.\n\n## Step 1 \u2014 resolve the project, source, brand\n\n1. `video_project_read { brand_id, project_id }` \u2192 keep `brand_id`, `source_sample_id`, `name`,\n `status`, the **top-level** `app_url` + `brand_url` (the links you hand the user for the in-app\n review in Step 3 and delivery in Step 5), AND the user's **`creative_brief`**, project\n **`assets`**, `character_id`, `default_voice_id` \u2014 these are the authoritative inputs the user\n chose in the composer (see Step 1.5). Do NOT discard them. Then `export GW_PROJECT_ID=<project_id>`.\n Keep its saved `campaign_id` and `campaign_concept_id` too. If the customer explicitly\n wants to attach this existing video to a known campaign/concept, verify the IDs with the\n campaign read and use `video_project_upsert { brand_id, project_id, patch: {\n campaign_association: { campaign_id, campaign_concept_id? } } }` as a separate, sole-field\n patch. Read back the same project and confirm the saved IDs. Linking never needs a new\n project, render, generation call or approval. A conflicting saved link stays intact; explain\n it instead of silently moving the video. Leave unlinked legacy videos alone unless asked.\n\n### Step 1.5 \u2014 the project brief is AUTHORITATIVE (honor it; don't re-ask)\n\nThe composer already collected the user's creative direction onto the project. **Read it and treat\nit as ground truth \u2014 it OVERRIDES the template recipe's defaults, and it REPLACES the clarifying\nquestions you would otherwise ask.** For a field the brief leaves empty, use the Brain search's\nevidence brief first, then the recipe default, then (last) asking. Map the fields you WILL honor:\n\n- `creative_brief.productName` / `.offer` / `.angle` \u2192 the product, offer/code, and angle. Do\n **not** ask \"which product / what offer / what angle\" if these are set.\n- `creative_brief.concept` (on a batch child) \u2192 this concept's **`angle` / `hook` / `offer` /\n `message` / `note`** \u2014 the per-concept differentiator. Honor it verbatim; it's WHY the user asked\n for N concepts. `angle: \"auto\"` or empty means \"you choose.\"\n- Project `assets` + `creative_brief.reference_image_urls` \u2192 the user's **own reference images**.\n Use them as the product/brand refs (alongside the brand kit), don't ignore them for generic recipe\n assets.\n- `character_id` \u2192 the avatar/creator to use. `default_voice_id` \u2192 the voice for any VO (put its\n NAME in the review `subtitle`). Use these instead of picking your own.\n- `creative_brief.durationSeconds` \u2192 target length; honor it when the format allows.\n `creative_brief.ratio` \u2192 video ads are ALWAYS 9:16 (1080\u00d71920). If the brief asks for another\n ratio, make 9:16 anyway and say so in the review; never export another size (a recipe's\n \"also 1:1\" option included).\n- `polish_policy` (`standard` | `extra`) \u2192 `extra` means spend the extra pass on QC/polish.\n\n2. Brand gate: `brand_read { brand_id, sections: [\"summary\", \"kit\", \"products\", \"learnings\"] }`\n (older clients: `brand_get_context` with the same sections). Ask for all four: the default\n leaves out the kit and the brand's saved rules, and a video made without them is off-brand.\n If the kit's `researchStatus` (or the brand's `research_status`) is `complete`, REUSE it \u2014\n never re-research. A pending status alone does not require research; use verified stored\n facts and ask only for an actual gap. If the context is empty or the customer requests\n research, load `catalog_fetch { type: \"skill\", slug:\n \"brand-research\" }` and follow its stored-pack workflow. Only when that verified pack is\n saved in the supported research workspace, finalize with\n `brand_update { brand_id, patch: { finalize_research: true } }`, then read the brand back.\n Never send raw research JSON through `kit_patch`: the public tool accepts only the existing\n `video_lab` asset slot there. If there is no verified stored pack, submit researched facts\n through typed `patch.knowledge` / `patch.kit` as pending agent proposals; do not pretend\n research is finalized or its proposals are approved. Then do Step 1.7 with verified facts.\n3. For a new plan, `catalog_fetch { type: \"template\", slug: <source_sample_id> }` \u2192 the source\n video: `media_url`, `recipe`, `format` (e.g. \"podcast-skit\", \"imessage\"),\n `extracted_script`, `how_to`, `remix_spec`. For an approved resume, restore the recorded\n package and dependencies instead of fetching today's recipe over the saved plan.\n\n### Step 1.6 \u2014 a remix of a FINISHED video (the project read has a `remix` block)\n\nA project made from **Community videos** remakes another customer's finished video for THIS brand.\nIts `video_project_read` returns a top-level `remix` block\n(`{ remix_of_project_id, instruction, direction }`), and `reference_video_url` is that finished video.\n\n- **Watch the reference video first** (download `reference_video_url`, pull frames + the transcript).\n It is the target: match its structure, beat order, pacing, framing, look, voice and tone.\n- **`remix.direction` is its approved review set** (scenes and lines, set/look notes, take prompts,\n captions, music, voice). Start your review set from it instead of the template's defaults; it\n already carries every change that customer made to the template.\n- **`remix.direction.implementation`, when present, is HOW it was made**: the pipeline and the model\n each step used, the style and negative prompts, the character and per-scene still/motion prompts\n with their guards, voice and music settings, the mix, and `fixes` (what went wrong and how it was\n fixed). Reuse the same models, style/negative prompts and guards, rewrite the prompts' subjects for\n this brand, and apply every `fix` up front so you don't repeat the same mistake.\n- **Rewrite everything for THIS brand.** \"[source brand]\", \"[source product]\", \"[link]\", \"[email]\"\n and \"[code]\" mark the other customer's details: never write them, and never reuse their claims,\n numbers, URLs, offers or CTA. Every product, claim, name, image and CTA comes from this brand's\n kit, products and media. Their footage (screen recordings, product shots) and their creator face\n are NOT carried over: use this brand's own assets and make a new creator from the description.\n- Precedence: this project's own `creative_brief` and assets (Step 1.5) > `remix.direction` >\n the template recipe's defaults.\n- In the Step 3 review, say it is a remix of that video and list what you kept vs. changed.\n\n### Step 1.7 \u2014 the brand rules file and the brand assets (every run, before any writing)\n\nBefore new writing, write `working/brand-rules.json` from the Step 1 brand read. Preserve an\napproved run's saved rules on resume; reconcile an intentional rule change through the existing\nreview gate. Every later step reads THIS file, not your memory of the chat:\n\n```json\n{\n \"name\": \"Acme\",\n \"pronunciations\": [{ \"term\": \"Acme\", \"say_as\": \"ak-mee\", \"learning_id\": \"\u2026\" }],\n \"must_say\": [{ \"text\": \"\u2026\", \"learning_id\": \"\u2026\" }],\n \"never_say\": [{ \"text\": \"Never claim it cures insomnia\", \"learning_id\": \"\u2026\" }],\n \"products\": [{ \"id\": \"\u2026\", \"name\": \"\u2026\", \"facts\": [\"size, flavour, price, ingredients as stored\"], \"images\": [\"\u2026\"] }],\n \"logo\": { \"url\": \"\u2026\", \"confidence\": \"<kit.logoConfidence>\" },\n \"fonts\": [\"\u2026\"],\n \"palette\": [\"#\u2026\"]\n}\n```\n\n- **Video taste is direction, not dialogue.** Before mapping brand rules, extract entries\n prefixed `Video preference:` from both `learnings` and `kit.instructions` into the verified\n taste brief, including accepted proposals. They govern pacing, voices, captions, visuals and\n format choice; do not copy them into `must_say` / `never_say` or read them aloud. Carry the\n brief into the choices, scene planning and review.\n- **Sources.** `learnings` are the brand's saved rules (the user's past corrections among them):\n `must` / `do` \u2192 `must_say`, `dont` \u2192 `never_say`, and a `must` whose text reads\n `Pronounce \"<term>\" as \"<say_as>\"` (straight or curly quotes) \u2192 `pronunciations`. Add `kit.instructions` (free-text\n standing rules) only when they require actual spoken wording or prohibit a claim. Production\n directions stay in the brief. A required spoken line or prohibited claim remains its own\n ordinary `must` / `dont` rule.\n- **Which product.** The one the brief names (`creative_brief.productName`); with none, the row\n whose name matches the product the user asked for, or the brand itself for a one-product\n brand. Product lists often hold other brands' items or old ads saved as products: if more than\n one row could be it, ask in the choices round. Never mix facts across rows.\n- **Product facts** come from that row (name, description, variant, price) and, when the row is\n empty, from the kit (`valueProps`, `description`, `tagline`): nothing else. Write them to\n `products[].facts`; the script may only state what is there.\n- **Logo.** Download the kit's logo FILE (`kit.logoUrl`, else `kit.logos[0]`) to `working/brand/`,\n keeping its real extension (an SVG stays `.svg`; rasterise it to a 1024px-wide PNG with\n `rsvg-convert` or `cairosvg` when a renderer needs pixels). It is used as-is on every scene and\n end card that shows a logo: **never generate, redraw, re-letter or restyle a logo with an image\n model.** If the kit's `logoConfidence` says favicon-grade, or the file's long side is under\n 256 px (or it is under 40,000 px\u00b2), it is a site favicon, not a logo: do not upscale it. Ask the user for a real logo (or offer the brand name\n set as text in the brand font) in the SAME question round as the recipe's `choices`.\n- **Font.** `kit.typography.heading` when its `source` is `user` (the user chose it), else\n `kit.fonts.heading`. Download the font file (the kit's own, or the same family from Google\n Fonts) to `working/brand/` and use it for every on-screen line. With no match, use the closest\n free font and say so in the review.\n- **No wordmark file.** When a recipe wants a wordmark SVG and the kit has only a logo image, set\n the brand name as text in the brand font beside the logo file. Never generate one.\n- Record in `brand-rules.json` which logo the video will actually composite (`logo.file`), or\n `\"logo\": { \"mode\": \"text\" }` when it will show only the brand name set in the brand font.\n- **Product images.** Download this product's own images to `working/brand/`. Use only images of\n THIS product: never a catalogue image of another product, a mascot, a lifestyle photo of a\n person, or a stand-in. If the product has no usable image, ask in the choices round.\n\n### Brand corrections stick \u2014 save them to the brand the moment they are made\n\nWhen the user corrects something about the BRAND in chat \u2014 how a name is said, a claim that may\nnot be made, a product fact, a visual rule (\"never use red\", \"the logo goes top-left\") \u2014 save it\nin the SAME turn, before anything else:\n\n`brand_update { brand_id, knowledge_intent: \"user_correction\", user_statement: <the user's exact correction>, patch: { facts: [{ kind, text }] } }`\n\n| Correction | `kind` | `text` |\n|---|---|---|\n| Pronunciation | `must` | `Pronounce \"Acme\" as \"ak-mee\"` (exactly this form) |\n| A claim or word to avoid | `dont` | `Never say or imply: <the claim>` |\n| Something that must be said | `must` | `<the rule>` |\n| A product fact | `must` | `<product name>: <the fact>` |\n| A visual rule | `do` / `dont` | `<the rule>` |\n\n- If it changes an EXISTING rule (a new pronunciation for the same term), update that rule by id\n (`facts: [{ id: <learning_id>, text }]`) instead of adding a second one.\n- Use the correction intent and user's statement when supported, as described below. Read\n `brand_read` learnings back and verify the rule and source before claiming it was saved.\n- Then tell the user in one line: \"Saved to your brand for future videos.\" Update\n `working/brand-rules.json` and apply the rule to THIS video too.\n- A one-off note about this video (\"make it shorter\", \"use the blue background here\") is NOT a\n brand rule: don't save it.\n\n## Save durable brand answers, then verify them\n\nRead the selected brand with `brand_read { brand_id, sections: [\"summary\", \"kit\", \"products\", \"learnings\", \"onboarding\"] }`\n(older clients: `brand_get_context` with the same sections, only when that tool is advertised).\nKeep founder answers, user corrections, research and your own hypotheses distinct. Reuse\nmatching saved answers; ask only about gaps.\n\nWhen the user asks to remember a rule, answers a brand interview, or explicitly corrects a\nstanding fact, save that answer in the same turn. The capture request authorizes those answers;\ndo not ask for approval again. A direction for this one video stays in its brief. If the scope\nis genuinely ambiguous, ask whether it applies to future videos before saving a standing rule.\n\nUse the **live registered schema**. Where supported, call `brand_update` with\n`knowledge_intent: \"user_correction\"` and `user_statement` containing the user's exact,\nverbatim answer, not your paraphrase or researched text.\nFor an inference or suggested improvement, use `knowledge_intent: \"agent_proposal\"`. Show the\nbefore/after change from your prior read and proposed value; retain the returned proposal IDs\nand say the user must accept it in the app. Link only a review surface actually returned by a\ntool; the compact `knowledge_updates` response does not itself contain a diff or URL.\nA pending proposal is not a saved fact. Never call an unavailable\ntool or silently relabel research or your inference as something the user said.\n\nThe safe structured shape is `patch: { knowledge: { positioning?, audience?, voice?,\ninstructions?, brandType?, tagline?, valueProps? } }`, using only fields present in the live\nschema. Inferred rules/taste go in an `instructions` proposal with a rationale, never in\n`patch.facts`. Prefix every video-only preference in that proposed text with \"Video preference:\"\nso it remains production direction after acceptance. Preserve unrelated instructions when\nproposing a merged replacement.\n\n| User answer | Canonical write |\n| --- | --- |\n| 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. |\n| 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: \u2026\" rather than turn a belief into a verified result. |\n| Required wording or pronunciation | `patch.facts: [{ kind: \"must\", text }]`; pronunciation is exactly `Pronounce \"<term>\" as \"<say_as>\"`. |\n| Forbidden claim, word or visual | `patch.facts: [{ kind: \"dont\", text }]`. |\n| 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:\". |\n\nFacts are existing `ad_brand_learning` rows with user provenance; they are not a second profile.\nUpdate a matching rule by its returned `id` instead of adding duplicates. Preserve unrelated\nrules and the user's exact meaning. Only use the legacy facts shape for explicitly user-authored\nanswers when the live schema lacks intent fields; agent suggestions still need a proposal path.\n\n**Claims and plans have separate gates.** A founder assertion or proof point is not an approved,\nevidence-backed claim or consent to quote a customer. Use the existing evidence/claims and\noperating-plan tools only if registered, following their proposal, evidence and confirmation\nrequirements. Never encode a spend cap, approver or emergency stop as a learning. If that write\npath is missing, report the specific unsaved item and keep it pending for the supported review\nsurface; do not claim it was saved or create a parallel local profile.\n\nAfter every write, **read back before saying saved**: use `brand_read` with `kit`, `learnings`\nor `onboarding` as appropriate, or `brand_onboarding { action: \"status\", brand_id }` after an\nonboarding answer. Verify the intended field/rule, its source and the absence of a conflicting\nduplicate. A generic success response, pending proposal, ignored key or truncated result is not\nproof. Report partial saves honestly. Carry the verified rules into the current task and the\nrouted skill; claims still pass their own safety gate.\n\n## Video taste \u2014 reuse examples and preferences\n\nWhen asked to capture video taste, or when the user volunteers a durable video preference,\nfirst read the saved learnings and `media_list { brand_id, scope: \"brand\", scope_id: brand_id,\ntags: [\"video-taste\"], limit: 100 }`. Follow `next_cursor` before deciding an example is absent.\nDo not force a taste interview before an unrelated task or ask again for an existing preference.\n\nSave what the user has already supplied first. Then ask only the missing useful question, for\nexample: \"What do you like about this video\u2014its pace, voice, captions, or look?\" An inaccessible\nlink can still be saved as a link with the user's explanation; do not pretend you watched it.\n\n- **Direct clip or video file:** register with `media_upload { brand_id, scope: \"brand\",\n scope_id: brand_id, kind: \"video\", source: { type: \"url\", url }, tags: [\"video-taste\", \"reference-only\"],\n metadata: { purpose: \"video_taste\", source_url: url, provenance: \"user\", captured_at: <ISO timestamp>,\n preference: <the user's explanation> } }`. For a file use the live file/bytes upload flow and\n `media_confirm` after a presigned upload. Registration of a URL does not copy or inspect it.\n- **Instagram/post/page link:** the same registration with `kind: \"document\"`; it is a link\n bookmark, not downloadable footage or an indexed transcript. Do not fabricate a direct clip URL.\n- **Preferences:** save the user's reasons, likes and dislikes through the facts mapping above.\n Read and preserve existing facts before updating one. Do not invent `video_preferences` or\n new `video_lab` keys; the live kit patch accepts only its documented asset fields.\n- **Deduplicate:** reuse a matching returned media row, then `media_update` its title/tags/metadata\n if needed; preserve existing metadata and tags. Do not create another row for the same example.\n Read back with `media_list` and `brand_read` learnings before claiming it was saved.\n Check the returned row belongs to this brand: URL deduplication may return another brand's\n existing row. Do not relabel that row or claim success unless the current brand's scoped read\n actually returns it. Report an unsaved association if no supported attach path is available.\n\nNever put third-party taste examples into kit reference images: `kind: \"reference\"` at brand\nscope writes there. The tags and metadata above record purpose and provenance; **they do not\ngrant or enforce usage rights**. Study the structure, pacing and look only. Never use the example's\nfootage, face, product, testimonial or claims in a new ad without independently verified permission.\n\nRead video-only entries prefixed `Video preference:` from both saved learnings and\n`kit.instructions`, including accepted proposals. Keep them out of required or forbidden\ndialogue. Build a brief from the verified readback: preferred pace, voice, caption treatment, visual style,\nformats to favour/avoid, reference links and the user's reasons. Say what is still unknown.\nPass it with the brand rules into the existing video workflow. A one-video request overrides a\ndefault for that project; it does not silently rewrite the brand's standing preference.\n\n## Step 2 \u2014 read the template's recipe (it carries everything; NO hardcoded format map)\n\nThe ad format is a **template (data) in the ad_sample DB**, not a per-format skill.\n`catalog_fetch { type: \"template\" }` returns the template's `recipe` \u2014 a self-contained brief you\nread and execute. **Do NOT map `format` to a hardcoded recipe slug** (there is no such table):\n\n- `recipe.format` \u2014 the format label (e.g. `vignette`), for display only.\n- `recipe.atoms` \u2014 the **capabilities** this template composes (e.g. `create-vo-elevenlabs`,\n `create-image-gpt-image-fal`, `render-podcast-skit`, `review-ugc-render`, `watch`). Fetch each\n with `catalog_fetch { type: \"skill\", slug }` (or `gooseworks fetch <name>` when the CLI is\n installed) \u2014 they are reused across templates.\n- `recipe.instructions` \u2014 the **playbook** to follow: `instructions.inline` prose, or\n `instructions.doc_url` (an S3 markdown doc \u2014 fetch it).\n- `recipe.config` \u2014 every param (prompts, layout, timings, palette, model choices).\n- `recipe.inputs` \u2014 the brand-asset contract (which product / logo / offer this template needs).\n- `recipe.choices` \u2014 the creative calls the USER makes (who is on screen, narrator, tone, setting,\n art style, music). Ask every unanswered one in ONE round before any paid step, with its options plus\n \"you pick\". Its `reference` is what the demo used: an example, never the default.\n- `recipe.assets` \u2014 reference material as S3 links (reference render, style guide, example frames) \u2014\n fetch as needed.\n\nRuntime: **read the recipe \u2192 fetch each capability in `recipe.atoms` \u2192 follow\n`recipe.instructions` with `recipe.config` + the brand's bound `inputs`.** The template IS the recipe;\nthere is no `format \u2192 recipe-slug` table and no per-format skill to fetch.\n\nSave each fetched capability's content, scripts + files under `/tmp/gooseworks-scripts/<name>/`\n(layout in \"Running in a GooseWorks sandbox\"). If a capability is a Node package (a phone-mockup\nrenderer), install its documented dependencies in the folder containing its `package.json`,\nand preserve the recorder's documented `NODE_PATH` \u2014 local machines only; in a sandbox that\nformat stops (no Chromium).\n\n**Selected browser readiness \u2014 before ANY paid ingredient:** identify the actual browser script\nfrom the fetched capability's instructions (including an HTML end-card renderer if used).\nFor Node scripts using `require('playwright')` with default `chromium.launch()`, run\n`gooseworks doctor --renderer-script \"/absolute/path/to/the/fetched/scripts/record.js\"`.\nSubstitute the actual script, and keep the same cwd and environment as the render, including\n`NODE_PATH` and `PLAYWRIGHT_BROWSERS_PATH`. It resolves Playwright relative to that script,\nlaunches and closes Chromium with the default settings and bounded waits, and downloads nothing.\nOn failure, stop before spending, show its folder-specific repair and recheck after setup.\nFor non-browser formats use `doctor --no-browser` plus their documented runtime checks;\nnever combine `--no-browser` with `--renderer-script` or use it to bypass a browser renderer.\n\n**No CLI or CLI without these flags:** run an equivalent free probe in a separate Node process: use\n`require('node:module').createRequire(require('node:path').resolve(actualRendererScript))`\nto load `playwright`; keep the render's cwd, environment and default launch settings. Await\n`chromium.launch({ timeout: 15000 })`, then await `browser.close()` (bound close to 3 seconds).\nBound the whole process to 20 seconds and stop its own process tree on failure or timeout.\nReport the resolved module path/version and error without credentials. Missing module, executable,\nheadless runtime or failed launch is a failed check; a cache folder, executablePath alone or\nanother project's browser is not a pass. Other browser packages, Python renderers or custom\nlaunch settings need the same free launch/close check through their documented runtime and\nactual settings. Do not replace their browser/channel/flags to get a pass. Setup is a separate\naction under existing permissions; this check never silently installs or downloads anything.\n\n> **Migration note:** older phone-mockup formats (`imessage` / `chatgpt` / `apple-notes`) whose DB\n> recipe does not yet carry `atoms` / `instructions` still hold the legacy `recipe.thread` payload;\n> migrate them to this shape (capabilities + instructions in the DB) \u2014 do not reintroduce a CLI map.\n\n## Step 2.5 \u2014 plan words and visuals with `write-video-ad-script`\n\nEvery video concept gets creative strategy, including silent formats. Fetch\n**`write-video-ad-script`** through the skill catalogue and follow it before assembling\nreview ingredients. It reuses or fetches **`ad-angle-miner`**, binds the researched promise\nto this template's full recipe, writes words and visuals together, and checks product\nclaims and production fit. Its independent critic is a quality screen, not a forecast\nof ad performance. The existing Step 3 review and approval remain unchanged.\n\n- **Carry the research into the writer.** Pass the brand and exact product, audience,\n objective, offer, CTA, selected template recipe, available assets and the full miner\n bank or its readable workspace pointer. A selected miner idea includes its angle id,\n evidence and proof plan, not only its hook. Reuse the shared video-angle-bank.v1 from\n the brand's video-scripts workspace or this run. Prepare angle-context.json with the\n writer's preparation script with `--brief working/script/creative-brief.json` and use\n its strict rule check before review. First run the fetched writer's free\n `python3 <saved-writer-package>/scripts/verify_handoff.py --package-dir <saved-writer-package> --out working/script/writer-handoff-check.json` check against those\n actual saved prepare/lint scripts. Retain the result and script hashes with the run.\n A provided writer package with a missing checker, failed check or unsupported flag\n is incompatible: stop this handoff and refresh the package on the same connection.\n Never use the provisional agent path after a provided package fails validation.\n Only an actual `not_found` for an optional writer permits the explicit provisional\n agent check below; a required writer remains blocked. Never silently remove the brief,\n strict check or new shape requirement to run an older parser.\n Save the sourced brief in the writer's documented\n shape: exact product/variant, buyer situation, supported mechanism, offer/CTA, constraints,\n delivery intent, source references, locked copy, applicable prior decisions and unknowns.\n New custom/template shapes set `requires_creative_brief: true`; both writer and critic\n receive the same angle-context. Carry that brief revision into the scene/tool plan and\n production manifest; missing evidence stays explicit rather than becoming a claim.\n- **Already decided stays decided.** The user's angle, hook, batch concept, recipe choice\n or remix direction is binding. Preserve the selected angle id. Compare executions or\n hooks inside it; do not reopen the angle choice. An incompatible promise needs a\n supported execution or an explicit format change before production.\n- **The recipe owns the production contract.** Its story mechanism, speakers, timing,\n text limits and visual capabilities win. Inspect the actual recipe and available assets;\n a catalogue card alone cannot establish fit. An impossible essential visual blocks\n the proposal until repaired. Do not force a testimonial into every format.\n- **Research only what is missing.** Fetching the miner does not spend or rerun research.\n Reuse current facts and prior evidence. The Brain search's evidence brief (citations, the\n state of each query and open gaps) is the first source for the brief's source references and\n prior decisions. If paid collection is needed, its permission\n rides in the existing choices round. A new product with no reviews can use verified\n facts and a feasible demo; do not require paid research merely to fill a quote quota.\n- **Choose eligible concepts only.** Take the strongest supported concept and validated\n hook into review, with up to two viable alternatives when the direction is open.\n Do not pad three concepts or select the highest-ranked rejected one. If all fail,\n repair within the brief and recipe before Step 3.\n- **Optional writer not found or unavailable research.** Only when the optional writer\n returns `not_found`, complete an explicit agent check of claim support, recipe limits,\n visual feasibility and hook payoff. Record missing provenance and say in the review when\n research is provisional. A provided incompatible writer or another fetch error stops\n the handoff. Never report a failed check as a pass.\n- **The user's exact lines** remain verbatim; use report-only checks and raise material\n timing, claim or format conflicts without silently rewriting them.\n- **No spoken words.** Still fit the visual promise, reveal, cards and CTA to the silent\n recipe. Skip speech writing and spoken checks, not the strategy step.\n- **Keep private provenance separate.** Store source quotes and links in the research\n workspace; put only a concise strategy explanation in remixable review data.\n\n## Shared editorial craft inside this template flow\n\nFetch `video-production-harness` and read its `references/editorial-review.md`,\n`references/specialist-handoff.md` and `references/hook-compatibility.md`, plus the\nreview/edit/polish/promote/wrap steps when used. Verify every named file is returned as\nnonempty text before relying on that package. A version/content hash alone cannot establish\nthat the required guides were published. Missing guides block new template creative work\nbefore project writes or paid previews; resolve the package on this same connection. Keep\nan unchanged approved resume on its recorded package and approvals.\nSave the fetched version/content hash with this run's capability records. These supply\ncraft and evidence rules inside the template flow; this entry's existing review, paid\napproval, storage and two-repair limit still apply.\nDo not turn a template into a custom project or import the custom host's extra gates.\n\nBefore executing a fetched recipe atom, pass the exact brief/script revision, scene/beat IDs,\ntiming, selected product/assets/source windows, performance choice and expected output roles.\nCheck actual scripts/dependencies and free runtime readiness, then validate returned media\nagainst that handoff. An interrupted run reuses valid approved media with matching digest;\na file merely existing does not prove validity. A missing package/required renderer blocks\nthat route before paid generation, with a concrete supported alternative for review.\n\nRough review asks what the cut communicates and whether the proof/payoff serves the hook;\nfine review diagnoses performance, sequence and local execution; final review checks each\nactual export. Save the received message from the cut before comparing with the brief where\npossible. A technically clean incoherent cut returns to script/sequence work; a local caption\ndefect gets a local repair. These internal questions share existing review surfaces.\n\nBefore a hook change, classify same-promise compatible opening versus new-promise/body recut.\nFor example, an automatic report supports \u201cstop copying campaign results\u201d; it does not alone\nprove \u201cknow where to spend the next dollar.\u201d Propose required script/body/coverage changes for\nthe latter. Every restyled opening gets fresh image/action/audio/text review even when words\nare unchanged. Fetch the existing `render-hook-replacement` before selecting that route;\na not-found/empty result stops it before generation. Use its actual preflight and preservation\nchecks when available; never build a duplicate renderer or infer publication from source.\nCompare complete labeled candidates, retain source/body/ending and record the hypothesis.\nNo quality score or recommendation is an observed performance winner.\n\n## Step 3 \u2014 assemble the review set, then get ONE approval (before the expensive render)\n\nThis is a **review-once** flow: save the whole review set to the project, get ONE approval in this\nchat, then run the\nexpensive render + any remaining paid work end-to-end. Never spend on the expensive render before\napproval, and don't drip pieces out one at a time and re-pause.\n\n**What goes in the review \u2014 show the REAL cheap pieces, PROMPT only the expensive render.** Split\nevery piece three ways by cost, NOT just \"free vs paid\":\n- **FREE** (an iMessage / Apple-Notes HTML mockup, a text/CTA line \u2014 rendered locally, no proxy\n call) \u2192 generate NOW and mirror the real asset.\n- **CHEAP paid** \u2014 a single still/image, the creator/avatar frame, the end card, a short voiceover\n or music bed (each costs cents \u2192 roughly **\u2264 100 credits**) \u2192 **generate these NOW too** and\n mirror the real asset. The few credits buy a real review: the user SEES the actual creator face\n and end card and HEARS the VO, instead of judging a prompt. **This OVERRIDES any recipe rule that\n says to gate ALL paid calls** \u2014 only the expensive render below is gated.\n- **EXPENSIVE paid** \u2014 the video take / final AI render / per-line lipsync clips (hundreds of\n credits) \u2192 do NOT generate. Put its **exact prompt/spec** (+ ref image URLs) in the tile. This is\n the ONE thing approved as a prompt (you can't preview a hundreds-of-credits video for free); it's\n generated only in Step 4.\n\n**The expensive render's exact prompt must be in the panel BEFORE you ask for approval** \u2014 so a\nsingle \"go\" runs it (plus any remaining paid work) without re-pausing mid-run.\n\n**Show every cost in CREDITS, never dollars.** 1 credit = $0.01 and media generations bill at\nprovider-cost \u00d7 1.2, so **credits \u2248 round-up(provider-$ \u00d7 120)** per generation, plus a flat\n**200-credit base per video**. Convert any $ figures to credits and show ONLY credits to the user \u2014\nnever print a \"$\u2026\" amount.\n\n**Never assemble/stitch the finished video for review.** The review is of the individual pieces (or\ntheir prompts) \u2014 never a \"full cascade\" / \"approved cut\" clip. Building the whole video before\napproval defeats the gate (the user opens the review to an already-finished video) and wastes the\nrender (GOOSE-2542). The full video is assembled ONLY in Step 4, after approval. A `video`\ningredient here is only a genuinely separate SOURCE clip the format needs (e.g. supplied b-roll).\n\n1. **Assemble every piece the format needs \u2014 not just the script.** Read the recipe for the exact\n list. For a podcast skit that's the **script** (both hosts' lines), the two **host stills**,\n one **voice sample per host**, and the **end card**; an iMessage video has the bubble thread,\n the conversation image(s) and the end card. For each piece, decide FREE / CHEAP-paid /\n EXPENSIVE-paid (above):\n - **FREE or CHEAP paid** (\u2264 ~100 credits) \u2192 generate it now and upload it with\n `media_upload { brand_id, scope: \"video_project\", scope_id: project_id, kind, path:\n \"working/review/<name>\", ingredient_key, input_digest, source: { type: \"file\", filename:\n \"<name>\", content_type } }` \u2192 PUT \u2192 `media_confirm { brand_id, media_id: media.id }`;\n after confirmation succeeds, set that\n piece's `path` (+ `media_id`, `ingredient_key`) in `script_drafts` to the project-relative\n `working/review/<name>`. First check \"Save as you go\" \u2014 a piece already saved with the same\n digest is downloaded, not regenerated.\n - **EXPENSIVE paid** \u2192 do NOT generate. Put the **exact prompt/spec** (and any ref image URLs)\n in the tile's `text` / `subtitle` so the user reviews what will be spent on. No `path` yet \u2014\n it's generated in Step 4.\n Save **render_estimate.total_credits**: previews already spent + pending render + the 200 base fee.\n Use current server pricing, never a catalog range in place of the full total.\n **Brand check of the script, before it goes in the panel:** every line, caption and on-screen\n text is checked against `working/brand-rules.json`. Nothing in `never_say` appears, in words or\n in meaning (a paraphrase of a banned claim is still banned). Every product detail (name,\n flavour, size, price, ingredient, result) comes from `products[]` or the kit: anything else is\n cut, not invented. Add a `note` ingredient labelled \"Brand rules applied\" that lists the\n pronunciations used and the rules the script respects, so the user sees them.\n **Answer clarifying questions\n from the project brief FIRST (Step 1.5)** \u2014 only ask the user for a field (angle, which product,\n offer/code) the `creative_brief` leaves empty AND the recipe can't default. Do not re-ask for\n anything the composer already captured.\n2. **Mirror the whole ingredient set for review** \u2014 `video_project_upsert { brand_id, project_id,\n patch: { script: { script_drafts, script } } }`. `script_drafts` is a structured payload of\n **container-tagged ingredients** so the app renders each piece the right way:\n `{ format, scenes?, ingredients: [{ container, label, subtitle?, path?, url?, text? }], render_estimate? }`.\n Each ingredient's `container` tells the app HOW to show it:\n - `image` (a frame shown in the video), `endcard` (the end card), `avatar` (a character\n headshot), `background` \u2192 rendered as an image tile.\n - `voice` (a voiceover clip \u2014 put the voice NAME in `subtitle`), `music` (the bed),\n `audio` \u2192 rendered as an audio player.\n - `video` (a clip) \u2192 a video player. `text` (a copy line like the CTA) \u2192 a text tile.\n - `script` / `thread` / `note` / `conversation` \u2192 the written script (or set `scenes[]`\n for the podcast shape, or pass the readable `script` string).\n **Label every ingredient** (\"Hook image\", \"End card\", \"Voiceover\", \"Host A\", \"HER\"). The upsert\n writes no render and costs no credits \u2014 it just populates the review panel.\n3. **STOP for ONE approval, in this chat.** Only a complete current saved review set with\n render_estimate.total_credits can be approved. Widget: one line, no duplicate plan or app\n link; otherwise print card.text_summary. The choices are Approve, Change and Not now.\n \u201cNot now\u201d keeps the plan. In a coworker sandbox end the turn after saving the review set;\n the customer's approval arrives as the next message.\n Record the explicit yes with `video_project_upsert { brand_id, project_id, patch: { approve:\n { user_quote: \"<their exact words>\", total_credits: <the saved total> } } }` before rendering.\n A single project requires recorded approval too; never treat an absent approval as permission.\n Changes require an updated saved review, clearing prior approval, and approval of the new\n total. One yes authorizes the remaining approved chain. On insufficient_balance start\n nothing; offer a shorter video or top-up. Remove batch concepts with patch.concepts remove:true,\n then show the changed total before approval.\n\n## Step 4 \u2014 render, report stages, publish\n\n1. **Open the render row FIRST** \u2014 right after recording the Step 3 approval, before any paid\n generation (if this returns `approval_required`, the approval is missing or was cleared: go\n back to Step 3 and ask in this chat, don't retry):\n `video_render_run { brand_id, project_id, kind: \"full\" }` (no `dry_run`; returns\n `render_id`) \u2192 keep `render_id`, then mark it running:\n `video_render_run { brand_id, project_id, render: { render_id, status: \"running\",\n workflow_stage: \"preparing\", progress_note: \"starting\", progress_percent: 5 } }`. The user sees this\n live in the app and gets a WhatsApp \"started\" message automatically \u2014 don't message them yourself\n about start / blocked / complete.\nAfter EVERY progress callback inspect stop. If true, start no new paid step; record\n status:\"stopped\" with a plain note and report what is kept and credits used. SPEND_CAP_REACHED\n stops the same way. Never raise a video's budget on your own judgment. Raise it only when a paid\n step was refused with SPEND_CAP_REACHED or the progress card offers \"Finish it\" or a choice past the budget.\n Then read the project and say cost.raise_quote in one line: \"Finishing needs up to <by_credits>\n more credits, so your budget goes from <from_credits> to <to_credits>. OK?\" Only after the\n customer's yes to that number: patch.approve { scope:\"raise_cap\", total_credits: <to_credits>,\n user_quote: \"<their exact words>\" }. total_credits is the new budget itself, never credits to\n add; do not send raise_cap_credits for one video. If they pick a smaller choice past the\n budget, send that choice's new budget as total_credits (never above to_credits). Sending the same\n total_credits again changes nothing (already_raised: carry on). An earlier yes, or \"I don't care\n about the cost\", is not approval of a new budget. If cost.raise_quote is null because the plan was saved\n again since its approval (plan_status is not \"approved\"), show the plan with its total and\n approve it again; otherwise the approved budget already covers the work: carry on, do not ask.\n After a raise, open a new render that reuses the saved pieces. A refused raise changes nothing:\n cap_raise_not_needed means carry on within the budget, or, when it says the plan was saved again,\n show the plan with its total and approve the plan again; cap_raise_total_required means read the\n project again and send total_credits equal to cost.raise_quote.to_credits after the customer\n approves that number; cap_raise_changed or cap_raise_too_large mean read the project again and\n show the current cost.raise_quote. Custom videos never use raise_cap.\n Send render.steps with the same neutral names each time and a live count only in the\n current detail; use render.choices when blocked.\n2. Now generate every PAID piece you showed as a prompt in Step 3 \u2014 the AI stills/video, lipsync\n clips, voice, music \u2014 through the media proxies (below), each from its approved prompt, with\n `GW_PROJECT_ID` exported. **Save as you go** (section above): skip any piece already saved\n with the same `input_digest` (download it), and upload each new piece with its\n `ingredient_key` + `input_digest` the moment it passes QC.\n Keep the approved script and review unchanged throughout production and QC; confirmed media\n and progress preserve resume state. Stop and replan/reapprove any material creative change.\n A voiceover made with `data_post_provider` (ElevenLabs `\u2026/with-timestamps`) returns its\n `alignment` only in the reply: write it to `working/vo/<scene>.timestamps.json` at once\n (captions are timed from it) and record the returned `media_id` on the ingredient.\n **Brand pronunciations in every voiceover:** the text sent to the voice has each\n `pronunciations[].term` replaced by its `say_as` (`create-vo-elevenlabs`:\n `gen_vo.py \u2026 --rules working/brand-rules.json`; when a render atom calls the voice itself,\n swap the terms in the text you hand it). Captions, on-screen text and the review keep the\n written name. Write `working/approved-script.txt` (the Step 4.3 audio check) with the SPOKEN\n form. **Logo, font, product:** every logo is the file from Step 1.7 composited as-is; every\n product shot uses the Step 1.7 product images as its reference.\n Then assemble per the recipe (ffmpeg stitch; PIL captions / end card;\n Playwright record only where the format needs it and the host has Chromium \u2192 `mix-master` audio).\n **Report progress at each milestone** \u2014 about one update per milestone, never per poll:\n `video_render_run { brand_id, project_id, render: { render_id, status: \"running\", workflow_stage, progress_note, progress_percent } }`\n (`progress_note` = plain words the customer reads in the app, \u2264200 chars; no model, tool or\n step names):\n - voiceovers done \u2192 `\"preparing\"`, `\"voices recorded\"`, 20\n - stills done \u2192 `\"preparing\"`, `\"scenes designed\"`, 35\n - each lipsync / video clip \u2192 `\"rendering\"`, e.g. `\"filming scene 5 of 8\"`, 35\u201375\n - assembly \u2192 `\"rendering\"`, `\"putting the video together\"`, 85\n - QC (4.3) \u2192 `\"final check\"`, 95\n **Hard stop that needs the user** \u2192 `{ render_id, status: \"running\", workflow_stage: \"blocked\",\n error_message: \"<what's wrong + what you need>\" }`; an unrecoverable failure \u2192 `status: \"failed\"`\n + `error_message`. Completing (4.4) sets the bar to 100.\n3. **MANDATORY final-video QC gate \u2014 YOU review EVERY finished master before pinning it\n (`patch.final_render_id`), whatever the format (UGC or not).** This is your own automated quality\n check, separate from the user's Step-3 approval \u2014 it does not go back to the user. The render row\n is already open (4.1); this gate stands between a rendered master and PINNING/publishing it, so a\n bad render never gets set as final. A master that looks fine on a still can still have a\n mis-voiced word, a caption drifting off its line, a beat out of order, or a deformation \u2014 review\n the actual VIDEO, not stills. Run the passes that APPLY to this format:\n - **Audio \u2194 script** \u2014 any master with SPEECH (VO or native/Seedance voice); **skip for\n music-only / no-speech formats.** `review-ugc-render` is format-agnostic despite the name \u2014\n a deterministic Whisper transcript-vs-script diff: persist the approved spoken lines to\n `working/approved-script.txt`, fetch `review-ugc-render` (`catalog_fetch`) and run\n `review_render.py --video <master>.mp4 --script-file working/approved-script.txt --json\n working/review-verdict.json` (exit 0 PASS / 2 FAIL / 3 ERROR). For each brand pronunciation add\n `--brand-term \"<term>\"`, plus `--brand-term` for each word of `say_as` that is not an\n everyday word (the flag strips those tokens from the WHOLE diff, so never pass \"a\", \"one\",\n \"works\" alone): Whisper spells a respelled name back as the brand word (\"Goose Works\" heard\n as \"Gooseworks\"), so the diff must accept it. The proof of\n intended delivery is the text/settings you sent (keep them in the review); actual\n pronunciation/performance requires isolated and mixed-audio listening at normal speed.\n Preserve pauses/emphasis and final consonants; never mandate faster or louder delivery.\n Verify a transcript mismatch against the actual audio before calling it a defect. It flags a mis-voiced word\n (approved \"human-vetted\" \u2192 \"human witted\"), a dropped phrase, or silence. It routes Whisper\n through the gooseworks proxy when `OPENAI_BASE_URL` is set (sandbox:\n `$GW_WHISPER_PROXY_URL/v1`); with no backend at all, run `fal-ai/whisper` via the FAL proxy\n and diff the transcript yourself.\n - **Captions / subtitles** \u2014 ANY captioned format (the most common non-UGC defect); **skip for\n UGC/Seedance masters, which carry no subtitle track.** Diff the caption file you burned\n (SRT/ASS/PNG cue list) against the SAME Whisper transcript + word timings \u2014 every caption line\n must match the heard/scripted words and sit within ~0.3s of when they're spoken; then in the\n visual pass below, inspect each cue and its start/end boundaries in actual frames, plus\n full-speed playback at destination size, to confirm reading time, product visibility,\n hierarchy and no duplicate overlay or end-card collision. Silent/text-led formats use\n approved visible wording and reading windows, not nonexistent speech timings. Mismatched text or >0.3s drift fails the gate.\n - **Visual + structure** \u2014 always: run the `watch` skill on the master \u2014 beat/scene order + SFX,\n 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\n deformation/artifact, duration within ~20% of the source.\n - **Finished ad + brand fidelity** \u2014 always: fetch `review-finished-ad` (`catalog_fetch`,\n `pip install --quiet numpy pillow` if needed) and run\n `review_finished_ad.py --video <master>.mp4 --json working/review/finished-ad.json\n --sheet working/review/finished-ad-sheet.png --logo <the logo file the video composites>\n --palette \"<kit palette, comma-separated>\" --product-images <the Step 1.7 images>\n --font <brand font file> --brand-name \"<name>\" --endcard-s <end card length>`. Pass the\n file that is ACTUALLY on screen (the kit logo, or the recipe's wordmark file). When the video\n shows only the brand name as text (`\"logo\": {\"mode\": \"text\"}`), omit `--logo` and judge the\n text on the sheet. Add `--no-speech` for a format with no VO or dialogue, `--logo-at <s>` for\n each mid-video logo, and any gate flags the recipe's instructions name (a chat format's\n reading holds need a longer `--max-freeze-s`). Exit 0 PASS / 2 FAIL / 3 ERROR. It\n checks size, hook, pacing, dead air, black frames, the kit logo on the end card\n (a wrong, redrawn or favicon logo fails) and the palette. **Then open the sheet it writes\n and judge every line of its `judge_on_sheet`:** captions/CTA/logo outside the red safe\n zones, the brand font, every product shot matching the product images, the product the same\n in every scene, the logo unaltered. A failed eye check is a FAIL like any other.\n - **Output size** \u2014 always **1080\u00d71920 (9:16)**. Video ads are only ever 9:16. Lipsync / video\n models often return 720p or odd sizes \u2014 scale (and pad if the aspect differs) every clip to\n 1080\u00d71920 BEFORE the concat, never ship the model's native size.\n Save exact output checksum/version, first-to-last normal-speed playback and full-audio\n coverage. Frame sheets, transcripts and process success do not substitute for listening or\n continuous motion. Missing required capability leaves review incomplete/blocked. Each\n supported crop, duration, language or caption derivative needs its own actual-file review;\n unsupported destination requirements need a reviewed route, never a silent export change.\n Honor intended silence, loops and endings rather than forcing music or fades on every ad.\n If ANY applicable pass fails, FIX it (regenerate/stitch the offending window, re-composite the\n end card from the real logo file, rebuild captions) and re-run the passes \u2014 only a clean pass\n proceeds to pinning. **At most 2 repair rounds.** If a pass still fails after them, do NOT pin\n and do NOT present the video as finished: upload it (4.4) and close the row with exactly\n `video_render_run { brand_id, project_id, render: { render_id, status: \"complete\",\n workflow_stage: \"blocked\", quality_status: \"blocked\", repair_pass_count: 2, output_url,\n thumbnail_url, quality_report } }` (the failing checks as `fail` in the report), and tell the user plainly in\n chat which checks failed, what you tried, and the choices (fix a specific thing, re-roll, or\n use it anyway). Pin it only if they say to use it anyway. The app shows a blocked render as\n \"Needs attention\". **This gate is universal: it runs from this skill for every format, so a\n recipe never has to opt in.**\n4. **For a passing final, create the finished-video checkpoint first**, following \"Finished video, interrupted saving\"\n above. With the CLI, use its prepare/resume flow instead of duplicating the writes below; it\n saves the same already-opened render and pins only the checked output. Save the final review set\n and production manifest (Step 4.5/4.6) before preparing that checkpoint. Without it, follow the\n same durable protocol. A blocked version follows Step 4.3's existing upload/report-only path;\n never route it through the passing-final helper or pin it automatically.\n Publish: `media_upload { brand_id, scope: \"video_project\", scope_id: project_id, kind: \"render\",\n path: \"working/final.mp4\", ingredient_key: \"final\", input_digest, source: { type: \"file\", filename: \"final.mp4\", content_type:\n \"video/mp4\" } }` \u2192 PUT the master to `upload.url` with `upload.required_headers` \u2192\n `media_confirm { brand_id, media_id: media.id }`. Same for the poster (`kind: \"thumbnail\"`, `path: \"working/final-thumb.jpg\"`, `ingredient_key: \"final-thumb\"`).\n Keep each `upload.render_file_url`. Verify the PUT returned 2xx and the file you uploaded is a\n real, non-empty MP4 (ffprobe it), and require both confirmations to succeed BEFORE marking\n the render complete.\n 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\n `\"blocked\"` when the gate still fails after 2 repair rounds) + `quality_report` \u2014 ALWAYS\n attach it; a batch concept cannot complete without a passing one; exact shape, strict (no extra keys):\n `{ version: 1, summary: string, checks: { source, brand, product, hook_and_scene_order,\n voice_and_script, captions, endcard_and_cta, duration_and_ratio, visual_artifacts }, detected_issues?:\n string[], repair_actions?: string[] }` where EVERY check is `{ status: \"pass\"|\"fail\"|\"not_applicable\",\n note?: string }`). Fill each check from the Step 4.3 passes: `brand` \u2190 logo_asset + logo +\n palette + font + logo unaltered; `product` \u2190 product likeness + product consistency;\n `hook_and_scene_order` \u2190 hook + pacing + the `watch` beat order; `voice_and_script` \u2190 the\n Whisper diff (pronunciations included) + no `never_say` line; `captions` \u2190 the caption diff +\n safe zones; `duration_and_ratio` \u2190 ratio + duration; `visual_artifacts` \u2190 black frames +\n frozen stretches + the `watch` pass; `endcard_and_cta` \u2190 the end card holds the real logo +\n CTA. Put each failing check's note in `detected_issues` and each fix in `repair_actions`.\n **output_url MUST be the durable render-file URL**\n (`upload.render_file_url`, i.e. `/api/ads/projects/<project_id>/render-file?path=working/final.mp4`\n \u2014 the app re-presigns it on every view) \u2014 NEVER a raw proxy/CDN/presigned URL (those expire).\n Same for `thumbnail_url`.\n5. **Save the final review set BEFORE pinning** \u2014 it must describe the video you actually rendered.\n Only after all paid production, QC, repairs and pending provider work are finished and settled,\n save one final descriptive/provenance update: confirmed pieces, exact settings and repairs that\n preserve the approved creative choices. Material creative changes, including changed lines,\n look, timing, captions or music, require a changed complete plan, total and fresh approval BEFORE\n the changed production. Never relabel a changed plan as provenance or silently reuse approval.\n In the checkpoint flow, save the final review before Step 4.4; do not rewrite it afterward merely\n to follow the numbered order. Resume checks that this reviewed set stayed unchanged.\n When final provenance needs updating, upsert the review set once:\n `video_project_upsert { brand_id, project_id, patch: { script: {\n script_drafts, script } } }` with the final lines, final pieces (mark generated takes as done, not\n \"not generated yet\") and the settings you used. The project keeps this, not your chat: it is\n what the app shows, and what a Community remix of this video copies. Instructions that live only\n in this conversation are lost when it ends. **Keep the approved detail**: never shorten a piece to a\n summary (e.g. per-scene prompts or their \"no shake / no letterbox\" guards). Only update what changed.\n After this write, do not start new paid work. Save the production manifest, prepare the final\n checkpoint and complete the same render with its existing finishing allowance and guards.\n6. **Save the production manifest** \u2014 HOW you made it, so the next run (or a remix) starts from what\n worked instead of rediscovering it: `video_project_upsert { brand_id, project_id, patch: { production:\n { version: 1, pipeline: [{ step, model, purpose?, settings? }], style: { prompt, negative, notes? },\n characters: [{ name, prompt }], scenes: [{ id, line, still_prompt, motion_prompt, duration_s }],\n voice: { voice_id, name, model, settings }, music: { prompt, model, length_s }, assembly: { \u2026 },\n fixes: [{ problem, fix }], notes? } } }`. Use the exact models and prompts you sent (full text, guards\n included), and list every fix you had to make (e.g. \"VO ran 47s \u2192 tightened four lines, voice 1.08x\";\n \"hair drifted \u2192 restated the hair colour\"). No URLs, keys or raw logs; it must stay under 48 KB.\n In the checkpoint flow, save this before Step 4.4 too and skip the duplicate write afterward.\n7. Pin it \u2014 only a `passed` render (or a `blocked` one the user said to use anyway):\n If checkpoint recovery already verified selection of this render, skip this duplicate write.\n An unfinished checkpoint must use the guarded selection protocol above; never replace it\n with an unguarded pin.\n `video_project_upsert { brand_id, project_id, patch: { final_render_id: render_id } }`,\n then use the returned card: widget hosts get one line and no duplicate links; text hosts get\n card.text_summary and the returned delivery links verbatim. Never end with a local file path.\n\nNarrate each long step in one line via `video_project_upsert { brand_id, project_id, patch:\n{ message: { role: \"agent\", content } } }` \u2014 never sit silent on a queue > 90s. Write it for the\ncustomer (\"Filming the scenes, about 5 more minutes\"), per \"How to talk to the customer\" above.\n\n## Media generation \u2014 the GooseWorks proxies (queue loop)\n\nMedia APIs go through GooseWorks proxies; do NOT use an SDK's default host (your token isn't a\nFAL/ElevenLabs token \u2192 401, and in a sandbox a raw provider call is never allowed). Prefer the\natoms' own `media_proxy.py` (`fal_generate`, `fal_generate_video`, `eleven_music`, \u2026) \u2014 it already\nhandles auth, the queue host-swap, project attribution (`GW_PROJECT_ID`) and failure logging.\n\n- **Sandbox:** FAL base = `$GW_FAL_PROXY_URL`, ElevenLabs = `$GW_ELEVENLABS_PROXY_URL`, FAL\n storage = `$GW_FAL_STORAGE_PROXY_URL`; auth = `?token=$GW_MEDIA_PROXY_TOKEN` (or\n `Authorization: Bearer`), plus `&project_id=$GW_PROJECT_ID`. The token already names the agent.\n- **Local:** base = `<api_base>/api/internal/<proxy>` (`fal-proxy`, `fal-storage-proxy`,\n `elevenlabs-proxy`); pass `?token=<api_key>&agent_id=<agent_id>&project_id=<project_id>`\n (agent_id bills the Ads agent; project_id attributes the spend to this ad project so the user\n sees per-project spend in the app. ALWAYS pass it).\n\n**FAL queue gotcha** (#1 waste of generations): submit returns `status_url`/`response_url` on\n`queue.fal.run` (the real host, not the proxy). Polling those 401s forever \u2014 rewrite their host\nto the proxy base (keep the path), re-add the auth params. Only the final `*.fal.media`\nfile is a real public URL. Helper (works in both modes):\n\n```python\nimport json, os, pathlib, time, requests\nfrom urllib.parse import urlparse\n\ndef _cfg():\n \"\"\"(fal_base, token, agent_id). Sandbox env first, then ~/.gooseworks/credentials.json.\"\"\"\n tok = os.environ.get(\"GW_MEDIA_PROXY_TOKEN\")\n if tok:\n base = os.environ.get(\"GW_FAL_PROXY_URL\") or os.environ[\"GW_API_BASE\"].rstrip(\"/\") + \"/api/internal/fal-proxy\"\n return base.rstrip(\"/\"), tok, None\n c = json.loads(pathlib.Path(os.path.expanduser(\"~/.gooseworks/credentials.json\")).read_text())\n return c[\"api_base\"].rstrip(\"/\") + \"/api/internal/fal-proxy\", c[\"api_key\"], c.get(\"agent_id\")\n\ndef _params(tok, agent):\n p = {\"token\": tok}\n if agent: p[\"agent_id\"] = agent\n pid = os.environ.get(\"GW_PROJECT_ID\")\n if pid: p[\"project_id\"] = pid # attributes the spend to this ad project\n return p\n\ndef fal_generate(model_path, payload, timeout_s=180, poll_s=3):\n \"\"\"model_path e.g. 'fal-ai/nano-banana-2/edit' (the recipe names the model).\n Export GW_PROJECT_ID first. Returns the result image URL (a public *.fal.media URL).\"\"\"\n base, tok, agent = _cfg()\n sub = requests.post(f\"{base}/{model_path}\", params=_params(tok, agent), json=payload).json()\n to_proxy = lambda u: base + urlparse(u).path\n status_url, response_url = to_proxy(sub[\"status_url\"]), to_proxy(sub[\"response_url\"])\n deadline = time.time() + timeout_s\n while time.time() < deadline:\n st = requests.get(status_url, params=_params(tok, agent)).json()\n if st.get(\"status\") == \"COMPLETED\":\n return requests.get(response_url, params=_params(tok, agent)).json()[\"images\"][0][\"url\"]\n if st.get(\"status\") in (\"FAILED\", \"ERROR\"):\n raise RuntimeError(f\"FAL failed: {st}\")\n time.sleep(poll_s)\n raise TimeoutError(\"FAL polling exceeded timeout\")\n```\n\nElevenLabs (VO / music) is the same shape against the ElevenLabs proxy base. To feed FAL a local\nfile (a product image, a VO track), it must be a PUBLIC URL: upload it with `media_upload`\n(`scope: \"video_project\"`, source `file`, no `path`) \u2192 PUT \u2192 `media_confirm` and use the returned\n`media.url` if it is a public https URL (curl it: HTTP 200 without auth), or host it through the\nFAL storage proxy. Never pass a `render-file` URL to a provider \u2014 it needs app auth.\n\n## Follow-up actions use the watched version\n\nKeep original note text, exact source render/time and intended viewer effect. Record an\nacceptance condition and disposition in existing feedback/project artifacts; applying a\ncommand means changed, not verified. Consolidate duplicates without deleting their source\nIDs. Conflicting \u201ccalmer\u201d and \u201cmore urgent\u201d instructions need the authorized decision owner's\nrecorded choice, even if they touch different files. Offer concrete cheap alternatives for\n\u201cmore premium\u201d inside the approved scope before escalating taste.\n\nMap changed copy/performance/shot/pause to affected picture, timings, captions, music/SFX,\napprovals and derivatives; preserve unrelated approved work. Rewatch the full candidate and\nverify the original effect before resolving a note. Accepted final, candidate and original\nnote source remain distinct across selection and rollback. Keep editable script/timeline,\nsource assets/takes, stems, captions, package versions and prior decisions in the existing\nproduction manifest/history; retrieve applicable rejections/pronunciation/delivery rules\nbefore a related brief. Project taste is not a standing brand rule. Record audience results\nonly with actual variant/audience/placement/objective/window; otherwise unavailable.\n\n- \u201cFix this\u201d: use the render/project context attached to the message (or named version, else\n the final one). Save patch.fix { of_render_id, changes, total_credits }. Never replace a supplied\n watched render with the final. After explicit approval save patch.approve { user_quote,\n total_credits, scope:\"fix\" }, then video_render_run { kind:\"partial\", fix_of_render_id }.\n Re-make only changed scenes, keep earlier versions and charge no second base fee.\n- \u201cMake 3 more like this\u201d: video_project_upsert { brand_id, name, remix_of_project_id,\n remix_of_render_id, vary, concepts:3 }. Preserve a provided watched render; omit only when none is named.\n- \u201cTest 1 of each\u201d: approve only:\"test\"; \u201cMake the other N\u201d: only:\"rest\". \u201cStop the rest\u201d:\n job_cancel { job_id:<batch_id> }. Finished videos stay saved.\n- Share for review uses patch.share_for_review { subject }; return the real link.\n\n## Rules\n\n- **Canonical MCP tools first** (`video_project_read`, `video_project_upsert`, `catalog_fetch`,\n `media_upload` + `media_confirm`, the `video_render_run` render-row actions, `account_whoami`);\n legacy names only when the client lacks the canonical tool.\n- **A missing key is never a blocker.** Paid media goes through the proxy: a one-off image or clip\n is `data_post_provider { provider: \"fal\", \u2026 }` + `job_get` (see \"Paid media over the MCP\");\n never ask for FAL_KEY, never use `photos_generate` for anything but a physical product.\n- **The CLI and credentials.json are optional.** In a GooseWorks sandbox (`GW_MEDIA_PROXY_TOKEN`\n set) use the env proxies and `catalog_fetch`; never call a provider with a raw key.\n- **No Chromium in a sandbox** \u2014 a browser-rendered format stops there, before any spend, and says so.\n- **Toolchain before spend** \u2014 common checks use `gooseworks doctor --no-browser`; every selected\n browser renderer must also pass `--renderer-script` or its equivalent exact-runtime launch\n check in Step 2. Stop with the folder-specific fix if anything is missing.\n- **Assemble the whole review set first**, mirror it with `video_project_upsert patch.script`, and\n get ONE approval in this chat, recorded with `patch.approve`, BEFORE the expensive render\n (review-once). Never send the user to the app to approve; the app is only for reviewing.\n- **Show the REAL cheap pieces; PROMPT only the expensive render.** Generate the FREE + CHEAP-paid\n pieces (\u2264 ~100 credits \u2014 stills, creator frame, end card, short VO/music) and mirror the real\n assets; put ONLY the expensive video take/render in the panel as its exact prompt. That prompt\n must be in the panel before you ask to approve, so a single \"go\" runs the render + any remaining\n paid work (\u2192 QC \u2192 publish) with no re-pausing.\n- **Operator may be absent (sandbox):** decide mechanical choices yourself; ask in chat only for a\n true taste call.\n- **Costs in CREDITS, never dollars.** credits \u2248 round-up(provider-$ \u00d7 120) per generation + a flat\n 200-credit base per video; never show a \"$\u2026\" figure to the user.\n- **Never assemble the full video before approval.** The review shows the\n individual PIECES, never the finished cut (or their prompts) \u2014 not a\n stitched/composited cut; do not add a \"full cascade\" / finished-video clip\n as a review ingredient (GOOSE-2542). The assembled video is produced only in\n Step 4.\n- **Open the render row at the start of Step 4** (4.1, after the approval) and keep its stage /\n progress current; mark it `complete` only after the master passes QC. `output_url` = the\n durable render-file URL, never a CDN URL.\n- **Always export `GW_PROJECT_ID`** (and pass `project_id` on hand-rolled proxy calls) so the\n credits attribute to this ad project.\n- **Verify a real, non-empty MP4** (watch it) before marking the render complete.\n- **Reuse the brand** when its research is complete; never re-research.\n- **Brand rules first:** load kit + products + learnings (Step 1), write `working/brand-rules.json`\n (Step 1.7), and follow it: pronunciations in every voiceover, nothing from `never_say`, product\n facts only from the product rows, the kit's logo FILE (never a generated or favicon logo), the\n brand font, this product's own images.\n- **Save brand corrections** from chat with `brand_update patch.facts` in the same turn, and say so.\n- **Finished-ad gate on every master** (`review-finished-ad` + the sheet); at most 2 repair\n rounds, then publish as `blocked` with the report and warn the user \u2014 never pass off a failing\n video as finished.\n- On a hard error (auth/quota/model/timeout) set the render `failed` with a short\n `error_message` (`video_render_run { \u2026, render: { render_id, status: \"failed\", error_message } }`) and stop \u2014 don't ship the source unchanged. **Also log\n it** (see \"Report problems\") so we can see + fix it.\n- Finish according to card.display_hint: widget hosts get one short line without duplicate links;\n text hosts get card.text_summary and the returned delivery links verbatim.\n\n## Use uploaded footage\n\nWhen the customer asks to use existing footage, check the actual advertised schemas. Use `media_search` with purpose `production`, kind `video`, the brand and current project when needed. Omit query spend for free retrieval. Read facts/scenes with `media_analyze`, then use `media_excerpt` action `inspect` to review actual bounded frames and timed transcript before selecting. A description or thumbnail URL alone is not visual review.\n\nFreeze `source_excerpt:{asset_id,analysis_revision,scene_id,start_ms,end_ms,audio_mode}`. Revision is the original-byte SHA, scene may be null for a known user trim, bounds use integer milliseconds, and audio is original or muted. Call `media_excerpt` action `attach` with that exact selection and a stable idempotency key. The returned project remains unfinished and unapproved. Open its existing Studio review; preserve user locks and requested format, and keep footage separate from image packshot indexes. An incompatible format requires an explicit choice before changing it.\n\nFor custom production, fetch the current shared `video-production-harness` and follow script/ingredient gates. Download the verified original and trim its exact selected window at normal speed with the approved audio. Recheck current revision and production permission before consumption and final upload/completion. Save excerpt lineage in plan and ingredient readback. Missing optional semantics does not require another upload; known user-selected stored footage remains usable. Research and competitor references never become production footage. Selected originals require no paid generation; only newly generated/replaced ingredients consume the approved budget.\n"
5
- }