Design Md logo

Design Md

Community
CaesiumY
design-md

Add a new design.md catalog entry to ko-design-md. Use this skill IMMEDIATELY when the user wants to onboard a new brand into THIS project's catalog — produce services/{slug}.md (Stitch v0.1 format) plus services/{slug}.tokens.json (token-card sidecar) plus public/preview/{slug}/preview.html (one file carrying both themes) plus the OG image. Trigger phrases include "add to design.md catalog", "new design.md entry for X", "onboard X to ko-design-md", "X를 ko-design-md에 추가", "X의 design.md 만들어줘", "/design-md", "X 카탈로그 항목 만들기", or any variant where the user is asking to populate this catalog with a new brand entry. Do NOT use for editing prose in an existing entry, fixing one frontmatter field, generating non-catalog design docs, or working in any other repository. The skill operates only inside the ko-design-md repo and verifies this via `package.json` name.

Overview

PublisherCaesiumY
Repositoryko-design-md
Skill namedesign-md
Stars
65
Forks
5
Bundled files
5
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 5 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by CaesiumY on GitHub. Read the source before you install it.

Installation

Install the Design Md AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/CaesiumY/ko-design-md/tree/main/.claude/skills/design-md
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/CaesiumY/ko-design-md.git /tmp/ko-design-md
mkdir -p .claude/skills
cp -r /tmp/ko-design-md/.claude/skills/design-md .claude/skills/design-md
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Design Md in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Design Md on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Design Md is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

/design-md skill — orchestration body

This skill builds a complete catalog entry through a 5-subagent pipeline with one user checkpoint every entry hits (Stage 7), plus a conditional one ahead of it when a design board is the upstream (Stage 4c). The pipeline is heavy (research, drafting, two review loops) so resumability matters: each stage's artifact lives on disk in .claude/cache/design-md/{slug}/ and the next stage reads from there. State is encoded by file presence — no separate state.json needed for v1.

Pipeline shape

[INTAKE] → research-collector → design-md-author ⇄ design-md-reviewer (loop ≤3)
                                                          ↓ score≥8 or N=3
                                                    [USER CHECKPOINT]
                                                          ↓ approve
                                                [WRITE_MD] → [TOKENS]
                                                          ↓
                                  preview-html-author ⇄ preview-html-reviewer (loop ≤3, non-blocking;
                                                          machine gate + render contrast before each review)
                                                          ↓
                                                   [WRITE_PREVIEW]
                                                          ↓
                                                       [BUILD_OG]
                                                          ↓
                                                       [VERIFY]
                                                          ↓
                                                         END

Loop termination: design loop is blocking — score must reach 8/10 within 3 iterations or the user decides at the checkpoint. Preview loop is non-blocking — proceed with warning if score < 8 at iteration 3.

Key reference files (read these before dispatching subagents that need them):

  • .claude/skills/design-md/references/stitch-format.md
  • .claude/skills/design-md/references/design-md-template.md
  • .claude/skills/design-md/references/rubric-design.md
  • .claude/skills/design-md/references/rubric-preview.md

Stage 1 — Preflight

Verify the working environment before doing anything user-visible.

  1. Bash: pwd to capture the absolute repo root. Hold this value as ${repo_root} in your reasoning and substitute it literally into every later Bash command and dispatch prompt that touches a repo path. The shell preserves cwd across calls, but pinning the absolute path makes Stage 8/10/11 robust to any inadvertent cd.
  2. Bash: date +%Y-%m-%d to capture today's date. Hold this value as ${today} in your reasoning. Stage 6a passes this to the author for both the last_updated and created_at frontmatter fields — a brand-new entry is added and last-updated on the same day. The site's build (src/lib/site-frontmatter.ts) rejects any other format, and missing-created-at in src/lib/draft-validator.ts blocks a draft that omits created_at.
  3. Read ${repo_root}/package.json. If "name" is not exactly "ko-design-md", abort with: "이 스킬은 ko-design-md 레포 안에서만 동작합니다. 현재 디렉터리: ${repo_root}". Do not proceed.
  4. Verify ${repo_root}/src/lib/content-types.ts is readable. If not, abort.
  5. Read ${repo_root}/src/lib/content-types.ts and extract the live CATEGORIES const. Use this as the source of truth for the intake category picker (do NOT hardcode the enum from memory — it can drift).

Stage 2 — Conversational intake

Use a single AskUserQuestion form with these 3 questions (multi-select where indicated):

  1. 브랜드명 (text via "Other" → custom input): e.g. "토스", "당근", "구름". Use the Korean company/brand display name as it should appear in the name frontmatter, not the design system product name. If research later surfaces a distinct design system name (e.g. "SEED Design", "Vapor UI"), the author stores that in optional design_system_name.
  2. 참고 URL (text via "Other"): comma-separated URLs. 2개 이상 권장 — 1개만 입력 시 research-collector가 INSUFFICIENT_SOURCES로 중단할 수 있고, 그 경우 스크린샷 보강 필요. Brand homepage, design system page, blog post about their UI, etc.
  3. 카테고리 (single-select): all values from CATEGORIES const, in order. Last option is etc.

Do not ask for a language. An entry is one Korean design.md — lang is always ko (docs/adr/0001-korean-design-md-only.md), and every dispatch below passes it as a literal.

Then ask four follow-up text inputs:

  • 스크린샷 경로 (optional) — comma-separated absolute paths to screenshot files. The user can type "없음" to skip.

  • 로고 자산 경로 (required) — an existing local file path for a brand logo. Accept only .svg, .png, .webp, or .avif. There is no "없음" answer: every catalog entry carries a logo, and validate:draft blocks a draft without one (missing-logo). If the user has no file at hand, they answer 후보 찾기, which sets logo_candidates_requested = true and leaves logo_asset_path empty — the candidates are gathered at Stage 4a, once Stage 3 has derived the slug and Stage 4 has created the cache (see Logo candidates below).

    CRITICAL — pick a small square symbol mark, NOT a wordmark. Two square slots in the site consume this asset: the catalog grid card (~48–96 px on screen) AND the OG image's top-left brand mark (32×32 px in the 1200×630 social card, see src/og/template.tsx). The OG renderer (Satori) has limited object-fit support, so a non-square asset is stretched into the 32×32 box rather than letterboxed — the catalog card has the same constraint at its own scale. Choose accordingly:

    • ✅ Pick the brand's standalone symbol / mark / favicon shape with a transparent background — e.g. SOCAR's angular blue mark, Toss's curved oval lens, Gmarket's circular G, Baemin's symbol. Match the style of existing public/logos/{toss,socar,baemin,…}.png (square, no text, no baked-in frame).
    • ❌ Avoid the horizontal wordmark / lockup (the brand name written out, e.g. "Gmarket", "toss", "쏘카") — wordmarks render too small in the grid card or break its aspect.
    • ❌ Avoid iOS-squircle / framed app icons with a rounded gradient background baked in — that frame conflicts with the catalog card's own background. Prefer the unframed symbol form. One exception: a brand that publishes no symbol at all (wordmark only), or publishes its symbol only as web icons, may use its app-store app icon as the fallback — see Logo candidates below.
    • When a bundle provides multiple variants (e.g. logo-brand.png wordmark vs logo-circular-g.png symbol vs logo-app-icon.png framed), the unframed symbol is correct for the catalog grid. Filename hints for the GRID-WRONG forms: *wordmark*, *logotype*, *-brand*, *-horizontal*, *-app-icon* (framed). Filename hints for the GRID-RIGHT form: *-symbol*, *-mark*, *-circular*, *-icon* (when unframed), or a generic {slug}.png that is already a symbol.

    Optional second asset — wordmark/logotype for the preview hero. The catalog grid uses the symbol, but the preview HTML hero (public/preview/{slug}/preview.html) has room for a richer brand lockup with the brand name visible. If the source provides BOTH a symbol AND a horizontal wordmark/logotype, capture both paths. Stage 4a will place the wordmark at public/logos/{slug}-logotype.{ext} (matching the existing toss-logotype.png convention), and the preview-html-author renders the wordmark in the hero where there is space. The grid card always uses the symbol; the wordmark has no frontmatter field — it stays a site-internal preview-only asset (the design.md's logo frontmatter URL still points to the grid logo (the symbol, or its Logo candidates fallback) so the file remains portable outside ko-design-md).

  • 디자인 시스템 문서 사이트 URL (optional) — if the brand publishes its design system as a documentation website (not only Figma), the root URL of that site (e.g. https://socarframe.socar.kr/). Stage 4b crawls it into a research corpus. The user can type "없음" to skip.

  • 디자인 보드 / 핸드오프 번들 경로 (optional) — comma-separated entries for the board the user wants approved: a published Claude Design artifact link (claude.ai/artifact/… or claude.ai/code/artifact/…), the board's own source files (.dc.html artboards and canvas.json from a Design artifact, a handoff bundle's colors_and_type.css, *.md and component source), a directory a handoff bundle was already extracted into, or exported frame images. Ask for the source before the frames: source files carry the board's values as exact text, while a frame is a picture research can only estimate from, so frames alone are the last resort. The user can type "없음" to skip. This is what makes Stage 4c fire; without it there is nothing to approve and the run goes straight from Stage 4b to Stage 5. Ask for it even when the brand has a docs site — the two are different upstreams and an entry can have both. A handoff link expires; an artifact link does not. A Claude Design handoff URL 404s in roughly fifteen minutes, so the user must have already saved that bundle locally — do not accept a handoff URL here. A published artifact link stays readable, and Stage 4c fetches its source files.

Capture the answers as: brand_name, source_urls (parsed array), category, screenshot_paths (parsed array, may be empty), logo_asset_path (a file path, or empty when 후보 찾기 was answered), logo_candidates_requested (true only for 후보 찾기), docs_site_url (string or empty), design_board_paths (parsed array, may be empty).

Screenshot path preflight: for each path in screenshot_paths, run Bash: [ -f "$path" ]. If any path is missing, surface the missing list to the user and re-prompt the screenshot question. This avoids research-collector failing silently mid-read.

Logo path preflight: unless logo_candidates_requested is true, run Bash: [ -f "$logo_asset_path" ] and verify the extension matches svg|png|webp|avif. If missing or unsupported, surface the problem and re-prompt the logo question.

Logo candidates (runs at Stage 4a step 3 — never during intake, because {slug} and its cache directory do not exist yet): look on the brand's own domains first — a standalone symbol or the favicon (convert .ico to .png) — and, when the brand publishes only a wordmark, fall back to its app-store app icon. A brand that publishes its symbol only as web icons may use the app icon too: its own domains serve the unframed symbol only as a favicon, web app manifest icons or apple-touch-icon, never as an SVG or a separately published logo file (a lockup that puts the symbol and the wordmark in one file does not count as a standalone symbol). Decide this by where the files are served, not by how sharp they look — web icon sets are often a small original scaled up, which renders blurry in the card. A brand with only a favicon falls under this rule too, by design. Offer both candidates; when the user picks the app icon, record that reason in the NOTICE row. This rule does not trigger a bulk change of existing entries; each one is decided when it is re-audited. The app icon is the one allowed exception to the no-framed-icon rule (Stage 2 and Stage 4a): prefer an official variant without the baked-in background, and otherwise use the icon framed as the brand published it. Never crop, recolor or otherwise derive a new image from it — a self-made derivative is not an asset the catalog is entitled to use (NOTICE covers official assets for identification and reference only). A pure container conversion is not a derivation: taking the largest frame out of an .ico and saving it as .png with its pixels, size and transparency unchanged is allowed, since the catalog accepts only .svg/.png/.webp/.avif. Only when no app icon exists either, a wordmark may be offered with a warning that it is a known catalog-grid mismatch. If no candidate exists even then, stop the run (취소, cache kept) — do not continue without a logo and do not loop back to intake. Download candidates into .claude/cache/design-md/{slug}/logo/ only, show them to the user, and take the one they pick as logo_asset_path. Never place a downloaded logo under public/logos/ without that pick.

Stage 3 — Slug derivation + conflict resolution

Derive slug from brand_name:

  1. NFD-normalize and strip diacritics/non-ASCII.
  2. Lowercase, replace [^a-z0-9]+ with -, trim leading/trailing -.
  3. If the result is empty (Korean-only brand with no Latin form), prompt the user via AskUserQuestion for an explicit slug. Question wording: "slug은 영문 소문자/숫자/하이픈만 가능합니다 (예: toss, karrot-market)." Validate the user's input matches ^[a-z0-9-]+$; on mismatch, re-prompt.

Check for conflicts via Bash (ls services/{slug}.md 2>/dev/null):

  • No conflict → proceed.
  • Conflict, and the existing slug is in LOGO_TAKEDOWNS (src/lib/logo-takedowns.ts) → stop and tell the user: this brand's logo was removed at the rights holder's request, and onboarding it again under another slug would re-fetch that logo through Logo candidates. Do not offer "다른 slug 사용".
  • Conflict otherwise → AskUserQuestion:
    • "다른 slug 사용" → user provides a new slug, recheck.
    • "기존 항목 업데이트" → set mode = update. The pipeline still runs but final write overwrites.
    • "취소" → abort.

Stage 4 — Cache setup

Create the staging directory:

bash
mkdir -p .claude/cache/design-md/{slug}

This directory holds all intermediate artifacts. It's gitignored (.claude/cache/ was added to .gitignore when the skill was installed) so partial work won't leak into PRs.

Stage 4a — Logo asset resolution

Before resolving paths — verify the logo asset is the right FORM. The catalog grid card uses a small square logo slot, so the chosen asset MUST be a symbol / mark / favicon shape (transparent background, no text), NOT a horizontal wordmark and NOT an iOS-squircle app icon with a baked-in background. When auto-picking from a bundle/zip that contains multiple variants, prefer filenames matching *-symbol*, *-mark*, *-circular*, or unframed *-icon*; reject filenames matching *wordmark*, *logotype*, *-horizontal*, *-brand* (often the wordmark), or *-app-icon* (often the iOS-squircle framed form). If the brand publishes no symbol, or publishes it only as web icons (see Logo candidates), the app-store app icon from Logo candidates is the allowed exception to this rule and takes priority over any wordmark. Whenever the app icon is used because the symbol exists only as web icons — picked from Logo candidates or given as a path at intake — its NOTICE row names that reason. Only when no app icon exists either may a wordmark be offered, and then only after the user confirms it — wordmarks are a known catalog-grid mismatch (see Stage 2's logo intake rule and the existing public/logos/{toss,socar,baemin,...}.png reference). This check applies inside step 1 below.

Resolve two logo values before dispatching author agents — different downstream concerns need different forms:

  • logo_url — fully-qualified URL like https://getdesign.kr/logos/toss.png. Goes into design.md frontmatter, where it must stay meaningful when the file is copied outside the ko-design-md site (PRD User Story 1 — vibe-coding flow).
  • logo_src_path — site-relative path like /logos/toss.png. Goes into preview HTML <img src>, which is only ever loaded inside the catalog site's iframe. Keeping it relative avoids making dev/staging depend on the production-domain asset.

Both are always set — an entry without a logo does not leave this stage.

The canonical site origin is https://getdesign.kr. Change this constant in one place only — this paragraph — if the origin ever moves.

  1. Takedown check first. If {slug} is in LOGO_TAKEDOWNS (src/lib/logo-takedowns.ts), its logo was removed at the rights holder's request (docs/TAKEDOWN.md). Do not search for or place any logo — stop the run and tell the user; restoring that logo is the maintainer's decision, not this pipeline's. A new slug does not lift a takedown: also stop when brand_name matches the name or design_system_name of any entry listed in LOGO_TAKEDOWNS (read those services/*.md frontmatters), since a different slug for the same brand would otherwise re-fetch the removed logo. A fully removed entry leaves no LOGO_TAKEDOWNS row, so also search every ### Removed section of CHANGELOG.md for brand_name or {slug} on a line saying 권리자 요청 (docs/TAKEDOWN.md §7 writes both) — a hit stops the run the same way. A private takedown (Security Advisory) leaves no public trace at all, so before running Logo candidates also ask the user with AskUserQuestion whether this brand has a takedown request on record in a private advisory; a yes stops the run. Takedown entries are not updated through this skill at all (Stage 3 stops on them too) — re-audit such an entry by hand, since validate:draft exempts its absent logo key.
  2. If logo_asset_path is a file path (never the case when logo_candidates_requested is true, until step 3 fills it):
    • Verify it exists and has a supported extension (svg, png, webp, avif).
    • If it already lives under ${repo_root}/public/logos/, set logo_src_path = /logos/{basename} and logo_url = https://getdesign.kr/logos/{basename}.
    • Otherwise copy it to ${repo_root}/public/logos/{slug}.{ext} and set logo_src_path = /logos/{slug}.{ext} and logo_url = https://getdesign.kr/logos/{slug}.{ext}. This is allowed only for a user-supplied local file or a Logo candidates file the user picked.
  3. If logo_candidates_requested is true, first auto-detect an asset already placed for this slug. With mode = update, look first at the file the existing services/{slug}.md frontmatter logo points to (entries such as vapor-ui · seed-design · teamsparta use a file not named {slug}.*) and keep it — set logo_url to that frontmatter value verbatim and logo_src_path to it with the https://getdesign.kr origin removed; otherwise take the first existing file in public/logos/{slug}.{svg,png,webp,avif} (in that order) and set logo_src_path = /logos/{slug}.{ext} and logo_url = https://getdesign.kr/logos/{slug}.{ext}.
  4. If logo_candidates_requested is true and step 2 found nothing, run Logo candidates now: Stage 3 has derived {slug} and Stage 4 has created the cache, so the candidates land in .claude/cache/design-md/{slug}/logo/. Take the user's pick as logo_asset_path and run step 1 with it. If no candidate exists, stop the run as Logo candidates says. A logo-less entry is not a valid pipeline outcome — the catalog grid card would fall back to a first-letter badge and the OG image to text only, and validate:draft blocks the draft (missing-logo).
  5. Optional wordmark / logotype for the preview hero. If a wordmark variant was captured at Stage 2 (a horizontal lockup that contains the brand name as text — e.g. logo-brand.png, *-logotype.svg), copy it to ${repo_root}/public/logos/{slug}-logotype.{ext} and set logo_wordmark_src_path = /logos/{slug}-logotype.{ext}. If no wordmark was captured at intake but a file already exists at public/logos/{slug}-logotype.{svg,png,webp,avif}, auto-detect it (same precedence order as the symbol). Otherwise set logo_wordmark_src_path = "". There is NO frontmatter URL for the wordmark — it is a site-internal preview-only asset; the design.md logo field always references the grid logo (the symbol, or its Logo candidates fallback — when that fallback is itself a confirmed wordmark it goes to logo, not to {slug}-logotype) so the file remains portable outside ko-design-md.

The auto-detect pattern for the catalog grid logo is exactly public/logos/{slug}.{svg,png,webp,avif}; for the optional wordmark it is public/logos/{slug}-logotype.{svg,png,webp,avif}. Every later stage must preserve the symbol values exactly — design-md-author writes logo_url verbatim into frontmatter, preview-html-author embeds logo_src_path as <img src> (or logo_wordmark_src_path in the hero when that is non-empty), and the Stage 10 grep checks match each file against the appropriate form.

Stage 4b — Docs-site crawl (conditional)

If docs_site_url is empty or "없음", skip this stage and set crawl_corpus_path = "none".

Otherwise, crawl the brand's documentation site into the cache directory so research-collector can use it as a primary source. This runs the docs-crawler skill's engine — a sitemap-driven crawl with a JS-render fallback that also localizes images (external and inline base64) into crawl/images/, so the cached corpus is self-contained:

bash
cd "${repo_root}" && pnpm crawl:docs "${docs_site_url}" --out "${repo_root}/.claude/cache/design-md/{slug}"

The crawl writes crawl-corpus.md (the merged corpus) plus crawl/pages/*.md, the downloaded crawl/images/, and crawl/manifest.json into the (gitignored) cache directory. The first crawl of a JavaScript-rendered site auto-installs a headless browser (~150MB, one-time).

After it returns, verify the corpus landed:

bash
[ -s "${repo_root}/.claude/cache/design-md/{slug}/crawl-corpus.md" ] && echo CORPUS_OK || echo CORPUS_MISSING
  • CORPUS_OK → set crawl_corpus_path = ${repo_root}/.claude/cache/design-md/{slug}/crawl-corpus.md.
  • CORPUS_MISSING, or the crawl exited non-zero → the crawl failed. It is best-effort: research can still proceed from source_urls. AskUserQuestion: "문서 사이트 크롤 실패 — (a) 다시 시도 / (b) 크롤 없이 진행 / (c) 취소". On "다시 시도" re-run the crawl; on "크롤 없이 진행" set crawl_corpus_path = "none"; on "취소" abort with the resume path.

Stage 4c — Design board checkpoint (conditional)

Run this stage when design_board_paths from Stage 2 is non-empty; otherwise skip it and go to Stage 5. That variable is the trigger — the condition is not a judgment call.

Preflight: fetch the board's source first, then read it. For an artifact link, use the Artifact tool — list its published files (action: "list", scope: "files"), read every artboard and data file into the cache (action: "read" with paths and out_dir set to .claude/cache/design-md/{slug}/board/), and put the saved files in place of the link. If the session has no Artifact tool, ask the user to save the board's source files locally instead. For each local path, Bash: [ -e "$path" ]. Surface any missing path and re-prompt, the same as the screenshot preflight. If every entry is a frame image, ask once with AskUserQuestion whether the board's source exists (its artifact link or source files) before going on with the frames alone — keep the frames either way, as what the user looked at. Then Read the board (image paths read as images, source files read as text; a directory is expanded recursively, subdirectories included, and every file in it read). A file research cannot read — neither text nor an image Read opens (a font, an archive, an .avif) — is named to the user here, before approval, so the board they approve is exactly the set of files that will reach research.

Show what the board settled and what it left open: the palette in both themes, the type scale, the components it laid out, and anything it declined to define. Then AskUserQuestion: "디자인 보드를 확인해 주세요 — (a) 승인하고 리서치로 / (b) 보드를 고치고 다시 / (c) 취소". On (b), the user's corrections go back to the board; nothing downstream is generated until it is approved.

On approval, carry the board forward — the gate is worthless if the values stop here. Before Stage 5 dispatches, collect the approved board into board_source_paths, reusing the listing the preflight already produced: a path that was a directory is expanded recursively to the files inside it and its subdirectories (a bundle keeps component source under components/), and the directory string itself is never handed over. Every file research-collector can read goes in — source files it reads as text (.dc.html, .html, .css, .md, .json, component source such as .jsx·.tsx·.js, .svg, .txt) and frame images it reads as images (whatever Read opens as one; extensions are matched case-insensitively, so an exported FRAME.PNG counts). Decide by "can it be read", never by a list of extensions: a handoff bundle also carries component source (Drawer.jsx), and an allow-list drops whatever it did not name. A file it cannot read goes nowhere, and the preflight has already named it. Board frames do not go into screenshot_paths. That list holds captures of the live service, and a board frame in it reads as one more capture: research would cite it as [src:screenshot:…], estimate from it, and could overrule the board where a public source disagrees — the board rule would cover only the board's text half. research-collector's tools are WebFetch, WebSearch, Read, Write — it has no Bash to list a directory, and Read cannot open one, so a directory handed over here is a source that silently never gets read. The source matters most: a handoff bundle's canonical tokens live in colors_and_type.css, and a Design artifact's values live in its .dc.html source, so handing over only images used to drop the board's exact values and leave research to re-read them from pictures. Without this step Stage 5 receives only source_urls and the crawl corpus, and the run rebuilds from public research alone while the approval implies otherwise.

This is what puts the board upstream of research.md, and through it upstream of the draft, the preview, the sidecar and the OG image.

The asymmetry is what justifies a second gate. Approving a wrong board costs the entire run, because every later artifact is rebuilt from it — and the Stage 7 checkpoint cannot recover it, since by then the draft has already transcribed the wrong values and reads as internally consistent. Approving a right one pays off at Stage 12: on remember the board's twenty role colors survived the whole chain into the preview unchanged, and the one discrepancy the comparison found was not a value but where a value had been applied — which no gate in this pipeline looks at.

An entry researched from public sources alone has no board to approve and goes straight to Stage 5.

Stage 5 — Research (research-collector)

Dispatch via Agent tool with subagent_type: "research-collector". Pass this prompt:

Research the brand "{brand_name}" (slug: {slug}) for ko-design-md catalog onboarding.

source_urls: {comma-separated URLs}
screenshot_paths: {comma-separated paths or "none"}
board_source_paths: {comma-separated files of the approved board collected at Stage 4c — source files and frame images — or "none"}
crawl_corpus_path: {crawl_corpus_path from Stage 4b — absolute path to crawl-corpus.md, or "none"}
category: {category}
lang: ko
cache_dir: {absolute path to .claude/cache/design-md/{slug}/}

Follow your agent definition. If board_source_paths is not "none", read it first: it is the board the user approved at Stage 4c. If crawl_corpus_path is not "none", read that corpus next as your primary public source. Write exactly one file at {cache_dir}/research.md with the cited-claims structure. Halt with INSUFFICIENT_SOURCES only if crawl_corpus_path is "none" AND fewer than 2 URLs return 2xx.

After the agent returns, Read {cache_dir}/research.md.

  • If the first line of ## Sources is INSUFFICIENT_SOURCES, surface this to the user via AskUserQuestion with options: "URL 추가 입력" / "스크린샷 경로 추가" / "취소". On URL/screenshot addition, re-dispatch research-collector with the augmented inputs.
  • Section sanity check: Bash: grep -c '^## ' {cache_dir}/research.md. Expected output is 10 — the nine content sections plus ## Sources, one per H2 section research-collector documents. If less than 10, the agent silently produced a malformed file — re-dispatch with an instruction prefixed: "Your previous research.md was malformed (only N sections found). Produce ALL 10 H2 sections in the documented order, including ## Sources, even if some are (no public evidence found)."
  • Otherwise, proceed.

Stage 6 — Draft + design.md review loop

Iteration counter N = 1. Loop:

6a. Dispatch design-md-author

Via Agent with subagent_type: "design-md-author". Pass:

Author a Stitch v0.1-format design.md draft for "{brand_name}".

cache_dir: ${repo_root}/.claude/cache/design-md/{slug}/
slug: {slug}
name: {brand_name}
category: {category}
lang: ko
today: {today as YYYY-MM-DD}
logo_url: {logo_url}
research_path: ${repo_root}/.claude/cache/design-md/{slug}/research.md
prior_review_path: ${repo_root}/.claude/cache/design-md/{slug}/review-{N-1}.json or "none" on first pass
format_reference_path: ${repo_root}/.claude/skills/design-md/references/stitch-format.md
template_path: ${repo_root}/.claude/skills/design-md/references/design-md-template.md
demo_paths: (none — leave empty by default; pass an existing ${repo_root}/services/*.md only if a stylistic peer genuinely fits the new brand. The early _demo-*.md fixtures have been removed.)

Follow your agent definition. Write {cache_dir}/draft.md.

After return, verify {cache_dir}/draft.md exists and is non-empty. If missing, the author failed — log the issue, retry once with the same prompt; if still missing, abort with a diagnostic message.

6a2. Deterministic draft gate (machine validation)

Before spending a reviewer dispatch, run the draft validator — it covers every mechanically checkable rubric item (frontmatter read and field values, section presence/order, OKLCH-only token values, [src:N]/References integrity, expected logo) so the reviewer model never has to "grep mentally":

bash
cd "${repo_root}" && pnpm validate:draft .claude/cache/design-md/{slug}/draft.md \
  --slug {slug} --expected-logo {logo_url} --lang ko \
  --iteration {N} --json-out "${repo_root}/.claude/cache/design-md/{slug}/review-machine-{N}.json"
  • Exit 0 → proceed to 6b, passing the machine report path (see the 6b prompt).
  • Exit 1 (block issues) → do NOT dispatch the reviewer. Re-dispatch 6a with prior_review_path = the review-machine-{N}.json above (its issues[] uses the same severity/section/fix shape the author already consumes). Machine retries use a sub-counter K (max 2) and do not increment N — machine fixes are cheap and must not consume the semantic-review budget.
  • K exhausted with blocks remaining → dispatch 6b anyway; the reviewer receives the failing machine report and the normal loop/checkpoint rules take over (no new termination path).

6b. Dispatch design-md-reviewer

Via Agent with subagent_type: "design-md-reviewer". Pass:

Score the draft.md at {cache_dir}/draft.md against the rubric.

cache_dir: {abs path}/.claude/cache/design-md/{slug}/
draft_path: {cache_dir}/draft.md
research_path: {cache_dir}/research.md
content_types_path: {abs path}/src/lib/content-types.ts
rubric_path: {abs path}/.claude/skills/design-md/references/rubric-design.md
expected_logo_url: {logo_url}
machine_report_path: {cache_dir}/review-machine-{N}.json
iteration_n: {N}
output_path: {cache_dir}/review-{N}.json

Follow your agent definition. Write exactly one file at output_path.

The machine report has already verified the deterministically checkable items
(frontmatter read and field values, section presence/order, OKLCH-only token scan,
[src:N]/References integrity, expected logo). Do not re-verify those — spend
your review on judgment items: Brand fidelity semantics against research.md,
Voice/tone, and cross-section token contradictions.

After return, Read {cache_dir}/review-{N}.json.

6c. Loop decision

  • If review.passed && review.score >= 8 → exit loop, go to Stage 7.
  • Else if N < 3 → N += 1, go back to 6a (the author will read review-{N-1}.json and revise).
  • Else (N == 3 and not passed) → exit loop with a warn flag; go to Stage 7. The user will see the failed verdict at the checkpoint and decide.

Stage 7 — User checkpoint

This is the gate every entry passes through — Stage 4c precedes it only when a design board is upstream. Show the user:

  1. The current draft.md content (read it and display the full file inline, formatted as markdown — paste in code fences).
  2. The latest review-{final}.json verdict — extract score, passed, verdict, and bullet the issues array.
  3. If iteration > 1, show a brief diff highlight: "Iter 1 score: X → Iter {final} score: Y" plus the top 1–2 issues that improved between iterations (compare review-1.json.issues and review-{final}.json.issues).

Then AskUserQuestion:

OptionEffect
"승인하고 계속"Approve as-is. Proceed to Stage 8.
"수정 사항 알려주고 한 번 더"User provides feedback in the "Other" custom input. Append the user's notes to the prior review-N.json's issues array (with severity: block) and re-dispatch the author for one more revision. After this extra revision, run the reviewer once more, then return to checkpoint with the new draft.
"취소"Abort. Cache dir is left intact. Print: "취소되었습니다. 재개하려면 cache 디렉터리에서 작업을 이어가세요: .claude/cache/design-md/{slug}/"

Stage 8 — Write design.md to services/

After approval:

  1. Bash: cp ${repo_root}/.claude/cache/design-md/{slug}/draft.md ${repo_root}/services/{slug}.md
  2. Read the placed file to confirm content arrived intact.
  3. Generate the token sidecar — Bash: pnpm tokens:build {slug} extracts services/{slug}.tokens.json from the design.md you just placed. This is the visual design-token data (colors / typography / spacing / radius / elevation) that drives the detail page's Tokens tab (TokenCardSection, src/routes/services/-components/token-card-section.tsx); src/lib/content-collection.ts loads it as doc.tokens (runtime is a plain JSON.parse, no markdown parsing). Inspect the printed Nc Nt Ns Nr line (a trailing Ne appears when the frontmatter elevation: map holds shadow values). The extractor reads only the frontmatter token maps (colors: / typography: / spacing: / rounded:) — nothing in the body — and reads them as YAML does, so how a flat map is indented or broken across lines changes nothing (a token indented under another token is a nested map, which the Stage 6a2 machine gate blocks): a token is one name: value entry — name: oklch(...) (colors), name: 16px (spacing/rounded). Typography is the exception: it nests. A style name, then fontSize / fontWeight / lineHeight / letterSpacing under it (the inline name: { fontSize: 24px } reads the same). A style that sets none of those four — name: { size, weight, line-height }, name: 16 / 24 / 700, a whole-style alias — yields zero type tokens, so the Stage 6a2 machine gate blocks it. Semantic aliases (fill-brand: "{colors.blue-500}" — always quoted; a bare fill-brand: blue-500 is blocked) are intentionally excluded — they stay in the prose only. If any of the four counts is unexpectedly 0, that frontmatter map isn't in a codegen-readable form. (Nc and Nt are already guarded by now: the Stage 6a2 machine gate blocks a draft whose typography: holds any style the extractor cannot read, and one whose colors: it reads nothing from — it keeps a lower-case colour function or a hex, not a named colour (white) or an upper-case function (OKLCH(…)), which the linter still counts. Colour alias rows are left out of the sidecar by design, so Nc may be below the palette the linter sees; a named colour or an upper-case function beside oklch(…) ones is blocked row by row (non-oklch-token-value), so write every colour as oklch(…). Those two maps are required; spacing: and rounded: are not, since a brand may publish neither. So this check mostly guards Ns / Nr; a 0 there is only a signal when the research shows the brand publishes that scale.) Ne is exempt from that rule — it is absent whenever the entry publishes no shadow values (bezier and class101 carry usage labels and z-indices in a text fence instead, and are legitimately shadow-less), so a missing Ne is only a signal when you authored real box-shadow values in elevation:. The deterministic path is to route back to a Stage 6 draft revision with a blocking prior-review issue naming the unreadable section (a human operator running the skill by hand may instead fix the section directly), then re-run pnpm tokens:build {slug} so the entry ships with full token cards.

If the cp itself fails (filesystem error), surface the error and route back to the checkpoint.

Stage 9 — Preview HTML author + review loop

Iteration counter M = 1. Same shape as Stage 6, dispatching preview-html-author and preview-html-reviewer.

9a. Dispatch preview-html-author

Build preview.html for "{brand_name}".

cache_dir: {abs path}/.claude/cache/design-md/{slug}/
slug: {slug}
name: {brand_name}
lang: ko
design_md_path: {abs path}/services/{slug}.md
runtime_tokens_path: {abs path}/public/preview/_runtime/tokens.css
runtime_iframe_path: {abs path}/public/preview/_runtime/iframe.js
logo_src_path: {logo_src_path}
logo_wordmark_src_path: {logo_wordmark_src_path or "none"}
demo_html_paths: (none — leave empty by default; pass an existing {abs path}/public/preview/*/preview.html only if a visual peer genuinely fits. The early demo-courier/demo-pay previews have been removed.)
prior_review_path: {cache_dir}/preview-review-{M-1}.json or "none"

Follow your agent definition. Write {cache_dir}/preview.html. If `logo_wordmark_src_path` is not "none", render that wordmark `<img>` in the hero brand lockup (the hero has room for the brand name) and reserve `logo_src_path` (the small symbol) for compact references inside the component showcase, favicons, or chip-sized contexts. If `logo_wordmark_src_path` is "none", use `logo_src_path` in the hero too. Size hero `<img>` by `height` + `width: auto` so either aspect ratio (square symbol or horizontal wordmark) renders correctly.

9a2. Deterministic preview gate (machine validation)

Same shape as 6a2 — run the preview validator before spending a reviewer dispatch:

bash
cd "${repo_root}" && pnpm validate:previews \
  --preview .claude/cache/design-md/{slug}/preview.html \
  --design-md "${repo_root}/services/{slug}.md" \
  --expected-logo-src {logo_src_path} \
  --expected-wordmark-src {logo_wordmark_src_path or none} \
  --iteration {M} --json-out "${repo_root}/.claude/cache/design-md/{slug}/preview-review-machine-{M}.json"

It hard-checks the structural rubric items (data-theme/lang, absolute runtime paths, foreign scripts, transfer size, hero logo src, catalog disclosure strip) plus two content rules that used to be reviewer judgment — a swatch catalog (fill-only elements per theme, Item 2) and a type-scale showcase (design.md typography token names printed as visible text labels, Item 3). Size is weighed in brotli bytes, the form Vercel actually serves, with a raw cap kept only as a safety net against runaway generated markup. It also emits warn-level responsive heuristics plus an oklch coverage metric (matched/total per theme) in metrics.

  • Exit 0 → proceed to 9a3, then 9b with the machine report path.
  • Exit 1 → do NOT dispatch the reviewer. Re-dispatch 9a with prior_review_path = the preview-review-machine-{M}.json. Machine retries use a sub-counter K (max 2) and do not increment M.
  • K exhausted with blocks remaining → run 9a3 and dispatch 9b anyway; the normal non-blocking loop rules take over.

9a3. Render contrast observation

Every check so far read source text; nothing in the loop has rendered the preview yet. Before each 9b dispatch, render the staged file and measure its contrast with the same observation harness the CI contrast job uses — same widths (375/768/976/1440), both themes, hover at 976:

bash
cd "${repo_root}" && pnpm audit:contrast:staged \
  --staged .claude/cache/design-md/{slug}/preview.html \
  --iteration {M} --json-out "${repo_root}/.claude/cache/design-md/{slug}/preview-review-machine-{M}.json"

It writes into the same machine report 9a2 just wrote, so the reviewer and the retrying author keep their one path each. It adds a render object and, for each contrast shortfall, a warn item in issues (dark-theme text first — the only kind the rubric scores — then light-theme text, then non-text, each lowest ratio first, capped; the rest are counted in render.observed.omitted). It never adds a block and never changes passed.

  • Always proceed to 9b after it, whatever it found. 9a3 is an observation, not a gate: a staged preview has no baseline row, so a shortfall cannot be told apart from two published brand values that simply fall short, and blocking would push the author to discard a published value to get through. Convergence stays with the reviewer's score; drift stays with the CI contrast job.
  • render.skipped (no Chromium on this machine, or the measurement broke) → note it for Stage 13 as render_result = skipped with the reason, and proceed. Do not install a browser inside the session. A skipped report carries no observed key at all, so it can never be read as a clean run.
  • Otherwise, when render.observed.measured is 0, record render_result = empty — the page rendered but nothing was collected, which is no more a pass than a skip. When render.observed.fail is 0, record render_result = ok with render.observed.held (rows the judge could not decide — a gradient, an overlay); else render_result = warn with the fail count.
  • Exit 2 means the command or a file it names was wrong — an unknown flag, a staged path that is not a readable file, a report path that cannot be written, an existing report from a different iteration than --iteration, or an existing report file that is not a machine report (not JSON, or not the 9a2 envelope — a reviewer's preview-review-{M}.json named by mistake is refused rather than overwritten). Fix that and rerun; it is not a measurement result. The command has no other failure exit: anything that goes wrong while measuring is the render.skipped outcome above.

9b. Dispatch preview-html-reviewer

Score the preview HTML files at {cache_dir} against the rubric.

cache_dir: {abs path}/.claude/cache/design-md/{slug}/
preview_path: {cache_dir}/preview.html
design_md_path: {abs path}/services/{slug}.md
rubric_path: {abs path}/.claude/skills/design-md/references/rubric-preview.md
expected_logo_src_path: {logo_src_path}
machine_report_path: {cache_dir}/preview-review-machine-{M}.json
iteration_n: {M}
output_path: {cache_dir}/preview-review-{M}.json

Follow your agent definition. Write exactly one file at output_path.

The machine report has already verified the structural Item 1 checks
(data-theme, absolute runtime paths, foreign scripts, transfer size, hero
logo src, catalog disclosure strip). Do not re-verify those — adopt the
report's result for Item 1. It also carries two content blocks: a swatch
catalog forces Item 2 to 0 and a type-scale showcase forces Item 3 to 0,
adopted wholesale on the same rule and mirrored into `issues` as `block`.
Spend the rest of your review on judgment: Color fidelity semantics (use the
report's oklch coverage metric as the Item 2 input), Component coverage,
Typography hierarchy, and dark-mode appropriateness.

The report's `render` object is the one rendered observation in the loop.
Under `render.observed`, its contrast `warn` items are measured shortfalls:
score them as the rubric's Item 5 says and mirror each into your `issues`
as `warn`, so the next author pass sees them. Under `render.skipped`, nothing
was rendered — say "렌더 관측 없음" in your verdict and judge Item 5 from the
CSS alone.

9c. Loop decision (non-blocking)

  • If passed && score >= 8 → exit loop, go to Stage 10.
  • Else if M < 3 → M += 1, go back to 9a.
  • Else (M == 3 and not passed) → log the warning, exit loop, go to Stage 10 anyway. Preview review is non-blocking because visual previews iterate naturally during real use; the user already approved the design.md the preview is built from.

Stage 10 — Write previews to public/

bash
mkdir -p ${repo_root}/public/preview/{slug}
cp ${repo_root}/.claude/cache/design-md/{slug}/preview.html ${repo_root}/public/preview/{slug}/preview.html

Logo deterministic check

Verify the placed main markdown contains the absolute URL form and the preview HTML contains the site-relative form:

bash
# Markdown: frontmatter `logo` is the symbol's absolute URL (portable across copies of the file).
rg -q -F "logo: {logo_url}" "${repo_root}/services/{slug}.md" || echo "LOGO_MISSING_MD"

# Preview HTML hero src: when the wordmark exists, the hero uses the wordmark; otherwise the symbol.
# `{logo_wordmark_src_path}` is the literal string "none" when no wordmark was captured —
# `${var:-fallback}` would treat "none" as a non-empty value and skip the fallback, so use an
# explicit conditional that handles both "none" and the empty case.
HERO_SRC="{logo_wordmark_src_path}"
if [ "$HERO_SRC" = "none" ] || [ -z "$HERO_SRC" ]; then
  HERO_SRC="{logo_src_path}"
fi
rg -q -F "src=\"${HERO_SRC}\"" "${repo_root}/public/preview/{slug}/preview.html" || echo "LOGO_MISSING"

If any sentinel prints, do not proceed to Stage 11. If the markdown is missing the logo, re-run Stage 6a with a blocking prior-review issue that says logo_url must appear as frontmatter logo (the exact fully-qualified URL — not a site-relative shortcut). If the preview is missing the hero logo, re-run Stage 9a with a blocking prior-preview issue that says the exact HERO_SRC (site-relative form — wordmark when defined, else symbol) must render as an <img src> in the shared markup — not inside a theme variant.

Catalog disclosure deterministic check

validate:previews already blocks on this at 9a2, so reaching here with a missing strip means the file changed after the gate. Re-check the placed file:

bash
F="${repo_root}/public/preview/{slug}/preview.html"
rg -q -F 'class="catalog-disclaimer"' "$F" || echo "DISCLAIMER_MISSING"
rg -q -F '제휴·후원 관계가 없습니다' "$F" || echo "DISCLAIMER_NO_NONAFFILIATION"
rg -q -F '더미 데이터' "$F" || echo "DISCLAIMER_NO_DUMMYDATA"
rg -qU '<body>(\s|<!--[\s\S]*?-->)*<div class="catalog-disclaimer"' "$F" || echo "DISCLAIMER_MISPLACED"

-U (multiline) is required for the placement check — the strip sits on the line after <body>, so a line-scoped match never sees both. The comment alternation matches DISCLAIMER_FIRST_CHILD in src/lib/preview-validator.ts: a comment between <body> and the strip is still a strip-first document, and flagging it here would send Stage 9a back to fix markup that 9a2 already passed.

These sentinels are deliberately stricter than the validator on one axis: quoting. preview-validator.ts accepts class='catalog-disclaimer' because it also validates hand-edited and pre-existing files. Stage 10 re-checks pipeline output, where .claude/agents/preview-html-author.md prescribes the strip byte for byte with double quotes — so -F on the verbatim form is the point, not an oversight. A single-quoted strip reaching here means the author deviated from a verbatim instruction, which is worth a Stage 9a round. Do not "fix" this by loosening it to match the validator.

The two phrase sentinels are literal (-F) for the same reason: the non-affiliation sentence is shared verbatim with src/components/site/footer.tsx and pinned by src/lib/license-notice-consistency.test.ts.

If any DISCLAIMER_* sentinel prints, do not proceed to Stage 11. Re-run Stage 9a with a blocking prior-preview issue quoting the verbatim strip from .claude/agents/preview-html-author.md and stating it must be the first child of <body> — in the shared markup, not inside a theme variant.

Preview token alias registration

The moment the previews land in public/, the OKLCH drift gate starts expecting this slug to be accounted for. Run it:

bash
cd "${repo_root}" && pnpm test src/lib/oklch-drift-corpus.test.ts

That gate compares each preview's --custom-property: oklch(…) declarations against the name: oklch(…) definitions in services/{slug}.md, by exact name. Previews almost always namespace their variables (--tds-blue-500 for md blue-500), so each slug declares a rewrite rule in PREVIEW_TOKEN_ALIASES (src/lib/oklch-drift.ts). Without one, every declaration in this preview is unreachable and the gate silently checks nothing for the new entry — which is what the corpus test refuses to let happen.

Stage 9a2's oklch coverage metric does not cover this. It searches the HTML for the design.md's OKLCH values as substrings; it never looks at custom-property names, and src/lib/preview-validator.ts does not consult the drift gate at all. A fully namespaced preview can score 100% coverage at 9a2 and still match zero declarations here.

And no gate here looks at WHERE a value is painted. The drift gate compares a preview's --custom-property: oklch(…) declaration against the md's definition of the same name. pnpm audit:oklch compares an OKLCH literal against the hex annotated beside it. Both answer "is this value right?"; neither answers "is it on the right element?" — var(--x) usage sites are not scanned by either. remember declared its surface tokens correctly (light 10 and dark 10 matched the design boards 20/20) and painted one of them on six cards where the research had observed a single 550px column, at radius-md where the observation said 4px. Every gate was unanimous: audit:oklch 0 mismatched, the drift gate matched, validate:previews --slug remember 0 blocking 0 warning. Checking the application site means opening the research cache or the brand publication and comparing element by element — the Stage 12 board cross-check's work and a human's, not a gate's. Do not read a clean audit:oklch as "the preview is faithful".

Three rule shapes are in use, all measured against real entries:

md namepreview namerule
blue-500--tds-blue-500["", "tds-"] — prepend
ldsg-color-linegreen--ldsg-linegreen["ldsg-color-", "ldsg-"] — replace
gray0--g-gray-0[["gray", "g-gray-"], ["", "g-"]] — one family differs from the rest

Rules are tried in order, first match wins, and an empty from matches anything so it belongs last.

If the test fails with these slugs have no entry in MATCH_FLOOR, add the rule, then record the count the failure message prints into MATCH_FLOOR (src/lib/oklch-drift-corpus.test.ts) — that table is a per-slug floor, so it also has to be raised deliberately rather than guessed. Recording a bare 0 is refused: only bezier is entitled to it, because its preview is hex rather than OKLCH and no naming rule can reach it.

Both files are outside this skill's write scope, so this does not route back to Stage 9a — a human operator running the skill by hand makes these two edits directly. Skipping them does not corrupt the entry; it leaves the drift gate blind to it, and CI fails on the pull request rather than here.

Token coverage row

The same moment creates a row this entry owes to src/lib/token-coverage.test.ts. Run it:

bash
cd "${repo_root}" && pnpm test src/lib/token-coverage.test.ts

That file pins, per entry, how many name: oklch(…) definitions each token gate can see — exact in both directions, because a count that rises can mean a reader widened, not that tokens were added. A new entry fails it with these entries have no row in TOKEN_COVERAGE, and the message prints the row itself: paste that line into TOKEN_COVERAGE at its sorted position, then read it. drift: 0 is refused (the drift gate would have no md-side name to compare for the whole entry — fix the frontmatter token map instead), while annotated: 0 can be right (an entry that comments its colours in prose with no hex, as baemin and toss do). Nothing else in the repo prints these numbers, so do not guess them. A row per entry is what lets two catalogue pull requests be open at once without the second one failing on the first one's merge (#324).

This file is outside the skill's write scope too: the operator makes the edit by hand, next to the MATCH_FLOOR row above.

The rest of the per-slug rows

Six more registries remain — BASELINE_TABLE always, the rest only under the condition each names — and none is reachable from this stage: each fails in CI on someone who never saw it. CLAUDE.md 「카탈로그 정책」 carries the full list; the ones not already covered above are:

  • BASELINE_TABLE (src/lib/contrast-baseline.ts) — four rows per slug, mirrored byte for byte into docs/preview-contrast-baseline.md. pnpm gate:contrast prints the rows on failure, and it drives a browser, so CI is the authority on the numbers.
  • The NOTICE asset inventory — any file Stage 4a placed under public/logos/ owes a row, and an app icon used under the web-icons exception names that reason in it (Stage 4a). license-notice-consistency.test.ts compares both directions.
  • The missing-primary list (src/lib/google-designmd-corpus.test.ts) — only when the entry has no token literally named primary. Do not invent one to avoid the row.
  • KNOWN_SPEC_LIMITATIONS (src/lib/spec-limitations.ts) — only when the entry publishes a % radius, which the published spec cannot express (a multi-stop gradient goes in the gradients: map instead, where it raises nothing). It is a two-way ratchet on the slug's error count, so a later silent fix fails it too. Record the count; do not flatten the value to satisfy the linter. validate:draft warns spec-unrecorded-limitation with the exact findings, so the count to record is in its output.
  • COMPONENT_COUNTS (src/lib/google-designmd-corpus.test.ts) — whenever the frontmatter carries a components: map, which a new entry does as soon as one component has a value the map admits (among the spec's eight properties, a quoted reference or one px/em/rem dimension — stitch-format.md, "Component tokens in the frontmatter components: map"). Exact count of its components.
  • CASES (src/lib/role-reference-rows.test.ts) — only when the body's ## Colors role table is also carried as frontmatter reference rows. Not a row but a comparison function written for the slug; the shape that triggers it is in stitch-format.md → "Role → palette reference rows".

All are outside this skill's write scope: the operator edits them by hand, the same as the two above.

Stage 11 — Build OG image

bash
cd "${repo_root}" && pnpm build:og

After the command:

If exit non-zero: capture stderr. Likely cause is frontmatter that the site's reading (buildDoc in src/lib/site-frontmatter.ts) rejects after slipping past the 6a2 gate (e.g. a hand edit at the Stage 7 checkpoint): a last_updated / created_at that is not a real YYYY-MM-DD date, or an estimated_tokens that is not a number. An off-enum category does not fail build:og (the site's reading does not judge the enum); once the draft is past 6a2, only pnpm validate:catalog catches it, so run it before finishing. Surface stderr via text. Offer via AskUserQuestion: "frontmatter 직접 수정 후 재시도" (open ${repo_root}/services/{slug}.md for editing; on user confirmation that they've edited, re-run pnpm build:og and re-validate — loop up to 3 retries), "취소 (파일 유지)" (partial state is acceptable since the index works without an OG image — the route falls back per build-og.ts). Do NOT auto-rollback the placed .md. After 3 failed retries, fall through to "취소" with a diagnostic message.

If exit zero: validate the OG output (catches the corrupt-PNG silent failure mode that happens on satori panic):

bash
[ -s "${repo_root}/public/og/{slug}.png" ] || echo "OG_EMPTY"
file "${repo_root}/public/og/{slug}.png" | grep -q PNG || echo "OG_NOT_PNG"

If either check prints its sentinel, treat as the same error path as a non-zero exit (surface, ask the user, do not auto-rollback).

Stage 12 — Verification (preview MCP)

Start the dev server and confirm the new entry renders correctly. This is the strongest end-to-end check.

  1. Port preflight: Bash: lsof -i :3000 -t 2>/dev/null. If the output is non-empty, port 3000 is already in use (the user has a dev server running). AskUserQuestion: "포트 3000이 사용 중입니다 — (a) 기존 서버 종료 후 재시작 / (b) 검증 단계 건너뛰기 / (c) 취소". On "건너뛰기", skip to Stage 13 with a verification_skipped: port_collision flag in the report.

  2. Bash (run_in_background): cd "${repo_root}" && pnpm dev — runs on port 3000.

  3. Server readiness poll: Bash: for i in $(seq 1 30); do curl -sf http://localhost:3000 -o /dev/null && echo READY && break; sleep 0.5; done. The dev server takes a few seconds to bind; without this poll, preview_start may hit a connection refused before Vite is up. If the loop completes without printing READY, fall through to the curl fallback at the end of this stage.

  4. mcp__Claude_Preview__preview_start with URL http://localhost:3000/services/{slug}.

  5. mcp__Claude_Preview__preview_eval: document.title — should contain {brand_name} and ko/design.md. Then confirm the token-card section loaded from the Stage 8 sidecar: preview_eval: document.querySelector('[aria-label="Design tokens"]')?.querySelector('p')?.textContent ?? 'MISSING' — should return the count badge ({N} Colors · {N} Type · …). MISSING means services/{slug}.tokens.json is absent or failed coerceServiceTokens; verify it exists and is valid JSON before continuing.

  6. preview_eval against the iframe: confirm document.querySelector('iframe')?.src contains /preview/{slug}/preview.html, and that the iframe document's data-theme matches the route default (the useState initial value of previewTheme in src/routes/services/$slug.tsx) — one file serves both themes now.

  7. preview_screenshot once on the default tab.

  8. preview_eval: navigate to ?tab=md and confirm DESIGN.md tab renders the syntax-highlighted markdown.

  9. preview_screenshot once on the ?tab=md view.

  10. Agent endpoint check: Bash: curl -sf -o /dev/null -w "%{http_code} %{content_type}\n" http://localhost:3000/services/{slug}/llms.txt — expect 200 text/plain; charset=utf-8. This raw-markdown sibling route reads from services/{slug}.md directly, so a failure here means either the file wasn't placed correctly or the project's /services/$slug/llms.txt route regressed. Surface non-200 output to the user before stopping the server.

  11. Responsive sweep (mobile / tablet / desktop) — confirm the preview demo doesn't break at narrow widths (the regression class hotfixed in PR #77). Load each demo top-level, not through the width-constrained detail-page iframe. Sweep both themes of the one file: load it, then run the probe once with data-theme="light" and once with "dark". They share layout, but PR #77's lesson is that a fix can miss one theme, and the two scopes can still size text differently. preview_eval: window.location.href = "http://localhost:3000/preview/{slug}/preview.html", and set the theme with document.documentElement.setAttribute('data-theme', '{theme}') before each probe, then at 375 (mobile) / 768 (tablet) / 976 (detail-page embed width — the historical blind spot where multi-column cells are tightest, see PR #150) / 1440 (desktop) × 900 run mcp__Claude_Preview__preview_resize to {width} × 900 followed by preview_eval of the overflow probe:

    js
    (() => {
      const d = document.documentElement;
      const vw = d.clientWidth;
      const overflowPx = d.scrollWidth - vw;
      const broken = overflowPx > 1;
      // Collect culprits only when the document actually overflows, AND skip any element
      // inside an overflow-x: hidden/auto/scroll ancestor: an intentional horizontally-
      // scrollable row (chip row, carousel) reports right > vw without extending the
      // document, so it is not the break even when another element broke the page.
      const culprits = !broken ? [] : Array.from(document.querySelectorAll('body *'))
        .filter(el => {
          const r = el.getBoundingClientRect();
          if (r.right <= vw + 1 && r.left >= -1) return false;
          for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) {
            const ox = getComputedStyle(p).overflowX;
            if (ox === 'hidden' || ox === 'auto' || ox === 'scroll') return false;
          }
          return true;
        })
        .slice(0, 6)
        .map(el => {
          // getAttribute('class') works for SVG too (el.className is an SVGAnimatedString there).
          const cls = (el.getAttribute('class') || '').trim();
          return el.tagName.toLowerCase() + (cls ? '.' + cls.split(/\s+/).slice(0, 2).join('.') : '');
        });
      return { viewport: vw, scrollWidth: d.scrollWidth, overflowPx, broken, culprits };
    })()

    broken = overflowPx > 1 (1px tolerance) means a horizontal scrollbar — a broken responsive layout. The check keys on the document scrollWidth, so an element inside an intentional horizontally-scrollable row (a chip row, a carousel) does not trip it. culprits (collected only when broken, and excluding any element nested in an overflow-x scroll/clip ancestor) lists the genuinely page-extending elements (e.g. .comp-card, .ftile) so the fix can be targeted. Collect every broken result as breaks[] = {file, width, overflowPx, culprits}.

    Auto-fix loop (≤ 2 attempts) when breaks is non-empty — reuses the Stage 9a dispatch and the Stage 10 copy, so there is no new mechanism:

    1. Write a synthetic review at {cache_dir}/preview-review-resp-{attempt}.json whose issues[] carries one severity: "block" entry per break: {"severity":"block","section":"responsive — {theme} @{width}px","fix":"Horizontal overflow {overflowPx}px at {width}px. Offending: {culprits}. Repair per the author's Responsive & mobile-overflow guard — the usual root cause is a bare 1fr grid track flooring at its content min-content, so switch content tracks to minmax(0, 1fr), add a mobile grid-collapse @media rule, and put min-width: 0 on items wrapping fixed-width children. A layout fix belongs in the shared rules, not duplicated per theme scope."}.
    2. Dispatch preview-html-author exactly as in Stage 9a, with prior_review_path = that JSON. The author rewrites {cache_dir}/preview.html.
    3. Re-copy staging → public with the Stage 10 cp commands (the Stage 10 logo deterministic check still applies).
    4. preview_eval: window.location.reload() (the /preview/* no-cache header serves the fresh file), then re-run the sweep. Stop when breaks is empty or after 2 attempts.

    Result: if breaks is empty → responsive_result = ok (record the attempt count). If still non-empty after 2 attempts → preview_screenshot at the narrowest failing width for evidence and set responsive_result = warn with the residual breaks. Non-blocking either way (consistent with the non-blocking preview loop). The OG image is derived from design.md, so re-fixed previews do not trigger an OG rebuild. (The port-collision path never reaches this step — it returns to Stage 13 at step 1 — so handling for that case lives in the Stage 13 report, not here.)

  12. Design-board cross-check (conditional) — run when design_board_paths from Stage 2 is non-empty; otherwise set board_result = skipped (no board) and go to step 13. Stage 4c approved the board before anything was generated; this is the other half of that gate — the finished preview measured back against it. The question is not whether the values are right. pnpm audit:oklch and the drift gate already answered that, and on remember they answered it correctly: the board's twenty role colors reached the preview 20/20 unchanged. The question is where each value landed, which no gate in this pipeline asks — see Preview token alias registration in Stage 10, where that blind spot is stated against audit:oklch and the drift gate.

    Read the board again from the files Stage 4c collected into board_source_paths (for an artifact link, the copies it saved in the cache) — frames as images, source files as text. The preview is still loaded top-level from step 11; preview_resize back to 1440 × 900 and run this probe once per theme, setting data-theme the same way step 11 does:

    js
    (() => {
      const seen = new Map();
      for (const el of document.querySelectorAll('body *')) {
        const r = el.getBoundingClientRect();
        if (r.width < 4 || r.height < 4) continue;
        const cs = getComputedStyle(el);
        for (const prop of ['backgroundColor', 'color', 'borderTopColor']) {
          const v = cs[prop];
          if (!v || v === 'transparent' || v === 'rgba(0, 0, 0, 0)') continue;
          const key = prop + ' ' + v;
          const bucket = seen.get(key) ?? [];
          const cls = (el.getAttribute('class') || '').trim();
          bucket.push({
            el: el.tagName.toLowerCase() + (cls ? '.' + cls.split(/\s+/).slice(0, 2).join('.') : ''),
            w: Math.round(r.width),
            h: Math.round(r.height),
            radius: cs.borderRadius,
          });
          seen.set(key, bucket);
        }
      }
      return [...seen]
        .map(([k, v]) => ({ k, count: v.length, sample: v.slice(0, 3) }))
        .sort((a, b) => b.count - a.count);
    })()

    Buckets are keyed on the serialized computed string, whatever the browser emits for it — Chromium keeps oklch() colors in their own space rather than converting to rgb(), and that format is not stable across versions. You are matching buckets to board tokens by reading them, not by string-comparing against the md, so the format does not matter.

    For each token the board defines, check three things against the board — its source files and any frames — and {cache_dir}/research.md: which element carries it, how many of them, and that element's radius and box size. A discrepancy is a token whose bucket disagrees on any of the three. That is exactly the shape of the only thing this comparison has ever caught — on remember one surface color was painted on six cards where the research had observed a single 550px column, at radius-md where the observation said 4px, while every gate was green.

    Record, do not auto-fix. Set board_result = ok when no bucket disagrees, or board_result = discrepancies carrying one line per finding ({token} — 보드/관측: {expected}, 프리뷰: {observed}). Do not route back into the Stage 9a loop the way the responsive sweep does: that loop's input is a layout rule the author can apply blind, whereas this finding is a judgment about a brand publication the author cannot see, and a wrong auto-fix repaints a correct value onto a different wrong element. Stage 13 reports it and the person decides.

  13. Stop the dev server: Bash: kill $(lsof -t -i:3000) 2>/dev/null || true. Killing by port is portable across macOS/Linux and avoids accidentally killing other pnpm processes the user might be running. The || true keeps the skill from aborting if the process already exited.

If preview MCP tools are unavailable, fall back to Bash: curl -sf http://localhost:3000/services/{slug} | grep -q '<iframe' — non-zero exit means the page failed to render. The responsive sweep (step 11) requires the preview MCP tools; without them, note responsive_result = skipped (no preview MCP) in the report, and the board cross-check (step 12) the same way as board_result = skipped (no preview MCP).

Stage 13 — Final report and cleanup

Print a summary message containing:

  • Files written (with absolute paths):

    • services/{slug}.md
    • services/{slug}.tokens.json (visual design-token sidecar → detail-page card view)
    • public/preview/{slug}/preview.html
    • public/og/{slug}.png (from pnpm build:og)
  • Surfaced URLs (paths only — host depends on env):

    • /services/{slug} — HTML detail page (Live Preview · Tokens · DESIGN.md tabs; the Tokens tab appears only when the sidecar carries tokens).
    • /services/{slug}/llms.txt — raw text/plain design.md (frontmatter + body) for LLMs / agents to fetch directly. Discoverable via <link rel="alternate" type="text/plain"> on the HTML page.
  • Final review scores: design {score}/10, preview {score}/10.

  • Screenshots taken during verification (paths or inline).

  • Render contrast (Stage 9a3, from the last iteration's machine report) — pick the line by state:

    • render_result = ok → 렌더 대비: ✅ 미달 없음 — 라이트·다크 × 375/768/976/1440 실측 (판정 보류 {held}건)
    • render_result = warn → 렌더 대비: ⚠️ 미달 {fail}건 (리뷰어에 warn 으로 전달, 목록 밖 {omitted}건)
    • render_result = empty → 렌더 대비: ⏭ 잰 행 없음 — 렌더는 됐지만 수집된 텍스트·비텍스트가 0개
    • render_result = skipped → 렌더 대비: ⏭ 관측 건너뜀 — {reason}

    Neither the skipped line nor the empty line may read like the ok line: no Chromium, or a run that collected nothing, means nothing was measured, which is not the same fact as nothing falling short. The ok line carries the held count for the same reason — rows on a gradient or under an overlay were measured but not judged. If the Stage 12 responsive auto-fix rewrote the preview after the loop, add (Stage 12 자동수정 전 파일 기준) to the line — 9a3 measured the file the loop ended with, not the one that shipped.

  • Responsive verification (Stage 12 sweep) — pick the line by state:

    • responsive_result = ok → 반응형: ✅ 375/768/976/1440 가로 오버플로 없음 (자동수정 {attempts}회)
    • responsive_result = warn → 반응형: ⚠️ 잔여 오버플로 — {file} @{width}px {overflowPx}px, 요소 {culprits} (스크린샷 {path}, 자동수정 2회 후 잔존)
    • skipped — set when verification_skipped: port_collision (step 1 returned early, so responsive_result was never assigned) or responsive_result = skipped (preview MCP unavailable) → 반응형: ⏭ 검증 건너뜀 (포트 충돌 / preview MCP 없음)
  • Design-board cross-check (Stage 12, after the responsive sweep) — pick the line by state:

    • board_result = ok → 보드 대조: ✅ 보드 토큰의 적용 위치·개수·형상이 관측과 일치
    • board_result = discrepancies → 보드 대조: ⚠️ 적용 위치 불일치 {N}건 — {finding 한 줄씩}
    • board_result = skipped (no board) → 보드 대조: ⏭ 보드 없음 — 공개 자료만으로 만든 항목이라 대조할 상류가 없습니다
    • board_result = skipped (no preview MCP), or step 1 returned early on verification_skipped: port_collision so board_result was never assigned → 보드 대조: ⏭ 검증 건너뜀 (포트 충돌 / preview MCP 없음)

    No two of the four may print the same line — that is what the contract test pins. Three of them are where it actually happens: discrepancies carries a ⚠️ and is never mistaken for silence, while the other three all look like nothing-to-see. "보드가 없었다" · "대조했고 어긋난 것이 없었다" · "대조가 아예 안 돌았다" 는 서로 다른 사실이고, 이 저장소에서 반복적으로 같은 침묵으로 보고돼 왔다. 특히 skipped 를 침묵으로 처리하면 사람은 초록으로 읽는다.

  • Leftover TODOs:

    • Every warn in the final preview review, whatever the score. The rubric's four advisory sections — Mobile overflow, Dummy-data labelling, Explanatory prose, Reduced motion — add no points by design, so a preview can carry all of them and still pass 9c's score >= 8 on the first iteration and exit without the author ever seeing the review. Reporting them only when iteration 3 fell short drops them in exactly the case they exist for: remember scored 10/10 with 61% of its rendered text restating the design.md. List each one's section and fix verbatim. If the list is empty, say so — an absent line reads as "none found" whether or not the check ran. The one exception is the render contrast warns the reviewer mirrored from 9a3 (their section starts with contrast —): the Render contrast line above already reports them, and each carries the same guidance sentence, so list only their element paths under that line instead of repeating every fix — twenty copies of one sentence bury the advisory warns this list exists for.
  • What is left for the person to look at. Close the report by saying the loops are already done — the draft gate and review (6a2/6b), the preview gate, render contrast observation and review (9a2/9a3/9b) and the Stage 12 sweep have all run — and then name what they do not cover, so the user spends their pass on the residue instead of re-checking what a machine just checked. On remember every gate was green when the user found four things: the preview was more than half explanatory text, button sizes disagreed between sections, a mobile mock duplicated what the responsive rules already produced at 375px, and a dialog carried an animation nobody wanted. Taste, proportion and redundancy are that residue. When board_result is ok or discrepancies, say so and drop application site from that list — the board cross-check just looked at it. When it is either of the skipped states, application site is still residue and the closing line must name it, because on remember that is where the one real discrepancy was. Do not present the preview as unverified, and do not present it as finished either.

AskUserQuestion: "캐시 정리할까요?"

  • "지금 삭제" → rm -rf .claude/cache/design-md/{slug}/
  • "보관 (디버깅용)" → leave intact.

Edge cases

  • WebFetch blocked / paywalled — research-collector halts with INSUFFICIENT_SOURCES, skill body re-prompts user (Stage 5).
  • Docs-site crawl yields 0 pages / fails — Stage 4b surfaces the failure via AskUserQuestion; research proceeds from source_urls alone with crawl_corpus_path = "none".
  • Reviewer never reaches 8 in 3 iterations — checkpoint shows the failing draft and verdict; user decides via AskUserQuestion.
  • No Chromium for the render observation (Stage 9a3) — the report says render.skipped with a reason; continue to 9b and carry the skip into the Stage 13 line. Never install a browser mid-session to make it run.
  • Responsive overflow persists after 2 auto-fix attempts (Stage 12 step 11) — surface it as a ⚠️ in the Stage 13 report (residual breaks + screenshot) and finish anyway. Non-blocking, consistent with the preview loop; the placed design.md and OG image are unaffected.
  • Board cross-check finds a discrepancy (Stage 12, after the responsive sweep) — surface it as a ⚠️ in the Stage 13 report, one line per finding, and finish anyway. Non-blocking like the responsive residue, but unlike it this one does not route back into the Stage 9a loop: the author cannot see the board, so an auto-fix moves a correct value onto a different wrong element instead of repairing anything. The placed design.md, sidecar and OG image are unaffected.
  • User aborts at checkpoint — leave cache intact, print resume path. Do not delete partial work.
  • pnpm build:og fails — Stage 11's error path. Do not auto-rollback the placed .md.
  • Subagent missing expected output file — retry once. Second failure aborts with diagnostic.
  • Pretendard CDN dependency — tokens.css imports Pretendard from jsDelivr. Online-only assumption; flag in the final report.
  • Preview HTML / logo cache policy — /preview/* and /logos/* are served with Cache-Control: no-cache by previewCacheHeadersPlugin in vite.config.ts (both vite dev and vite preview). This prevents browsers heuristic-caching iframe HTML and logo files after entry edits — the cache trap that previously required users to hard-refresh after every preview/logo change. Per-entry HTML does NOT need its own <meta http-equiv="Cache-Control"> (unreliable across browsers anyway); the server-level header is the source of truth. Production cache headers are handled by the host (Vercel defaults).

What this skill must NOT do

  • Edit any file outside of services/, public/preview/, public/logos/{slug}.{svg,png,webp,avif} for a user-supplied local logo file or a Logo candidates file the user picked (Stage 4a), and .claude/cache/design-md/{slug}/.
  • Skip the user checkpoint. Even if the design review passes 10/10 on iteration 1, show the draft to the user before writing to services/.
  • Run subagents in parallel within a stage. The design-md author and reviewer are explicitly sequential because the reviewer needs the author's output. Same for preview HTML.
  • Use npm instead of pnpm for any script invocation. The project uses pnpm (memorized preference).
  • Write outside cache_dir from any subagent. Subagents have constrained tool access for this reason; respect that.

Why this design

  • Five specialized subagents (vs. one general agent looping): author and reviewer are intentionally separated to avoid self-grading bias. Same model in both roles with different prompts produces noticeably stricter reviews. All five agent definitions pin model: inherit (the documented default, stated explicitly) so the whole pipeline follows the session model — no stage silently runs on a different tier when the operator switches models.
  • Machine gates before reviewer dispatches (6a2/9a2): every mechanically checkable rule lives in pnpm validate:draft / pnpm validate:previews, so reviewer quality degrades gracefully with model capability — a weaker reviewer model still receives deterministic findings instead of being trusted to "grep mentally".
  • One rendered observation inside the loop (9a3): no subagent can render, so before 9a3 the first render happened at Stage 12, after the loop had closed. 9a3 runs the CI contrast harness on the staged file and writes into the same machine report, so the reviewer gains a measured input without a second channel. It reports and never blocks — see ADR 0009.
  • Single mandatory checkpoint at design.md (plus a conditional one upstream of it): in this pipeline the preview is built from the approved design.md, so the design.md is upstream of everything after the checkpoint. Locking it after one approval gate gives the user maximum control with minimum interruption. This ordering describes what this pipeline produces, not every entry: when an entry's design.md and preview were both transcribed from a Claude Design handoff bundle, the bundle is upstream of both, and the design.md's silence is not evidence against the preview (.claude/skills/preview-prose-audit/SKILL.md). The gate follows the upstream, which is why Stage 4c exists and why it is conditional: when a board sits above the design.md, approving only the design.md approves a transcription of something the user never saw, and a faithful transcription of a wrong board passes Stage 7 looking entirely consistent.
  • Stitch v0.1 standard sections: every catalog entry follows the Stitch v0.1 structure (English headings, OKLCH tokens, citation hygiene). The early _demo-*.md fixtures that used Korean editorial headings have been removed; if older entries surface in git history they are superseded.
  • OKLCH everywhere, never hex: downstream LLMs (which are the primary audience for design.md) reason about lightness/chroma/hue components more reliably than hex codes.
  • File-presence state encoding: simpler than a state.json for v1; resumable because the cache dir's contents fully describe pipeline progress.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Design Md AI skill do?

Add a new design.md catalog entry to ko-design-md. Use this skill IMMEDIATELY when the user wants to onboard a new brand into THIS project's catalog — produce services/{slug}.md (Stitch v0.1 format) plus services/{slug}.tokens.json (token-card sidecar) plus public/preview/{slug}/preview.html (one file carrying both themes) plus the OG image. Trigger phrases include "add to design.md catalog", "new design.md entry for X", "onboard X to ko-design-md", "X를 ko-design-md에 추가", "X의 design.md 만들어줘", "/design-md", "X 카탈로그 항목 만들기", or any variant where the user is asking to populate this catalog wit...

Why use Design Md on TypingMind?

Because you install it once and use it with any model. Design Md is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Design Md in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/CaesiumY/ko-design-md/tree/main/.claude/skills/design-md. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Design Md?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Design Md?

As many as you like. As long as a model supports skills, you can use Design Md with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Design Md AI skill free?

Yes. It is published on GitHub by CaesiumY under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇