@cliangdev/conductor 0.25.0 → 0.26.0

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor:creative
3
- description: One entry point for Conductor creative work — checks the workspace brand, offers to set it up if it's never been configured, asks what to make (static ad, short video, an existing clip), gathers the few inputs needed, then hands off to the conductor-creative skill through render, preview and attach.
3
+ description: One entry point for Conductor creative work — checks the workspace brand, offers to set it up if it's never been configured, asks what to make (static ad, short video, an existing clip), gathers the few inputs needed, then hands off to the conductor-creative skill through draft, preview, approval, commit and attach.
4
4
  allowed-tools: mcp__conductor__*, AskUserQuestion, Skill, WebFetch, Read, Glob, Grep
5
5
  ---
6
6
 
@@ -18,39 +18,10 @@ follow it.
18
18
 
19
19
  ## Step 1 — Check the brand
20
20
 
21
- Call `get_brand_kit` (pass `kitId` only if the person already named a specific brand).
22
-
23
- **If `configured` is `false`**, this workspace has no brand set up yet — offer to fix that before
24
- making anything:
25
-
26
- ```json
27
- {
28
- "questions": [{
29
- "question": "This workspace's Brand Kit hasn't been set up yet (default colours, no font, no logo). Set it up now?",
30
- "header": "Brand Kit",
31
- "options": [
32
- {"label": "From a website", "description": "Fetch a URL and infer colours, voice and name"},
33
- {"label": "From a brand doc", "description": "Read a local file (guidelines, style doc)"},
34
- {"label": "Answer a few questions", "description": "Name, colours, font, CTA line, approved lines"},
35
- {"label": "Skip for now", "description": "Proceed with the default kit as-is"}
36
- ],
37
- "multiSelect": false
38
- }]
39
- }
40
- ```
41
-
42
- - **From a website**: ask for the URL, `WebFetch` it, infer a plausible accent colour, name, CTA
43
- claim and a couple of approved lines from the copy on the page. Show what you inferred and confirm
44
- before writing.
45
- - **From a brand doc**: ask for the file path, `Read` it, extract the same fields, confirm before
46
- writing.
47
- - **Answer a few questions**: ask for name, primary/accent colour (hex or a description you convert),
48
- font (if any), the CTA claim, and any lines that must run verbatim. Keep it to one AskUserQuestion
49
- batch, not a long interview.
50
- - Once confirmed, call `update_brand_kit` with whatever was gathered. If a logo file or URL is
51
- available, `upload_brand_image` for the `mark` slot at least. Verify with `get_brand_kit` —
52
- `configured` should now read `true`.
53
- - **Skip for now**: proceed; the skill below will render against the default kit as given.
21
+ Read `references/brand-kit-setup.md` in the `conductor-creative` skill
22
+ (`.claude/skills/conductor-creative/references/brand-kit-setup.md`, in this project or under `~/.claude`)
23
+ and follow it: check the Brand Kit, and offer to set it up if it has never been configured. Then
24
+ continue with Step 2.
54
25
 
55
26
  ## Step 2 — What to make
56
27
 
@@ -77,22 +48,43 @@ Keep this light — one or two follow-up questions, not a form:
77
48
 
78
49
  - **Static ad / short video from a photo**: the headline (or let the skill draft one from the
79
50
  Knowledge page), and a photo. Call `list_creative_media({kind: "IMAGE"})` first and offer what's
80
- already on hand before asking for a new upload.
51
+ already on hand before asking for a new photo. A new photo is a local file path (a URL: download it
52
+ to a local file first) — it is only uploaded after the person approves the preview.
81
53
  - **Video from a clip I have / Post an existing video as-is**: call
82
- `list_creative_media({kind: "VIDEO"})` first; if nothing fits, ask for a local path or URL.
54
+ `list_creative_media({kind: "VIDEO"})` first; if nothing fits, ask for a local path (a URL:
55
+ download it first). A clip used as a MOTION background is previewed from the local file too.
83
56
  - **Test a headline variant**: ask which existing Creative (id or display id like `12a`) and the new
84
57
  headline.
85
58
 
86
59
  ## Step 4 — Hand off to the skill
87
60
 
88
61
  Call `Skill(skill: "conductor-creative")` and follow it from wherever this command's gathering left
89
- off — angle, copy rules, layout, `create_creative`/`update_creative`, render, look at the result
90
- honestly, readiness. Don't re-derive what that skill already covers.
91
-
92
- ## Step 5 — Finish with a render → preview → attach, or a clear next step
93
-
94
- Once the skill's readiness check passes: full `render_creative`, `preview_creative` to show the
95
- result, and `attach_creative_to_post` if there's already a Post to attach it to (ask for the Post's
96
- id if not obvious from context). If there is no Post yet, say so plainly and name the next step —
97
- creating one via the `conductor-publisher` skill — rather than leaving the artwork stranded with no
98
- stated destination.
62
+ off — angle, copy rules, layout, then the draft-first flow: `preview_creative_draft`, look at the
63
+ image, ask for approval, and only then `commit_creative_draft`. Don't re-derive what that skill
64
+ already covers.
65
+
66
+ ## Step 5 — Preview, approve, commit, attach
67
+
68
+ Nothing is saved to Conductor until the person approves what they have seen:
69
+
70
+ 1. `preview_creative_draft` with what was gathered (local files go in `photoPath`/`clipPath`/
71
+ `audioPath`; an edit to an existing Creative passes `baseCreativeId`). It renders on this machine
72
+ and returns the image — no Creative is created and nothing is uploaded.
73
+ 2. Read `passed` and `checks` in the result first. The preview runs the full render's per-placement
74
+ checks (text spill, platform safe zones such as TikTok/Reels' overlay band, contrast, fonts,
75
+ images), so a design that would fail the real render shows up here. If `passed` is false, fix the
76
+ design (shorter copy, `layoutOverrides`, `typeOverrides`, a different layout) and preview again
77
+ **before** showing it or asking anything — never offer Approve & upload while a blocking check
78
+ fails, and tell the person what you changed. (Bigger or smaller text on one placement is
79
+ `typeOverrides`, e.g. `{"9x16": [96]}`, not a layout switch.)
80
+ 3. Once `passed` is true, look at the image against the skill's checklist, then show it and ask with
81
+ `AskUserQuestion`: Approve & upload / Request changes / Stop. On changes, preview again; on
82
+ Stop, leave it — the draft stays local under `.conductor/drafts/`.
83
+ 4. On approval, `commit_creative_draft` with the `draftDir`. It uploads the files, saves the
84
+ Creative and does the full render. (A headline variant is previewed with `baseCreativeId` plus the
85
+ new headline, but on approval is saved with `create_creative` and `variantOf` — never commit that
86
+ draft, which would update the original.)
87
+ 5. Once the skill's readiness check passes, `attach_creative_to_post` if there's already a Post to
88
+ attach it to (ask for the Post's id if not obvious from context). If there is no Post yet, say so
89
+ plainly and name the next step — creating one via the `conductor-publisher` skill — rather than
90
+ leaving the artwork stranded with no stated destination.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor-creative
3
- description: Creates on-brand marketing creative through the Conductor MCP tools — static ads and short videos alike, rendered locally on this machine and checked by eye before it's called done. Covers a photo-plus-headline ad at every enabled placement (e.g. TikTok/Reels 9:16, Instagram feed 4:5, Facebook 1:1); a short video made from a still photo or an existing clip (animated text, zoom/pan, background music, a TikTok/Reels/Shorts-ready cut); posting a finished video as-is; a story or carousel sequence; a lettered headline/hook variant for testing; and, when a workspace's Brand Kit has never been set up, setting one up first (colours, font, logo, copy rules) from a website, a brand doc, or a few direct answers. Use when asked to make, design, write or export a social ad, a promo image, a short-form video, an animated ad, or to configure/update a project's brand.
3
+ description: Creates on-brand marketing creative through the Conductor MCP tools — static ads and short videos alike, rendered locally on this machine as a draft, checked by eye and approved before anything is saved to Conductor. Covers a photo-plus-headline ad at every enabled placement (e.g. TikTok/Reels 9:16, Instagram feed 4:5, Facebook 1:1); a short video made from a still photo or an existing clip (animated text, zoom/pan, background music, a TikTok/Reels/Shorts-ready cut); posting a finished video as-is; a story or carousel sequence; a lettered headline/hook variant for testing; and, when a workspace's Brand Kit has never been set up, setting one up first (colours, font, logo, copy rules) from a website, a brand doc, or a few direct answers. Use when asked to make, design, write or export a social ad, a promo image, a short-form video, an animated ad, or to configure/update a project's brand.
4
4
  user-invocable: true
5
5
  allowed-tools: mcp__conductor__*, AskUserQuestion, Read, Glob, Grep, WebFetch
6
6
  ---
@@ -17,7 +17,13 @@ things that already exist for this workspace — a Brand Kit (data: tokens, copy
17
17
  lines) and a Knowledge page (prose: positioning, voice, facts). Read them rather than inventing.
18
18
 
19
19
  Rendering happens locally, through this MCP server, with a browser on this machine — there is no
20
- separate render service to wait on. `render_creative` typically finishes in well under a minute.
20
+ separate render service to wait on. A preview typically finishes in well under a minute.
21
+
22
+ **Draft first, save after approval.** Every Creative here goes draft, look, approve, commit:
23
+ `preview_creative_draft` renders the artwork on this machine and writes **nothing** to Conductor — no
24
+ Creative, no upload — then the person looks at it, and only on their approval does
25
+ `commit_creative_draft` upload the media, save the Creative and do the full render. Never commit a
26
+ draft the person has not seen and approved.
21
27
 
22
28
  ## How to make a Creative
23
29
 
@@ -50,8 +56,8 @@ Prefer an approved line from the Knowledge page verbatim. Otherwise write one th
50
56
  real and specific, not a generic claim.
51
57
 
52
58
  If the Brand Kit's `accentPhraseRequired` is true, mark exactly one phrase per headline with
53
- asterisks: `Every saved link, *finally usable*.` Never zero, never two — `create_creative` and
54
- `update_creative` refuse a headline that gets this wrong, naming the rule.
59
+ asterisks: `Every saved link, *finally usable*.` Never zero, never two — `preview_creative_draft`,
60
+ `create_creative` and `update_creative` refuse a headline that gets this wrong, naming the rule.
55
61
 
56
62
  Keep it short — checked at thumbnail size, not read up close.
57
63
 
@@ -69,10 +75,13 @@ uploading a new one — id, dimensions, checked/blocked state and provenance for
69
75
  the project's media library. A photo needs `checked: true` and not `blocked` before a Creative can
70
76
  go to `READY`.
71
77
 
72
- A new photo goes through `upload_creative_media` (local path or URL), which requires `source` (a
73
- URL or a short provenance note, e.g. "Generated with a named model on a given date") and `licence`
74
- (e.g. "Own work", "Unsplash Licence", "Generated, house use"). This closes a real legal exposure: an
75
- unattributed or unlicensed photo running as paid or organic media.
78
+ A photo that is not in the library yet is previewed straight from its local file (`photoPath` on
79
+ `preview_creative_draft`; a URL is downloaded to a local file first) and is only uploaded when the
80
+ draft is committed — pass `source` (a URL or a short provenance note, e.g. "Generated with a named
81
+ model on a given date") and `licence` (e.g. "Own work", "Unsplash Licence", "Generated, house use")
82
+ to `commit_creative_draft`, which records them on every file it uploads. This closes a real legal
83
+ exposure: an unattributed or unlicensed photo running as paid or organic media. A photo already in
84
+ the library is used by its id (`photoId`) with no upload at all.
76
85
 
77
86
  **Open the photo and look at it before using it.** A picture can break every copy rule with text,
78
87
  a logo or a claim burned into its pixels, and no automated check can see that — this is the first
@@ -83,48 +92,98 @@ Match the crop to the subject: a layout that lays copy over the photo (e.g. `ble
83
92
  the top third in collision with the headline — prefer a layout that keeps the photo and copy on
84
93
  separate panels (e.g. `stacked`) for those.
85
94
 
86
- ### 6. Create, then render
87
-
88
- `create_creative` with the Brand Kit (if not the default), layout, theme, photo, headline, body,
89
- caption and alt text. To test a headline variant of an existing concept, pass `variantOf` with that
90
- Creative's id instead of starting fresh — it inherits everything else and gets the next letter.
91
- Then call `get_creative` to verify what was actually stored.
92
-
93
- `render_creative` (previewOnly: true first, for a quick contact sheet) runs the render locally and
94
- returns the render id, state, frame URLs and any warnings. Then `preview_creative` to actually look
95
- at it — it returns the contact sheet as an image directly, not just a URL to trust.
95
+ ### 6. Preview the draft
96
+
97
+ `preview_creative_draft` with the Brand Kit (if not the default), layout, theme, photo (`photoPath`
98
+ for a local file, `photoId` for one already in the library), headline, body, caption and alt text.
99
+ It renders a contact sheet on this machine and returns it as an image, with the `draftDir` it wrote
100
+ to (`.conductor/drafts/<timestamp>-<slug>/`), the readiness gaps and any render warnings. **Nothing
101
+ has been saved to Conductor.** A copy-rule or accent-phrase violation comes back as an error naming
102
+ the rule and field, without rendering — fix the copy and preview again.
103
+
104
+ To change an existing Creative's copy, layout or photo, preview the edit first with `baseCreativeId`:
105
+ only the fields you pass change, and the commit then updates that Creative instead of creating one.
106
+
107
+ To test a headline variant of an existing concept, preview the new headline the same way
108
+ (`baseCreativeId` plus `headline`) so the person sees it on the artwork — but do **not** commit that
109
+ draft, since a commit would update the original. Once they approve, call `create_creative` with
110
+ `variantOf` and the headline instead: it inherits everything else and gets the next letter.
111
+
112
+ Pass `full: true` to render every placement at full size instead of the contact sheet (slower; the
113
+ image returned is the first placement).
114
+
115
+ **Read `checks` and `passed` before you show anything.** The contact sheet never runs the render's
116
+ safety checks, so the preview also runs them for every placement (and, for a video, its end card)
117
+ and returns them as `checks: [{placementKey, index?, rule, message, severity}]` plus `passed`. An
118
+ `error` check (`safeZone`, `spill`, `contrast`, `font`, `image`, ...) is exactly what would fail the
119
+ full render after the person approved: e.g. an App Store badge intruding into the band TikTok/Reels
120
+ reserve on 9:16. A `warning` (a soft, upscaled photo) does not block, but mention it.
121
+
122
+ When `passed` is false, **do not show the image or ask for approval yet.** Fix the design and
123
+ preview again, repeating until `passed` is true:
124
+
125
+ - the copy collides with a reserved zone: shorten the headline or drop the body;
126
+ - the photo takes too much of a placement: lower its height there with `layoutOverrides.band`
127
+ (e.g. `{"band": {"9x16": 1100}}`);
128
+ - the bottom row (CTA/badge) does not clear the safe zone: raise the clearance with
129
+ `layoutOverrides.padBottom`, or pick a layout that keeps it clear;
130
+ - the text is too large or too small for a placement: set it with `typeOverrides` (below).
131
+
132
+ Tell the person what you changed and why when you do show it. Never offer **Approve & upload**
133
+ while a blocking check fails.
134
+
135
+ **Bigger or smaller text on one placement: use `typeOverrides`, not a different layout.** It pins
136
+ the headline per placement as `{"<placementKey>": [fontSize, lineHeight?, letterSpacing?]}`, e.g.
137
+ `{"9x16": [96], "1x1": [88]}` for a bigger headline on 9:16 and 1:1. `fontSize` is in artboard px
138
+ (a 9:16 board is 1080 wide), `lineHeight` a multiplier (e.g. `1.0`) and `letterSpacing` in px
139
+ (usually negative); leave the last two out to take the engine's defaults for that size. A placement
140
+ you do not name stays auto-fitted. A pinned size is not shrunk to fit, so re-check `checks` after
141
+ setting one. It works on `preview_creative_draft` and `create_creative`/`update_creative`; changing
142
+ the headline or layout of a saved Creative clears pinned sizes unless you send them again.
96
143
 
97
144
  ### 7. Look at the result honestly
98
145
 
99
- **A successful render proves the frame is structurally correct, not that the ad is good.** Its
100
- in-page checks catch text spilling out of a safe zone, a font that failed to load, a photo that
146
+ **Passing checks prove the frame is structurally correct, not that the ad is good.** The checks
147
+ (`passed: true`) catch text spilling out of a safe zone, a font that failed to load, a photo that
101
148
  never decoded, or contrast below the accessible minimum — not whether the headline reads at a
102
149
  glance, whether the accent phrase falls on the words that carry the meaning, or whether the copy is
103
- sitting on the subject's face. Judge those yourself from the image `preview_creative` hands back. If
104
- the copy collides with the photo, switch layout (see step 5) and re-render rather than shrinking
105
- text until it technically fits.
150
+ sitting on the subject's face. Judge those yourself from the image `preview_creative_draft` hands
151
+ back. If the copy collides with the photo, switch layout (see step 5) and preview again rather than
152
+ shrinking text until it technically fits.
153
+
154
+ Only once `passed` is true, show the image to the person and ask with `AskUserQuestion` —
155
+ **Approve & upload**, **Request changes**, **Stop**. If `passed` is false you are not at this step
156
+ yet: fix it and re-preview first (step 6). On changes, preview again with the edits (another draft; nothing was saved). On
157
+ Stop, leave it: the draft stays on this machine under `.conductor/drafts/` and is cleaned up
158
+ automatically once it is committed or two weeks old. Only on **Approve & upload**, call
159
+ `commit_creative_draft({draftDir})` with the `draftDir` the approved preview returned.
106
160
 
107
161
  ### 8. Check readiness before calling it done
108
162
 
109
- `get_creative` includes a `readiness` block: caption, alt text, the photo's checked/blocked state
163
+ The preview already lists the readiness gaps, so fix what you can (caption, alt text, a checked
164
+ photo) before asking for approval. After the commit, `get_creative` includes a `readiness` block: caption, alt text, the photo's checked/blocked state
110
165
  and provenance, and the Creative's own state (not `DRAFT`) are blocking; an AI-generation disclosure
111
166
  is informational only. Do not call a Creative done while `ready` is false.
112
167
 
113
- ### 9. Render for real, then hand off
168
+ ### 9. Commit, then hand off
114
169
 
115
- Once the preview looks right, `render_creative` again with `previewOnly: false` (or omitted) for the
116
- full-size placement frames, then `attach_creative_to_post` with the Post's Work Item id — it maps
117
- each placement to the right destinations (9:16 to TikTok/Reels, 4:5 to Instagram feed, 1:1 to
118
- Facebook, story to story targets) for every destination that has not set its own custom media, and
119
- never touches one that has. Verify with `get_post_status`. From here, use the `conductor-publisher`
120
- skill for scheduling, review and publishing — this skill's job ends at attaching the artwork.
170
+ `commit_creative_draft` uploads the draft's local files, creates the Creative (or updates
171
+ `baseCreativeId`'s), and runs the full-size render in one call, then returns the Creative id and
172
+ display id, the frames and the readiness. It can be run once per draft; if its render fails the
173
+ Creative is already saved — call `render_creative`, don't commit again. Verify with `get_creative`.
174
+ Then `attach_creative_to_post` with the Post's Work Item id — it maps each placement to the right
175
+ destinations (9:16 to TikTok/Reels, 4:5 to Instagram feed, 1:1 to Facebook, story to story targets)
176
+ for every destination that has not set its own custom media, and never touches one that has. Verify
177
+ with `get_post_status`. From here, use the `conductor-publisher` skill for scheduling, review and
178
+ publishing — this skill's job ends at attaching the artwork.
121
179
 
122
180
  ## Making a sequence instead of a single ad
123
181
 
124
182
  For a platform surface that's tapped or swiped through, set `sequenceKind` to `story` (2 to 7
125
- beats, aim for 3 to 5) or `carousel` (2 to 10 cards, 5 to 7 performs best) on `create_creative` or
126
- `update_creative`, with a `sequence` array. Each beat inherits the Creative and overrides only its
127
- own `headline`, and optionally `body`, `photoId` or `cta`. Structure a story hook, mechanism,
183
+ beats, aim for 3 to 5) or `carousel` (2 to 10 cards, 5 to 7 performs best) on
184
+ `preview_creative_draft` (then `commit_creative_draft`), with a `sequence` array. Each beat inherits
185
+ the Creative and overrides only its own `headline`, and optionally `body`, `photoPath` (a local
186
+ photo, uploaded on commit) or `photoId`, or `cta`. Structure a story hook, mechanism,
128
187
  payoff — the first beat has to earn the next tap on its own, since that's where the drop-off is
129
188
  worst. A logo lockup and CTA claim land on the last beat only by default; `cta: true` on an earlier
130
189
  beat overrides that. A carousel renders every card at one shared aspect ratio (`carouselRatio`),
@@ -136,6 +195,9 @@ Not every video needs the brand layout above — a finished clip (a demo recordi
136
195
  something a person or another tool already produced) can go up as-is. That's a CLIP Creative: no
137
196
  photo, headline or layout, just the video.
138
197
 
198
+ A CLIP has no artwork to render, so it is the one flow without a draft preview: open the video
199
+ itself and check it with the person before uploading.
200
+
139
201
  1. Check `list_creative_media({kind: "VIDEO"})` for a clip already in the project before uploading a
140
202
  new one. A new one goes through `upload_creative_media` (local path or URL) — same provenance
141
203
  fields as a photo (`source`, `licence`, `aiGenerated`). It detects video vs. audio vs. photo
@@ -162,10 +224,11 @@ of a static frame — no separate video tool needed. Use it when a still doesn't
162
224
  destination rewards video over an image.
163
225
 
164
226
  1. Pick a photo (step 5) or, for a moving background, a video — check `list_creative_media({kind:
165
- "VIDEO"})` first, then `upload_creative_media` only if nothing already fits — and note its media
166
- id. For background music, `list_creative_media({kind: "AUDIO"})` the same way before uploading a
167
- new track.
168
- 2. `create_creative`/`update_creative` with `kind: "MOTION"` plus:
227
+ "VIDEO"})` first and use its media id if something fits; otherwise pass the local file as
228
+ `clipPath` (previewed from disk, uploaded on commit). For background music,
229
+ `list_creative_media({kind: "AUDIO"})` the same way, else `audioPath`.
230
+ 2. `preview_creative_draft` with `kind: "MOTION"` (to change a saved MOTION Creative, `baseCreativeId`
231
+ instead) plus:
169
232
  - `motion.preset` — how the copy enters: `fade-up` (default), `word-by-word`, `accent-pop`, or
170
233
  `none` (everything visible from the first frame).
171
234
  - `motion.durationSec` — 3 to 60, default 8.
@@ -177,13 +240,13 @@ destination rewards video over an image.
177
240
  - `audio` — `source: "clip"` (the background clip's own recorded sound — the default when there
178
241
  is one), `"track"` (a separate library track: `trackId` from `upload_creative_media`, plus
179
242
  `volume` and `fadeOutSec`), or `"none"`.
180
- 3. `render_creative` runs a local, ffmpeg-driven encode — roughly 20 seconds per placement for an
181
- 8-second video. No ffmpeg on this machine fails with a clear message naming how to get one rather
182
- than silently producing nothing.
183
- 4. `preview_creative` renders three key moments across the timeline into one sheet image (not the
184
- finished video). Judge it the same honest way as step 7 above — does the copy's entrance finish
185
- before the next beat, does the end card actually hold long enough to read — before spending a
186
- full render on it.
243
+ 3. The preview renders three key moments across the timeline into one sheet image (not the finished
244
+ video). Judge it the same honest way as step 7 above — does the copy's entrance finish before the
245
+ next beat, does the end card actually hold long enough to read — and get approval before
246
+ spending a full render on it.
247
+ 4. `commit_creative_draft` then uploads the media, saves the Creative and runs the local,
248
+ ffmpeg-driven encode — roughly 20 seconds per placement for an 8-second video. No ffmpeg on this
249
+ machine fails with a clear message naming how to get one rather than silently producing nothing.
187
250
  5. `attach_creative_to_post`, same as any Creative — one MP4 per placement.
188
251
 
189
252
  A library music track's `licence` has to be recorded on upload (same as a photo's provenance) — the
@@ -203,6 +266,9 @@ Automated checks are real but incomplete. These are the ones worth remembering t
203
266
  - **A Brand Kit with no copy rules, or no accent-phrase requirement, never fails a save on that
204
267
  account.** That is permissive by design, not a bug — but it means the model, not the platform, is
205
268
  the only check on brand voice for that workspace until someone configures rules.
269
+ - **A draft is not a saved Creative.** `preview_creative_draft` writes nothing to Conductor, so a
270
+ Creative or media id does not exist until `commit_creative_draft` runs — don't hand a draft's
271
+ folder to `attach_creative_to_post` or a Post.
206
272
  - **A stale write is refused, not silently overwritten.** `update_creative` requires the Creative's
207
273
  current `version`; a mismatch comes back as a 409 naming the conflict rather than clobbering a
208
274
  concurrent edit — reread with `get_creative` and reapply, don't retry blind.
@@ -0,0 +1,41 @@
1
+ # Brand Kit check and setup
2
+
3
+ Shared by `/conductor:creative` and the `conductor-creative` skill: check whether this workspace's
4
+ Brand Kit has been set up, and offer to set it up if not. The text below is the command's Step 1,
5
+ unchanged.
6
+
7
+ ## Step 1 — Check the brand
8
+
9
+ Call `get_brand_kit` (pass `kitId` only if the person already named a specific brand).
10
+
11
+ **If `configured` is `false`**, this workspace has no brand set up yet — offer to fix that before
12
+ making anything:
13
+
14
+ ```json
15
+ {
16
+ "questions": [{
17
+ "question": "This workspace's Brand Kit hasn't been set up yet (default colours, no font, no logo). Set it up now?",
18
+ "header": "Brand Kit",
19
+ "options": [
20
+ {"label": "From a website", "description": "Fetch a URL and infer colours, voice and name"},
21
+ {"label": "From a brand doc", "description": "Read a local file (guidelines, style doc)"},
22
+ {"label": "Answer a few questions", "description": "Name, colours, font, CTA line, approved lines"},
23
+ {"label": "Skip for now", "description": "Proceed with the default kit as-is"}
24
+ ],
25
+ "multiSelect": false
26
+ }]
27
+ }
28
+ ```
29
+
30
+ - **From a website**: ask for the URL, `WebFetch` it, infer a plausible accent colour, name, CTA
31
+ claim and a couple of approved lines from the copy on the page. Show what you inferred and confirm
32
+ before writing.
33
+ - **From a brand doc**: ask for the file path, `Read` it, extract the same fields, confirm before
34
+ writing.
35
+ - **Answer a few questions**: ask for name, primary/accent colour (hex or a description you convert),
36
+ font (if any), the CTA claim, and any lines that must run verbatim. Keep it to one AskUserQuestion
37
+ batch, not a long interview.
38
+ - Once confirmed, call `update_brand_kit` with whatever was gathered. If a logo file or URL is
39
+ available, `upload_brand_image` for the `mark` slot at least. Verify with `get_brand_kit` —
40
+ `configured` should now read `true`.
41
+ - **Skip for now**: proceed; the skill below will render against the default kit as given.
@@ -171,7 +171,7 @@
171
171
  },
172
172
  {
173
173
  "name": "conductor:creative",
174
- "description": "One entry point for creative work \u2014 checks the workspace brand (offers to set it up if unconfigured), asks what to make (static ad, short video, an existing clip), then hands off to the conductor-creative skill through render, preview and attach."
174
+ "description": "One entry point for creative work \u2014 checks the workspace brand (offers to set it up if unconfigured), asks what to make (static ad, short video, an existing clip), then hands off to the conductor-creative skill through draft, preview, approval, commit and attach."
175
175
  }
176
176
  ],
177
177
  "agents": [
@@ -195,7 +195,7 @@
195
195
  },
196
196
  {
197
197
  "name": "conductor-creative",
198
- "description": "Makes a Creative end to end, static or short video: sets up the Brand Kit if unconfigured, reads it and its Knowledge page, picks media and an angle, creates the Creative, renders and looks at the result, fixes what's wrong, then hands it to conductor-publisher."
198
+ "description": "Makes a Creative end to end, static or short video: sets up the Brand Kit if unconfigured, reads it and its Knowledge page, picks media and an angle, previews a draft locally and shows it for approval before anything is saved, commits it, fixes what's wrong, then hands it to conductor-publisher."
199
199
  }
200
200
  ]
201
201
  },
@@ -781,7 +781,7 @@
781
781
  },
782
782
  {
783
783
  "name": "upload_creative_photo",
784
- "description": "Deprecated alias for upload_creative_media — same behavior, including for video/audio.",
784
+ "description": "Deprecated alias for upload_creative_media \u2014 same behavior, including for video/audio.",
785
785
  "category": "marketing"
786
786
  },
787
787
  {
@@ -800,6 +800,19 @@
800
800
  "creativeId"
801
801
  ]
802
802
  },
803
+ {
804
+ "name": "preview_creative_draft",
805
+ "description": "Look at a Creative before anything is saved: renders a draft locally from the create_creative fields plus local files (photoPath, clipPath, audioPath) and returns the contact sheet as an image. Never writes to Conductor; commit only after the person approves.",
806
+ "category": "marketing"
807
+ },
808
+ {
809
+ "name": "commit_creative_draft",
810
+ "description": "Save an approved draft: uploads its local files, creates (or updates) the Creative, and renders the real frames. The first write to Conductor; a draft commits once. Call get_creative to verify.",
811
+ "category": "marketing",
812
+ "requiredParams": [
813
+ "draftDir"
814
+ ]
815
+ },
803
816
  {
804
817
  "name": "attach_creative_to_post",
805
818
  "description": "Attach a Creative's rendered frames to a Post as its media, placement to platform, for every destination without its own custom media. Call get_post_status (postId) to verify.",
@@ -121,12 +121,23 @@ function sameRgb(a, b) {
121
121
  export async function runAssertions(board, placement, brand, { outputScale = 2 } = {}) {
122
122
  const errors = [];
123
123
  const warnings = [];
124
+ // The same findings with a stable `rule` id each, for callers (the draft preview) that report them
125
+ // structured. `errors` / `warnings` stay plain message arrays, exactly as before.
126
+ const checks = [];
127
+ const fail = (rule, message) => {
128
+ errors.push(message);
129
+ checks.push({ rule, severity: 'error', message });
130
+ };
131
+ const warn = (rule, message) => {
132
+ warnings.push(message);
133
+ checks.push({ rule, severity: 'warning', message });
134
+ };
124
135
  const br = rectOf(board);
125
136
 
126
137
  const nodes = [...board.querySelectorAll(CHECKED_SELECTOR)].map((n) => ({ name: n.className, rect: rectOf(n) }));
127
138
 
128
139
  const spill = spillDetect(br, nodes);
129
- if (spill.length) errors.push(`text spill: ${spill.join('; ')}`);
140
+ if (spill.length) fail('spill', `text spill: ${spill.join('; ')}`);
130
141
 
131
142
  const safe = {
132
143
  top: Number(board.dataset.safeTop) || 0,
@@ -134,19 +145,19 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
134
145
  right: Number(board.dataset.safeRight) || 0,
135
146
  };
136
147
  const intrusion = safeZoneIntrusion(br, nodes, safe);
137
- if (intrusion.length) errors.push(`safe-zone intrusion: ${intrusion.join('; ')}`);
148
+ if (intrusion.length) fail('safeZone', `safe-zone intrusion: ${intrusion.join('; ')}`);
138
149
 
139
150
  if (brand && brand.fontFamily) {
140
151
  const headlineEl = board.querySelector('.cc-headline');
141
152
  const size = Math.round(parseFloat(getComputedStyle(headlineEl).fontSize)) || 16;
142
153
  if (!document.fonts.check(`800 ${size}px "${brand.fontFamily}"`)) {
143
- errors.push(`font "${brand.fontFamily}" did not load (would export in a fallback face)`);
154
+ fail('font', `font "${brand.fontFamily}" did not load (would export in a fallback face)`);
144
155
  }
145
156
  }
146
157
 
147
158
  const imgs = [...board.querySelectorAll('img')];
148
159
  const broken = imgs.filter((i) => !i.complete || i.naturalWidth === 0).map((i) => i.getAttribute('src'));
149
- if (broken.length) errors.push(`image(s) failed to load: ${broken.join(', ')}`);
160
+ if (broken.length) fail('image', `image(s) failed to load: ${broken.join(', ')}`);
150
161
 
151
162
  // A bleed/card layout paints its photo as a CSS background, invisible to
152
163
  // the <img> check above — probe it directly, the same way a typo'd path
@@ -161,11 +172,11 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
161
172
  img.src = bgUrlMatch[1];
162
173
  });
163
174
  if (!probe.ok) {
164
- errors.push(`background photo failed to load: ${bgUrlMatch[1]}`);
175
+ fail('image', `background photo failed to load: ${bgUrlMatch[1]}`);
165
176
  } else if (placement && probe.w < placement.w * outputScale && probe.h < placement.h * outputScale) {
166
177
  // Soft photography check: a source smaller than the output frame (2x for images, 1x for video) is
167
178
  // upscaled, so the photo goes soft while the text stays crisp.
168
- warnings.push(`photo is ${probe.w}x${probe.h}, upscaled to fit ${placement.w * outputScale}x${placement.h * outputScale}`);
179
+ warn('photoResolution', `photo is ${probe.w}x${probe.h}, upscaled to fit ${placement.w * outputScale}x${placement.h * outputScale}`);
169
180
  }
170
181
  }
171
182
 
@@ -176,9 +187,9 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
176
187
  const expectedRaw = getComputedStyle(board).getPropertyValue('--cc-accent').trim();
177
188
  const expectedRgb = expectedRaw ? resolveCssColor(expectedRaw) : null;
178
189
  if (!emRgb) {
179
- errors.push('accent colour did not resolve on the headline');
190
+ fail('accent', 'accent colour did not resolve on the headline');
180
191
  } else if (expectedRgb && !sameRgb(emRgb, expectedRgb)) {
181
- errors.push(`accent colour unresolved: headline em is ${getComputedStyle(em).color}, brand accent resolves to ${expectedRaw}`);
192
+ fail('accent', `accent colour unresolved: headline em is ${getComputedStyle(em).color}, brand accent resolves to ${expectedRaw}`);
182
193
  }
183
194
  }
184
195
 
@@ -207,15 +218,15 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
207
218
  if (fgRgb && bgRgb) contrast = contrastRatio(fgRgb, bgRgb);
208
219
  }
209
220
  if (contrast !== null && contrast < 3) {
210
- errors.push(`headline contrast is ${contrast}:1 against its background, below the 3:1 floor for large text`);
221
+ fail('contrast', `headline contrast is ${contrast}:1 against its background, below the 3:1 floor for large text`);
211
222
  }
212
223
 
213
224
  const boxRect = board.getBoundingClientRect();
214
225
  const width = Math.round(boxRect.width);
215
226
  const height = Math.round(boxRect.height);
216
227
  if (placement && (width !== placement.w || height !== placement.h)) {
217
- errors.push(`artboard is ${width}x${height}, expected ${placement.w}x${placement.h}`);
228
+ fail('size', `artboard is ${width}x${height}, expected ${placement.w}x${placement.h}`);
218
229
  }
219
230
 
220
- return { errors, warnings, contrast, width, height };
231
+ return { errors, warnings, checks, contrast, width, height };
221
232
  }
@@ -15,7 +15,7 @@
15
15
  * }
16
16
  *
17
17
  * Result contract:
18
- * window.__RENDER_RESULT = { ok, errors?, warnings?, width, height } | { ok: false, error }
18
+ * window.__RENDER_RESULT = { ok, errors?, warnings?, checks?, width, height } | { ok: false, error }
19
19
  * window.__ready = true // set exactly once, success or failure, when the
20
20
  * // job's page.waitForFunction should stop waiting
21
21
  *
@@ -108,8 +108,8 @@ async function main() {
108
108
  return;
109
109
  }
110
110
 
111
- const { errors, warnings, width, height } = await runAssertions(board, placement, spec.brand || {});
112
- finish({ ok: errors.length === 0, errors, warnings, width, height });
111
+ const { errors, warnings, checks, width, height } = await runAssertions(board, placement, spec.brand || {});
112
+ finish({ ok: errors.length === 0, errors, warnings, checks, width, height });
113
113
  } catch (err) {
114
114
  finish({ ok: false, error: String((err && err.message) || err) });
115
115
  }