skilldrop-cli 0.13.5 → 0.13.6

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 (39) hide show
  1. package/README.md +3 -2
  2. package/catalogue.json +19 -1
  3. package/contracts/catalogue.schema.json +1 -0
  4. package/model-routing.json +8 -0
  5. package/package.json +1 -1
  6. package/packs/design/pack.json +22 -0
  7. package/packs/design/skills/brand-kit/SKILL.md +105 -0
  8. package/packs/design/skills/brand-kit/evals/eval_queries.json +30 -0
  9. package/packs/design/skills/brand-kit/evals/evals.json +18 -0
  10. package/packs/design/skills/brand-kit/examples/northwind-brand.md +48 -0
  11. package/packs/design/skills/brand-kit/manifest.json +44 -0
  12. package/packs/design/skills/brand-kit/scripts/check_brand.py +114 -0
  13. package/packs/design/skills/brand-kit/templates/brand.json +54 -0
  14. package/packs/{stakeholder-comms → design}/skills/deck-builder/SKILL.md +27 -9
  15. package/packs/{stakeholder-comms → design}/skills/deck-builder/manifest.json +2 -2
  16. package/packs/{stakeholder-comms → design}/skills/deck-builder/scripts/build_deck.py +105 -3
  17. package/packs/{stakeholder-comms → design}/skills/deck-builder/templates/deck-spec.json +4 -1
  18. package/packs/design/skills/marketing-flyer/SKILL.md +113 -0
  19. package/packs/design/skills/marketing-flyer/evals/eval_queries.json +30 -0
  20. package/packs/design/skills/marketing-flyer/evals/evals.json +18 -0
  21. package/packs/design/skills/marketing-flyer/examples/community-health-fair.md +68 -0
  22. package/packs/design/skills/marketing-flyer/manifest.json +39 -0
  23. package/packs/design/skills/marketing-flyer/reference.md +54 -0
  24. package/packs/design/skills/marketing-flyer/scripts/build_flyer.py +323 -0
  25. package/packs/design/skills/marketing-flyer/templates/flyer-spec.json +26 -0
  26. package/packs/stakeholder-comms/pack.json +1 -1
  27. package/profiles.json +1 -1
  28. /package/packs/{stakeholder-comms → design}/skills/deck-builder/evals/eval_queries.json +0 -0
  29. /package/packs/{stakeholder-comms → design}/skills/deck-builder/evals/evals.json +0 -0
  30. /package/packs/{stakeholder-comms → design}/skills/deck-builder/examples/exec-board-update.md +0 -0
  31. /package/packs/{stakeholder-comms → design}/skills/deck-builder/examples/templated-brand-deck.md +0 -0
  32. /package/packs/{stakeholder-comms → design}/skills/deck-builder/reference.md +0 -0
  33. /package/packs/{stakeholder-comms → design}/skills/deck-builder/requirements.txt +0 -0
  34. /package/packs/{stakeholder-comms → design}/skills/deck-builder/templates/palettes.json +0 -0
  35. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/SKILL.md +0 -0
  36. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/evals/eval_queries.json +0 -0
  37. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/evals/evals.json +0 -0
  38. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/manifest.json +0 -0
  39. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/templates/deck-outline.md +0 -0
@@ -7,10 +7,11 @@ description: Generate a real PowerPoint (.pptx) file from content, audience, and
7
7
 
8
8
  This skill produces an **actual editable `.pptx` file**, not an outline. It runs `python-pptx`
9
9
  to materialise slides — either on a brand template's own masters and layouts, or on the skill's
10
- built-in design driven by a colour palette.
10
+ built-in design in the user's brand: their colours, logo and fonts.
11
11
 
12
- It pairs with two others:
13
- - **[`audience-profile`](../audience-profile/SKILL.md)** — decides how dense, how many, which sections
12
+ It pairs with three others:
13
+ - **[`brand-kit`](../brand-kit/SKILL.md)** — captures the brand once as a `brand.json` this skill reads, so nobody answers the brand questions for every deck
14
+ - **[`audience-profile`](../../../stakeholder-comms/skills/audience-profile/SKILL.md)** — decides how dense, how many, which sections
14
15
  - **[`slide-outliner`](../slide-outliner/SKILL.md)** — drafts per-slide content before this skill builds it
15
16
 
16
17
  For a single fast pipeline, run all three in order: profile → outline → build.
@@ -26,9 +27,11 @@ with the design silently defaulted.
26
27
  > Before I build, confirm these — reply "go" to take the defaults:
27
28
  > 1. **Audience** — `exec` · `board` · `technical` · `sales` · `internal` · `investor` · `partner` · `customer`
28
29
  > 2. **Format** — live presentation / read-ahead / async share, **how many minutes**, and **16:9** (default) or **4:3**
29
- > 3. **Design** — either
30
- > - a **template**: path to a corporate `.pptx` / `.potx`, and the deck inherits its masters, fonts, colours and slide size; or
31
- > - a **palette**: `corporate-blue` (default for B2B) · `monochrome` · `vibrant` · `dark-mode` · `editorial` · `forest` · `sunset`, or brand hex values for `primary` / `secondary` / `accent` / `background` / `text`
30
+ > 3. **Brand** — the deck should look like it came from your team. Send any of:
31
+ > - a **`brand.json`** from brand-kit, which covers everything below in one file;
32
+ > - a **template**: path to a corporate `.pptx` / `.potx`, and the deck inherits its masters, fonts, colours, logo and slide size;
33
+ > - your **logo** as a PNG (and a white version for dark slides), **brand colours** as hex values for `primary` / `secondary` / `accent` / `background` / `text`, and your **heading and body fonts**;
34
+ > - or, with no brand, a named **palette**: `corporate-blue` (default for B2B) · `monochrome` · `vibrant` · `dark-mode` · `editorial` · `forest` · `sunset`
32
35
  > 4. **Anything to embed** — numbers to chart, a table, diagram PNGs
33
36
 
34
37
  Rules for reading the answers:
@@ -44,10 +47,15 @@ Rules for reading the answers:
44
47
  [`reference.md`](reference.md#color-palette-principles) — say which roles were filled in.
45
48
  - **Template beats palette** for chrome; the palette still colours charts, tables, big numbers
46
49
  and quote bars, so ask for brand hexes even when a template is supplied.
50
+ - **Brand material beats a named palette.** If the user sent a logo or brand colours, never fall
51
+ back to `corporate-blue`. If they have brand material but no `brand.json`, suggest running
52
+ `brand-kit` first, so the next deck and any flyer match this one.
53
+ - **Logos must be PNG or JPG.** PowerPoint can't place SVG or EPS: ask for a PNG export, and
54
+ never build with the logo silently missing.
47
55
 
48
56
  **Non-interactive runs** (subagent, CI, headless): audience, format, time and palette degrade to
49
57
  `[assumption]` lines stated at the top of the response — default `exec`, live, 16:9,
50
- `corporate-blue`. Missing *content* does not degrade: emit `BLOCKED: need deck content
58
+ `corporate-blue`, or the `brand.json` when one is in the input. Never invent a logo or brand colour. Missing *content* does not degrade: emit `BLOCKED: need deck content
51
59
  (outline, doc, or point list)` and build nothing.
52
60
 
53
61
  ### 2. Plan the slide list against the audience archetype
@@ -61,7 +69,7 @@ Rules for reading the answers:
61
69
  | `internal` | 5–10 | Mixed | Context • Proposal • Discussion topics • Action items |
62
70
  | `investor` | 10–15 | Polished, narrative | Problem • Market • Product • Traction • Team • Ask |
63
71
 
64
- Full archetype spec: [`audience-profile`](../audience-profile/SKILL.md). Slide-count budgets per
72
+ Full archetype spec: [`audience-profile`](../../../stakeholder-comms/skills/audience-profile/SKILL.md). Slide-count budgets per
65
73
  format and time: [`reference.md`](reference.md#slide-count-budgets).
66
74
 
67
75
  ### 3. Build a deck spec
@@ -77,6 +85,7 @@ A JSON document — full schema, with every layout and the template block, in
77
85
  "template": "./brand/acme-master.potx",
78
86
  "layout_map": { "title": "Title Slide", "content": "Title and Content" },
79
87
  "palette": { "primary": "#1A2A6C", "accent": "#FDBB2D" },
88
+ "brand": "./brand/brand.json",
80
89
  "slides": [
81
90
  { "layout": "title", "title": "...", "subtitle": "...", "presenter": "...", "date": "..." },
82
91
  { "layout": "content", "title": "...", "bullets": ["..."], "notes": "..." },
@@ -129,6 +138,13 @@ python3 "${CLAUDE_SKILL_DIR}/scripts/build_deck.py" /tmp/deck-spec.json -o ./out
129
138
  cd path/to/deck-builder && python3 scripts/build_deck.py /tmp/deck-spec.json -o ./out/deck.pptx
130
139
  ```
131
140
 
141
+ Add `--brand ./brand/brand.json` to build in the user's brand (or set `"brand"` in the spec).
142
+ The brand fills whatever the spec leaves out: palette colours, the template, logo files and
143
+ fonts. On the built-in design the logo goes on every slide: large on the cover, small in the
144
+ lower-left corner elsewhere, and the on-dark version on dark section and closing slides. The
145
+ brand's heading and body fonts are applied to titles, text, tables and charts. A template
146
+ already carries its own logo and fonts, so they come from the template instead.
147
+
132
148
  Add `--strict` to fail the build on a missing or unreadable image instead of drawing a
133
149
  placeholder box — use it in CI, not in a conversation.
134
150
 
@@ -157,7 +173,8 @@ A generated deck is a strong first draft. Always tell the user to:
157
173
 
158
174
  ## Quality bar
159
175
 
160
- - **Design is chosen by the user, never silently defaulted** — a template path or a named palette, confirmed in the setup block.
176
+ - **Design is chosen by the user, never silently defaulted** — a `brand.json`, a template, brand colours and logo, or a named palette, confirmed in the setup block.
177
+ - **The user's brand shows on every slide** — their logo (PNG) and colours, and their fonts when they gave them. A deck built from brand material that comes out in `corporate-blue` has failed.
161
178
  - **A supplied template is actually inherited** — `layout_map` covers `title`, `section`, `content` and `closing` at minimum. A branded deck rendered on the blank layout has thrown away the branding.
162
179
  - **Title slide isn't blank** — real title, subtitle, presenter, date.
163
180
  - **Numbers are charted, not bulleted** — three or more comparable figures belong in a `chart` or a `table`, not a bullet list.
@@ -171,6 +188,7 @@ A generated deck is a strong first draft. Always tell the user to:
171
188
  ## Anti-patterns to avoid
172
189
 
173
190
  - ❌ Ignoring a template the user already named, or asking for a palette after they supplied one. The template is the answer to "what should this look like".
191
+ - ❌ Asking for brand colours when a `brand.json` was supplied, or building without the logo because it was an SVG. Ask for a PNG export instead.
174
192
  - ❌ Mapping every logical layout onto the template's one title-and-bullets layout. That is a branded deck that still looks generated.
175
193
  - ❌ Generating without confirming audience. The same content for execs and engineers is a different deck.
176
194
  - ❌ Inventing a statistic to fill a `big_number`, or a series to fill a chart. No source, no chart.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deck-builder",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Generate a real PowerPoint (.pptx) file from content, audience, and either a brand template or a color palette — with charts, tables and images, not only bullets. Slide count, density and layout are tuned to the audience: execs, boards, technical reviewers, sales prospects, investors, internal teams. Use when the user wants an actual editable .pptx rather than an outline, wants their corporate template or brand colours applied, or says \"build the deck\", \"make it a pptx\", \"use our template\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -8,7 +8,7 @@
8
8
  "pip": ["python-pptx==1.0.2"]
9
9
  },
10
10
  "env": { "required": [], "optional": [] },
11
- "related": ["audience-profile", "slide-outliner", "doc-critique"],
11
+ "related": ["brand-kit", "audience-profile", "slide-outliner", "doc-critique"],
12
12
  "install": {
13
13
  "pip": "python3 -m pip install -r requirements.txt"
14
14
  },
@@ -9,6 +9,9 @@ The spec schema is documented in templates/deck-spec.json. Top-level fields:
9
9
  colors and slide size are inherited
10
10
  - layout_map: logical layout -> template layout name or index (template mode)
11
11
  - palette: a hex-color block, or a name resolvable from templates/palettes.json
12
+ - brand: path to a brand.json (the brand-kit skill writes one) or the same object
13
+ inline. It supplies anything the spec leaves out: palette colours, template,
14
+ logo files and fonts. --brand on the command line overrides it.
12
15
  - slide_numbers: true/false (default: true when the deck has more than 10 slides)
13
16
  - slides: list of slide specs, each with a `layout` field
14
17
 
@@ -16,7 +19,7 @@ Supported layouts: title, section, content, two_column, big_number, quote,
16
19
  image, table, chart, closing.
17
20
 
18
21
  Usage:
19
- python3 build_deck.py <spec.json> [-o output.pptx] [--strict]
22
+ python3 build_deck.py <spec.json> [-o output.pptx] [--strict] [--brand brand.json]
20
23
  """
21
24
 
22
25
  from __future__ import annotations
@@ -73,6 +76,12 @@ RASTER_OK = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tif", ".tiff"}
73
76
  WARNINGS: list[str] = []
74
77
 
75
78
 
79
+ # Set from brand.json in build(): fonts for the built-in design, and the logo files placed on
80
+ # its slides. A supplied template already carries its own fonts and logo, so these are only
81
+ # used when a slide renders on the built-in design.
82
+ BRAND = {"heading_font": None, "body_font": None, "logo": None, "logo_on_dark": None}
83
+
84
+
76
85
  def warn(msg: str) -> None:
77
86
  WARNINGS.append(msg)
78
87
  sys.stderr.write(f"warning: {msg}\n")
@@ -405,6 +414,9 @@ def add_textbox(slide, text: str, *, left, top, width, height,
405
414
  run.font.size = size
406
415
  run.font.bold = bold
407
416
  run.font.color.rgb = hex_color(color)
417
+ font = BRAND["heading_font"] if (bold or size >= Pt(24)) else BRAND["body_font"]
418
+ if font:
419
+ run.font.name = font
408
420
 
409
421
 
410
422
  def add_bullets(slide, bullets, *, left, top, width, height,
@@ -419,6 +431,8 @@ def add_bullets(slide, bullets, *, left, top, width, height,
419
431
  for run in p.runs:
420
432
  run.font.size = size
421
433
  run.font.color.rgb = hex_color(color)
434
+ if BRAND["body_font"]:
435
+ run.font.name = BRAND["body_font"]
422
436
 
423
437
 
424
438
  def add_rect(slide, *, left, top, width, height, fill: str) -> None:
@@ -503,6 +517,8 @@ def place_table(slide, columns, rows, region: Region, c: Canvas, palette: dict)
503
517
  run.font.bold = True
504
518
  run.font.size = c.pt(body_pt + 1)
505
519
  run.font.color.rgb = hex_color("#FFFFFF")
520
+ if BRAND["heading_font"]:
521
+ run.font.name = BRAND["heading_font"]
506
522
 
507
523
  for i, row in enumerate(rows, start=1):
508
524
  for j in range(n_cols):
@@ -514,6 +530,8 @@ def place_table(slide, columns, rows, region: Region, c: Canvas, palette: dict)
514
530
  for p in cell.text_frame.paragraphs:
515
531
  for run in p.runs:
516
532
  run.font.size = c.pt(body_pt)
533
+ if BRAND["body_font"]:
534
+ run.font.name = BRAND["body_font"]
517
535
  run.font.color.rgb = hex_color(palette["text"])
518
536
 
519
537
 
@@ -543,6 +561,8 @@ def place_chart(slide, chart_spec: dict, region: Region, c: Canvas, palette: dic
543
561
  region.width, region.height, data)
544
562
  chart = gfx.chart
545
563
  chart.font.size = c.pt(13)
564
+ if BRAND["body_font"]:
565
+ chart.font.name = BRAND["body_font"]
546
566
  chart.font.color.rgb = hex_color(palette["text"])
547
567
 
548
568
  is_pie = kind in ("pie", "doughnut")
@@ -934,9 +954,81 @@ def layout_numbers_slides(layout) -> bool:
934
954
  # ----------------------------------------------------------------- driver
935
955
 
936
956
 
937
- def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool) -> None:
957
+ def load_brand(ref, base: Path):
958
+ """brand.json path or inline object -> (dict, directory its relative paths resolve from)."""
959
+ if not ref:
960
+ return {}, base
961
+ if isinstance(ref, dict):
962
+ return ref, base
963
+ path = (base / ref).expanduser() if not Path(ref).is_absolute() else Path(ref)
964
+ if not path.exists():
965
+ warn(f"brand file not found: {path}; building without it")
966
+ return {}, base
967
+ try:
968
+ return json.loads(path.read_text()), path.parent
969
+ except json.JSONDecodeError as e:
970
+ warn(f"brand file {path} is not valid JSON ({e}); building without it")
971
+ return {}, base
972
+
973
+
974
+ def apply_brand(spec: dict, brand: dict, brand_dir: Path, spec_dir: Path) -> None:
975
+ """Fill what the spec leaves out from the brand. The spec always wins: a deck can still
976
+ override one colour or swap the template without editing the brand file."""
977
+ colors = {k: v for k, v in (brand.get("colors") or {}).items()
978
+ if k in ("primary", "secondary", "accent", "background", "text") and v}
979
+ if colors and not spec.get("palette"):
980
+ spec["palette"] = colors
981
+ elif colors and isinstance(spec.get("palette"), dict):
982
+ spec["palette"] = {**colors, **spec["palette"]}
983
+ tpl = (brand.get("templates") or {}).get("pptx") or brand.get("template")
984
+ if tpl and not spec.get("template"):
985
+ spec["template"] = str((brand_dir / tpl).resolve()) if not Path(tpl).is_absolute() else tpl
986
+ fonts = brand.get("fonts") or {}
987
+ BRAND["heading_font"] = (fonts.get("heading") or {}).get("family") if isinstance(fonts.get("heading"), dict) else fonts.get("heading")
988
+ BRAND["body_font"] = (fonts.get("body") or {}).get("family") if isinstance(fonts.get("body"), dict) else fonts.get("body")
989
+ logo = brand.get("logo") or {}
990
+ for key, field in (("logo", "primary"), ("logo_on_dark", "on_dark")):
991
+ ref = logo.get(field) if isinstance(logo, dict) else (logo if field == "primary" else None)
992
+ if not ref:
993
+ continue
994
+ lp = (brand_dir / ref) if not Path(ref).is_absolute() else Path(ref)
995
+ if lp.suffix.lower() not in (".png", ".jpg", ".jpeg", ".gif", ".bmp"):
996
+ warn(f"logo {lp.name} is {lp.suffix or 'not an image'}; PowerPoint needs PNG or JPG. Export the logo as PNG.")
997
+ elif not lp.exists():
998
+ warn(f"logo not found: {lp}")
999
+ else:
1000
+ BRAND[key] = str(lp)
1001
+
1002
+
1003
+ def is_dark(hex_str: str) -> bool:
1004
+ h = hex_str.lstrip("#")
1005
+ r, g, b = (int(h[i:i + 2], 16) / 255 for i in (0, 2, 4))
1006
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.45
1007
+
1008
+
1009
+ def place_logo(slide, c: "Canvas", palette: dict, logical: str) -> None:
1010
+ """The brand logo on the built-in design: large on the cover, small in a fixed corner on
1011
+ every other slide, using the on-dark variant on dark slides when there is one."""
1012
+ if not BRAND["logo"] and not BRAND["logo_on_dark"]:
1013
+ return
1014
+ dark_bg = logical in ("section", "closing") and is_dark(palette["primary"]) or \
1015
+ (logical == "title" and is_dark(palette["background"]))
1016
+ path = (BRAND["logo_on_dark"] or BRAND["logo"]) if dark_bg else (BRAND["logo"] or BRAND["logo_on_dark"])
1017
+ if logical == "title":
1018
+ slide.shapes.add_picture(path, c.x(0.75), c.y(0.6), height=c.y(0.7))
1019
+ elif logical == "closing":
1020
+ pic = slide.shapes.add_picture(path, 0, c.h - c.y(1.25), height=c.y(0.6))
1021
+ pic.left = int((c.w - pic.width) / 2)
1022
+ else:
1023
+ slide.shapes.add_picture(path, c.x(0.5), c.h - c.y(0.55), height=c.y(0.32))
1024
+
1025
+
1026
+ def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool, brand_ref=None) -> None:
938
1027
  spec = json.loads(spec_path.read_text())
939
1028
  spec_dir = spec_path.parent
1029
+ brand, brand_dir = load_brand(brand_ref or spec.get("brand"), spec_dir)
1030
+ if brand:
1031
+ apply_brand(spec, brand, brand_dir, spec_dir)
940
1032
  palette = resolve_palette(spec.get("palette"), script_dir)
941
1033
 
942
1034
  slides = spec.get("slides", [])
@@ -970,6 +1062,8 @@ def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool) ->
970
1062
  render_templated(slide, canvas, palette, slide_spec, ctx, logical)
971
1063
  else:
972
1064
  RENDERERS[logical](slide, canvas, palette, slide_spec, ctx)
1065
+ if not using_template: # a template's masters already carry its logo
1066
+ place_logo(slide, canvas, palette, logical)
973
1067
 
974
1068
  if slide_spec.get("notes"):
975
1069
  set_notes(slide, slide_spec["notes"])
@@ -982,6 +1076,10 @@ def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool) ->
982
1076
  prs.save(str(out_path))
983
1077
 
984
1078
  mode = "template" if using_template else "built-in design"
1079
+ if brand:
1080
+ mode += ", brand: " + ", ".join(x for x, on in (("colours", bool(brand.get("colors"))),
1081
+ ("logo", bool(BRAND["logo"] or BRAND["logo_on_dark"])),
1082
+ ("fonts", bool(BRAND["heading_font"] or BRAND["body_font"]))) if on)
985
1083
  print(f"wrote {out_path} ({len(slides)} slides, {mode})")
986
1084
  if WARNINGS:
987
1085
  print(f"{len(WARNINGS)} warning(s) — see stderr; fix them before presenting")
@@ -995,6 +1093,9 @@ def main(argv) -> int:
995
1093
  help="Output .pptx path (default: alongside the spec)")
996
1094
  ap.add_argument("--strict", action="store_true",
997
1095
  help="Fail on a missing/unsupported image instead of drawing a placeholder")
1096
+ ap.add_argument("--brand", metavar="BRAND_JSON",
1097
+ help="brand.json (from the brand-kit skill): colours, template, logo, fonts. "
1098
+ "Fills anything the spec leaves out")
998
1099
  ap.add_argument("--list-layouts", metavar="TEMPLATE",
999
1100
  help="Print a template's layout names and indices, then exit "
1000
1101
  "(use them to write layout_map)")
@@ -1024,7 +1125,8 @@ def main(argv) -> int:
1024
1125
  out_path = (Path(args.output).expanduser().resolve()
1025
1126
  if args.output else spec_path.with_suffix(".pptx"))
1026
1127
 
1027
- build(spec_path, out_path, Path(__file__).resolve().parent, strict=args.strict)
1128
+ build(spec_path, out_path, Path(__file__).resolve().parent, strict=args.strict,
1129
+ brand_ref=str(Path(args.brand).expanduser().resolve()) if args.brand else None)
1028
1130
  return 0
1029
1131
 
1030
1132
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Full deck-spec schema. The build_deck.py script consumes this. Top-level fields: title, audience, aspect, template, layout_map, palette, slide_numbers, slides[].",
2
+ "_comment": "Full deck-spec schema. The build_deck.py script consumes this. Top-level fields: title, audience, aspect, template, layout_map, palette, brand, slide_numbers, slides[].",
3
3
 
4
4
  "title": "ProjectX Migration — Board Update",
5
5
  "audience": "board",
@@ -22,6 +22,9 @@
22
22
  "closing": "Closing"
23
23
  },
24
24
 
25
+ "_brand_comment": "Path to a brand.json from the brand-kit skill, or the same object inline. It fills anything this spec leaves out: palette colours, template, logo files (logo.primary, logo.on_dark — PNG or JPG) and fonts (fonts.heading.family, fonts.body.family). --brand on the command line overrides it.",
26
+ "brand": "./brand/brand.json",
27
+
25
28
  "palette": {
26
29
  "primary": "#1A2A6C",
27
30
  "secondary": "#4A5A8C",
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: marketing-flyer
3
+ description: Generate a print-ready, on-brand marketing flyer — event, launch, open day, offer, hiring — as one self-contained HTML file (Letter, A4, A5, or square and story sizes for social) that saves to PDF, built from the user's own logo, colours, fonts and content, with one call to action, a short headline, contrast checked, and no invented dates, prices or contact details. Use when the user wants a flyer, poster, one-pager, handout, leaflet or event announcement "in our branding", or says "make a flyer for…".
4
+ ---
5
+
6
+ # marketing-flyer
7
+
8
+ You make a flyer that looks like it came from the user's own team, because it's built from
9
+ their brand: their logo, their colours, their fonts, their words. The output is one HTML file
10
+ with the images embedded, sized for print, that saves to PDF from any browser. When a
11
+ `brand.json` from [`brand-kit`](../brand-kit/SKILL.md) exists, use it, and the flyer will match
12
+ the user's decks from [`deck-builder`](../deck-builder/SKILL.md).
13
+
14
+ ## How to respond
15
+
16
+ 1. **Ask for everything in one message.** Take what's already been given, and suggest
17
+ defaults so the user can reply "go".
18
+
19
+ > Before I build the flyer, send these. Anything you skip, I'll use the default shown:
20
+ > 1. **Brand:** a `brand.json` from brand-kit, *or* your logo file (and a white version for
21
+ > dark backgrounds), brand colours as hex codes, and heading and body fonts.
22
+ > *Default: no brand, which looks generic, so please send at least a logo and one colour.*
23
+ > 2. **What it's for, and who reads it:** an event, a launch, an offer, hiring, a service
24
+ > 3. **The words:** a headline (ten words or fewer), a one-line subhead, up to four key
25
+ > points, and details such as date, time, place and price
26
+ > 4. **One call to action:** what the reader should do, and the web address, phone or email for it.
27
+ > A QR code image too, if you have one.
28
+ > 5. **Images:** a photo for the top of the flyer, if any, with a line describing it
29
+ > 6. **Size:** `letter` · `a4` · `a5` · `square` (Instagram/LinkedIn) · `story` · and
30
+ > portrait or landscape. *Default: letter, portrait.*
31
+ > 7. **Layout:** `hero` (photo across the top) · `split` (photo down one side) ·
32
+ > `event` (a big date block) · `minimal`. *Default: event when there's a date, otherwise hero.*
33
+
34
+ **Content is the hard input.** Never invent a date, time, price, address, phone number,
35
+ web address or statistic. If the user gave only a topic, ask for the details. A flyer with
36
+ a wrong date is worse than no flyer.
37
+
38
+ 2. **Write the copy to fit a flyer.** Rewrite long text into the flyer's shape and show the
39
+ user what you changed:
40
+ - A headline of ten words or fewer that names the thing, such as "Free community health
41
+ fair", not "You're invited!".
42
+ - A subhead that says who it's for, or why it's worth coming.
43
+ - At most four points, each one line, so they can be read in passing.
44
+ - Exactly **one** call to action, worded as a verb ("Save your spot", "Call to book"). Two
45
+ calls to action means the reader does neither.
46
+
47
+ Use the brand's voice from `brand.json` when it has one: words to use, words to avoid.
48
+
49
+ 3. **Write the spec** in the shape of [`templates/flyer-spec.json`](templates/flyer-spec.json),
50
+ with paths relative to the spec file.
51
+
52
+ 4. **Build it.**
53
+
54
+ ```bash
55
+ # Claude Code
56
+ python3 "${CLAUDE_SKILL_DIR}/scripts/build_flyer.py" flyer-spec.json -o flyer.html --brand ./brand/brand.json --pdf flyer.pdf
57
+ # Other IDEs (from the skill folder)
58
+ python3 scripts/build_flyer.py flyer-spec.json -o flyer.html --brand ./brand/brand.json --pdf flyer.pdf
59
+ ```
60
+
61
+ `--pdf` and `--png` use a local Chrome or Chromium when one is installed. Without one, the
62
+ user opens `flyer.html` and prints it to PDF at 100% scale with margins set to none.
63
+ Use `--png` for the `square` and `story` sizes, which are posted as images.
64
+
65
+ 5. **Read the output and every warning.** The script prints three contrast figures: text,
66
+ the header band, and the call to action. It picks white or the brand's text colour on each
67
+ filled band, whichever reads better. It warns about a long headline, more than four
68
+ points, more than one call to action, a missing logo or image, and no brand. Fix what you
69
+ can, and report the rest to the user with what it means.
70
+
71
+ 6. **Tell the user how to check it before printing:** read every date, time and address
72
+ aloud against the source, check the web address works, and print one test copy.
73
+
74
+ **Non-interactive runs** (subagent, CI, headless): missing *content* does not get a default.
75
+ Emit `BLOCKED: need the flyer's details: <what is missing>` and build nothing. Missing brand
76
+ falls back to defaults with an `[assumption]` line at the top; never make up a logo.
77
+
78
+ ## Useful references in this skill
79
+
80
+ - [`reference.md`](reference.md) — sizes and print rules, layouts and when to use each, copy budgets, contrast
81
+ - [`templates/flyer-spec.json`](templates/flyer-spec.json) — the spec, every field shown
82
+ - [`scripts/build_flyer.py`](scripts/build_flyer.py) — the builder (stdlib only): HTML, plus PDF and PNG through headless Chrome
83
+ - [`examples/community-health-fair.md`](examples/community-health-fair.md) — worked example: an email from the user becomes a flyer spec and a branded flyer
84
+
85
+ ## Quality bar
86
+
87
+ - **It is in the user's brand:** their logo, their colours as hex codes, their fonts. Default colours only when the user has no brand material, and then said out loud.
88
+ - **Every fact comes from the user.** Dates, times, places, prices and contact details are copied, not composed.
89
+ - **One call to action,** worded as a verb, with the address or number that makes it actionable.
90
+ - **A headline of ten words or fewer, at most four points.** Anything longer belongs on the linked page.
91
+ - **Text is readable everywhere:** body text at least 4.5:1 against the background, and the script's band and call-to-action figures reported.
92
+ - **The file is the whole flyer.** Images are embedded, with no links to local files that break when it's emailed.
93
+
94
+ ## When to use this skill
95
+
96
+ - ✅ An event, a launch, an open day, an offer, a hiring drive, a service announcement
97
+ - ✅ A one-page handout or leaflet in the company's branding
98
+ - ✅ A square or story-sized image for social posts that matches the printed flyer
99
+
100
+ ## When NOT to use this skill
101
+
102
+ - ❌ A slide deck; use `deck-builder`
103
+ - ❌ A multi-page brochure or a report, which this doesn't lay out
104
+ - ❌ Designing a brand from scratch; capture an existing one with `brand-kit`
105
+
106
+ ## Anti-patterns to avoid
107
+
108
+ - ❌ **"You're invited!" as the headline.** Name the event, the offer or the product.
109
+ - ❌ **Three buttons** ("Register", "Learn more", "Follow us"). Pick the one action that matters.
110
+ - ❌ **Filling a missing date with "TBC"** or a plausible guess. Ask, or build nothing.
111
+ - ❌ **White text on a pale accent** because the brand's buttons are white. The script flips it; don't override it.
112
+ - ❌ **A paragraph of body copy.** No one reads a paragraph on a noticeboard.
113
+ - ❌ **Sending the HTML with images linked from the user's disk.** Always build with the script, which embeds them.
@@ -0,0 +1,30 @@
1
+ [
2
+ {
3
+ "query": "Make a flyer for our open day in our brand colours",
4
+ "should_trigger": true
5
+ },
6
+ {
7
+ "query": "Design a one-page handout announcing the new clinic",
8
+ "should_trigger": true
9
+ },
10
+ {
11
+ "query": "I need a poster for the charity run, with our logo",
12
+ "should_trigger": true
13
+ },
14
+ {
15
+ "query": "Create an Instagram-sized announcement image that matches our flyer",
16
+ "should_trigger": true
17
+ },
18
+ {
19
+ "query": "Build the board deck as a pptx",
20
+ "should_trigger": false
21
+ },
22
+ {
23
+ "query": "Write the press release for the launch",
24
+ "should_trigger": false
25
+ },
26
+ {
27
+ "query": "Set up our brand colours and fonts so we can reuse them",
28
+ "should_trigger": false
29
+ }
30
+ ]
@@ -0,0 +1,18 @@
1
+ {
2
+ "skill_name": "marketing-flyer",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "Make a flyer for our community health fair using ./brand/brand.json. It's Saturday 14 November, 10am to 2pm, at the Riverside Park pavilion, free. There'll be blood pressure and glucose checks, flu shots with no appointment, a kids' corner and cooking demos. People should register at northwindhealth.example/fair. Letter size.",
7
+ "assertions": [
8
+ "Uses the supplied brand.json, so the flyer carries the brand logo and colours",
9
+ "The headline is ten words or fewer and names the event",
10
+ "There is exactly one call to action, worded as a verb, with northwindhealth.example/fair",
11
+ "The date, time, place and price match the prompt exactly; nothing is invented (no phone number, no extra dates)",
12
+ "No more than four points",
13
+ "Runs build_flyer.py and reports the contrast figures and any warnings",
14
+ "Picks the event layout, or explains choosing another, because the date is the key fact"
15
+ ]
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,68 @@
1
+ # Worked example — an email about a health fair becomes a branded flyer
2
+
3
+ ## Input given to the skill
4
+
5
+ > Can you make a flyer for our community health fair? Use our brand kit in ./brand/. It's
6
+ > Saturday 14 November, 10am to 2pm, at the Riverside Park pavilion, and it's free. We'll have
7
+ > free blood pressure and glucose checks, flu shots (no appointment), a kids' corner and some
8
+ > healthy cooking demos. Oh and a raffle. People should register at
9
+ > northwindhealth.example/fair so we know numbers. Letter size please, we'll print it at the office.
10
+
11
+ ---
12
+
13
+ **Copy, fitted to a flyer.** Here is what I changed from your email:
14
+
15
+ - **Headline:** "Free community health fair" (5 words)
16
+ - **Subhead:** "Checkups, flu shots and healthy cooking — for the whole family"
17
+ - **Points (3):** free blood pressure and glucose checks · flu shots, no appointment needed ·
18
+ kids' corner and healthy cooking demos. *The raffle is left off: four points is the most a
19
+ flyer carries, and the other three bring people in. Say if you want it instead of the
20
+ cooking demos.*
21
+ - **Call to action:** "Save your spot" → northwindhealth.example/fair
22
+ - **Layout:** `event`, because the date is the fact people need first
23
+
24
+ **Spec** (`flyer-spec.json`):
25
+
26
+ ```json
27
+ {
28
+ "brand": "./brand/brand.json",
29
+ "size": "letter",
30
+ "layout": "event",
31
+ "headline": "Free community health fair",
32
+ "subhead": "Checkups, flu shots and healthy cooking — for the whole family",
33
+ "points": ["Free blood pressure and glucose checks", "Flu shots, no appointment needed", "Kids' corner and healthy cooking demos"],
34
+ "details": [
35
+ { "label": "When", "value": "Sat 14 Nov, 10am–2pm" },
36
+ { "label": "Where", "value": "Riverside Park pavilion" },
37
+ { "label": "Cost", "value": "Free" }
38
+ ],
39
+ "cta": { "text": "Save your spot", "url": "northwindhealth.example/fair" },
40
+ "image": { "path": "./riverside-park.png", "alt": "Families at Riverside Park" },
41
+ "footer": "© 2026 Northwind Health"
42
+ }
43
+ ```
44
+
45
+ **Build:**
46
+
47
+ ```
48
+ $ python3 scripts/build_flyer.py flyer-spec.json -o community-health-fair.html --brand ./brand/brand.json --pdf community-health-fair.pdf
49
+ wrote ./community-health-fair.html (event, letter; brand: yes; logo: yes)
50
+ contrast: text 14.8:1 · band 8.2:1 · call to action 7.2:1
51
+ wrote ./community-health-fair.pdf
52
+ ```
53
+
54
+ **The flyer:** Northwind's green band across the top with the white logo. A large amber date
55
+ block reading *Sat 14 Nov, 10am–2pm* sits beside the Georgia headline. Below are the park
56
+ photo, the three points with amber markers, Where and Cost, and an amber "Save your spot" panel
57
+ with the web address in dark text, because white on that amber would be hard to read. The
58
+ footer carries the website and the copyright line.
59
+
60
+ **Before you print:** read the date, time and place aloud against your email, open
61
+ northwindhealth.example/fair to check it works, and print one test copy.
62
+
63
+ ---
64
+
65
+ **Why this is a good output:** every fact is copied from the email, and the one cut (the raffle)
66
+ is named and offered back instead of silently dropped. There's one call to action. The layout
67
+ puts the date first because that's what a passer-by needs. The contrast figures are reported,
68
+ and the panel text uses the colour that reads.
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "marketing-flyer",
3
+ "version": "0.1.0",
4
+ "description": "Generate a print-ready, on-brand marketing flyer — event, launch, open day, offer, hiring — as one self-contained HTML file (Letter, A4, A5, or square and story sizes for social) that saves to PDF, built from the user's own logo, colours, fonts and content, with one call to action, a short headline, contrast checked, and no invented dates, prices or contact details. Use when the user wants a flyer, poster, one-pager, handout, leaflet or event announcement \"in our branding\", or says \"make a flyer for…\".",
5
+ "entrypoint": "SKILL.md",
6
+ "deps": {
7
+ "npm": [],
8
+ "pip": []
9
+ },
10
+ "env": {
11
+ "required": [],
12
+ "optional": []
13
+ },
14
+ "related": [
15
+ "brand-kit",
16
+ "deck-builder"
17
+ ],
18
+ "handoff": [
19
+ {
20
+ "to": "brand-kit",
21
+ "when": "the user has brand material but no brand.json",
22
+ "purpose": "Capture the logo, colours and fonts once, so the flyer and later decks match.",
23
+ "fallback": "If brand-kit is absent, ask for the logo path and brand hex codes in the setup block and put them in the spec's brand object inline."
24
+ }
25
+ ],
26
+ "tags": [
27
+ "flyer",
28
+ "poster",
29
+ "marketing",
30
+ "print",
31
+ "pdf",
32
+ "brand",
33
+ "design"
34
+ ],
35
+ "model": {
36
+ "tier": "standard",
37
+ "rationale": "Writes short copy to fit and fills a spec for a deterministic builder. Bounded generation."
38
+ }
39
+ }