open-design/docs/skills-protocol.md
Tom Huang 1edab990bb
feat(craft): add brand-agnostic craft references + Refero-derived lint rules (#225)
* feat(craft): add brand-agnostic craft references and refero-derived lint rules

Introduce `craft/` as a third top-level content axis alongside `skills/`
and `design-systems/`, holding universal (brand-agnostic) craft rules
that apply on top of any DESIGN.md. Skills opt in via a new
`od.craft.requires` front-matter array; the daemon resolves the slug
list and injects the matching files between DESIGN.md and the skill
body in the system prompt.

Initial vendor (MIT, adapted from referodesign/refero_skill): typography
craft, color craft, anti-ai-slop. Pilot wired on saas-landing.

Extend the existing lint-artifact pass with two refero-derived rules:
- P0 ai-default-indigo — solid #6366f1 / #4f46e5 / #4338ca / #8b5cf6 as
  accent (not just gradients) is the most-reported AI tell.
- P1 all-caps-no-tracking — `text-transform: uppercase` rules without
  ≥0.06em letter-spacing.

The craft loader silently drops missing files so a skill can
forward-reference future sections (e.g. `motion`) without breaking.

* fix(daemon): skip :root token blocks in ai-default-indigo lint

The ai-default-indigo P0 check scanned the whole HTML for the raw
hex, so brands that intentionally encode indigo as `--accent: #6366f1`
in :root and consume it via var(--accent) downstream were flagged
as AI-default — a false positive that forced the agent to "fix"
valid output. Strip :root token-definition blocks (including
attribute-selector theme variants) before scanning, mirroring the
existing pattern used by the raw-hex P1 check. Hex still flagged
when it appears in component rules or inline styles.

* docs(craft): address PR #225 P3 review feedback

- craft/README.md: explain why missing craft sections are silently
  dropped (forward-compatibility) instead of surfacing a warning.
- craft/typography.md: ground the 0.06em ALL CAPS tracking floor in
  Bringhurst-derived typographic practice rather than presenting
  the threshold as unattributed.
- craft/color.md: cover the edge case where a brand's DESIGN.md
  intentionally encodes indigo as --accent — `var(--accent)` uses
  remain unflagged because the linter only inspects hardcoded hex.
- docs/skills-protocol.md: link the "missing files dropped silently"
  note back to craft/README.md for the canonical slug list and the
  rationale behind the choice.

* fix(craft): address PR #225 P0 review feedback

- tools/pack: copy `craft/` into the packaged resource root alongside
  `skills`, `design-systems`, and `frames`, so the `od.craft.requires`
  integration isn't a silent no-op when the daemon resolves
  `${OD_RESOURCE_ROOT}/craft` in packaged builds.
- packages/contracts: add `craftRequires?: string[]` to `SkillSummary`
  (and therefore `SkillDetail`) so the field that `listSkills()`
  already returns and `/api/skills(/:id)` already serializes via
  `...rest` is part of the documented web/daemon contract instead of
  leaking through as an untyped property.
- apps/daemon/lint-artifact: expand the indigo token-strip pass to
  cover selector lists containing `:root` (e.g. `:root, [data-theme="light"]`)
  and any rule whose body is custom-property-only (e.g. a
  `[data-theme="dark"] { --accent: ... }` theme variant). Real
  component rules with a hardcoded indigo are still preserved so the
  P0 finding still fires; tests cover the new selector-list and
  theme-variant cases.

* fix(craft): address PR #225 follow-up review feedback

- lint-artifact: scope the indigo token-strip to <style> blocks so the
  rule-shaped regex no longer captures leading `<style>` text into the
  selector (which broke `:root` recognition for token blocks that mix
  `color-scheme`/etc. with `--accent`). Run the strip on the extracted
  CSS instead, with a regression covering `:root { color-scheme: light;
  --accent: #6366f1 }`.
- lint-artifact: tighten the custom-property-only exemption to global
  theme-scope selectors (`:root`, `html`, `body`, bare attribute
  selectors like `[data-theme="dark"]`). Component-local rules such as
  `.cta { --cta-bg: #6366f1 }` are no longer exempted, so an agent
  cannot launder default indigo through a local var. Regression test
  added.
- craft/anti-ai-slop.md: stop claiming every rule below is enforced by
  the linter; only several are. The unenforced rules (standard
  Hero→Features→Pricing→FAQ→CTA flow, decorative blob/wave SVG
  backgrounds, perfect symmetry) are now flagged inline as
  "(guidance, not auto-checked)" so the contract with the lint surface
  stays honest.

* fix(daemon): tighten lint-artifact iteration and :root token gating

- all-caps-no-tracking: iterate every <style> block. The previous
  check called `exec` once on a non-global regex, so an artifact
  whose offending uppercase rule sat in a second <style> block
  (e.g. a reset block followed by a components block) slipped
  past. Switch to `matchAll` and break across both loops once a
  violation is found. Regression test covers a second-block
  uppercase rule.
- ai-default-indigo: stop unconditionally exempting any selector
  list containing `:root`. The exemption now requires both
  conditions to hold: every selector in the list is global theme
  scope AND the body is token-shaped (CSS custom properties or
  the `color-scheme` keyword). So `:root { background: #6366f1 }`
  and `:root, .cta { --cta-bg: #6366f1 }` no longer launder a
  hardcoded indigo through the strip pass. Regression tests cover
  both bypass shapes.

* fix(daemon): scope theme-attr exemption and strip CSS comments in token blocks

Address PR #225 review feedback on `ai-default-indigo`:

- The bare-attribute branch of `selectorListIsGlobalThemeScope` accepted
  any `[attr=...]` selector, so a custom-property-only rule on a
  component/state attribute (e.g. `[data-variant="primary"]`,
  `[aria-current="page"]`) was treated as a global theme block and
  stripped before the indigo scan — exactly the component-local indigo
  laundering this lint is meant to catch. Restrict the exemption to a
  small allowlist of known theme switches: `data-theme`,
  `data-color-scheme`, `data-mode`.
- `stripTokenBlocksFromCss` split rule bodies on `;` and matched each
  fragment from the start, so a token block whose body contained a
  normal CSS comment such as `:root { /* brand accent */ --accent:
  #6366f1; }` produced a fragment beginning with the comment, failed
  `isTokenShapedDeclaration`, and the rule was left in scope of the
  indigo scan — a false P0 on a legitimate token definition. Strip CSS
  comments before splitting/classifying declarations.

Add regression coverage: arbitrary component/state attribute selectors
still trip `ai-default-indigo`; `data-color-scheme` theme variants stay
exempted; `:root` token blocks with leading, trailing, and
between-declaration CSS comments are recognized.

* fix(daemon): strip CSS comments and recognize tokens nested in at-rules

The all-caps-no-tracking scan ran against raw `<style>` content, so a
commented-out rule like `/* .eyebrow { text-transform: uppercase; } */`
matched `upperRe` and emitted a P1 for CSS the browser ignores. Strip
CSS comments from the style body before structural matching.

`stripTokenBlocksFromCss` only matched flat `selector { body }` rules,
so a media-query-wrapped token block like
`@media (prefers-color-scheme: dark) { :root { --accent: #6366f1 } }`
had its outer `@media` rule treated as the selector/body pair and the
inner `:root` token block was never stripped, producing a P0 false
positive on legitimate responsive theme CSS. Tighten the body
alternation to `[^{}]*` so the regex matches innermost rules and
recognizes the inner `:root` block directly while preserving the
outer at-rule wrapper.

* fix(daemon): align ai-default-indigo list with documented cardinal sins

The lint's AI_DEFAULT_INDIGO subset omitted #3730a3 and #a855f7, which
craft/anti-ai-slop.md lists as P0-blocked solid accents. An artifact
could hard-code one of those documented colors as a button fill and
slip past the indigo scan unless it happened to be inside a gradient.

Bring the lint set to the exact list documented in the craft doc, and
tighten the doc's wording from "etc." to an explicit enumeration that
points at AI_DEFAULT_INDIGO so the prompt contract and daemon behavior
stay in sync. Add regression tests pinning each newly-included hex.

* fix(daemon): tighten theme-scope selector and scan inline ALL CAPS

The theme-scope exemption used to accept any attribute on `:root`,
`html`, or `body` (e.g. `:root[data-variant="primary"]`), letting an
agent launder default indigo through a component/state attribute and
slip past the `ai-default-indigo` lint. The prefixed branches now
require the attribute name to be one of GLOBAL_THEME_ATTRIBUTES,
matching the bare-attribute branch.

The `all-caps-no-tracking` rule only iterated `<style>` blocks, so
inline declarations like `<span style="text-transform: uppercase">`
produced no finding even though craft/typography.md treats the
≥0.06em tracking floor as having no exceptions. Added a second scan
over `style="..."` attributes that runs the same letter-spacing
check and dedupes against the existing `<style>`-block finding so
the agent gets a single corrective signal per artifact.

* fix(daemon): align uppercase tracking px floor with the 0.06em rule

The previous absolute fallback (>=1.5px) was stricter than the craft
rule it enforces. `font-size: 12px; letter-spacing: 1px` is 0.083em
— above the 0.06em floor — but 1.5px would reject it and trigger an
unnecessary correction loop on compliant small-label CSS.

Extract `hasAdequateUppercaseTracking`: read `font-size` from the same
rule body and compare px tracking against `fontSize * 0.06`; fall back
to a conservative >=1px floor when font-size is inherited (covers the
default 16px body where 1px ≈ 0.0625em). Apply the helper to both the
<style>-block scan and the inline-style scan, and add 12–14px label
tests in both branches.

* fix(daemon): treat rem letter-spacing as absolute, not per-element em

`rem` was previously folded into the same branch as `em` and accepted
at the 0.06 threshold. But `rem` is relative to the root font-size
(16px default), not the element's own font-size, so on a 48px heading
`letter-spacing: 0.06rem` resolves to 0.96px — about 0.02em of the
element, well below the 0.06em rule the lint enforces.

Convert rem to absolute px through the 16px root assumption and reuse
the same px-vs-element-font-size resolution: same-rule `font-size: <n>px`
gives an exact `n * 0.06` floor; otherwise the conservative >=1px
fallback applies. Add regression tests for 48px headings with 0.06rem
tracking (must flag) plus the 16px-element and rem-floor matches that
must keep passing, in both <style>-block and inline-style branches.

* fix(daemon): resolve var() refs in uppercase tracking lint

`hasAdequateUppercaseTracking` only matched literal numeric values,
so a tokenized rule like `letter-spacing: var(--caps-tracking)` —
exactly the pattern the craft prompt steers artifacts toward — was
falsely reported as `all-caps-no-tracking`. Extract `--name: value`
declarations from global theme scopes (`:root`, `html`, theme-attribute
selectors) once per artifact, then expand simple `var(--name)` (and
`var(--name, fallback)`) references in the inspected rule body before
applying the existing 0.06em / px-floor / rem-conversion logic.
References without a matching token and no fallback stay in place,
preserving the conservative "missing tracking" finding.

* fix(daemon): resolve rem and var() font-size in uppercase tracking lint

Previously the px-vs-element-font-size resolution only matched
`font-size: <n>px`. Any rem-based or tokenized display size fell
through to the lenient `>= 1px` body-text fallback, so an artifact
emitting `.display { font-size: 3rem; text-transform: uppercase;
letter-spacing: 1px; }` (a ~48px heading with a 2.88px floor) slipped
past the lint that this helper exists to enforce.

Resolve `rem` font-size via the same root-font assumption already used
for tracking, and treat any explicitly declared but unresolvable unit
(`em`, `%`, `calc(...)`, an unresolved `var(...)`) conservatively —
refuse the lenient fallback so the rule must use either an `em`
letter-spacing or a verifiable px/rem font-size.

`var()` font-size declarations resolve through the existing
`resolveCssVars` pass before the size scan runs, so the same fix
catches the tokenized-display-size pattern (`--display-size: 3rem`).

* fix(daemon): parse declarations to ignore custom-prop names in uppercase tracking lint

The hasAdequateUppercaseTracking and resolveFontSizePx helpers used substring regexes against the rule body, so a token-name declaration such as `--letter-spacing: 0.08em` or `--display-font-size: 48px` could satisfy the `letter-spacing` / `font-size` checks even though it has no rendered effect — letting actual ALL-CAPS-without-tracking rules slip past the P1 lint.

Parse the declaration list, compare exact property names, and skip declarations whose property starts with `--`. Adds regression tests covering token-name letter-spacing (style-block + inline) and a token-name font-size masking the bail-out branch.

* fix(daemon): scope indigo token exemption to --accent only

Previously stripTokenBlocksFromCss removed every custom-property-only
global theme block before the ai-default-indigo scan, which let a
laundered indigo token like `:root { --primary: #6366f1 }` consumed
via `var(--primary)` slip past the lint. The craft contract is that
the only escape hatch is encoding indigo as the design system's
`--accent` token; any other token name is still the LLM-default
color hidden behind an arbitrary name. Narrow the strip pass so a
non-`--accent` token whose value carries an AI-default indigo hex
keeps the rule in scope, and add regression tests for `--primary` /
`--button-bg` global tokens feeding a CTA, including the at-rule
and theme-attribute variants.

* fix(daemon): model CSS cascade in tracking lint and detect blue→cyan trust gradients

Address PR #225 review feedback (3 comments):

- `letter-spacing` / `font-size` selection now picks the LAST matching
  declaration in the rule body, modeling CSS source-order cascade.
  `.eyebrow { letter-spacing: 0.08em; letter-spacing: 0.02em }` renders
  the noncompliant 0.02em the browser actually shows; the previous
  first-match behaviour silently passed it.
- `extractCssTokens` now records every distinct value seen for a token
  across global theme scopes, and `hasAdequateUppercaseTracking`
  enumerates each combination so a default-theme value below the floor
  cannot be rescued by a scoped override that happened to be parsed
  later (`:root { --caps-tracking: 0.02em }` +
  `[data-theme="dark"] { --caps-tracking: 0.08em }` now fires).
- New `trust-gradient` P0 rule pairs blue/sky tokens against cyan
  tokens in `linear-gradient(...)` bodies so `blue→cyan` two-stop
  trust gradients (documented as a cardinal sin in
  `craft/anti-ai-slop.md`) are actually enforced — both the hex form
  (`linear-gradient(90deg, #3b82f6, #06b6d4)`) and the keyword form
  (`linear-gradient(90deg, blue, cyan)`).

Adds 11 regression tests covering each path (cascade override in
<style> and inline form, font-size cascade shifting the floor, both
orderings of the conflicting-token cascade, the don't-over-fire case
when every theme value clears the floor, hex / keyword / sky variants
of the trust gradient, and the don't-double-fire case when
purple-gradient already caught a mixed gradient).

* fix(daemon): apply per-scope cascade in extractCssTokens

When the same CSS custom property is declared more than once inside a
single rule body (e.g. `:root { --caps-tracking: 0.02em;
--caps-tracking: 0.08em }`), CSS source-order cascade collapses to the
last value; the earlier declaration never reaches any element.
`extractCssTokens` was treating intra-scope duplicates as simultaneous
theme alternatives, so `hasAdequateUppercaseTracking` enumerated the
stale 0.02em and emitted a spurious all-caps-no-tracking finding.

Collapse duplicate token declarations within a rule body to the last
value before merging into the cross-scope distinct-value map. Cross-scope
overrides (separate `:root` and `[data-theme]` rules) remain preserved
as distinct values so the conservative theme-cascade check still fires
when ANY applicable theme renders below the floor.

* fix(daemon): scope tracking lint to innermost rules and per-theme tokens

Restrict the upperRe body alternation to [^{}]* so the regex matches
innermost CSS rules and skips at-rule wrappers — an outer @media or
@supports could otherwise capture as a single rule whose selector was
the at-rule and whose body began with the inner selector token, masking
the same-rule font-size and letting noncompliant tracking on large
headings slip through the lenient inherited-size fallback.

Replace the by-name-distinct-values token map with per-scope token
records and a buildResolvedThemes pass that materializes one effective
map per theme. Paired token declarations now stay paired during
evaluation, so theme variants like :root + [data-theme=dark] no longer
generate cross-theme cartesian pairings (e.g. default-size + dark-track)
that emit false positives on legitimate light/dark themes.

---------

Co-authored-by: looper <looper@open-claude.dev>
2026-05-02 11:00:33 +08:00

355 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Skills Protocol
**Parent:** [`spec.md`](spec.md) · **Siblings:** [`architecture.md`](architecture.md) · [`agent-adapters.md`](agent-adapters.md) · [`modes.md`](modes.md)
A **Skill** is the atomic unit of design capability in OD. We adopt Claude Code's `SKILL.md` convention verbatim as the base format, then add optional fields for design-specific features (preview type, input schema, slider parameters). A skill written for plain Claude Code runs in OD. An OD skill that doesn't use our extensions runs in plain Claude Code.
> **Compatibility promise:** A skill like [`guizang-ppt-skill`](https://github.com/op7418/guizang-ppt-skill) works in OD **without modification**. It just drops into `~/.claude/skills/` and OD discovers it.
---
## 1. Base format (unchanged from Claude Code)
Every skill is a directory containing at minimum a `SKILL.md`:
```
<skill-root>/
├── SKILL.md # manifest + workflow instructions
├── assets/ # templates, images, boilerplate the skill writes
│ └── …
└── references/ # knowledge files the skill reads during planning
├── components.md
├── layouts.md
└── …
```
`SKILL.md` front-matter (YAML):
```yaml
---
name: magazine-web-ppt
description: |
Magazine-style horizontal-swipe web deck.
Trigger keywords: 杂志风 PPT, magazine deck, swipe slides.
triggers:
- "magazine deck"
- "杂志风 PPT"
- "horizontal swipe presentation"
---
```
Body is free-form Markdown that describes the workflow the agent should follow — typically a numbered step list plus principles. This is what [guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill) does.
**OD reads all of this as-is.** No changes required.
## 2. OD extensions (optional)
Skills can declare additional front-matter fields to unlock OD-specific UI. All fields are optional; absent fields fall back to sensible defaults.
```yaml
---
name: magazine-web-ppt
description:
triggers: […]
# --- OD extensions below this line ---
od:
mode: deck # one of: prototype | deck | template | design-system
preview:
type: html # html | jsx | pptx | markdown
entry: index.html # relative path produced by the skill
reload: debounce-100 # how the preview refreshes
design_system:
requires: true # this skill reads the active DESIGN.md
sections: [color, typography] # which sections it actually uses (for prompt pruning)
craft: # universal, brand-agnostic craft references
requires: [typography, color, anti-ai-slop]
inputs: # typed inputs the user can fill in the UI
- name: title
type: string
required: true
- name: slide_count
type: integer
default: 8
min: 4
max: 20
- name: theme
type: enum
values: [editorial, minimal, brutalist, dark-glass, warm]
default: editorial
parameters: # live-tweakable sliders after first generation
- name: accent_hue
type: hue # hue | spacing | font-scale | opacity
default: 18
range: [0, 360]
- name: section_spacing
type: spacing
default: 48
range: [16, 128]
outputs:
primary: index.html
secondary: [slides.json] # for PPTX export
capabilities_required:
- surgical_edit # comment mode needs this
- file_write
---
```
### 2.1 What OD uses each field for
| Field | Used by |
|---|---|
| `od.mode` | routing (which mode picker the skill shows up under) |
| `od.preview.type` | picking the right iframe renderer |
| `od.design_system.requires` | whether to inject `DESIGN.md` |
| `od.design_system.sections` | pruning the injected DESIGN.md to relevant sections only (token savings) |
| `od.craft.requires` | which brand-agnostic `craft/<slug>.md` references to inject (e.g. `typography`, `color`, `anti-ai-slop`); injected between DESIGN.md and the skill body |
| `od.inputs` | rendering a typed form in the sidebar instead of only free-text |
| `od.parameters` | rendering live sliders that re-prompt on change |
| `od.outputs.primary` | which file the iframe loads |
| `od.outputs.secondary` | which files export pipelines read (e.g. `slides.json` for PPTX) |
| `od.capabilities_required` | gating: if the active agent lacks surgical edit, comment mode is disabled for this skill |
### 2.2 If a skill omits `od:` entirely
Defaults:
- `mode`: inferred from name/description (best-effort keyword match) or "prototype"
- `preview.type`: sniff for `*.html` → html, `*.jsx` → jsx, else "markdown"
- `preview.entry`: first file matching the sniffed type
- `design_system.requires`: true if the skill body mentions "design system" or "DESIGN.md"
- `inputs`, `parameters`: none (free-text prompt only)
The goal: **zero-config compatibility** for existing Claude Code skills.
## 3. Skill discovery & precedence
The daemon's skill registry scans three locations:
| Location | Priority | Purpose |
|---|---|---|
| `./.claude/skills/` | 1 (highest) | project-private skills, not committed |
| `./skills/` | 2 | project-committed skills |
| `~/.claude/skills/` | 3 | user-global skills |
Conflicts by `name` resolve to the higher-priority version. All locations are watched with `chokidar` in dev and re-scanned on `SIGHUP` in production.
### Symlink strategy (borrowed from [cc-switch](https://github.com/farion1231/cc-switch))
`cc-switch` maintains a central skill dir at `~/.cc-switch/skills/` and symlinks it into each agent's expected location (`~/.claude/skills/`, `~/.codex/skills/`, etc.). OD can opt into the same model:
```
~/.open-design/skills/
magazine-web-ppt/ (canonical location)
~/.claude/skills/
magazine-web-ppt → ~/.open-design/skills/magazine-web-ppt
~/.codex/skills/
magazine-web-ppt → ~/.open-design/skills/magazine-web-ppt
```
One install → every agent sees the skill. This is optional; users who only use one agent don't need it.
## 4. Skill types (by mode)
Each mode expects a slightly different skill shape. The required outputs and expected workflow differ.
### 4.1 `prototype-skill`
- **Purpose:** single-screen interactive prototype.
- **Preview:** `html` or `jsx`.
- **Primary output:** `index.html` or `Prototype.jsx`.
- **Typical workflow:** clarify brief → resolve design tokens → write component tree → write file.
- **Example skills:** `saas-landing`, `dashboard`, `login-flow`, `empty-states`.
### 4.2 `deck-skill`
- **Purpose:** multi-slide presentation.
- **Preview:** `html` (single-file deck with in-page navigation).
- **Primary output:** `index.html`.
- **Secondary output:** `slides.json` (for PPTX export).
- **Typical workflow:** clarify topic + slide count → pick theme → populate slides from layout catalog → self-check against quality rubric.
- **Reference implementation:** [guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill) — fork this for v1.
### 4.3 `template-skill`
- **Purpose:** start from a pre-built artifact; agent only personalizes content, doesn't design from scratch.
- **Preview:** inherits from the template bundle (`html` typically).
- **Primary output:** a populated copy of the template.
- **Typical workflow:** copy `assets/template/` to artifact dir → replace content placeholders → optionally tweak tokens to match design system.
- **Why separate from `prototype-skill`:** much faster (no design decisions), higher-quality floor, worse ceiling.
### 4.4 `design-system-skill`
- **Purpose:** produce a `DESIGN.md` from inputs (brand brief, screenshot, URL).
- **Preview:** `markdown` (render the resulting DESIGN.md with a sample-components preview).
- **Primary output:** `DESIGN.md`.
- **Typical workflow:** analyze input → draft 9 sections per awesome-claude-design schema → generate sample component preview → finalize.
- **Post-run:** OD prompts the user to set this DESIGN.md as the project's active design system.
## 5. The DESIGN.md as skill context
Every nondesign-system skill (modes 13) can consume the active `DESIGN.md`. OD injects it as:
1. **System-prompt prefix** (required sections only, per `od.design_system.sections`).
2. **File available in CWD** named `DESIGN.md` — skills can `Read` it directly via their agent.
3. **Template variable** `{{ design_system }}` if the skill body references it in Mustache-style.
The 9-section DESIGN.md format is **not invented by OD**; it's the [awesome-claude-design](https://github.com/VoltAgent/awesome-claude-design) convention, reproduced here for convenience:
```markdown
# <Brand Name>
## Visual Theme & Atmosphere
## Color Palette & Roles
## Typography Rules
## Component Stylings
## Layout Principles
## Depth & Elevation
## Do's and Don'ts
## Responsive Behavior
## Agent Prompt Guide
```
Full schema and examples: [`schemas/design-system.md`](schemas/design-system.md) and [`examples/DESIGN.sample.md`](examples/DESIGN.sample.md) (TODO).
## 5.5 Craft references (`craft/`)
Some craft knowledge is **universal** — true regardless of brand. ALL CAPS always needs ≥0.06em letter-spacing; `var(--accent)` should appear at most 2 times per screen; `#6366f1` is always the AI-default tell. These rules don't belong in any one `DESIGN.md` because they apply across every brand.
OD ships these as a third axis at `<projectRoot>/craft/`:
```
craft/
├── README.md
├── typography.md
├── color.md
└── anti-ai-slop.md
```
A skill opts in by listing the slugs it needs:
```yaml
od:
craft:
requires: [typography, color, anti-ai-slop]
```
Resolution at compose time:
1. `apps/daemon/src/skills.ts` reads `od.craft.requires` from front-matter and surfaces it on the skill record.
2. `apps/daemon/src/craft.ts` reads each `<slug>.md` from `CRAFT_DIR`. Missing files are dropped silently — a skill can forward-reference `craft/motion.md` before we ship it. See [`craft/README.md`](../craft/README.md) for the canonical slug list and the rationale behind the silent-fallback choice.
3. `apps/daemon/src/prompts/system.ts` injects the concatenated craft body **between** the active DESIGN.md and the skill body. Brand tokens in DESIGN.md win on conflict; craft rules cover everything DESIGN.md does not override.
The split keeps DESIGN.md authors free of universal-craft duplication and keeps craft authors free of brand-specific drift.
## 6. Skill installation
```sh
od skill add https://github.com/op7418/guizang-ppt-skill
# → clones into ~/.open-design/skills/magazine-web-ppt
# → symlinks into ~/.claude/skills/ (and any other active agent dirs)
# → re-indexes registry
od skill add ./path/to/my-skill
# → symlinks local dir (no copy) into skills registry
od skill list
# → table: name, mode, source, agent compatibility
od skill remove <name>
# → unlinks; does not delete the source
```
## 7. Worked example — running `guizang-ppt-skill` under OD
The skill is unchanged. Here's the full path:
1. User: `od skill add https://github.com/op7418/guizang-ppt-skill`
2. Registry indexes it. No `od:` block in front-matter → defaults applied:
- `mode`: inferred from body mentioning "PPT" → `deck`.
- `preview.type`: sniffed from `assets/template.html``html`.
- `preview.entry`: `index.html` (convention).
- `design_system.requires`: false (skill body doesn't mention DESIGN.md).
3. User switches to `deck` mode in the web UI; skill appears in the skill picker.
4. User types "给我做一份杂志风 8 页投资人 PPT".
5. Daemon dispatches to active agent (Claude Code) with:
- system message: skill's `SKILL.md` body
- cwd: `./.od/artifacts/2026-04-24-pitch-deck/`
- files already placed in cwd: `template.html` (from skill's `assets/`)
6. Agent runs its 6-step workflow (clarify → copy template → populate → self-check → preview → refine).
7. OD streams the agent's tool calls as UI events; artifact dir grows.
8. Agent signals done; daemon sets preview iframe to `index.html`.
9. User clicks "Export PPTX" — export pipeline notices the skill has no `slides.json` output (the upstream skill doesn't produce one). OD falls back to "print to PDF then page-to-slide PPTX," which is uglier but works. This is a known limitation documented per-skill.
## 8. Writing a new skill — minimal example
```
saas-landing-skill/
├── SKILL.md
└── assets/
└── base.html
```
```markdown
---
name: saas-landing
description: |
Produce a single-page SaaS landing with hero, features, social proof, pricing, CTA.
Trigger: "saas landing", "marketing page", "product landing".
triggers:
- "saas landing"
- "marketing page"
od:
mode: prototype
preview:
type: html
entry: index.html
design_system:
requires: true
sections: [color, typography, layout, components]
inputs:
- name: product_name
type: string
required: true
- name: tagline
type: string
required: true
- name: has_pricing
type: boolean
default: true
parameters:
- name: hero_density
type: spacing
default: 96
range: [48, 200]
---
# Workflow
1. Read DESIGN.md from cwd. Adopt its color/typography/layout rules.
2. Copy `assets/base.html` to `index.html` in cwd.
3. Fill sections: hero, features (36), social proof, pricing (if `has_pricing`), CTA, footer.
4. Inline all CSS. Use system font stack as fallback if DESIGN.md typography fails to load.
5. Respect `hero_density` parameter as the hero section's vertical padding in px.
6. Write `index.html`. Done.
```
## 9. Testing skills
A skill ships with optional test inputs that OD uses for CI:
```
<skill-root>/
└── tests/
├── basic.prompt
├── basic.expected.manifest.json # assertions: files produced, preview.type, etc.
└── basic.expected.regex.txt # text regex assertions against the primary output
```
`od skill test <name>` runs the skill against each case using a cheap model (e.g. Haiku 4.5) and asserts on the manifest + regex. Low-fidelity but catches structural regressions.
## 10. Open questions
- **Skill signing.** Can we verify a skill hasn't been tampered with between publish and install? Simplest answer: `od skill add` records the git commit SHA; reinstall-on-update warns on signature change. Deferred to v1.
- **Skill composition.** Can a `prototype-skill` call a `deck-skill` for a sub-artifact? Not in v1; skills are leaf-level. Composition would require a meta-skill concept, which is speculative.
- **Parameter stability.** When sliders change, should the agent re-plan or just re-render? Lean: re-render (fast path), with an "also re-plan" button for larger changes.