Brand Studio
This folder is the reusable skill payload. It should stay thin: SKILL.md,
small scripts, references, and templates. The runtime lives under scripts/;
do not add a top-level src/ package or tests inside an installed skill
payload. A valid skill payload may have scripts/, references/, assets/,
and agents/.
Preserve the boundary:
org standard -> repo visual state -> production plan -> generated candidates -> user acceptance -> settled repo assets -> next production
Never put visual style prompt text in campaign files. Campaigns describe content and deliverables only.
Do not model asset collection as a user-facing command surface. Assets enter the durable corpus only when a user accepts generated candidates or explicitly marks existing work as accepted during review. Treat low-level scripts as internal agent helpers, not as instructions for users to add assets manually.
Use stage names to classify user intent before acting:
init-org: create the organization brand standard.update-org: revise organization standards, base theme, or references.retire-org: deprecate organization standards or references; do not default to physical deletion.init-repo: create a product repo's Brand Studio metadata and local theme.gen-repo: generate scratch candidates for a product repo.settle-repo: move accepted candidates into durable product assets.update-repo: revise product theme or replace an asset through a new candidate and review cycle.delete-repo: delete scratch candidates only.retire-repo: mark durable product assets inactive or superseded; purge physical files only with explicit approval and reference checks.
Agents may read images, infer intent, choose the stage, write proposals, and select producer prompts. Deterministic helpers must handle path resolution, validation, producer handoff context, checksum reporting, accepted-state updates, approved-manifest writes, and any destructive operation.
Stage routing rules:
| User intent | Stage | Allowed durable writes |
|---|---|---|
| "initialize our brand/org standard" | init-org | public/brand/ only |
| "update the org standard/reference" | update-org | focused public/brand/ edits |
| "remove/deprecate this org rule/reference" | retire-org | deprecation notes first |
| "init this product/repo" | init-repo | metadata, theme, empty state |
| "make/generate a release card/banner" | gen-repo | none; scratch output only |
| "this one is good/use it" | settle-repo | approved asset and state updates |
| "revise theme/update this asset" | update-repo | proposal or new settled revision |
| "delete this draft/candidate" | delete-repo | scratch deletion only |
| "remove this published asset" | retire-repo | state retirement; purge needs approval |
If a request can map to multiple stages, stop after the safest earlier stage.
For example, "update this release card" means update-repo asset followed by a
new gen-repo candidate; it does not mean editing an approved PNG in place.
Only move from gen-repo to settle-repo after user acceptance.
Skill Distribution
For personal or organization use, prefer a fork of the upstream skill repo. Treat the upstream repo as the generic runtime/template source, and treat the personal or org fork as the shared source for team metadata, policies, producer preferences, templates, install notes, and org brand standards.
When working inside a product repo, use the product repo's pinned fork or local
skill install. Do not silently switch to upstream. Product-specific theme.md,
campaigns, accepted state, and public assets remain in the product repo or its
asset repo; cross-person defaults belong in the fork.
Asset location is a hard boundary. Product assets — generated candidates,
accepted images/video/copy, theme.references targets, campaigns, accepted
state — live in the product repo, never in this org skill repo or its fork.
The skill repo holds only runtime, producers, templates, and cross-product
defaults: zero real product bytes. This includes test fixtures — reference
product assets by their product-repo path, never embed copies (the runtime uses
references as prompt strings, so a product-repo-only path is fine).
Org Brand Standard Init
For init-org, initialize the organization fork itself and store shared brand
standards under public/brand/ in that fork. This is the distribution surface
product repos inherit from; it is not a product accepted corpus.
Recommended org fork shape:
public/brand/
brand-standard.md
theme.base.md
references/
brand-standard.md is the human-readable standard: positioning, visual
language, palette, typography direction, composition rules, logo usage, voice,
do/don't, product adaptation rules, and source-input rationale. theme.base.md
is the machine-readable base style lock. Do not require a separate brief.md;
if rationale is useful, keep it in brand-standard.md under a source inputs or
rationale section. Treat files under public/brand/ as the curated org brand
distribution; do not add an org accepted-state loop unless the org fork later
needs a separate review ledger.
For org init, read user-supplied logo, website, deck, product UI, and marketing
references first. If no files are supplied, scan public/brand/references/ and
then public/brand/ once for images. Do not scan the whole fork, and do not
create product-style campaign, accepted, or asset-state files for the org fork.
Use the canonical runtime helper only to create or check the deterministic org brand files:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" org init --write
The helper creates missing brand-standard.md, theme.base.md, and
references/ paths, respecting brandStandard.* metadata when present. It does
not read images, infer brand direction, overwrite existing files, or create repo
asset state.
Product metadata can point back to the org standard:
brandStandard:
source: org-fork
path: public/brand/brand-standard.md
themeBase: public/brand/theme.base.md
references: public/brand/references
version: 1.0.0
During product init, read the org brand standard first, then combine it with the
product's uploaded images, declared asset roots, and repo context to write the
product-local theme.md.
For update-org, revise brand-standard.md, theme.base.md, or files under
references/ only after reading the current standard and the user's new source
inputs. Prefer a focused patch and record rationale in brand-standard.md.
Never rewrite the whole org standard when a small rule, palette, reference, or
avoid-list update is enough.
For retire-org, mark standards or references as deprecated in
brand-standard.md before removing files. Physical deletion from
public/brand/ requires explicit user instruction and a check for downstream
repo references when those repos are available locally.
Image-First Init, Metadata Underneath
For init-repo, prefer image-first initialization when the user supplies brand
images, screenshots, or existing marketing assets. Use the agent's native
image-reading capability to infer the first brand direction, then write the
Brand Studio files directly. Do not ask the user to fill out YAML or a brand
brief before producing the first usable draft.
Accept prompts like:
$brand-studio init this repo from the attached brand images
$brand-studio init this repo from existing repo assets
For image-first init:
- Locate the skill root from the installed skill or repo submodule.
- Find or create
marketing.studio.yamlusing metadata paths. If the file is missing, inferproject.idfrom the repo directory name, useroot: ., useassets/marketingforproject.marketingRoot, useassets/marketing/campaigns/releaseandassets/marketing/campaigns/promofor campaign domains, use.studio/marketing/outfor scratch output, and usepublic/marketingfor approved assets. Omitorganizationunless the user supplied it or the repo already makes it clear. - Run
repo initin dry-run mode first. Only create directories after the paths match the current repo shape. - Read the supplied images directly. Extract a compact brand direction:
palette, typography direction, visual language, materials, lighting, mood,
composition rules, avoid list, and initial style aliases such as
launch-heroandsocial-default. If no images are attached, scan the declared asset roots once for image files and use those as initialization context. Do not add a separate init-assets path schema or per-image role schema. - Write
brief.mdas the human-readable rationale andtheme.mdas the machine-readable style lock with valid design-token frontmatter. - Write
campaigns/init-preview.campaign.yamlas a representative preview. - Create root
asset-state.yaml/accepted.yamlplusportfolios/release/andportfolios/promo/state if they do not exist. - Run validation and a dry render with the existing launcher.
- Show the generated file paths and dry-run outputs. The user can edit
brief.mdortheme.md, or ask for a revision.
Do not add image understanding to studio.py. Codex, Claude, or the active
agent reads images; the studio remains deterministic and only handles paths,
validation, repo init, and dry-run rendering.
If theme.md already exists, do not silently replace it. Revise it in place
only when the user clearly asks to reinitialize; otherwise write a proposal
under the metadata-declared marketing root and ask before promotion.
This skill is still metadata-first internally: do not rely on hard-coded product paths. Find or create a small metadata file in the product repo, then pass it to the adapter scripts.
Template:
project:
id: my-product
root: .
marketingRoot: assets/marketing
organization:
id: my-org
name: My Org
skillDistribution:
upstream: sma1lboy/brand-studio
fork: my-org/brand-studio
scope: org
ref: main
brandStandard:
source: org-fork
path: public/brand/brand-standard.md
themeBase: public/brand/theme.base.md
references: public/brand/references
version: 1.0.0
theme:
path: assets/marketing/theme.md
references: assets/marketing/references
campaigns:
release: assets/marketing/campaigns/release
promo: assets/marketing/campaigns/promo
skills:
# Bind each capability to a producer: a bundled producers/<id> or a locally
# installed skill name. Empty = unbound (never silently routed to image).
# See references/recommendations.yaml for suggested producers per capability.
image: ""
video: ""
copy: ""
slide: ""
logo: ""
social: ""
backends:
# Execution engine per modality. The studio runtime only declares/threads this
# into producer-context.json; the producer subagent runs it. Credentials live
# in the runner's env, never here. Empty = the bound producer uses its own engine.
text-to-image:
runner: "" # codex-exec | api | mcp | custom
command: "" # e.g. "codex exec ..." (user-local CLI)
model: "" # e.g. gpt-image-2
campaign:
name: launch
path: assets/marketing/campaigns/promo/launch.campaign.yaml
artifacts:
scratch: .studio/marketing/out
approved: public/marketing
state:
plans: assets/marketing/plans
assetIndex: assets/marketing/asset-state.yaml
accepted: assets/marketing/accepted.yaml
directoryStateFile: asset-state.yaml
portfolios:
release:
accepted: assets/marketing/portfolios/release/accepted.yaml
assetState: assets/marketing/portfolios/release/asset-state.yaml
patterns: assets/marketing/portfolios/release/patterns.md
promo:
accepted: assets/marketing/portfolios/promo/accepted.yaml
assetState: assets/marketing/portfolios/promo/asset-state.yaml
patterns: assets/marketing/portfolios/promo/patterns.md
sources:
assetRoots:
- assets/marketing
- public/marketing
relatedRepos: []
policy:
requireHumanApprovalBeforeRender: true
requireHumanApprovalBeforeStateUpdate: true
allowRootWorkspaceBootstrap: false
assets/brand-studio-template.yaml contains a copyable starter. If the
repo already has its own marketing/branding layout, match it instead of moving
files to a generic root-level directory.
Resolve Roots
Keep these roots separate:
- Project root: the user's current product repo.
- Marketing root: the product-owned source location from metadata, such as
assets/marketing. - Campaign domains: release-note/changelog campaigns live under
campaigns.release; normal campaign-first promotion lives undercampaigns.promo. - Org brand standard: shared brand rules and base theme declared by
brandStandard, usually from the org fork'spublic/brand/. - Repo asset tree: the repo-owned hierarchy under declared asset roots. Treat the repo and its asset directories as the asset namespace.
- Scratch output: the product-owned temporary render location from metadata,
such as
.studio/marketing/out. - Approved assets: the product-owned location for user-accepted generated files.
- Accepted state: the transitional aggregate accepted index, usually
assets/marketing/accepted.yaml. - Portfolio state: domain-specific accepted assets and patterns under
portfolios/release/andportfolios/promo/. - Directory state:
state.directoryStateFile, usuallyasset-state.yaml, found under declared asset roots and read before production. - Related repo state: local sibling repo metadata/state declared under
sources.relatedRepos. - Skill root: this installed
skills/brand-studiofolder.
Never create visible root-level state directories in a product repo —
workspace/, outputs/, published/, releases/, or anything similar. Use
metadata paths. If skill-owned state has no metadata-declared home, it defaults
to a hidden .brand-studio/ directory at the product-repo root (scratch renders
stay under the metadata artifacts.scratch path, e.g. .studio/). User-facing
content (settled assets, marketing copy, campaigns) settles into
metadata-declared visible roots; everything else stays dotted.
The launcher is:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" --metadata marketing.studio.yaml
Pass --project-root whenever the command may run from a skill checkout,
submodule, monorepo tool directory, or any cwd that is not the product repo root.
All metadata-relative paths are resolved under the resolved project root. The
launcher runs the bundled scripts in this skill. There is no uvx remote
runtime fallback and no ancestor checkout discovery.
YAML metadata requires PyYAML. Use uv run python ... from this skill checkout
or install dependencies with uv sync; do not rely on the old simplified YAML
fallback for metadata lists.
Prefer the canonical command surface:
| Stage | Command |
|---|---|
init-org | org init [--write] |
init-repo | repo init [--write] |
| repo path preflight | repo paths |
| repo config check | repo check |
| repo state preflight | repo state |
| validation | repo validate |
| dry render | repo render --dry-run |
| release copy review | repo release copy --write --releases 4 |
gen-repo release | repo gen release --releases 4 --changelog <file> |
| producer prompt | repo prompt --campaign <name> --asset-id <id> |
| producer handoff | repo handoff --campaign <name> --asset-id <id> |
settle-repo | repo settle --campaign <name> --asset-id <id> --file <path> |
| report | repo report --file <path> |
delete-repo candidate | repo delete candidate --file <path> |
| producer catalog | python3 "$SKILL_ROOT/scripts/catalog.py" |
The old top-level helper commands are not a public command surface. Use
org ... or repo ...; helper functions under the hood are implementation
details. Use scripts/check_studio.sh and scripts/bootstrap_project.sh only
as internal setup wrappers. repo init is create-only, dry-run by default, and
must show the user the planned directories before any write.
Common Defaults
- Always dry-run before asking any external producer to generate live assets.
- Do not commit automatically.
- Do not call image APIs until the user has approved the cost/action.
- Do not update accepted state until the user has accepted specific generated candidates.
- Do not physically delete durable repo assets by default. Scratch candidates can be deleted after path validation; approved assets should be retired unless the user explicitly asks to purge them.
- Brand Studio itself does not drive git. If the product repo's own AGENTS.md or user instructions explicitly require commits, follow those higher-priority repo rules and stage only Brand Studio metadata, state, manifests, and accepted assets related to the requested work.
For the lifecycle, read references/workflows.md. For schema contracts, read
references/contracts.md.
Assets, Producers, Backends, and Orchestration
An asset is multimodal — image, video, brand-tone copy, slide, logo, social —
all uniformly settled into the same corpus. The studio runtime validates and
settles by modality, not an image-only allowlist; accepted entries carry
modality. brand-studio curates and settles; it never produces.
Producers are referenced, never owned:
- Bind each capability under
metadata.skills(image/video/copy/slide/logo/social) to a bundledproducers/<id>or an installed skill name. An unbound capability is unbound — it is never silently routed toimage. - Bundled producers under
producers/<id>/SKILL.mdare read-only: the agent reads them as procedure/context (they are not Skill-tool invocable, so they never pollute the user's global skill list).scripts/catalog.pyenumerates them into acapability -> producersregistry. Only bundle instruction-grade producers; heavy toolchains (e.g. remotion) stay at user/project scope. references/recommendations.yamlis the suggested-producer catalog (pointers, not implementations). Never auto-download, clone, install, or register a producer. When a capability has no producer, suggest one and ask the user.
Producer selection (main agent): map capability → best-match producer; on no clear match, ask the user; hybrid is allowed (compose several skills). When reading a producer/design skill, borrow its UI convention, not its specific technique — unless the user asks for that technique.
Weight profiles live under metadata weightProfiles. The studio handoff
passes only the resolved weight_profile name, not expanded percentages, so the
producer can resolve the profile from metadata without spending prompt tokens.
Interpret source keys consistently:
history: matching portfolio accepted assets and asset-state memory.request: the current user request plus campaign brief/content.org: organization brand standard and shared org-fork conventions.copy: release copy asset and version facts.producer: selected producer/subskill composition convention.
Treat weight profiles as soft source-priority hints. theme.md resolved style,
palette, typography, references, and avoid list are hard brand constraints and
override soft source weights.
Backends decide what engine renders a prompt, orthogonal to skills:
backends:
text-to-image:
runner: codex-exec # api | mcp | custom
command: "codex exec ..." # user-local CLI; auth stays in the runner env
model: gpt-image-2
The studio runtime only declares the backend and threads it into
producer-context.json and repo handoff output; it never executes one.
Running a backend is a generation call — the producer subagent does it.
Orchestration: main agent owns the human gates (approve cost/producer before
live generation; review + settle after). After approval it fans out one
subagent per deliverable/capability to invoke the bound producer + backend
and return data only (path, mime, dims, checksum). Subagents cannot prompt the
user, so all approval happens in main before fan-out. A single deliverable with
one producer can run inline. See CLAUDE.md for the full boundary.
Interactive Generation & Review Loop
Generation is a smooth, end-to-end human-in-the-loop loop. Never dump raw files on the user or stop at scratch — always generate → preview → ask → settle → offer the next round. The user only makes taste calls (which ones); they never wrangle files. Develop and settle are one complete cycle: settled assets feed the brand weight of the next round.
flowchart LR
S["brand state<br/>theme · references · accepted portfolio"] --> W["brand weight<br/>tokens + reference assets"]
W --> G["generate<br/>producers / backends, in parallel"]
G --> R["review<br/>round-review.html<br/>keep · maybe · drop + notes"]
R -->|keep| T["settle<br/>approved + manifest + portfolio"]
R -->|maybe / drop| X["stay in scratch"]
R -->|next direction| G
T --> S
The W (brand weight) node is itself several distinct input weights
converging — not one blob. Show them explicitly; it is what makes the loop precise:
flowchart LR
O["1 org brand standard"] --> W
T["2 product theme fields<br/>palette · type · avoid · tokens"] --> W
R["3 reference assets<br/>in-repo marks (glyph-k …)"] --> W
A["4 domain accepted samples<br/>release / promo, never crossed"] --> W
C["5 borrowed UI conventions<br/>convention, not technique"] --> W
D["6 deliverable content<br/>brief · headline · subject · size"] --> W
U["7 user direction<br/>round feedback / taste"] --> W
SB["8 sibling-project assets<br/>sources.relatedRepos (org family DNA)"] --> W
W[["brand weight<br/>producer-context.json"]] --> G["generate"]
For a new product with little of its own accepted corpus, inputs 1 and 8 (org
standard + sibling-project assets) carry the family look — pull siblings from
sources.relatedRepos so the new marks share the org's DNA instead of drifting
into generic app-icon territory.
Each input's source and contribution is tabled in CLAUDE.md ("What brand weight
is made of"). The runtime gathers these into one structured handoff; it never
bakes them into a single hardcoded prompt.
- Generate — actively choose methods, never default to one. Before fanning
out, survey what's available and think about which producers/backends fit
this deliverable: the bundled
producers/(viacatalog.py), installed skills,references/recommendations.yaml, and the configuredbackends. One goal usually has several genuinely different methods — a logo, say, can come from an SVG logo skill, a text-to-image backend (gpt-image-2), or remotion (programmatic / animated mark); a promo can be a still image or a remotion video. Do not lock the whole round to a single method (e.g. only gpt-image-2) just because it's the obvious default — fan out across the methods that would give meaningfully different candidates, and say which method each candidate used. Then: one subagent /codex execper candidate, distinct output files so they don't collide, all in parallel (no concurrency cap). For image backends, attach the brand's reference assets as visual inputs (e.g.codex exec -i <asset>), pulled from the declared asset roots /theme.references, so candidates inherit the real visual language, not just text tokens. If no method clearly fits, ask the user (see Producer selection); hybrid across methods is encouraged. Confirm the method set with the user before firing, and always offer an "explore more" option — never limit the choice to what's already installed. Alongside the bundled/installed methods, surface installable candidates fromreferences/recommendations.yaml(grouped by capability), and if nothing on hand fits, offer to search for / recommend a new producer skill to install. So the confirm step is: available methods + "explore/install more". - Preview — the standard review template (required every round) — render
the candidates into
assets/round-review.html, the reusable, data-driven review board. Write a per-roundround.jsonbeside it and openround-review.html?data=round-N.json; serve it withscripts/serve-review.pyon a fixed local port (headless browser andfetchneedhttp://, notfile://) and screenshot it. ALL of a round's assets are managed in this one board — whether they are candidates to pick one from OR a set of distinct deliverables (e.g. a logo plus a GitHub banner, a landing hero, and a social/X card). Never hand the user separate/standalone HTML pages or per-asset URLs to preview; one round = oneround.jsonin the one board, where the user reviews and decides keep / iterate. Do not hand-roll a one-off page — every round uses this template so review stays consistent and comparable across rounds, with round goal, prev-round nav, and next-round direction built in. The board ships over three channels — pick by audience: local (serve-review.py, default for a solo reviewer — decisions save straight to disk); share server (default for multi-person review: fillassets/share-review.htmlwith the round's items — inline SVGs / data-URI images, fully self-contained — andPOSTit to the org share server, e.g.https://brand-studio.sma1lboy.me/share→ one public link, no login, every reviewer submits by name, and the agent reads the merged verdicts back directly; seeshare-server/README.md); Artifact (fallback when no share server is deployed — note artifacts publish private-by-default and need the user to enable link sharing manually). Same rule on every channel: one round = one board. - Verdicts — keep / maybe / drop — the user marks each candidate
keep / maybe / drop with an optional per-icon note, plus a next-round
direction, right in the template. Three ways to get the decisions to the
agent: (A) Save to disk — the template POSTs to
serve-review.py, which writesdecisions-round-N.jsonnext to the round data; Read it directly, no paste. (B) Export / paste — the user copies/downloads the JSON and pastes it. (For only a handful of candidates an AskUserQuestion multi-select is an acceptable shortcut.) (C) Share server — each reviewer submits by name from the board (POST <share-url>/verdict, resubmit overwrites); the agent readsGET <share-url>/verdictsdirectly — no human shuttle. Poll it when the user says the round has been reviewed. (D) Artifact shared storage (fallback) — reviewers submit viawindow.storage.set("<series>:r<N>:verdict:<name>", json, true)(live on published artifacts only, silently inert on drafts — probe on load and surface status). Nothing outside an artifact can read its storage, so the board MUST merge submissions client-side and offer one copy-JSON button a human pastes back. Every remote board keeps a per-person export fallback. Never infer verdicts. - Settle — for each keep, run
repo settleinto the matching portfolio (release/promo) with itsmodalityand domain.maybe/dropand unpicked candidates stay in scratch (or are deleted only on request). - Next round — offer to iterate: refine a pick, try a new direction/style, or pull in a new reference element. Loop until the user is done.
round.json schema: { round, title, goal, prev: [{round, title, data}], items: [{id, concept, img}] }. Each round is one round.json; the same
assets/round-review.html renders it. Keep a rounds.json index next to it
([{round, title, data}], one entry per round) so every round page cross-links
to all the others (current highlighted); without it the nav falls back to
prev. The template persists verdicts/notes in the browser and exports
{ round, decisions: [{id, verdict, note}], next }.
Keep every asset-producing round on this loop — candidates and deliverable sets — so the experience stays smooth, everything for a round is reviewed in one place, and nothing accepted is lost or left un-settled.
Style Production
When a design skill, Claude, Codex, or a human produces style, freeze the
machine-readable tokens as YAML frontmatter in theme.md before render. Style production is not
a studio command; use the most relevant local design skill or a human-provided
brief and references, then write or update a proposal file under the
metadata-declared marketing root.
Selection order for design producers:
- If the user names a local design skill, prefer it.
- Otherwise prefer an already-installed local frontend/visual design skill.
- If none exists, stop and ask the user to install/specify one or provide a reviewed brief and references.
Do not download, clone, or install a remote design skill as an implicit fallback. Proposal review flow:
- Write the proposal under the metadata-declared marketing root.
- Validate it with the bundled helper.
- Dry-render against a representative campaign.
- Ask the user to review the proposal and candidates.
- Only after review, update the official
theme.mdpath.
Production Lifecycle
Portfolio Domains
Keep release and promo visual memory separate. Both domains share only the
brand base in theme.md: palette, typography direction, voice, avoid rules,
and brand tokens. Specific composition patterns do not cross domains by
default.
Use this repo state layout unless metadata overrides it:
assets/marketing/
theme.md
campaigns/
release/
promo/
accepted.yaml
asset-state.yaml
portfolios/
release/
accepted.yaml
asset-state.yaml
patterns.md
promo/
accepted.yaml
asset-state.yaml
patterns.md
For release images, read theme.md, portfolios/release/accepted.yaml,
portfolios/release/asset-state.yaml, portfolios/release/patterns.md, and
the changelog or release copy. Do not read promo accepted assets by default.
For promo images, read theme.md, portfolios/promo/accepted.yaml,
portfolios/promo/asset-state.yaml, campaign brief, and references. Do not
read release accepted assets by default.
Accepted entries must carry domain, source_kind, asset_type, and
style_family. Use domain: release, source_kind: changelog, and
style_family: log-full-editorial for changelog/release-note posters. Use
domain: promo, source_kind: campaign-brief, and
style_family: screen-first-field-scene for normal campaign-first promotion.
The root accepted.yaml may remain as a compatibility aggregate, but future
planning should use the matching portfolio accepted file as the style pool.
Classify product repo production requests into gen-repo, settle-repo,
update-repo, delete-repo, or retire-repo before running helpers.
Before live generation, confirm API usage, possible cost, and the exact
external producer skill. If the user directly asks for live generation, treat
that as action approval, but still state the selected producer and that the call
may bill before invoking it. If the request is ambiguous, stop and ask for
confirmation. The studio treats third-party production skills as local producer
capabilities, not vendored dependencies. It does not wrap GPT, OpenAI, or any
image API. Bind producer skills in metadata under skills, then use only
locally installed or explicitly configured producers. Do not auto-download,
auto-install, or silently switch production producers. Credentials belong to
the selected producer's environment; never print, commit, or copy them into
configuration files. producer.model is an optional hint; the selected
producer decides whether it supports it.
Validate producer constraints before handoff. For gpt-image, deliverable
dimensions must be positive, use supported image formats, keep reasonable aspect
ratios, and align width/height to 16px. Adjust campaign sizes during planning
or dry-run rather than after spending producer calls.
For gen-repo, use this loop:
- Run the read-only state preflight and read current org, repo, directory, matching portfolio accepted corpus, reference, and related-repo state declared by metadata.
- Write or update a production plan under
state.plans. - Validate the plan inputs and run a dry render.
- Ask the user to approve live generation cost and the external producer.
- Pass the dry-run context to the selected producer and place candidates in
artifacts.scratch. - Show candidate paths, producer output metadata, run lock, and review notes.
Stop here unless the user accepts a candidate. Scratch candidates are not durable assets.
For settle-repo, continue only after user acceptance:
- In a single-candidate context, language such as "this is good", "no changes", or "use this one" can count as accepting that candidate.
- In a multi-candidate context, ask for exact asset ids or file paths.
- Use the internal accept helper to copy accepted files into
artifacts.approved, write an approved manifest from real files, update the root aggregatestate.accepted, and update the matching portfolio accepted file. - Use the updated portfolio accepted state as input for the next production cycle.
The dry-run manifest.json describes SVG placeholders and prompt context. It is
not the approved manifest for real producer PNG/JPEG/WebP files. The approved
manifest is generated only after user acceptance, from the real file's mime
type, dimensions, and checksum.
The approved asset directory should come from metadata. It may be a public
package directory, a separate asset git repository, or a submodule. The skill
never edits .gitattributes and never runs git add, commit, or push.
Internal preflight helper:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" \
--metadata path/to/marketing.studio.yaml repo state
Use this output to ground the production plan. Do not treat it as an asset intake or promotion command.
Internal producer handoff helper, after dry-run and before any paid/live producer call:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" \
--metadata marketing.studio.yaml repo handoff \
--campaign launch \
--asset-id web-banner
Use this to read producer-context.json, validate the selected asset's prompt,
size, format, producer skill, and target scratch path, and print
not_generated_yet=true. This helper never calls the producer.
Internal producer prompt helper, after dry-run and before any paid/live producer call:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" \
--metadata marketing.studio.yaml repo prompt \
--campaign launch \
--asset-id web-banner
Use this to render the final text prompt from producer-context.json, the
selected asset, resolved style, and metadata weightProfiles. This helper
prints prompt text only; it never calls the producer or backend.
Internal acceptance helper, after the user has accepted a concrete candidate:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" \
--metadata marketing.studio.yaml repo settle \
--campaign launch \
--asset-id web-banner \
--domain promo \
--source-kind campaign-brief \
--asset-type hero \
--style-family screen-first-field-scene \
--file .studio/marketing/out/launch/web-banner.png \
--checksum-sha256 <sha256> \
--notes "Accepted by user review." \
--tags launch,web-banner \
--plan assets/marketing/plans/launch.plan.yaml \
--update-asset-state
Use this only after acceptance. In a single-candidate context, user language such
as "this is good", "no changes", or "use this one" can count as accepting that
candidate. In a multi-candidate context, ask for exact asset ids or file paths.
The helper copies from scratch to approved assets, writes an approved manifest,
validates mime, dimensions, and checksum, updates root accepted.yaml and the
matching portfolio accepted.yaml, optionally updates the matching portfolio
asset-state.yaml and plan status, and never runs git commands.
It prints the report fields needed for the final response, including
accepted=true, corpus=approved, domain, source_kind, asset_type,
style_family, mime_type, size, and checksum_sha256.
Internal report helper, for a real candidate before or after acceptance:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" \
--metadata marketing.studio.yaml repo report \
--file .studio/marketing/out/launch/web-banner.png
Use this before final reporting when no accept command just ran. If
corpus=scratch and accepted=false, say the file has not entered the durable
accepted corpus.
Repo Update and Delete
For update-repo theme, write a proposal under the metadata-declared marketing
root, validate it, dry-render a representative campaign, and ask before
promoting it to the official theme.path. Do not silently overwrite an
existing theme.
For update-repo asset, never overwrite an approved asset in place. Generate a
new scratch candidate, ask the user to review it, then use settle-repo. The
settled state should make replacement intent explicit through notes, tags, or
plan status so future production can tell the new asset supersedes the old one.
For delete-repo candidate, physical deletion is allowed only for files under
metadata artifacts.scratch after resolving the path under --project-root.
Do not remove approved assets, state files, org standards, or source references
through this path.
For durable repo assets under artifacts.approved, prefer retire-repo asset:
mark the asset inactive or superseded in repo-owned state and keep the file
unless the user explicitly asks for a purge. Before a purge, search the product
repo for references to the approved file path. If references exist, report them
and ask again before deleting. Brand Studio has no dedicated purge helper yet;
until one exists, do not perform durable deletion as an improvised state edit
unless the user has explicitly approved the exact file and state changes.
Final reporting must name the stage that actually completed:
- After
gen-repo, report scratch candidate paths and say they are not settled. - After
settle-repo, report approved paths, dimensions, checksum, and state. - After
update-repo, report whether the update is still a proposal/candidate or has been settled. - After
delete-repo, report only scratch files removed. - After
retire-repo, report state changes and whether any physical files remain.
Legacy Migration
Older repos may have brand.lock.yaml, brand.meta.yaml, elements.yaml, or
loose references/ without a current theme.md and portfolio accepted state.
Migrate without treating every existing file as accepted:
- Read old lock/meta/reference files as source context.
- Distill stable visual decisions into
theme.mdfrontmatter and notes. - Move reusable reference files under metadata
theme.references. - Write curated facts or patterns into
asset-state.yaml. - Write root
accepted.yamlplus the matching portfolioaccepted.yamlonly for files the user explicitly accepts or that the repo already documents as approved deliverables. - Run validate and dry-run before replacing the official theme.
Release-version helpers:
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" \
--metadata path/to/marketing.studio.yaml \
repo release copy --write --releases 4
Use repo release copy when the task is to review or revise release wording
before image generation. It reads release entries from standard CHANGELOG.md
locations and writes a structured copy.yaml under the scratch directory. It
reads one latest release by default; use --releases 4 for recent-release
notes. Treat that file as the text-asset handoff into campaign and image
production.
copy.yaml exposes releases[] as the canonical editable text asset, with
changes[] as direct changelog text. Single-product release entries do not
repeat package; only multi-package release copy needs release-level package
labels. It does not write a separate key_points block.
python3 "$SKILL_ROOT/scripts/studio.py" --project-root "$PWD" \
--metadata path/to/marketing.studio.yaml \
repo gen release --releases 4
Use repo gen release when the task is to produce release-version marketing
after a version step. Release producer prompts must make the release notes page the
main subject: header, metadata chips, version heading, and changelog rows. Do
not frame changelog content as a small side panel on a generic product hero. If
copy.yaml already exists, it uses that revised text asset; otherwise it writes
the same copy.yaml first. It then writes a normal campaign file under the
metadata-declared campaigns directory, runs the existing dry-run render flow, and
exports producer-context.json for the metadata-selected image producer skill.
The producer context must point at the release portfolio and must not include
promo accepted assets by default.
Use repo release campaign --write only when you need to inspect or edit the
generated campaign before producer handoff.
Release Image Production Checklist
repo gen release is a dry-run and producer handoff. It writes copy.yaml, a
campaign file, SVG placeholders, a dry-run manifest.json, and
producer-context.json, but it does not create a real image. Treat the output
as ready for producer handoff only after checking source_count,
changelog_count, and the actual copy.yaml releases[] count. In a
single-product flow, --releases 4 should yield exactly four release sections.
If counts are abnormal or versions repeat, rerun with an explicit product
changelog, for example --changelog packages/kobe/CHANGELOG.md, and do not pass
the polluted producer-context.json to the image producer.
Real release images must be PNG, JPEG, or WebP files emitted by the configured
producer. SVG placeholders are not final images. For producer handoff, run
repo handoff for the target asset, generate one primary
candidate such as release-card unless the user asked for the full set, state
the helper's selected producer skill and possible billing before calling it, and
write the result to the helper's target path under artifacts.scratch. Use the
metadata-selected image skill, such as gpt-image.
Settle accepted release assets into the release portfolio, not the promo
portfolio. Release is a version-fact asset pipeline:
CHANGELOG.md -> copy.yaml -> release campaign -> producer context -> real image -> release portfolio settle. It is not a normal campaign-first promotion.
Final responses after release image work must report the real image path,
dimensions, checksum, corpus, and whether it has been accepted. Use the latest
repo settle or repo report helper output as the source of truth. If the
PNG/JPEG/WebP is still in scratch, say that it has not entered the durable
accepted corpus.
Verification
After code or workflow changes:
uv run ruff check .
uv run pytest
# Full produce -> settle loop on a throwaway copy of the sandbox, with a stub
# backend (free, deterministic): validate -> render -> produce -> settle -> report.
uv run python tests/sandbox/drive.py
cd tests/sandbox
uv run python ../../skills/brand-studio/scripts/studio.py --project-root "$PWD" --metadata marketing.studio.yaml repo validate
uv run python ../../skills/brand-studio/scripts/studio.py --project-root "$PWD" --metadata marketing.studio.yaml repo render --dry-run
Check that no API key, authorization header, machine-specific path, or inline-encoded image payload is stored in tracked files.