feat(design): write/read DESIGN.md in the open spec; persisted format choice

gstack's design skills now write DESIGN.md in the open DESIGN.md format and
read tokens from it. {{DESIGN_MD_CHECK}} renders the format check through
bin/gstack-design-md.ts: design-consultation's Phase 0 settles the format once
(spec → update tokens in the front matter; legacy without a marker → one
AskUserQuestion: convert with a .legacy.bak, keep the legacy file, or start
fresh; the answer is written into the file as the format marker so no skill
asks again; a marker already present is obeyed silently; unknown → prose;
missing → Phase 6 writes one). Phase 6's template is the spec form: YAML front
matter with name, description, and exactly the five token groups (colors,
typography.display/body/label/mono with fontFeature: tnum on mono, rounded,
spacing, components with {path} references), then Overview (Creative North
Star, product context, mode per surface, references, key characteristics),
Colors (opening with the Restrained / Committed / Full palette / Drenched
strategy), Typography, Layout, Elevation & Depth, Shapes, Components, Do's and
Don'ts, plus gstack's Motion and Decisions Log as extras; the template ends
with a check that the file parses as `spec`.

design-review runs the `:calibrate` form in Setup: a spec file's flat tokens
are the calibration source (a value present in the tokens is never a finding),
the marker is respected, and conversion is never offered there; its DESIGN.md
export writes the spec form. design-html's token extraction writes the spec
form and respects an existing choice. review/design-checklist.md category 5 and
ship's review-lite step 1 name `gstack-design-md tokens` as the calibration
source; plan-design-review Pass 5 cites tokens by path when front matter
exists.

The contract owns the bin's DESIGN_MD_MARKER / REASON / WRITTEN / BACKUP
lines; the contract test's pending list closes. Carve guard: design-
consultation skeleton 66,500 → 67,500 (measured 67,014; +1,508 B against the
1.5 KB cap). Codex and Factory ship goldens refreshed (review-lite step 1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-09-08 16:26:15 +00:00
co-authored by Claude Fable 5.1
parent e49d1eea49
commit 35d641b4b2
23 changed files with 319 additions and 93 deletions
+13 -1
View File
@@ -466,7 +466,19 @@ You are a senior product designer with strong opinions about typography, color,
ls DESIGN.md design-system.md 2>/dev/null || echo "NO_DESIGN_FILE"
```
- If a DESIGN.md exists: Read it. Ask the user: "You already have a design system. Want to **update** it, **start fresh**, or **cancel**?"
- If a DESIGN.md exists: Read it. Ask the user: "You already have a design system. Want to **update** it, **start fresh**, or **cancel**?" Then settle its format once:
**DESIGN.md format** (the open format; Phase 6 has the template):
```bash
bun --no-env-file run $HOME/.claude/skills/gstack/bin/gstack-design-md.ts check DESIGN.md
```
- `DESIGN_MD_FORMAT: spec` → already the open format; `bun --no-env-file run $HOME/.claude/skills/gstack/bin/gstack-design-md.ts tokens DESIGN.md` prints the flat token map. Update tokens in the front matter, rationale in the sections.
- `legacy` with `DESIGN_MD_MARKER: none` → ask once (AskUserQuestion): **A) Convert** (recommended; `bun --no-env-file run $HOME/.claude/skills/gstack/bin/gstack-design-md.ts convert --write` keeps a `.legacy.bak` and every section) **B) Keep legacy** (`bun --no-env-file run $HOME/.claude/skills/gstack/bin/gstack-design-md.ts mark legacy-keep`; read as prose from now on) **C) Start fresh**. The answer lives in the file, so no skill asks again; a marker already present is obeyed silently.
- `unknown` → read as prose, say why once (`DESIGN_MD_REASON`); `DESIGN_MD_CONVERT_REFUSED` means both formats are mixed: leave it, tell the user.
- `missing` → Phase 6 writes one. Exit 3 (`DESIGN_MD_INTERNAL_ERROR`) is a gstack bug: report it, do not retry.
- If no DESIGN.md: continue.
**Gather product context from the codebase:**
+4 -1
View File
@@ -66,7 +66,10 @@ You are a senior product designer with strong opinions about typography, color,
ls DESIGN.md design-system.md 2>/dev/null || echo "NO_DESIGN_FILE"
```
- If a DESIGN.md exists: Read it. Ask the user: "You already have a design system. Want to **update** it, **start fresh**, or **cancel**?"
- If a DESIGN.md exists: Read it. Ask the user: "You already have a design system. Want to **update** it, **start fresh**, or **cancel**?" Then settle its format once:
{{DESIGN_MD_CHECK}}
- If no DESIGN.md: continue.
**Gather product context from the codebase:**
@@ -357,55 +357,116 @@ If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the pr
**If in plan mode:** Write the DESIGN.md content into the plan file as a "## Proposed DESIGN.md" section. Do NOT write the actual file — that happens at implementation time.
**If NOT in plan mode:** Write `DESIGN.md` to the repo root with this structure:
**If NOT in plan mode:** Write `DESIGN.md` to the repo root in the open DESIGN.md format (google-labs-code/design.md). The YAML front matter is normative: every token an agent needs lives there, in exactly five groups (`colors`, `typography`, `rounded`, `spacing`, `components`). The sections explain why the tokens exist and how to apply them, and never restate a token value. Line 2 is gstack's format marker, so no skill asks about conversion later. If a legacy file was kept in Phase 0, update that file in its own shape instead.
```markdown
# Design System — [Project Name]
---
# gstack: design-md-format=spec
name: [Project Name]
description: [one sentence: mood, material, energy]
colors:
primary: "#..." # descriptive slugs; hex, or the project's canonical color space
on-primary: "#..."
surface: "#..."
text: "#..."
text-muted: "#..."
accent: "#..."
success: "#..."
warning: "#..."
error: "#..."
typography:
display:
fontFamily: [face]
fontWeight: [weight]
fontSize: [clamp() or rem]
letterSpacing: [em]
body:
fontFamily: [face]
fontSize: 1rem
lineHeight: 1.5
label:
fontFamily: [face]
fontSize: 0.75rem
letterSpacing: 0.04em
mono:
fontFamily: [face]
fontFeature: tnum
rounded:
sm: 4px
md: 8px
lg: 12px
full: 9999px
spacing:
xs: 4px
sm: 8px
md: 16px
lg: 24px
xl: 32px
2xl: 48px
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
rounded: "{rounded.md}"
button-primary-hover:
backgroundColor: "#..."
input:
borderColor: "{colors.text-muted}"
rounded: "{rounded.sm}"
card:
backgroundColor: "{colors.surface}"
rounded: "{rounded.lg}"
nav-link:
textColor: "{colors.text}"
---
## Product Context
- **What this is:** [1-2 sentence description]
- **Who it's for:** [target users]
- **Space/industry:** [category, peers]
- **Project type:** [web app / dashboard / marketing site / editorial / internal tool]
# [Project Name]
## Aesthetic Direction
- **Direction:** [name]
- **Decoration level:** [minimal / intentional / expressive]
- **Mood:** [1-2 sentence description of how the product should feel]
- **Reference sites:** [URLs, if research was done]
## Overview
**Creative North Star:** [one sentence: the aesthetic direction and why it is right for these users]
**Product context:** [what this is, who it is for, the space and its peers, the project type]
**Mode per surface:** [Persuade / Operate / Read / Experience, per surface, in one line each]
**Reference sites:** [URLs, if research was done]
**Key characteristics:** [3-5 bullets: what someone notices in the first five seconds]
## Colors
**Strategy:** [Restrained / Committed / Full palette / Drenched] — [why]
**Light or dark:** [decided by the use scene: who, where, under what light]
Named rules: [which token carries interaction, which carries emphasis, what neutrals derive from, how dark mode redesigns surfaces (never a lightness inversion)]
## Typography
- **Display/Hero:** [font name] — [rationale]
- **Body:** [font name] — [rationale]
- **UI/Labels:** [font name or "same as body"]
- **Data/Tables:** [font name] — [rationale, must support tabular-nums]
- **Code:** [font name]
- **Loading:** [CDN URL or self-hosted strategy]
- **Scale:** [modular scale with specific px/rem values for each level]
## Color
- **Approach:** [restrained / balanced / expressive]
- **Primary:** [hex] — [what it represents, usage]
- **Secondary:** [hex] — [usage]
- **Neutrals:** [warm/cool grays, hex range from lightest to darkest]
- **Semantic:** success [hex], warning [hex], error [hex], info [hex]
- **Dark mode:** [strategy — redesign surfaces, reduce saturation 10-20%]
## Spacing
- **Base unit:** [4px or 8px]
- **Density:** [compact / comfortable / spacious]
- **Scale:** 2xs(2) xs(4) sm(8) md(16) lg(24) xl(32) 2xl(48) 3xl(64)
[Why these faces, in the mode's register: the world they come from, the roles they play, where the display voice is allowed. Loading strategy. Scale rationale. The overused-list exceptions you made and why.]
## Layout
- **Approach:** [grid-disciplined / creative-editorial / hybrid]
- **Grid:** [columns per breakpoint]
- **Max content width:** [value]
- **Border radius:** [hierarchical scale — e.g., sm:4px, md:8px, lg:12px, full:9999px]
[Grid per breakpoint, max content width, density, the spacing scale's rhythm (large step vs small step), what breaks the grid on purpose]
## Elevation & Depth
[How depth is shown: offset + soft blur shadows, surface tints, borders. Never a zero-offset glow.]
## Shapes
[Radius hierarchy and what each level is for; inner radius = outer radius gap on nested elements]
## Components
[Per component token group above: states (hover, focus-visible, active, disabled), what never changes, what adapts]
## Do's and Don'ts
- Do: [3-5 specific, checkable rules]
- Don't: [3-5 specific anti-patterns for THIS system, including the catalog entries most tempting for this category]
## Motion
- **Approach:** [minimal-functional / intentional / expressive]
- **Easing:** enter(ease-out) exit(ease-in) move(ease-in-out)
- **Duration:** micro(50-100ms) short(150-250ms) medium(250-400ms) long(400-700ms)
- **The one authored moment:** [what it is]
## Decisions Log
| Date | Decision | Rationale |
@@ -413,6 +474,8 @@ If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the pr
| [today] | Initial design system created | Created by /design-consultation based on [product context / research] |
```
Fill every token with a real value (no placeholders survive into the file); drop a `components` entry rather than invent one. Verify the result parses: `bun --no-env-file run ~/.claude/skills/gstack/bin/gstack-design-md.ts check DESIGN.md` must print `DESIGN_MD_FORMAT: spec`.
**Update CLAUDE.md** (or create it if it doesn't exist) — append this section:
```markdown
@@ -192,55 +192,116 @@ If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the pr
**If in plan mode:** Write the DESIGN.md content into the plan file as a "## Proposed DESIGN.md" section. Do NOT write the actual file — that happens at implementation time.
**If NOT in plan mode:** Write `DESIGN.md` to the repo root with this structure:
**If NOT in plan mode:** Write `DESIGN.md` to the repo root in the open DESIGN.md format (google-labs-code/design.md). The YAML front matter is normative: every token an agent needs lives there, in exactly five groups (`colors`, `typography`, `rounded`, `spacing`, `components`). The sections explain why the tokens exist and how to apply them, and never restate a token value. Line 2 is gstack's format marker, so no skill asks about conversion later. If a legacy file was kept in Phase 0, update that file in its own shape instead.
```markdown
# Design System — [Project Name]
---
# gstack: design-md-format=spec
name: [Project Name]
description: [one sentence: mood, material, energy]
colors:
primary: "#..." # descriptive slugs; hex, or the project's canonical color space
on-primary: "#..."
surface: "#..."
text: "#..."
text-muted: "#..."
accent: "#..."
success: "#..."
warning: "#..."
error: "#..."
typography:
display:
fontFamily: [face]
fontWeight: [weight]
fontSize: [clamp() or rem]
letterSpacing: [em]
body:
fontFamily: [face]
fontSize: 1rem
lineHeight: 1.5
label:
fontFamily: [face]
fontSize: 0.75rem
letterSpacing: 0.04em
mono:
fontFamily: [face]
fontFeature: tnum
rounded:
sm: 4px
md: 8px
lg: 12px
full: 9999px
spacing:
xs: 4px
sm: 8px
md: 16px
lg: 24px
xl: 32px
2xl: 48px
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
rounded: "{rounded.md}"
button-primary-hover:
backgroundColor: "#..."
input:
borderColor: "{colors.text-muted}"
rounded: "{rounded.sm}"
card:
backgroundColor: "{colors.surface}"
rounded: "{rounded.lg}"
nav-link:
textColor: "{colors.text}"
---
## Product Context
- **What this is:** [1-2 sentence description]
- **Who it's for:** [target users]
- **Space/industry:** [category, peers]
- **Project type:** [web app / dashboard / marketing site / editorial / internal tool]
# [Project Name]
## Aesthetic Direction
- **Direction:** [name]
- **Decoration level:** [minimal / intentional / expressive]
- **Mood:** [1-2 sentence description of how the product should feel]
- **Reference sites:** [URLs, if research was done]
## Overview
**Creative North Star:** [one sentence: the aesthetic direction and why it is right for these users]
**Product context:** [what this is, who it is for, the space and its peers, the project type]
**Mode per surface:** [Persuade / Operate / Read / Experience, per surface, in one line each]
**Reference sites:** [URLs, if research was done]
**Key characteristics:** [3-5 bullets: what someone notices in the first five seconds]
## Colors
**Strategy:** [Restrained / Committed / Full palette / Drenched] — [why]
**Light or dark:** [decided by the use scene: who, where, under what light]
Named rules: [which token carries interaction, which carries emphasis, what neutrals derive from, how dark mode redesigns surfaces (never a lightness inversion)]
## Typography
- **Display/Hero:** [font name] — [rationale]
- **Body:** [font name] — [rationale]
- **UI/Labels:** [font name or "same as body"]
- **Data/Tables:** [font name] — [rationale, must support tabular-nums]
- **Code:** [font name]
- **Loading:** [CDN URL or self-hosted strategy]
- **Scale:** [modular scale with specific px/rem values for each level]
## Color
- **Approach:** [restrained / balanced / expressive]
- **Primary:** [hex] — [what it represents, usage]
- **Secondary:** [hex] — [usage]
- **Neutrals:** [warm/cool grays, hex range from lightest to darkest]
- **Semantic:** success [hex], warning [hex], error [hex], info [hex]
- **Dark mode:** [strategy — redesign surfaces, reduce saturation 10-20%]
## Spacing
- **Base unit:** [4px or 8px]
- **Density:** [compact / comfortable / spacious]
- **Scale:** 2xs(2) xs(4) sm(8) md(16) lg(24) xl(32) 2xl(48) 3xl(64)
[Why these faces, in the mode's register: the world they come from, the roles they play, where the display voice is allowed. Loading strategy. Scale rationale. The overused-list exceptions you made and why.]
## Layout
- **Approach:** [grid-disciplined / creative-editorial / hybrid]
- **Grid:** [columns per breakpoint]
- **Max content width:** [value]
- **Border radius:** [hierarchical scale — e.g., sm:4px, md:8px, lg:12px, full:9999px]
[Grid per breakpoint, max content width, density, the spacing scale's rhythm (large step vs small step), what breaks the grid on purpose]
## Elevation & Depth
[How depth is shown: offset + soft blur shadows, surface tints, borders. Never a zero-offset glow.]
## Shapes
[Radius hierarchy and what each level is for; inner radius = outer radius gap on nested elements]
## Components
[Per component token group above: states (hover, focus-visible, active, disabled), what never changes, what adapts]
## Do's and Don'ts
- Do: [3-5 specific, checkable rules]
- Don't: [3-5 specific anti-patterns for THIS system, including the catalog entries most tempting for this category]
## Motion
- **Approach:** [minimal-functional / intentional / expressive]
- **Easing:** enter(ease-out) exit(ease-in) move(ease-in-out)
- **Duration:** micro(50-100ms) short(150-250ms) medium(250-400ms) long(400-700ms)
- **The one authored moment:** [what it is]
## Decisions Log
| Date | Decision | Rationale |
@@ -248,6 +309,8 @@ If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the pr
| [today] | Initial design system created | Created by /design-consultation based on [product context / research] |
```
Fill every token with a real value (no placeholders survive into the file); drop a `components` entry rather than invent one. Verify the result parses: `bun --no-env-file run ~/.claude/skills/gstack/bin/gstack-design-md.ts check DESIGN.md` must print `DESIGN_MD_FORMAT: spec`.
**Update CLAUDE.md** (or create it if it doesn't exist) — append this section:
```markdown