name: normalize-skills
namespace: maintainer
description: >-
Audit the health of every .agents/skills/*/SKILL.md and fill icon gaps on the landing skills marketplace. Two jobs: (1) AUDIT — for each skill folder, check that frontmatter parses, name is kebab-case (/^[a-z][a-z0-9-]*$/) and matches the folder, description is ≤1536 chars, namespace is user or maintainer, and a .claude/skills/<slug> symlink exists and resolves; report drift. Complements lint:skills, does not replace it. (2) ICONS — detect skills with no landing/public/assets/skills/<slug>.webp, give each a fitting pixel-art subject in landing/scripts/gen-skill-icons.sh, run generation for the missing slug(s) only, chroma-key + composite via build-skill-icons.py, verify the webp landed, then commit + push.
USE WHEN the user types /normalize-skills, says "audit the skills", "check skill frontmatter", "which skills are missing an icon", "generate the missing skill icons", "fill the icon gaps", "fix skill drift", or after renaming/adding a skill that needs a marketplace tile.
See body for ALSO FIRE / DO NOT FIRE / HARD INVARIANTS.
normalize-skills — skill-health audit + landing icon backfill
This is a maintainer skill. It does not touch .ralphy/workspaces/<ws>/projects/ for content work. It keeps the skill bundle internally consistent and the landing skills marketplace visually complete as skills are renamed or added.
Two layers it operates on:
- The skill bundle —
.agents/skills/<slug>/SKILL.mdplus the mirrored.claude/skills/<slug>symlinks (Claude Code reads the latter; they must point back at../../.agents/skills/<slug>). - The landing icon pipeline —
landing/scripts/gen-skill-icons.sh(green-screen pixel-art PNG viaralphy generate imageinto the throwaway projectlanding-skill-icons-001) →landing/scripts/build-skill-icons.py(chroma-key + composite) → the committed tilelanding/public/assets/skills/<slug>.webp. The loaderlanding/lib/skills-loader.tsshows the webp when present, else a two-letter monogram.
Read docs/skills-format.md (frontmatter contract) and docs/developing-ralphy.md (English-only, append-only) before your first edit in a session.
Trigger
Hard triggers (always act):
- User types
/normalize-skills. - "audit the skills" / "check skill frontmatter" / "fix skill drift".
- "which skills are missing an icon" / "generate the missing skill icons" / "fill the icon gaps".
ALSO FIRE (proactively, offer — don't auto-execute)
- A new skill folder just landed under
.agents/skills/with no matching.claude/skills/<slug>symlink → offer to create the symlink and generate its icon. - A skill was renamed (folder +
namechanged) and its old webp is now orphaned / the new slug has no webp → offer the audit + backfill.
DO NOT FIRE
- User is in user mode (making a video / operating the CLI). This skill is dev-only.
- User wants to author skill content (the body, triggers, workflow) — that is normal dev work against
.agents/skills/<slug>/SKILL.md, not this audit. - User wants to regenerate an existing icon whose webp is already committed — refuse unless they explicitly ask (append-only; see HARD INVARIANTS).
HARD INVARIANTS
- Append-only on icons. Never regenerate a webp that already exists without an explicit ask. The committed
landing/public/assets/skills/<slug>.webpis the in-repo source of truth for that tile. The 1024px green-screen PNG sources are NOT committed and are throwaway. Generate only for slugs with NO committed webp. The defaultgit statusafter a run is N new untracked webps, zero modified. - English only on disk. Every frontmatter field, slug, gen-script subject, and prose line lands in English (per
docs/developing-ralphy.md). Gate before commit:rg --pcre2 '\p{Cyrillic}' .agents/skills landing/scriptsmust be empty. - Generation needs
OPENROUTER_API_KEY. It is read from.envby the CLI. Each icon is ~$0.15 ongoogle/gemini-3-pro-image-preview(default). Sum the count of missing slugs × $0.15 and surface the cost estimate to the user before running a paid pass. - The audit never mutates
SKILL.mdcontent silently. Report drift and propose the fix; only apply a frontmatter edit on explicit say-so. This skill complementslint:skills(which errors on frontmatter); it adds the symlink + icon dimensions that lint does not cover. - Run gen per-slug for the missing slugs only —
bash landing/scripts/gen-skill-icons.sh <slug>. The build scriptbuild-skill-icons.pyreprocesses every PNG in the source dir, so after building, revert any committed webp it rewrote and delete any stray output, leaving only the new tiles (see Workflow step 6).
Workflow
AUDIT — skill-health pass
- Enumerate skills.
fd -t d -d 1 . .agents/skills(orls .agents/skills/). For each that contains aSKILL.md: - Frontmatter checks (mirror
scripts/lint-skills.ts, then add the extras):namepresent, kebab-case/^[a-z][a-z0-9-]*$/, equals the folder name.descriptionpresent, ≤ 1536 chars (move ALSO FIRE / DO NOT FIRE / HARD INVARIANTS into body sections if over).namespace, if present, isuserormaintainer(never invent a third value).
- Symlink check.
.claude/skills/<slug>exists, is a symlink, and resolves to../../.agents/skills/<slug>. Flag dangling symlinks (e.g. a renamed/removed skill whose.claude/skillsentry now points at nothing). - Run the real lint alongside:
bun run lint:skills(errors on frontmatter) — this skill reports, it does not replace the gate. - Report drift as a table: slug | frontmatter ok? | symlink ok? | has webp? Then stop and ask before mutating anything.
ICONS — backfill missing tiles
- Find icon gaps. For every skill slug, check
landing/public/assets/skills/<slug>.webp. The set with no webp is the work list. - Ensure each missing slug has a gen entry. Open
landing/scripts/gen-skill-icons.sh; if a slug is absent fromITEMS, add a line"<slug>|<concrete single pixel-art subject>"matching the existing style (one centered object: e.g.kanban board with three columns,framed poster with a big bold letter). Keep subjects English, concrete, single-object. - Estimate + confirm cost.
count × $0.15. Surface it. Generation is paid — get a go-ahead. - Generate per-slug (missing only):
Each printsfor slug in <missing-slugs>; do bash landing/scripts/gen-skill-icons.sh "$slug"; doneok: <slug>orFAIL: <slug>. On FAIL (geo-block / safety filter / quota), STOP, report which slug + the error, keep what succeeded — do not blind-retry. - Build the tiles.
cd landing && python3 scripts/build-skill-icons.py(needs Pillow;pip install Pillowif absent). It writes<slug>.webpfor every PNG in the source dir. - Reconcile to append-only (step-5 invariant). The build rewrites existing webps and may emit stale-named tiles from old PNG sources:
git checkout -- landing/public/assets/skills/ # restore committed tiles byte-for-byte rm -f landing/public/assets/skills/<stale>.webp # drop any output not in the missing set git status --short landing/public/assets/skills/ # MUST be only the N new untracked webps - Verify. Exactly N new
??webps, zeroMmodified. Visually inspect (read a Pillow contact sheet) — crisp pixel-art, correct category background, on-theme subject. - Build the landing to confirm the loader picks them up:
cd landing && bunx next build→ exit 0. - Commit + push (dev-release channel discipline —
originmain per repo convention): stage the new webps + thegen-skill-icons.shedits + any symlink fixes, run the Cyrillic gate, commit, push toorigin.
Cookbook
# Which skills have no committed icon?
for d in .agents/skills/*/; do s=$(basename "$d"); [ -f "$d/SKILL.md" ] || continue; \
[ -f "landing/public/assets/skills/$s.webp" ] || echo "missing: $s"; done
# Dangling .claude/skills symlinks
for l in .claude/skills/*; do [ -e "$l" ] || echo "broken: $l"; done
# Add a missing slug to the gen script, then generate just that one
# edit landing/scripts/gen-skill-icons.sh ITEMS: "myslug|single centered object"
bash landing/scripts/gen-skill-icons.sh myslug
cd landing && python3 scripts/build-skill-icons.py && cd ..
git checkout -- landing/public/assets/skills/ # keep the 25/N committed tiles unchanged
git status --short landing/public/assets/skills/
# Gates before commit
bun run lint:skills
rg --pcre2 '\p{Cyrillic}' .agents/skills landing/scripts # must be empty
cd landing && bunx next build # exit 0
Outputs
- A skill-health report: per-slug frontmatter / symlink / icon status, plus
lint:skillsresult. - N new
landing/public/assets/skills/<slug>.webptiles for the previously-iconless skills, with the 25 (or however many) pre-existing tiles byte-unchanged. - New
slug|subjectrows inlanding/scripts/gen-skill-icons.shfor any slug that lacked one. - A clean
next buildand a commit pushed toorigin(when the user asks to ship).
Note on the two category classifiers
build-skill-icons.py picks the tile background colour from category_for(slug) (prefix-based: dev- → Maintainer, ugc- → UGC niches, render-engine list → Render engine, else Workflow). skills-loader.ts uses a richer categoryFor that also folds content-niche slugs (poster, carousel, fb-creatives, analog-horror-psa, audio-explainer) into "UGC niches". These two can disagree on the tile background for a content-niche skill (the Python defaults it to Workflow-blue). The tile still renders; if you want the background to match the loader category, align category_for in the Python script — but that re-tints existing tiles, so treat it as a deliberate, separately-confirmed change, not part of a routine backfill.