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

14 KiB
Raw Blame History

Skills Protocol

Parent: spec.md · Siblings: architecture.md · agent-adapters.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 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):

---
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 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.

---
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.

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 — 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 convention, reproduced here for convenience:

# <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 and 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:

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 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

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.htmlhtml.
    • 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
---
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.