skilldrop-cli 0.13.4 → 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 (41) hide show
  1. package/README.md +32 -555
  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/dev-team/skills/contribution-wizard/SKILL.md +6 -6
  27. package/packs/dev-team/skills/contribution-wizard/manifest.json +2 -2
  28. package/packs/stakeholder-comms/pack.json +1 -1
  29. package/profiles.json +1 -1
  30. /package/packs/{stakeholder-comms → design}/skills/deck-builder/evals/eval_queries.json +0 -0
  31. /package/packs/{stakeholder-comms → design}/skills/deck-builder/evals/evals.json +0 -0
  32. /package/packs/{stakeholder-comms → design}/skills/deck-builder/examples/exec-board-update.md +0 -0
  33. /package/packs/{stakeholder-comms → design}/skills/deck-builder/examples/templated-brand-deck.md +0 -0
  34. /package/packs/{stakeholder-comms → design}/skills/deck-builder/reference.md +0 -0
  35. /package/packs/{stakeholder-comms → design}/skills/deck-builder/requirements.txt +0 -0
  36. /package/packs/{stakeholder-comms → design}/skills/deck-builder/templates/palettes.json +0 -0
  37. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/SKILL.md +0 -0
  38. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/evals/eval_queries.json +0 -0
  39. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/evals/evals.json +0 -0
  40. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/manifest.json +0 -0
  41. /package/packs/{stakeholder-comms → design}/skills/slide-outliner/templates/deck-outline.md +0 -0
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+ """Build a print-ready, on-brand flyer as one self-contained HTML file. Stdlib only.
3
+
4
+ The spec (see templates/flyer-spec.json) gives the content; a brand.json (the brand-kit
5
+ skill writes one) gives the logo, colours and fonts. Images are embedded as data URIs, so the
6
+ .html file is the whole flyer: open it and print, or save as PDF, at 100% scale.
7
+
8
+ Usage:
9
+ python3 build_flyer.py spec.json -o flyer.html [--brand brand.json] [--pdf flyer.pdf] [--png flyer.png]
10
+
11
+ --pdf and --png use a local Chrome or Chromium in headless mode when one is installed; without
12
+ one the script says so and the HTML is still the deliverable.
13
+ """
14
+ import argparse
15
+ import base64
16
+ import html
17
+ import json
18
+ import mimetypes
19
+ import os
20
+ import re
21
+ import shutil
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ SIZES = { # (width, height) in CSS units, portrait
27
+ "letter": ("8.5in", "11in"), "a4": ("210mm", "297mm"), "a5": ("148mm", "210mm"),
28
+ "square": ("1080px", "1080px"), "story": ("1080px", "1920px"),
29
+ }
30
+ PX = {"letter": (816, 1056), "a4": (794, 1123), "a5": (559, 794), "square": (1080, 1080), "story": (1080, 1920)}
31
+ LAYOUTS = ("hero", "split", "event", "minimal")
32
+ DEFAULTS = {"primary": "#1A2A6C", "secondary": "#4A5A8C", "accent": "#FDBB2D", "background": "#FFFFFF", "text": "#222222"}
33
+ WARNINGS = []
34
+
35
+
36
+ def warn(msg):
37
+ WARNINGS.append(msg)
38
+ print(f"WARN {msg}", file=sys.stderr)
39
+
40
+
41
+ def die(msg):
42
+ print(f"ERROR {msg}", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+
46
+ def lum(hex_str):
47
+ h = hex_str.lstrip("#")
48
+ def ch(v):
49
+ v = int(v, 16) / 255
50
+ return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4
51
+ r, g, b = (ch(h[i:i + 2]) for i in (0, 2, 4))
52
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
53
+
54
+
55
+ def contrast(a, b):
56
+ la, lb = sorted((lum(a), lum(b)), reverse=True)
57
+ return (la + 0.05) / (lb + 0.05)
58
+
59
+
60
+ def ink_on(bg, text, light="#FFFFFF"):
61
+ """The readable text colour on a filled band: white or the brand text colour, whichever
62
+ contrasts more — so a pale accent gets dark text instead of illegible white."""
63
+ return light if contrast(light, bg) >= contrast(text, bg) else text
64
+
65
+
66
+ def data_uri(path, label):
67
+ if not path:
68
+ return None
69
+ p = Path(path)
70
+ if not p.exists():
71
+ warn(f"{label} not found: {p} — the flyer is built without it")
72
+ return None
73
+ mime = mimetypes.guess_type(p.name)[0] or "application/octet-stream"
74
+ if not mime.startswith("image/"):
75
+ warn(f"{label} {p.name} is not an image ({mime})")
76
+ return None
77
+ return f"data:{mime};base64,{base64.b64encode(p.read_bytes()).decode()}"
78
+
79
+
80
+ def load_json(path):
81
+ try:
82
+ return json.loads(Path(path).read_text())
83
+ except (OSError, json.JSONDecodeError) as e:
84
+ die(f"cannot read {path}: {e}")
85
+
86
+
87
+ def resolve(base, ref):
88
+ return None if not ref else (Path(ref) if Path(ref).is_absolute() else (base / ref))
89
+
90
+
91
+ def font_stack(f, generic):
92
+ if not f:
93
+ return generic
94
+ fam = f.get("family") if isinstance(f, dict) else f
95
+ fb = (f.get("fallback") if isinstance(f, dict) else None) or generic
96
+ return f"'{fam}', {fb}" if fam else fb
97
+
98
+
99
+ def esc(s):
100
+ return html.escape(str(s or ""), quote=True)
101
+
102
+
103
+ def build(spec_path, out_path, brand_ref=None):
104
+ spec_path = Path(spec_path).resolve()
105
+ base = spec_path.parent
106
+ spec = load_json(spec_path)
107
+
108
+ brand, bbase = {}, base
109
+ ref = brand_ref or spec.get("brand")
110
+ if isinstance(ref, dict):
111
+ brand = ref
112
+ elif ref:
113
+ bp = resolve(base, ref) if not brand_ref else Path(ref).resolve()
114
+ if bp.exists():
115
+ brand, bbase = load_json(bp), bp.parent
116
+ else:
117
+ warn(f"brand file not found: {bp} — using default colours and no logo")
118
+ if not brand:
119
+ warn("no brand supplied — the flyer uses default colours. Run brand-kit, or pass --brand")
120
+
121
+ c = {**DEFAULTS, **{k: v for k, v in (brand.get("colors") or {}).items() if k in DEFAULTS and v},
122
+ **{k: v for k, v in (spec.get("colors") or {}).items() if k in DEFAULTS and v}}
123
+ for k, v in c.items():
124
+ if not re.match(r"^#[0-9A-Fa-f]{6}$", v):
125
+ die(f"colour {k} is {v!r}, not a 6-digit hex code")
126
+ if contrast(c["text"], c["background"]) < 4.5:
127
+ die(f"text on background contrast is {contrast(c['text'], c['background']):.2f}:1, below 4.5:1 — fix the brand colours")
128
+ on_primary, on_accent = ink_on(c["primary"], c["text"]), ink_on(c["accent"], c["text"])
129
+ fonts = brand.get("fonts") or {}
130
+ head_font, body_font = font_stack(fonts.get("heading"), "Georgia, serif"), font_stack(fonts.get("body"), "Helvetica, Arial, sans-serif")
131
+
132
+ logo = brand.get("logo") or {}
133
+ if isinstance(logo, str):
134
+ logo = {"primary": logo}
135
+ logo_light = data_uri(resolve(bbase, logo.get("primary")), "logo")
136
+ logo_dark = data_uri(resolve(bbase, logo.get("on_dark")), "on-dark logo") or logo_light
137
+ if not logo_light and not logo_dark:
138
+ warn("no logo — the brand name is set as text instead")
139
+ name = brand.get("name") or spec.get("name") or ""
140
+
141
+ size = str(spec.get("size", "letter")).lower()
142
+ if size not in SIZES:
143
+ warn(f"unknown size {size!r}; using letter")
144
+ size = "letter"
145
+ w, h = SIZES[size]
146
+ landscape = str(spec.get("orientation", "portrait")).lower() == "landscape"
147
+ if landscape:
148
+ w, h = h, w
149
+ layout = str(spec.get("layout", "hero")).lower()
150
+ if layout not in LAYOUTS:
151
+ warn(f"unknown layout {layout!r}; using hero")
152
+ layout = "hero"
153
+
154
+ headline = spec.get("headline") or die("spec needs a headline")
155
+ if len(headline.split()) > 10:
156
+ warn(f"headline is {len(headline.split())} words; ten or fewer read from across a room")
157
+ points = spec.get("points") or []
158
+ if len(points) > 4:
159
+ warn(f"{len(points)} points; a flyer carries four at most — the rest belong on the linked page")
160
+ body = spec.get("body") or ""
161
+ if len(body.split()) > 60:
162
+ warn(f"body is {len(body.split())} words; keep it under 60")
163
+ cta = spec.get("cta")
164
+ if isinstance(cta, list):
165
+ warn("more than one call to action; a flyer gets one — using the first")
166
+ cta = cta[0] if cta else None
167
+ if not cta:
168
+ warn("no call to action — what should a reader do next?")
169
+ img = spec.get("image") or {}
170
+ hero = data_uri(resolve(base, img.get("path")), "image") if isinstance(img, dict) else data_uri(resolve(base, img), "image")
171
+ if hero and isinstance(img, dict) and not img.get("alt"):
172
+ warn("image has no alt text")
173
+ qr = data_uri(resolve(base, spec.get("qr")), "QR code")
174
+ contact = {**(brand.get("contact") or {}), **(spec.get("contact") or {})}
175
+ footer = spec.get("footer") or (brand.get("legal") or {}).get("footer") or ""
176
+ details = spec.get("details") or []
177
+
178
+ def logo_img(dark):
179
+ src = logo_dark if dark else logo_light
180
+ return f'<img class="logo" src="{src}" alt="{esc(name)}">' if src else f'<span class="wordmark">{esc(name)}</span>'
181
+
182
+ pts = "".join(f"<li>{esc(p)}</li>" for p in points[:4])
183
+ det = "".join(f'<div class="det"><span>{esc(d.get("label"))}</span><b>{esc(d.get("value"))}</b></div>' for d in details)
184
+ cta_html = (f'<div class="cta"><b>{esc(cta.get("text"))}</b>'
185
+ + (f'<span>{esc(cta.get("url"))}</span>' if cta.get("url") else "")
186
+ + (f'<img class="qr" src="{qr}" alt="QR code for {esc(cta.get("url") or cta.get("text"))}">' if qr else "")
187
+ + "</div>") if cta else ""
188
+ contact_line = " · ".join(esc(v) for k, v in contact.items() if v and k in ("website", "email", "phone", "address"))
189
+ img_html = f'<div class="img" style="background-image:url({hero})" role="img" aria-label="{esc(img.get("alt", ""))}"></div>' if hero else ""
190
+
191
+ if layout == "split":
192
+ main = f"""<div class="split"><div class="side">{img_html or f'<div class="sideblock">{logo_img(True)}</div>'}</div>
193
+ <div class="col">{'' if not img_html else logo_img(False)}<h1>{esc(headline)}</h1>
194
+ {f'<p class="sub">{esc(spec.get("subhead"))}</p>' if spec.get("subhead") else ''}
195
+ {f'<p class="body">{esc(body)}</p>' if body else ''}<ul class="pts">{pts}</ul><div class="dets">{det}</div>{cta_html}</div></div>"""
196
+ elif layout == "event":
197
+ first = details[0] if details else {}
198
+ main = f"""<header class="band">{logo_img(True)}</header>
199
+ <div class="eventtop"><div class="date"><span>{esc(first.get("label", ""))}</span><b>{esc(first.get("value", ""))}</b></div>
200
+ <div><h1>{esc(headline)}</h1>{f'<p class="sub">{esc(spec.get("subhead"))}</p>' if spec.get("subhead") else ''}</div></div>
201
+ {img_html}<div class="col">{f'<p class="body">{esc(body)}</p>' if body else ''}<ul class="pts">{pts}</ul>
202
+ <div class="dets">{"".join(f'<div class="det"><span>{esc(d.get("label"))}</span><b>{esc(d.get("value"))}</b></div>' for d in details[1:])}</div>{cta_html}</div>"""
203
+ elif layout == "minimal":
204
+ main = f"""<div class="col min">{logo_img(False)}<h1>{esc(headline)}</h1>
205
+ {f'<p class="sub">{esc(spec.get("subhead"))}</p>' if spec.get("subhead") else ''}{img_html}
206
+ {f'<p class="body">{esc(body)}</p>' if body else ''}<ul class="pts">{pts}</ul><div class="dets">{det}</div>{cta_html}</div>"""
207
+ else: # hero
208
+ main = f"""<header class="band">{logo_img(True)}</header>{img_html}
209
+ <div class="col"><h1>{esc(headline)}</h1>{f'<p class="sub">{esc(spec.get("subhead"))}</p>' if spec.get("subhead") else ''}
210
+ {f'<p class="body">{esc(body)}</p>' if body else ''}<ul class="pts">{pts}</ul><div class="dets">{det}</div>{cta_html}</div>"""
211
+
212
+ unit = "px" if size in ("square", "story") else "pt"
213
+ # Social sizes are set in px. A square has a third less height than a page, so its type is
214
+ # smaller; a story is tall and takes larger type.
215
+ scale = {"square": 1.35, "story": 2.1}.get(size, 1.0)
216
+ doc = f"""<!doctype html>
217
+ <html lang="en"><head><meta charset="utf-8">
218
+ <title>{esc(headline)} — {esc(name)}</title>
219
+ <meta name="viewport" content="width=device-width, initial-scale=1">
220
+ <style>
221
+ @page {{ size: {w} {h}; margin: 0; }}
222
+ * {{ box-sizing: border-box; }}
223
+ html, body {{ margin: 0; padding: 0; background: #e9e9e6; }}
224
+ body {{ -webkit-print-color-adjust: exact; print-color-adjust: exact; }}
225
+ .page {{ width: {w}; height: {h}; margin: {'0' if unit == 'px' else '24px'} auto; background: {c['background']}; color: {c['text']};
226
+ font-family: {body_font}; position: relative; overflow: hidden; display: flex; flex-direction: column;
227
+ box-shadow: 0 6px 30px rgba(0,0,0,.18); font-size: {11 * scale}{unit}; line-height: 1.45; }}
228
+ @media print {{ html, body {{ background: none; }} .page {{ margin: 0; box-shadow: none; }} }}
229
+ h1 {{ font-family: {head_font}; color: {c['primary']}; font-size: {40 * scale}{unit}; line-height: 1.05; margin: 0 0 .35em; letter-spacing: -.01em; }}
230
+ .sub {{ font-size: {16 * scale}{unit}; margin: 0 0 .9em; color: {c['secondary'] if contrast(c['secondary'], c['background']) >= 4.5 else c['text']}; }}
231
+ .body {{ margin: 0 0 .9em; }}
232
+ .band {{ background: {c['primary']}; color: {on_primary}; padding: {18 * scale}{unit} {36 * scale}{unit}; display: flex; align-items: center; }}
233
+ .logo {{ max-height: {44 * scale}{unit}; max-width: 55%; }}
234
+ .wordmark {{ font-family: {head_font}; font-weight: 700; font-size: {20 * scale}{unit}; }}
235
+ .band .wordmark {{ color: {on_primary}; }}
236
+ .img {{ height: {'26%' if size == 'square' else '34%'}; background-size: cover; background-position: center; }}
237
+ .col {{ padding: {28 * scale}{unit} {36 * scale}{unit}; flex: 1; display: flex; flex-direction: column; }}
238
+ .col .logo {{ margin-bottom: {18 * scale}{unit}; align-self: flex-start; }}
239
+ .pts {{ list-style: none; padding: 0; margin: 0 0 1em; }}
240
+ .pts li {{ padding-left: 1.2em; position: relative; margin: .35em 0; font-size: {13 * scale}{unit}; }}
241
+ .pts li::before {{ content: ""; position: absolute; left: 0; top: .45em; width: .55em; height: .55em; background: {c['accent']}; border-radius: 2px; }}
242
+ .dets {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(30%, 1fr)); gap: .6em 1.2em; margin: 0 0 1.2em; }}
243
+ .det span {{ display: block; font-size: {9 * scale}{unit}; text-transform: uppercase; letter-spacing: .08em; color: {c['secondary'] if contrast(c['secondary'], c['background']) >= 4.5 else c['text']}; }}
244
+ .det b {{ font-size: {13 * scale}{unit}; }}
245
+ .cta {{ margin-top: auto; background: {c['accent']}; color: {on_accent}; border-radius: 10px; padding: {16 * scale}{unit} {20 * scale}{unit};
246
+ display: grid; grid-template-columns: 1fr auto; align-items: center; gap: .2em 1em; }}
247
+ .cta b {{ font-family: {head_font}; font-size: {20 * scale}{unit}; }}
248
+ .cta span {{ grid-column: 1; font-size: {12 * scale}{unit}; }}
249
+ .cta .qr {{ grid-column: 2; grid-row: 1 / span 2; width: {64 * scale}{unit}; height: {64 * scale}{unit}; background: #fff; padding: 4px; border-radius: 4px; }}
250
+ .split {{ display: grid; grid-template-columns: 42% 1fr; flex: 1; }}
251
+ .split .side .img {{ height: 100%; }}
252
+ .sideblock {{ background: {c['primary']}; height: 100%; display: flex; align-items: flex-start; padding: {36 * scale}{unit} {28 * scale}{unit}; }}
253
+ .eventtop {{ display: grid; grid-template-columns: auto 1fr; gap: 1.4em; align-items: center; padding: {28 * scale}{unit} {36 * scale}{unit} 0; }}
254
+ .date {{ background: {c['accent']}; color: {on_accent}; border-radius: 10px; padding: .8em 1em; text-align: center; min-width: 7em; }}
255
+ .date span {{ display: block; font-size: {9 * scale}{unit}; text-transform: uppercase; letter-spacing: .08em; }}
256
+ .date b {{ font-family: {head_font}; font-size: {18 * scale}{unit}; line-height: 1.15; }}
257
+ .eventtop + .img {{ margin-top: {20 * scale}{unit}; }}
258
+ .min {{ justify-content: flex-start; }}
259
+ .min .img {{ height: 30%; border-radius: 10px; margin: 0 0 1em; }}
260
+ .foot {{ padding: {12 * scale}{unit} {36 * scale}{unit}; font-size: {9 * scale}{unit}; color: {c['text']}; border-top: 1px solid {c['secondary']}33; display: flex; justify-content: space-between; gap: 1em; }}
261
+ </style></head>
262
+ <body><main class="page">
263
+ {main}
264
+ <footer class="foot"><span>{contact_line}</span><span>{esc(footer)}</span></footer>
265
+ </main></body></html>
266
+ """
267
+ out_path = Path(out_path).resolve()
268
+ out_path.parent.mkdir(parents=True, exist_ok=True)
269
+ out_path.write_text(doc, encoding="utf-8")
270
+ print(f"wrote {out_path} ({layout}, {size}{', landscape' if landscape else ''}; "
271
+ f"brand: {'yes' if brand else 'none'}; logo: {'yes' if (logo_light or logo_dark) else 'no'})")
272
+ print(f" contrast: text {contrast(c['text'], c['background']):.1f}:1 · band {contrast(on_primary, c['primary']):.1f}:1 · "
273
+ f"call to action {contrast(on_accent, c['accent']):.1f}:1")
274
+ return out_path, size, landscape
275
+
276
+
277
+ def chrome():
278
+ for p in ("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
279
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"):
280
+ if os.path.exists(p):
281
+ return p
282
+ for n in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome"):
283
+ if shutil.which(n):
284
+ return shutil.which(n)
285
+ return None
286
+
287
+
288
+ def export(html_path, size, pdf=None, png=None, landscape=False):
289
+ exe = chrome()
290
+ if not exe:
291
+ print("No Chrome or Chromium found: open the .html in a browser and print to PDF at 100% scale, margins none.")
292
+ return
293
+ url = Path(html_path).resolve().as_uri()
294
+ if pdf:
295
+ subprocess.run([exe, "--headless=new", "--disable-gpu", "--no-pdf-header-footer",
296
+ f"--print-to-pdf={Path(pdf).resolve()}", url], check=False, capture_output=True)
297
+ print(f"wrote {Path(pdf).resolve()}" if Path(pdf).exists() else "PDF export failed — print from the browser instead")
298
+ if png:
299
+ pw, ph = PX[size][::-1] if landscape else PX[size]
300
+ subprocess.run([exe, "--headless=new", "--disable-gpu", "--hide-scrollbars", f"--window-size={pw},{ph}",
301
+ f"--screenshot={Path(png).resolve()}", url], check=False, capture_output=True)
302
+ print(f"wrote {Path(png).resolve()}" if Path(png).exists() else "PNG export failed")
303
+
304
+
305
+ def main(argv):
306
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
307
+ ap.add_argument("spec")
308
+ ap.add_argument("-o", "--output", default=None, help="output .html (default: alongside the spec)")
309
+ ap.add_argument("--brand", default=None, help="brand.json from brand-kit; overrides the spec's brand field")
310
+ ap.add_argument("--pdf", default=None, help="also write a PDF through headless Chrome, when installed")
311
+ ap.add_argument("--png", default=None, help="also write a PNG (for social sizes) through headless Chrome")
312
+ a = ap.parse_args(argv)
313
+ out = a.output or str(Path(a.spec).with_suffix(".html"))
314
+ html_path, size, landscape = build(a.spec, out, a.brand)
315
+ if a.pdf or a.png:
316
+ export(html_path, size, a.pdf, a.png, landscape)
317
+ if WARNINGS:
318
+ print(f"{len(WARNINGS)} warning(s) — fix them or tell the user before sharing the flyer")
319
+ return 0
320
+
321
+
322
+ if __name__ == "__main__":
323
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,26 @@
1
+ {
2
+ "$comment": "marketing-flyer spec. Paths are relative to this file. brand may be a path to brand.json (from brand-kit) or the same object inline; --brand on the command line overrides it. colors overrides individual brand colours for this flyer only.",
3
+ "brand": "./brand/brand.json",
4
+ "size": "letter",
5
+ "orientation": "portrait",
6
+ "layout": "event",
7
+ "headline": "Free community health fair",
8
+ "subhead": "Checkups, flu shots and healthy cooking — for the whole family",
9
+ "body": "",
10
+ "points": [
11
+ "Free blood pressure and glucose checks",
12
+ "Flu shots, no appointment needed",
13
+ "Kids' corner and healthy cooking demos"
14
+ ],
15
+ "details": [
16
+ { "label": "When", "value": "Sat 14 Nov, 10am–2pm" },
17
+ { "label": "Where", "value": "Riverside Park pavilion" },
18
+ { "label": "Cost", "value": "Free" }
19
+ ],
20
+ "cta": { "text": "Save your spot", "url": "northwindhealth.example/fair" },
21
+ "qr": "./qr-fair.png",
22
+ "image": { "path": "./riverside-park.jpg", "alt": "Families on the lawn at Riverside Park" },
23
+ "contact": { "phone": "" },
24
+ "footer": "© 2026 Northwind Health",
25
+ "colors": {}
26
+ }
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: contribution-wizard
3
- description: Guides an author through creating a new skilldrop skill from scratch — generates the manifest, SKILL.md, eval cases, and README entry from a plain-language description of the skill's purpose. Use when authoring a new skill for this repo or a private fork, ensuring a new skill meets the schema and validation requirements before submission.
3
+ description: Guides an author through creating a new skilldrop skill from scratch — generates the manifest, SKILL.md, eval cases, and skill-catalogue entry from a plain-language description of the skill's purpose. Use when authoring a new skill for this repo or a private fork, ensuring a new skill meets the schema and validation requirements before submission.
4
4
  ---
5
5
 
6
6
  # contribution-wizard
7
7
 
8
- Walks an author through the full arc of creating a new skilldrop skill: intake → manifest + SKILL.md + evals + README entry. The output is copy-paste-ready — the author drops the files into `skills/<name>/` and runs `python3 validate.py` to confirm before opening a PR.
8
+ Walks an author through the full arc of creating a new skilldrop skill: intake → manifest + SKILL.md + evals + skill-catalogue entry. The output is copy-paste-ready — the author drops the files into `packs/<pack>/skills/<name>/` and runs `python3 validate.py` to confirm before opening a PR.
9
9
 
10
10
  ## How to respond
11
11
 
@@ -89,12 +89,12 @@ Eight eval cases covering: 2 happy-path, 2 minimal/edge input, 2 anti-pattern (t
89
89
  ]
90
90
  ```
91
91
 
92
- #### `README.md` entry
92
+ #### Skill catalogue entry
93
93
 
94
- One row for the skill table, matching the format of existing rows:
94
+ One row for `guides/reference/skill-catalogue.md`, under the right category, matching the format of existing rows:
95
95
 
96
96
  ```
97
- | [`<skill-name>`](packs/<pack>/skills/<skill-name>/SKILL.md) | <One sentence describing what the skill does and the output it produces.> |
97
+ | [`<skill-name>`](../../packs/<pack>/skills/<skill-name>/SKILL.md) | <One sentence describing what the skill does and the output it produces.> |
98
98
  ```
99
99
 
100
100
  #### Pack and outcome
@@ -110,7 +110,7 @@ Next steps:
110
110
  1. mkdir -p packs/<pack>/skills/<skill-name> && cd packs/<pack>/skills/<skill-name>
111
111
  2. Drop in manifest.json, SKILL.md, and evals/cases.json
112
112
  3. python3 validate.py # must pass before opening a PR
113
- 4. Add the README row to the correct section
113
+ 4. Add the row to guides/reference/skill-catalogue.md under the correct category
114
114
  5. Add the skill to an outcome in catalogue.json
115
115
  6. If the skill has a how-to guide, add it to guides/how-to/ and guides/README.md
116
116
  ```
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "contribution-wizard",
3
- "version": "1.0.1",
4
- "description": "Guides an author through creating a new skilldrop skill from scratch — generates the manifest, SKILL.md, eval cases, and README entry from a plain-language description of the skill's purpose. Use when authoring a new skill for this repo or a private fork, ensuring a new skill meets the schema and validation requirements before submission.",
3
+ "version": "1.0.2",
4
+ "description": "Guides an author through creating a new skilldrop skill from scratch — generates the manifest, SKILL.md, eval cases, and skill-catalogue entry from a plain-language description of the skill's purpose. Use when authoring a new skill for this repo or a private fork, ensuring a new skill meets the schema and validation requirements before submission.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": { "npm": [], "pip": [] },
7
7
  "env": { "required": [], "optional": [] },
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Non-technical audiences: audience profiling, deck outlines and real .pptx decks, exec summaries, decision logs, and guides.",
2
+ "description": "Non-technical audiences: audience profiling, exec summaries, decision logs, and guides. Decks live in the design pack.",
3
3
  "display_name": "Stakeholder comms",
4
4
  "keywords": ["exec-summary", "deck", "audience", "decision-log"],
5
5
  "links": {
package/profiles.json CHANGED
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "full": {
18
18
  "description": "The complete catalogue: every pack, every reviewer agent, every loop.",
19
- "packs": ["core", "solution-architect", "product-manager", "dev-team", "sre-oncall", "stakeholder-comms", "ai-engineering", "claude-api"],
19
+ "packs": ["core", "solution-architect", "product-manager", "dev-team", "sre-oncall", "stakeholder-comms", "design", "ai-engineering", "claude-api"],
20
20
  "agents": ["devils-advocate", "security-reviewer", "code-quality"],
21
21
  "loops": ["discover", "design", "build", "release", "operate", "ship-a-draft"]
22
22
  }