Add teach-impeccable and review commands, dynamic placeholders

New commands:
- /teach-impeccable: One-time setup that gathers UX-focused design
  context (explores codebase first, then asks targeted questions)
- /review: UX design review evaluating hierarchy, clarity, emotional
  resonance (complements technical /audit)

Build system improvements:
- Dynamic {{model}}, {{ask_instruction}}, {{config_file}} placeholders
  per provider (Claude/Gemini/GPT/the model)
- context: fork support for Claude Code sub-agents (audit, extract)
- Skill reference auto-added to design commands

Skill refinements:
- Merged Design Thinking and Context Awareness sections
- Removed redundant patterns and technical questions
- More direct feedback guidance (radical candor)

Website updates:
- 17 commands (was 16)
- Review demo showing UX issues (HIERARCHY, NO PRIMARY, DEAD END)
- teach-impeccable in System group of periodic table

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-01-07 16:56:08 -08:00
co-authored by Claude Opus 4.5
parent 8186f2a226
commit 0b9c1846a6
96 changed files with 1315 additions and 646 deletions
+8 -19
View File
@@ -1,5 +1,5 @@
import path from 'path';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
/**
* Generate markdown from structured patterns/antipatterns data
@@ -66,10 +66,12 @@ export function transformClaudeCode(commands, skills, distDir, patterns = null)
const frontmatter = generateYamlFrontmatter({
name: command.name,
description: command.description,
...(command.context && { context: command.context }),
...(command.args.length > 0 && { args: command.args })
});
const content = `${frontmatter}\n\n${command.body}`;
const commandBody = replacePlaceholders(command.body, 'claude-code');
const content = `${frontmatter}\n\n${commandBody}`;
const outputPath = path.join(commandsDir, `${command.name}.md`);
writeFile(outputPath, content);
}
@@ -92,22 +94,8 @@ export function transformClaudeCode(commands, skills, distDir, patterns = null)
const frontmatter = generateYamlFrontmatter(frontmatterObj);
let body = skill.body;
// Generate and merge patterns into frontend-design skill (before Domain Reference Files section)
if (skill.name === 'frontend-design' && patterns) {
const patternsMarkdown = generatePatternsMarkdown(patterns);
if (patternsMarkdown) {
const insertPoint = body.indexOf('---\n\n## Domain Reference Files');
if (insertPoint > -1) {
body = body.slice(0, insertPoint) + '\n\n' + patternsMarkdown + '\n\n' + body.slice(insertPoint);
} else {
body += '\n\n' + patternsMarkdown;
}
}
}
const content = `${frontmatter}\n\n${body}`;
const skillBody = replacePlaceholders(skill.body, 'claude-code');
const content = `${frontmatter}\n\n${skillBody}`;
const outputPath = path.join(skillDir, 'SKILL.md');
writeFile(outputPath, content);
@@ -117,7 +105,8 @@ export function transformClaudeCode(commands, skills, distDir, patterns = null)
ensureDir(refDir);
for (const ref of skill.references) {
const refOutputPath = path.join(refDir, `${ref.name}.md`);
writeFile(refOutputPath, ref.content);
const refContent = replacePlaceholders(ref.content, 'claude-code');
writeFile(refOutputPath, refContent);
refCount++;
}
}
+7 -20
View File
@@ -1,5 +1,5 @@
import path from 'path';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
/**
* Generate markdown from structured patterns/antipatterns data
@@ -77,8 +77,8 @@ export function transformCodex(commands, skills, distDir, patterns = null) {
yamlLines.push('---');
// Transform {{argname}} to $ARGNAME for Codex
let body = command.body;
// First replace our placeholders, then transform remaining {{argname}} to $ARGNAME
let body = replacePlaceholders(command.body, 'codex');
body = body.replace(/\{\{([^}]+)\}\}/g, (match, argName) => {
return `$${argName.toUpperCase()}`;
});
@@ -99,22 +99,8 @@ export function transformCodex(commands, skills, distDir, patterns = null) {
...(skill.license && { license: skill.license })
});
let body = skill.body;
// Generate and merge patterns into frontend-design skill
if (skill.name === 'frontend-design' && patterns) {
const patternsMarkdown = generatePatternsMarkdown(patterns);
if (patternsMarkdown) {
const insertPoint = body.indexOf('---\n\n## Domain Reference Files');
if (insertPoint > -1) {
body = body.slice(0, insertPoint) + '\n\n' + patternsMarkdown + '\n\n' + body.slice(insertPoint);
} else {
body += '\n\n' + patternsMarkdown;
}
}
}
const content = `${frontmatter}\n\n${body}`;
const skillBody = replacePlaceholders(skill.body, 'codex');
const content = `${frontmatter}\n\n${skillBody}`;
const outputPath = path.join(skillDir, 'SKILL.md');
writeFile(outputPath, content);
@@ -124,7 +110,8 @@ export function transformCodex(commands, skills, distDir, patterns = null) {
ensureDir(refDir);
for (const ref of skill.references) {
const refOutputPath = path.join(refDir, `${ref.name}.md`);
writeFile(refOutputPath, ref.content);
const refContent = replacePlaceholders(ref.content, 'codex');
writeFile(refOutputPath, refContent);
refCount++;
}
}
+7 -19
View File
@@ -1,5 +1,5 @@
import path from 'path';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
/**
* Generate markdown from structured patterns/antipatterns data
@@ -65,8 +65,9 @@ export function transformCursor(commands, skills, distDir, patterns = null) {
// Commands: Body only (Cursor doesn't support command frontmatter/args)
for (const command of commands) {
const commandBody = replacePlaceholders(command.body, 'cursor');
const outputPath = path.join(commandsDir, `${command.name}.md`);
writeFile(outputPath, command.body);
writeFile(outputPath, commandBody);
}
// Skills: Agent Skills standard with SKILL.md in subdirectories
@@ -80,22 +81,8 @@ export function transformCursor(commands, skills, distDir, patterns = null) {
...(skill.license && { license: skill.license })
});
let body = skill.body;
// Generate and merge patterns into frontend-design skill
if (skill.name === 'frontend-design' && patterns) {
const patternsMarkdown = generatePatternsMarkdown(patterns);
if (patternsMarkdown) {
const insertPoint = body.indexOf('---\n\n## Domain Reference Files');
if (insertPoint > -1) {
body = body.slice(0, insertPoint) + '\n\n' + patternsMarkdown + '\n\n' + body.slice(insertPoint);
} else {
body += '\n\n' + patternsMarkdown;
}
}
}
const content = `${frontmatter}\n\n${body}`;
const skillBody = replacePlaceholders(skill.body, 'cursor');
const content = `${frontmatter}\n\n${skillBody}`;
const outputPath = path.join(skillDir, 'SKILL.md');
writeFile(outputPath, content);
@@ -105,7 +92,8 @@ export function transformCursor(commands, skills, distDir, patterns = null) {
ensureDir(refDir);
for (const ref of skill.references) {
const refOutputPath = path.join(refDir, `${ref.name}.md`);
writeFile(refOutputPath, ref.content);
const refContent = replacePlaceholders(ref.content, 'cursor');
writeFile(refOutputPath, refContent);
refCount++;
}
}
+9 -4
View File
@@ -1,5 +1,5 @@
import path from 'path';
import { cleanDir, ensureDir, writeFile } from '../utils.js';
import { cleanDir, ensureDir, writeFile, replacePlaceholders } from '../utils.js';
/**
* Gemini Transformer (Full Featured - TOML + Modular Skills)
@@ -17,8 +17,9 @@ export function transformGemini(commands, skills, distDir, patterns = null) {
// Commands: Transform to TOML
for (const command of commands) {
// Replace named placeholders with {{args}}
let prompt = command.body.replace(/\{\{[^}]+\}\}/g, '{{args}}');
// First replace our placeholders, then replace remaining {{arg}} with {{args}}
let prompt = replacePlaceholders(command.body, 'gemini');
prompt = prompt.replace(/\{\{[^}]+\}\}/g, '{{args}}');
const toml = [
`description = "${command.description.replace(/"/g, '\\"')}"`,
@@ -50,11 +51,15 @@ export function transformGemini(commands, skills, distDir, patterns = null) {
if (skill.references && skill.references.length > 0) {
const refSections = skill.references.map(ref => {
refCount++;
return `\n\n---\n\n## Reference: ${ref.name}\n\n${ref.content}`;
const refContent = replacePlaceholders(ref.content, 'gemini');
return `\n\n---\n\n## Reference: ${ref.name}\n\n${refContent}`;
});
content += refSections.join('');
}
// Replace all placeholders
content = replacePlaceholders(content, 'gemini');
const outputPath = path.join(geminiDir, `GEMINI.${skill.name}.md`);
writeFile(outputPath, content);
}
+92 -58
View File
@@ -122,6 +122,7 @@ export function readSourceFiles(rootDir) {
name: frontmatter.name || name,
description: frontmatter.description || '',
args: frontmatter.args || [],
context: frontmatter.context || null,
body,
filePath
};
@@ -223,84 +224,117 @@ export function writeFile(filePath, content) {
}
/**
* Read and parse patterns.md
* Returns { patterns: [...], antipatterns: [...], body: string }
* Extract patterns from frontend-design SKILL.md
* Parses **DO**: and **DON'T**: lines, grouped by section headings
* Returns { patterns: [...], antipatterns: [...] }
*/
export function readPatterns(rootDir) {
const filePath = path.join(rootDir, 'source/patterns.md');
const skillPath = path.join(rootDir, 'source/skills/frontend-design/SKILL.md');
if (!fs.existsSync(filePath)) {
return { patterns: [], antipatterns: [], body: '' };
if (!fs.existsSync(skillPath)) {
return { patterns: [], antipatterns: [] };
}
const content = fs.readFileSync(filePath, 'utf-8');
const content = fs.readFileSync(skillPath, 'utf-8');
const lines = content.split('\n');
// Split frontmatter and body
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return { patterns: [], antipatterns: [], body: content };
}
const [, frontmatterText, body] = match;
// Parse both patterns and antipatterns sections
const patterns = [];
const antipatterns = [];
const lines = frontmatterText.split('\n');
let currentSection = null; // 'patterns' or 'antipatterns'
let currentCategory = null;
let inItems = false;
const patternsMap = {}; // category -> items[]
const antipatternsMap = {}; // category -> items[]
let currentSection = null;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const indent = line.length - line.trimStart().length;
// Top-level section declaration
if (indent === 0 && trimmed === 'patterns:') {
currentSection = 'patterns';
currentCategory = null;
inItems = false;
continue;
}
if (indent === 0 && trimmed === 'antipatterns:') {
currentSection = 'antipatterns';
currentCategory = null;
inItems = false;
continue;
}
// New category starts with "- name:"
if (trimmed.startsWith('- name:') && currentSection) {
currentCategory = {
name: trimmed.slice(7).trim(),
items: []
};
if (currentSection === 'patterns') {
patterns.push(currentCategory);
} else {
antipatterns.push(currentCategory);
// Track section headings (### Typography, ### Color & Theme, etc.)
if (trimmed.startsWith('### ')) {
currentSection = trimmed.slice(4).trim();
// Normalize "Color & Theme" to "Color & Contrast" for consistency
if (currentSection === 'Color & Theme') {
currentSection = 'Color & Contrast';
}
inItems = false;
continue;
}
// Items array declaration
if (trimmed === 'items:' && currentCategory) {
inItems = true;
// Parse **DO**: lines
if (trimmed.startsWith('**DO**:') && currentSection) {
const item = trimmed.slice(7).trim();
if (!patternsMap[currentSection]) {
patternsMap[currentSection] = [];
}
patternsMap[currentSection].push(item);
continue;
}
// Item within items array (indented with "- ")
if (trimmed.startsWith('- ') && inItems && currentCategory && indent >= 6) {
currentCategory.items.push(trimmed.slice(2).trim());
// Parse **DON'T**: lines
if (trimmed.startsWith("**DON'T**:") && currentSection) {
const item = trimmed.slice(10).trim();
if (!antipatternsMap[currentSection]) {
antipatternsMap[currentSection] = [];
}
antipatternsMap[currentSection].push(item);
continue;
}
}
return { patterns, antipatterns, body: body.trim() };
// Convert maps to arrays in consistent order
const sectionOrder = ['Typography', 'Color & Contrast', 'Layout & Space', 'Motion', 'Interaction', 'Responsive', 'UX Writing', 'Visual Details'];
const patterns = [];
const antipatterns = [];
for (const section of sectionOrder) {
if (patternsMap[section] && patternsMap[section].length > 0) {
patterns.push({ name: section, items: patternsMap[section] });
}
if (antipatternsMap[section] && antipatternsMap[section].length > 0) {
antipatterns.push({ name: section, items: antipatternsMap[section] });
}
}
return { patterns, antipatterns };
}
/**
* Provider-specific placeholders
*/
export const PROVIDER_PLACEHOLDERS = {
'claude-code': {
model: 'Claude',
config_file: 'CLAUDE.md',
ask_instruction: 'use the AskUserQuestion tool to clarify what you cannot infer.'
},
'cursor': {
model: 'the model',
config_file: '.cursorrules',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
},
'gemini': {
model: 'Gemini',
config_file: 'GEMINI.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
},
'codex': {
model: 'GPT',
config_file: 'AGENTS.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
}
};
/**
* Replace all {{placeholder}} tokens with provider-specific values
*/
export function replacePlaceholders(content, provider) {
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS['cursor'];
return content
.replace(/\{\{model\}\}/g, placeholders.model)
.replace(/\{\{config_file\}\}/g, placeholders.config_file)
.replace(/\{\{ask_instruction\}\}/g, placeholders.ask_instruction);
}
// Legacy alias for backward compatibility
export function replaceModelPlaceholder(content, provider) {
return replacePlaceholders(content, provider);
}
/**