@cliangdev/conductor 0.26.0 → 0.27.1

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 (40) hide show
  1. package/assets/claude/agents/conductor-coder.md +25 -0
  2. package/assets/claude/agents/conductor-content-art-director.md +68 -0
  3. package/assets/claude/agents/conductor-content-creative-director.md +56 -0
  4. package/assets/claude/agents/conductor-content-ideator.md +49 -0
  5. package/assets/claude/agents/conductor-content-scriptwriter.md +42 -0
  6. package/assets/claude/agents/conductor-content-strategist.md +47 -0
  7. package/assets/claude/commands/conductor/content.md +64 -0
  8. package/assets/claude/commands/conductor/creative.md +8 -0
  9. package/assets/claude/commands/conductor/implement.md +1 -1
  10. package/assets/claude/skills/conductor-content-studio/SKILL.md +323 -0
  11. package/assets/claude/skills/conductor-content-studio/references/capabilities.md +61 -0
  12. package/assets/claude/skills/conductor-content-studio/references/compliance-checklist.md +120 -0
  13. package/assets/claude/skills/conductor-content-studio/references/idea-template.md +54 -0
  14. package/assets/claude/skills/conductor-content-studio/references/platform-specs.md +59 -0
  15. package/assets/claude/skills/conductor-content-studio/references/publish-pack-template.md +121 -0
  16. package/assets/claude/skills/conductor-content-studio/references/rubric.md +99 -0
  17. package/assets/claude/skills/conductor-content-studio/references/script-template.md +53 -0
  18. package/assets/claude/skills/conductor-content-studio/references/shotlist-template.md +44 -0
  19. package/assets/claude/skills/conductor-creative/SKILL.md +4 -0
  20. package/assets/claude/skills/conductor-publisher/SKILL.md +26 -2
  21. package/assets/cli-manifest.json +32 -0
  22. package/dist/creative/assertions.js +57 -7
  23. package/dist/creative/font.js +29 -0
  24. package/dist/creative/frame.css +2 -0
  25. package/dist/creative/frame.js +7 -4
  26. package/dist/creative/layouts/bleed/layout.css +14 -7
  27. package/dist/creative/motion.js +61 -19
  28. package/dist/creative/render.js +21 -2
  29. package/dist/creative/sheet.js +7 -2
  30. package/dist/creative/styles.css +16 -7
  31. package/dist/lib/plugin-assets.d.ts.map +1 -1
  32. package/dist/lib/plugin-assets.js +16 -0
  33. package/dist/lib/plugin-assets.js.map +1 -1
  34. package/dist/mcp/tools/creative-drafts.d.ts.map +1 -1
  35. package/dist/mcp/tools/creative-drafts.js +5 -1
  36. package/dist/mcp/tools/creative-drafts.js.map +1 -1
  37. package/dist/mcp/tools/issues.d.ts.map +1 -1
  38. package/dist/mcp/tools/issues.js +6 -1
  39. package/dist/mcp/tools/issues.js.map +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ # Publish pack
2
+
3
+ Two files in the run folder, written by the orchestrator in stage S5b from the scriptwriter's
4
+ `## Publish copy` section in `script.md`, the art director's `## Publish assets` section in
5
+ `direction.md`, and the compliance results (`compliance-checklist.md`). The pack is what the
6
+ publisher uses later to pre-fill the Post, so it holds only what the Post needs. A run never
7
+ creates a Post.
8
+
9
+ Short-form craft this encodes: the search keyword sits in the caption's opening words, on-screen
10
+ text and speech (TikTok search reads all three); a hook swap is the standard creative iteration;
11
+ hook rate is 3-second plays over impressions and hold rate is completions over 3-second plays;
12
+ posts work with the sound off.
13
+
14
+ ## Hook types
15
+
16
+ Tag every hook, in ideas and in the pack, with one of: `question`, `bold-claim`,
17
+ `pattern-interrupt`, `relatable-pain`, `how-to`, `number`, `curiosity-gap`, `story`,
18
+ `social-proof`. Use another short kebab-case word only when none fits.
19
+
20
+ ## publish-pack.md (for people)
21
+
22
+ One section per platform the brief asks for. Omit a platform it does not.
23
+
24
+ ```
25
+ # Publish pack: <title>
26
+
27
+ Run: <runId> Creative: <display id, or "not yet committed"> Tags: hook <type>, angle <angle>, format <format>
28
+
29
+ ## <Platform> (<feed | reel | story>, as the destination's formats allow)
30
+ - **Caption:** <final text; the search keyword is in the first line, within the first ~125
31
+ characters; within the platform's limit>
32
+ - **Hashtags:** <3 to 5>
33
+ - **First comment (post manually):** <text, or "none">. Conductor cannot post comments.
34
+ - **Cover frame:** <video: timestamp in seconds and what is on it; carousel: card 1; still: n/a>
35
+ - **Alt text:** <one line per image or card, in order>
36
+ - **On-screen text placement:** <where each line sits, inside the safe zone>
37
+ - **Sound:** <source and licence: original audio | Commercial Music Library track "<name>" |
38
+ library track id and its licence | clip's own sound | none>
39
+ - **Posting window:** <best hour and weekday from get_marketing_insights byHour/byWeekday for this
40
+ platform, with the window it came from, or "no data yet">
41
+ - **Options:** <the option keys and values this platform gets, from publish-pack.json>
42
+
43
+ ## Alternate hooks (for variantOf hook experiments)
44
+ 1. <hook line> (<hookType>)
45
+ 2. <hook line> (<hookType>)
46
+ 3. <optional>
47
+
48
+ ## Compliance
49
+ <the table from compliance-checklist.md: check, answer, what was done, option set>
50
+
51
+ ## Third-party assets
52
+ | Asset | Whose | Permission or licence |
53
+ |---|---|---|
54
+ | <music, footage, logo, meme, likeness, trademark, parody> | ... | ... |
55
+ (Write "none" if the post uses only the brand's own and generated-by-Conductor material.)
56
+ ```
57
+
58
+ ## publish-pack.json (for the publisher)
59
+
60
+ ```json
61
+ {
62
+ "creativeId": null,
63
+ "platforms": [
64
+ {
65
+ "platform": "tiktok",
66
+ "format": "feed",
67
+ "captionOverride": "<final caption>",
68
+ "options": { "brandOrganicToggle": true, "isAigc": false, "videoCoverTimestampMs": 1500 }
69
+ },
70
+ {
71
+ "platform": "instagram",
72
+ "format": "feed",
73
+ "captionOverride": "<final caption>",
74
+ "options": { "altText": "<alt text for a single feed image>" }
75
+ }
76
+ ],
77
+ "altText": ["<card 1 alt text>", "<card 2 alt text>"],
78
+ "alternateHooks": [{ "hook": "<line>", "hookType": "question" }],
79
+ "tags": { "hookType": "bold-claim", "angle": "utility-education", "format": "carousel" }
80
+ }
81
+ ```
82
+
83
+ Rules:
84
+
85
+ - `creativeId` is null until the person approves the commit (S6, On Approve), then it is the
86
+ Creative's id. A `FILM` run leaves it null: the publisher is pointed at the pack by the person.
87
+ - `platforms[]` items are shaped to drop straight into `create_post` targets or
88
+ `set_publish_targets` targets: `platform`, `format` (`feed`, `reel` or `story`, and only one the destination's `formats` offers: at the time
89
+ of writing TikTok and YouTube are feed only, Facebook and Instagram offer all three),
90
+ `captionOverride`, and `options`. Name `options` `publishOptions` for `set_publish_targets`. They
91
+ carry no account: the publisher resolves the account (`account` or `connectionId`) from
92
+ `list_publish_targets`. `captionOverride` is ignored on a story.
93
+ - `captionOverride` is the **complete text that gets posted**: the caption, then a blank line, then that
94
+ platform's hashtags. The publisher posts it as-is, so hashtags listed only in `publish-pack.md` never
95
+ reach the platform. The first comment is the one thing that stays out: it is posted by hand.
96
+ - `options` holds real option keys only, in the camelCase the destination reports under
97
+ `optionKeys`. Never add a key that is not in this table, and never `privacyLevel` (the person
98
+ picks it from the account's allowed levels at publish time):
99
+
100
+ | Platform | Keys the pack may set | Applies to |
101
+ |---|---|---|
102
+ | TikTok | `brandContentToggle`, `brandOrganicToggle` | video and photo posts |
103
+ | TikTok | `isAigc` | video posts only |
104
+ | TikTok | `videoCoverTimestampMs` (milliseconds, from the cover frame) | video posts only |
105
+ | TikTok | `disableComment` | video and photo posts |
106
+ | TikTok | `disableDuet`, `disableStitch` | video posts only |
107
+ | Instagram | `altText` (at most 1000 characters) | a single feed image only |
108
+ | Instagram | `audioName`, `shareToFeed`, `collaborators` (at most 3, not on a carousel) | reels only |
109
+ | YouTube | `containsSyntheticMedia` | video |
110
+
111
+ Set only what the compliance check or the plan decided: leave a key out rather than writing a
112
+ default. Keys that need a Post's own asset id (`coverAssetId`, `thumbnailAssetId`,
113
+ `photoCoverIndex`) are not known until media is attached to a Post, so the pack does not carry
114
+ them; the cover frame is in `publish-pack.md` for the publisher to act on.
115
+ - `altText` is one string per image or card, in order. It is also the source for the Creative's
116
+ own `altText` field.
117
+ - `alternateHooks` and `tags` are for learning and experiments, not for publishing. `tags` is also
118
+ copied into `run.json`.
119
+ - `tags.angle` is the ideator's angle: `story-emotion`, `utility-education` or `trend-humor`.
120
+ `tags.format` is the idea's format (`still`, `carousel`, `story`, `motion`, `clip` or `film`).
121
+ - Valid JSON, no comments.
@@ -0,0 +1,99 @@
1
+ # Judging rubric
2
+
3
+ Used by the `conductor-content-creative-director` agent in `text-round` and `visual-round`, and by
4
+ the orchestrator when it needs to break a tie. Everything here exists to counter a known judge
5
+ failure mode, so follow it as written.
6
+
7
+ ## Why it is shaped this way
8
+
9
+ - LLM judges favour longer answers, the first-listed answer and the answer written in their own
10
+ style (Zheng et al. 2023, MT-Bench). So ideas arrive at the same length, under shuffled neutral
11
+ IDs, and finalists are compared in both orders.
12
+ - A judge that scores before it reasons anchors on its first impression, and a judge asked to
13
+ confirm a leader mostly confirms it (Verga et al. 2024 on panels of different models). So:
14
+ reasoning is written first, the judge is a different model tier from the ideators, and a
15
+ devil's-advocate pass runs against the leader.
16
+ - Ideas generated by one assisted process converge on the same few mechanics (Doshi & Hauser 2024),
17
+ and a model asked to debate itself repeats its first stance (Liang et al., degeneration of
18
+ thought). So novelty is scored explicitly, and the leader must be argued against.
19
+
20
+ ## Criteria and weights
21
+
22
+ Score each 1 to 5. Weighted total = sum(score x weight) / 5, so the maximum is 100.
23
+
24
+ | Criterion | Weight | 1 | 3 | 5 |
25
+ |---|---|---|---|---|
26
+ | Hook strength | 25 | Opens on a greeting, a logo or context; nothing makes a stranger stay | A clear promise or question, but generic; could open any account's post | A specific, surprising or tension-filled claim a stranger understands in 1s, spoken and on screen |
27
+ | Audience relevance | 20 | Aimed at "everyone"; ignores the brief's tension | Names the audience's topic but not their actual friction | Opens on a friction the brief names and the audience would recognise as theirs |
28
+ | Brand fit | 15 | Breaks a copy rule or approved-line, or reads off-voice | Within the rules but could be any brand | Uses an approved line or the knowledge page's voice naturally; no rule risk |
29
+ | Feasibility with assets on hand | 15 | Needs assets that do not exist and cannot be made here (and is tagged `RENDER`) | Needs one new photo or clip the user can supply easily | Fully buildable from `media.json` or a render with no photo; or an honest `FILM` with a simple shoot |
30
+ | Novelty | 15 | The mechanic is the most common one for this topic (listicle, "5 tips", before/after) | A familiar mechanic with a fresh angle | A mechanic not seen elsewhere in the pool or in the performance data, still easy to grasp |
31
+ | Shareability | 10 | Nothing to send to a friend; purely informational | Useful or pleasant, but no reason to send | A person would send it to a named other person ("this is you") or save it for later |
32
+
33
+ ## Scoring protocol
34
+
35
+ 1. Read `brief.md`, then every idea in `ideas/pool.md`. Ideas are identified by ID only; ignore the
36
+ order they appear in and any difference in polish.
37
+ 2. For each idea, per criterion, write one or two sentences of reasoning FIRST, citing the idea's
38
+ own words. Then write the score. Never write a score before its reasoning.
39
+ 3. Penalise nothing for length (ideas are length-matched) and reward nothing for confident tone.
40
+ 4. Compute the weighted total. Shortlist the top 3. If ties, apply the tie-break rule below.
41
+ 5. Any idea that breaks a copy rule or contradicts an approved line is capped at 2 on brand fit
42
+ and cannot be a finalist unless the fix is a one-word edit you state.
43
+
44
+ ## Pairwise protocol (visual round)
45
+
46
+ For each pair of finalists (A,B), (A,C), (B,C):
47
+
48
+ 1. Judge the pair twice, once with each order, as two independent comparisons. Write reasoning
49
+ first, then the verdict "first" or "second", then say which idea won.
50
+ 2. If the two orders name different winners (position flipped the result), the pair is a tie.
51
+ 3. Tally wins: win = 1, tie = 0.5 each. The leader has the most points.
52
+
53
+ ## Judging a rendered draft
54
+
55
+ Look at the actual image file, not the text description. Check:
56
+
57
+ - Readability at phone size: shrink it mentally to a thumbnail. Can the headline be read in a glance?
58
+ - Hook legibility in 1 second: is the first thing seen the hook, in high contrast?
59
+ - Safe zones: text out of the platform UI (see `platform-specs.md`), nothing sitting on a face.
60
+ - Brand look: tokens, font, logo lockup, accent phrase on the words that carry the meaning.
61
+ - Whether the picture and the words do different jobs (photo shows, words claim) rather than
62
+ repeating each other.
63
+
64
+ A finalist that could not be rendered keeps its text-round score; say so, and do not treat the
65
+ missing image as a defect of the idea, except when the failure came from copy the validator refused.
66
+ A `FILM` finalist has no image: judge its script and shot description against the same criteria and
67
+ note that the visual round could not test it.
68
+
69
+ ## Devil's-advocate pass
70
+
71
+ After the tally, and before naming a winner, write the strongest case against the leader in at
72
+ least five sentences: the likeliest way it fails with this audience, the platform risk, the
73
+ brand risk, and what the runner-up does better. Then decide whether the case is strong enough to
74
+ change the winner. State the answer plainly. Do not soften the case to protect the leader.
75
+
76
+ ### Veto questions
77
+
78
+ The pass must also answer these four about the leader, each yes or no with one line of reasoning.
79
+ They come from `compliance-checklist.md`.
80
+
81
+ 1. Does it rely on an **unsubstantiated claim** (nothing in the brief, knowledge or approved lines
82
+ confirms it)?
83
+ 2. Is there a **trademark or parody risk** (another company's product, brand, logo or likeness)?
84
+ 3. Is there a **representation or sensitivity problem**?
85
+ 4. Is **disclosure needed** (anyone paid, gifted, an employee or an affiliate) that the brief does
86
+ not already handle?
87
+
88
+ A "yes" that a copy edit can fix is fixed or noted for the scriptwriter. A "yes" that cannot be
89
+ fixed in copy disqualifies the idea: record it in `decision.md` under **Veto**, take the next
90
+ leader on the tally and run the same questions on it. Disclosure that only needs a toggle and a
91
+ caption line is not a disqualifier; note it.
92
+
93
+ ## Tie-break rule
94
+
95
+ If two ideas are within 3 points of weighted total, or tied on the pairwise tally, prefer in order:
96
+ 1. the higher hook-strength score;
97
+ 2. the idea buildable entirely from assets on hand (`RENDER` over `FILM`);
98
+ 3. the higher novelty score;
99
+ 4. the lower ID number (an arbitrary but fixed choice, so no one can say the order was chosen).
@@ -0,0 +1,53 @@
1
+ # Script template
2
+
3
+ `script.md` for the winning idea. Short-form craft rules this encodes: the hook lands in 0 to 3
4
+ seconds, spoken and on screen; something new happens every 3 to 5 seconds; there is a payoff, then
5
+ a call to action; the piece works with the sound off.
6
+
7
+ ```
8
+ # Script: <title>
9
+
10
+ Platform: <platform(s)> Length: <seconds> Tag: RENDER | FILM
11
+ Idea: <id> Source decision: decision.md
12
+
13
+ ## Hook (0-3s)
14
+ - Spoken: <line, max 12 words, or "none">
15
+ - On screen: <text, max 12 words>
16
+ - Visual: <what is shown>
17
+
18
+ ## Beats
19
+ | # | Time | Visual | On-screen text | Spoken / audio | What changes |
20
+ |---|------|--------|----------------|----------------|--------------|
21
+ | 1 | 3-6s | ... | ... | ... | <the new beat or pattern interrupt> |
22
+ | 2 | 6-10s | ... | ... | ... | ... |
23
+
24
+ ## Payoff
25
+ <the line or moment that delivers the hook's promise, with time>
26
+
27
+ ## CTA
28
+ <one action; use the kit's CTA claim or an approved line where there is one>
29
+
30
+ ## Publish copy
31
+ Per platform the brief asks for. The keyword the audience would search for sits in the caption's
32
+ opening words (and in the on-screen text and speech); the first line carries the hook.
33
+ - TikTok: <caption; keep the visible part to about 150 characters>
34
+ - Instagram: <caption; first line carries the hook, max 125 characters shown before "more">
35
+ - YouTube Shorts: <title max 100 characters>
36
+ - Facebook: <caption>
37
+ - Hashtags: <3 to 5, no more>
38
+ - First comment: <text, or "none"; posted by hand, because Conductor cannot post comments>
39
+ - Alternate hooks (hook swaps for variantOf experiments):
40
+ 1. <hook line> (<hookType>)
41
+ 2. <hook line> (<hookType>)
42
+ 3. <optional>
43
+
44
+ Hook types: question, bold-claim, pattern-interrupt, relatable-pain, how-to, number,
45
+ curiosity-gap, story, social-proof.
46
+
47
+ ## Copy-rule check
48
+ <each must-say / must-avoid item from brief.md, and ok or the fix>
49
+ ```
50
+
51
+ Rules: a beat every 3 to 5 seconds; no beat longer than 5 seconds without a visual or text change;
52
+ caption limits are in `platform-specs.md`; alternates differ from the main hook in hook type, not just wording (check current platform guidance); approved lines run
53
+ verbatim; leave out platforms the brief did not ask for.
@@ -0,0 +1,44 @@
1
+ # Shot list and direction template
2
+
3
+ `direction.md` (RENDER) and `shoot/shoot-script.md` (FILM) both use this table. A RENDER direction
4
+ has no camera, so "visual" is the layout or frame, and the final section maps the direction onto
5
+ the Creative's own fields.
6
+
7
+ ```
8
+ # Direction: <title>
9
+
10
+ ## Shot list
11
+ | Time | Visual | On-screen text | VO / audio | Asset |
12
+ |------|--------|----------------|------------|-------|
13
+ | 0-3s | ... | ... | ... | <media id, "to film", or none> |
14
+
15
+ ## Safe zones
16
+ - Keep text out of the top ~14%, the bottom ~35% and ~6% at each side (9:16; check current
17
+ platform guidance).
18
+ - Headline and CTA sit in the middle band. Nothing on a face.
19
+
20
+ ## Sound
21
+ <sound on or off by design; music track id if any; voice-over; captions are always on screen>
22
+
23
+ ## Publish assets
24
+ Per platform the script names:
25
+ - Cover frame: <video: timestamp in seconds and what is on it; carousel: card 1; still: n/a>
26
+ - Alt text: <one line per image or card, in order>
27
+ - On-screen text placement: <where each line sits, inside the safe zone for that platform>
28
+ - Sound source and licence: <original audio | Commercial Music Library track | library track id
29
+ and licence | clip's own sound | none>. Never a trending track a business account cannot license.
30
+
31
+ ## Capture settings (FILM only)
32
+ 9:16 vertical, 1080x1920, 30 fps, sound on, lit from the front, phone steady, shoot extra B-roll.
33
+
34
+ ## Ready-to-paste text (FILM only)
35
+ <on-screen text lines in order, and the captions per platform>
36
+
37
+ ## Mapping to the Creative (RENDER only)
38
+ - kind: STILL | MOTION
39
+ - layout / theme: <from the kit and registry>
40
+ - sequenceKind: story | carousel | none (carouselRatio for a carousel)
41
+ - motion: preset (fade-up | word-by-word | accent-pop | none), durationSec, background (photo or
42
+ clip, with its motion)
43
+ - audio: clip | track | none
44
+ ```
@@ -240,6 +240,10 @@ destination rewards video over an image.
240
240
  - `audio` — `source: "clip"` (the background clip's own recorded sound — the default when there
241
241
  is one), `"track"` (a separate library track: `trackId` from `upload_creative_media`, plus
242
242
  `volume` and `fadeOutSec`), or `"none"`.
243
+
244
+ A MOTION Creative is one animated frame per placement: it cannot also be a story or a carousel
245
+ (`sequenceKind`/`sequence`) — that is refused with a 422 (`motionNoSequence`). For several beats, make
246
+ one MOTION Creative per beat, or a STILL story/carousel.
243
247
  3. The preview renders three key moments across the timeline into one sheet image (not the finished
244
248
  video). Judge it the same honest way as step 7 above — does the copy's entrance finish before the
245
249
  next beat, does the end card actually hold long enough to read — and get approval before
@@ -34,6 +34,28 @@ back here with the rendered frames to attach and schedule.
34
34
  Also note any media the person handed you: local paths or public URLs, in the order they should
35
35
  appear. Media that must be measured (video) is measured by the tool; you do not need dimensions.
36
36
 
37
+ ### A publish pack, if one exists
38
+
39
+ Some earlier steps hand over a publish pack with the content: per-destination copy and options, plus
40
+ a first comment and notes. The content team (`conductor-content-studio`) writes one as
41
+ `.conductor/content-runs/<run>/publish-pack.json` (and a readable `publish-pack.md`); any other
42
+ producer can leave the same shape. When the content is a Creative, look for
43
+ `.conductor/content-runs/*/publish-pack.json` whose `creativeId` matches it, or use the one the
44
+ person points to. If you find one:
45
+
46
+ - Pre-fill each target's `captionOverride` and `options` (`publishOptions` on `set_publish_targets`)
47
+ from the pack's matching `platforms[]` entry, by platform and format. The pack has no accounts;
48
+ resolve those from `list_publish_targets` as usual.
49
+ - Check every option key against that destination's `optionKeys` and drop one it does not list,
50
+ saying so. Never invent a key the pack did not carry. A cover frame or alt text the pack names
51
+ that needs a Post's own asset (a cover image, per-card alt text) is a note for the person, not a
52
+ guess.
53
+ - Show the pre-filled copy and options in the confirmation table (see Step 4) and let the person
54
+ change any of them before anything is submitted. Their edit wins over the pack.
55
+ - Mention the pack's first comment as a **manual step**: Conductor cannot post comments, so give the
56
+ text to the person to paste once the post is live.
57
+ - Pack or no pack, the normal review applies. The pack settles copy and options; it approves nothing.
58
+
37
59
  ### Formats
38
60
 
39
61
  Some platforms publish more than one surface. Ask which one the person means when it is not
@@ -84,7 +106,8 @@ connectors gain capabilities — so read `optionKeys` off that destination's row
84
106
  platform, and never invent one because it sounds plausible; an unrecognized key is silently
85
107
  dropped rather than acted on. TikTok needs a `privacyLevel` from that account's
86
108
  `privacyLevelOptions` before it will publish at all; nothing else needs an option to go out, so
87
- absent a stated preference, leave every other option unset rather than filling in a default.
109
+ absent a stated preference (a publish pack, below, counts as one), leave every other option unset
110
+ rather than filling in a default.
88
111
 
89
112
  Then run this checklist against the copy, per destination, and **halt on any failure** — report it
90
113
  and ask how to proceed. Do not fix content yourself.
@@ -117,7 +140,8 @@ Read the result before saying anything:
117
140
 
118
141
  ## Step 4 — The confirmation table is the last line of defense
119
142
 
120
- End every successful run with a table, one row per destination:
143
+ End every successful run with a table, one row per destination (add a column for the options
144
+ set, such as a disclosure toggle or an AI label, whenever any were pre-filled or chosen):
121
145
 
122
146
  | Post | Destination | Account | When | State |
123
147
  |---|---|---|---|---|
@@ -172,12 +172,40 @@
172
172
  {
173
173
  "name": "conductor:creative",
174
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
+ },
176
+ {
177
+ "name": "conductor:content",
178
+ "description": "Run a content team: agents brainstorm competing short-form post ideas, pick one with a bias-resistant judge, write the script and direction, and render a local draft. You approve at the preview; only then is the Creative uploaded. Never creates a Post."
175
179
  }
176
180
  ],
177
181
  "agents": [
178
182
  {
179
183
  "name": "conductor-researcher",
180
184
  "description": "Researches unfamiliar technologies, libraries, and APIs for PRD creation. Auto-triggered by conductor:prd when confidence is low."
185
+ },
186
+ {
187
+ "name": "conductor-coder",
188
+ "description": "Implements one task test-first. Spawned in parallel by conductor:implement with the conductor-coder skill preloaded."
189
+ },
190
+ {
191
+ "name": "conductor-content-strategist",
192
+ "description": "Writes the content brief (objective, audience tension, must-say/avoid, assets, what has worked) from a workspace context snapshot. Stage of conductor-content-studio."
193
+ },
194
+ {
195
+ "name": "conductor-content-ideator",
196
+ "description": "Brainstorms distinct short-form post ideas from one assigned angle, tagging each RENDER or FILM. Three run in parallel inside conductor-content-studio."
197
+ },
198
+ {
199
+ "name": "conductor-content-creative-director",
200
+ "description": "Judges ideas with a fixed rubric: scores anonymized ideas, compares finalists pairwise from rendered drafts, argues against its own leader. Stage of conductor-content-studio."
201
+ },
202
+ {
203
+ "name": "conductor-content-scriptwriter",
204
+ "description": "Writes the winning idea's script: hook, timed beats, payoff, CTA and captions per platform. Stage of conductor-content-studio."
205
+ },
206
+ {
207
+ "name": "conductor-content-art-director",
208
+ "description": "Writes visual direction and the creative-spec.json for the draft renderer (or a shoot script for a filming idea). Stage of conductor-content-studio."
181
209
  }
182
210
  ],
183
211
  "skills": [
@@ -196,6 +224,10 @@
196
224
  {
197
225
  "name": "conductor-creative",
198
226
  "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."
227
+ },
228
+ {
229
+ "name": "conductor-content-studio",
230
+ "description": "Orchestrates the content team: context snapshot, parallel ideators, anonymized judging, finalist drafts, script and direction, then a local draft preview you approve before anything is uploaded. Ends at a committed Creative or a shoot script; never creates a Post."
199
231
  }
200
232
  ]
201
233
  },
@@ -22,6 +22,12 @@
22
22
  * loaded (including a bleed layout's CSS background photo, invisible to a
23
23
  * plain <img> check), headline contrast, the accent colour, and the
24
24
  * rendered box size.
25
+ *
26
+ * Presence checks (`missingPhoto`, `missingLockup`, `missingHeadline`): every check above judges what
27
+ * IS on the board, so a board that silently lost its photo or logo used to pass them all. The caller
28
+ * says what the spec requires (`expect`, see expectedElements) and a required element that is absent,
29
+ * collapsed or hidden fails. `bodyDropped` is the one warning in this family: a layout that has no room
30
+ * for the body line at a placement drops it by design, but the author supplied that copy.
25
31
  */
26
32
 
27
33
  /* ── pure math ─────────────────────────────────────────────────────────── */
@@ -86,6 +92,19 @@ export function safeZoneIntrusion(boardRect, nodes, safe) {
86
92
  return issues;
87
93
  }
88
94
 
95
+ /** Pure: what a render requires on the board, from the resolved ad and the brand kit it renders with —
96
+ * `{ photo, lockup, headline }`. A photo is required when the ad has a photo URL (a clip-only background
97
+ * does not need one); a lockup when the kit has a mark or a wordmark (a kit without logos renders none,
98
+ * by design); a headline when the ad has one. Pass the result as `runAssertions`'s `expect`. */
99
+ export function expectedElements(ad, brand) {
100
+ const logos = (brand && brand.logos) || {};
101
+ return {
102
+ photo: Boolean(ad && ad.photoUrl),
103
+ lockup: Boolean(logos.mark || logos.wordmarkLight || logos.wordmarkDark),
104
+ headline: Boolean(ad && ad.headline && String(ad.headline).trim()),
105
+ };
106
+ }
107
+
89
108
  /* ── DOM-dependent (real browser only) ────────────────────────────────────── */
90
109
 
91
110
  const CHECKED_SELECTOR = '.cc-headline, .cc-body, .cc-cta, .cc-lockup';
@@ -116,9 +135,10 @@ function sameRgb(a, b) {
116
135
  /* Runs every check against a live, fitted `.cc-board` and returns
117
136
  * `{ errors, warnings, width, height }`. `placement` is this placement's own
118
137
  * registry entry (pixel size, safe zone); `brand` is the same object handed
119
- * to renderBoard. An empty `errors` array means the frame is safe to ship;
120
- * `warnings` are non-fatal (e.g. a soft, upscaled photo). */
121
- export async function runAssertions(board, placement, brand, { outputScale = 2 } = {}) {
138
+ * to renderBoard; `expect` (optional, see expectedElements) names the elements the spec requires, so a
139
+ * missing one fails. An empty `errors` array means the frame is safe to ship; `warnings` are non-fatal
140
+ * (e.g. a soft, upscaled photo, or a body line the layout drops at this placement). */
141
+ export async function runAssertions(board, placement, brand, { outputScale = 2, expect = null } = {}) {
122
142
  const errors = [];
123
143
  const warnings = [];
124
144
  // The same findings with a stable `rule` id each, for callers (the draft preview) that report them
@@ -128,9 +148,9 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
128
148
  errors.push(message);
129
149
  checks.push({ rule, severity: 'error', message });
130
150
  };
131
- const warn = (rule, message) => {
151
+ const warn = (rule, message, extra) => {
132
152
  warnings.push(message);
133
- checks.push({ rule, severity: 'warning', message });
153
+ checks.push({ rule, severity: 'warning', message, ...extra });
134
154
  };
135
155
  const br = rectOf(board);
136
156
 
@@ -155,14 +175,44 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
155
175
  }
156
176
  }
157
177
 
178
+ // ── required elements: present, sized and visible (a spec that names them must get them) ──
179
+ if (expect) {
180
+ const shown = (n) => {
181
+ if (!n) return false;
182
+ const r = n.getBoundingClientRect();
183
+ const cs = getComputedStyle(n);
184
+ return r.width > 0 && r.height > 0 && cs.display !== 'none' && cs.visibility !== 'hidden';
185
+ };
186
+ if (expect.headline) {
187
+ const h = board.querySelector('.cc-headline');
188
+ if (!h || !h.textContent.trim() || !shown(h)) fail('missingHeadline', 'the headline is missing from the rendered frame');
189
+ }
190
+ if (expect.lockup && !shown(board.querySelector('.cc-lockup:not(.cc-lockup--spacer)'))) {
191
+ fail('missingLockup', 'the brand logo lockup is missing from the rendered frame (the brand kit has a logo)');
192
+ }
193
+ if (expect.photo) {
194
+ const layer = board.querySelector('.cc-bg-photo') || board.querySelector('.cc-board__band img, .cc-board__card img');
195
+ const hasImage = layer && (layer.tagName === 'IMG'
196
+ ? Boolean(layer.getAttribute('src')) && layer.complete && layer.naturalWidth > 0
197
+ : /url\(/.test(getComputedStyle(layer).backgroundImage || ''));
198
+ if (!shown(layer) || !hasImage) fail('missingPhoto', 'the photo is missing from the rendered frame (the spec has a photo URL)');
199
+ }
200
+ }
201
+ if (board.dataset.bodyDropped === 'true') {
202
+ const layout = board.dataset.layout || 'this layout';
203
+ const key = board.dataset.ratio;
204
+ warn('bodyDropped', `the body line is not shown: the ${layout} layout drops the body at ${key}. Put the words in the headline, or pick another layout or placement.`, { placementKey: key });
205
+ }
206
+
158
207
  const imgs = [...board.querySelectorAll('img')];
159
208
  const broken = imgs.filter((i) => !i.complete || i.naturalWidth === 0).map((i) => i.getAttribute('src'));
160
209
  if (broken.length) fail('image', `image(s) failed to load: ${broken.join(', ')}`);
161
210
 
162
- // A bleed/card layout paints its photo as a CSS background, invisible to
211
+ // A bleed layout paints its photo as a CSS background (the `.cc-bg-photo` layer), invisible to
163
212
  // the <img> check above — probe it directly, the same way a typo'd path
164
213
  // would otherwise export a flat, photo-less board and pass everything else.
165
- const bgImage = getComputedStyle(board).backgroundImage;
214
+ const photoLayer = board.querySelector('.cc-bg-photo');
215
+ const bgImage = getComputedStyle(photoLayer || board).backgroundImage;
166
216
  const bgUrlMatch = /url\("?(.+?)"?\)/.exec(bgImage || '');
167
217
  if (bgUrlMatch && bgUrlMatch[1]) {
168
218
  const probe = await new Promise((done) => {
@@ -37,3 +37,32 @@ export async function ensureBrandFont(brand) {
37
37
  await Promise.all([400, 700].map((w) => document.fonts.load(`${w} 32px "${family}"`).catch(() => [])));
38
38
  }
39
39
  }
40
+
41
+ /* Waits until every picture under `root` has loaded: each <img> (the logo lockup, the badge, a band/card
42
+ * photo) AND each bleed photo layer, which is a CSS background and so is not in `document.images` — the
43
+ * page's `load` event does not wait for it either. Without this a screenshot (the contact sheet most of
44
+ * all, which never ran this wait) could be taken before the photo or logos had arrived: a board with no
45
+ * photo and no logo that nothing flagged. A picture that fails to load resolves anyway; it is
46
+ * assertions.js's job (frame.html) to report it. Browser-only; a no-op outside a document. */
47
+ export async function settleImages(root) {
48
+ if (typeof document === 'undefined') return;
49
+ const scope = root || document;
50
+ const loads = [...scope.querySelectorAll('img')].map((img) => (
51
+ img.complete ? Promise.resolve() : new Promise((done) => {
52
+ img.addEventListener('load', done, { once: true });
53
+ img.addEventListener('error', done, { once: true });
54
+ })
55
+ ));
56
+ for (const layer of scope.querySelectorAll('.cc-bg-photo')) {
57
+ const m = /url\("?(.+?)"?\)/.exec(getComputedStyle(layer).backgroundImage || '');
58
+ if (!m) continue;
59
+ loads.push(new Promise((done) => {
60
+ const probe = new Image();
61
+ probe.onload = done;
62
+ probe.onerror = done;
63
+ probe.src = m[1];
64
+ }));
65
+ }
66
+ await Promise.all(loads);
67
+ await Promise.all([...scope.querySelectorAll('img')].map((img) => (img.decode ? img.decode().catch(() => {}) : null)));
68
+ }
@@ -87,6 +87,8 @@
87
87
  }
88
88
  .cc-board--light .cc-headline { color: var(--cc-ink); }
89
89
  .cc-headline em { font-style: italic; color: var(--cc-accent); }
90
+ .cc-headline em.cc-accent-after-space { margin-left: 0.07em; }
91
+ .cc-headline em.cc-accent-before-space { margin-right: 0.04em; }
90
92
  /* Word spans the motion presets animate: inline-block so transforms apply; the spaces between them stay
91
93
  * plain text, so lines wrap exactly as they do without the spans. */
92
94
  .cc-word { display: inline-block; }
@@ -37,8 +37,8 @@
37
37
  import { resolveAd, resolveSequence, renderBoard, fitBoard } from './render.js';
38
38
  import { placements as DEFAULT_PLACEMENTS } from './placements.js';
39
39
  import { layouts as DEFAULT_LAYOUTS } from './layouts/index.js';
40
- import { ensureBrandFont } from './font.js';
41
- import { runAssertions } from './assertions.js';
40
+ import { ensureBrandFont, settleImages } from './font.js';
41
+ import { runAssertions, expectedElements } from './assertions.js';
42
42
  import { applyMotion, seekBackgroundVideo } from './motion.js';
43
43
 
44
44
  function finish(result) {
@@ -83,10 +83,13 @@ async function main() {
83
83
 
84
84
  await document.fonts.ready;
85
85
  await Promise.all([...document.images].map((img) => (img.decode ? img.decode().catch(() => {}) : null)));
86
+ await settleImages(board); // also the bleed photo layer, a CSS background `document.images` does not list
86
87
  // Motion presets restructure the headline (word spans): build that structure, in its final state,
87
88
  // before fitting, so the type is sized for the layout every captured frame will have.
88
89
  if (spec.motion) applyMotion(board, spec.motion, spec.motion.durationSec || 8, { durationSec: spec.motion.durationSec || 8 });
89
90
  fitBoard(board, placementsReg);
91
+ // What this spec requires on the board: a required element that did not render fails the checks.
92
+ const expect = expectedElements(ad, spec.brand);
90
93
 
91
94
  if (spec.motion) {
92
95
  const durationSec = spec.motion.durationSec || 8;
@@ -102,13 +105,13 @@ async function main() {
102
105
  return true;
103
106
  };
104
107
  // A motion frame is captured at 1x (video), so the photo only needs to cover the placement itself.
105
- window.__assertBoard = async () => runAssertions(board, placement, spec.brand || {}, { outputScale: 1 });
108
+ window.__assertBoard = async () => runAssertions(board, placement, spec.brand || {}, { outputScale: 1, expect });
106
109
 
107
110
  finish({ ok: true, width: placement.w, height: placement.h });
108
111
  return;
109
112
  }
110
113
 
111
- const { errors, warnings, checks, width, height } = await runAssertions(board, placement, spec.brand || {});
114
+ const { errors, warnings, checks, width, height } = await runAssertions(board, placement, spec.brand || {}, { expect });
112
115
  finish({ ok: errors.length === 0, errors, warnings, checks, width, height });
113
116
  } catch (err) {
114
117
  finish({ ok: false, error: String((err && err.message) || err) });