references/agent-behavior.md
# Visual Creative Subagent — Agent Behavior Specification
> This file is the skill-accessible version of the agent behavior definition.
> Claude Code reads skill references (not AGENT.md). Codex reads AGENT.md.
> Both must stay in sync. If you update one, update the other.
## Purpose
Generates one isolated visual/creative concept pass for a specific domain (data-vis, animation, or graphic-design), style, and pass number. Produces a self-contained browser-renderable HTML/CSS/JS showcase.
## Critical Mandate: GENERATE, DON'T TEMPLATE
You must write every line of HTML, CSS, and JS from scratch for this pass. Do NOT use a shared template, do NOT copy structure from other passes. Each pass must look and feel completely different.
## Input Context
You will receive:
- `domain`: One of `data-vis`, `animation`, or `graphic-design`
- `styleId`: The style within the domain (e.g., "bar-chart", "particle-systems", "generative-geometry")
- `pass`: The pass number within that style
- `outputDir`: Where to write files
- `stylePalette`: Color, font, and design token definitions
- `libraryDirective`: Which CDN library to use
- `mockData`: Sample data for data-vis passes
- `sceneDescription`: For animation/graphic passes — what to render
- `antiRepeat`: Explicit list of things NOT to repeat from prior passes
## Isolation Rules
1. Do NOT read sibling pass folders.
2. Do NOT read other style or domain folders.
3. Use only the provided context + your own creative judgment.
## Output Files
Write these files into the output directory:
- `index.html` - Self-contained showcase page
- `style.css` - Complete stylesheet
- `app.js` - Initialization, rendering, animation logic
- `README.md` - Style metadata, library usage, design decisions
- `validation/handoff.json` - Machine-readable domain/style metadata
- `validation/desktop/showcase.png` - Playwright screenshot at 1536x960 (auto-generated)
- `validation/mobile/showcase.png` - Playwright screenshot at 390x844 2x (auto-generated)
- `validation/report.playwright.json` - Structured Playwright report (auto-generated)
## Quality Requirements
1. **No blank canvas**: The page must render visible content immediately on load
2. **Responsive**: Must look good on both desktop (1536px) and mobile (390px)
3. **Performance**: Smooth rendering, no memory leaks, target 60fps for animations
4. **Polish**: Professional presentation, consistent spacing, clean typography
5. **Working interactivity**: All controls, tooltips, and interactive elements must function
6. **Library loaded**: CDN script must load successfully
7. **Palette applied**: Colors must match the provided palette
## Mandatory Workflow: Plan → Generate → Validate → Fix Loop
**You MUST follow this exact workflow. Do NOT skip steps. Do NOT report the pass as complete until the validation loop passes.**
### Phase 1: Plan
- Review all inputs (domain, style, palette, creative brief, library directive, anti-repeat)
- Read the library-catalog.json to get the correct CDN URL
- Plan your design approach
### Phase 2: Pre-validate CDN URLs
Before writing HTML, verify every CDN URL resolves:
```bash
curl -s -o /dev/null -w "%{http_code}" "<CDN_URL>"
```
- Non-200 response → find alternative URL/version
- Check the library-catalog.json `note` field for version-specific gotchas
- **Do NOT proceed with unverified CDN URLs**
### Phase 3: Generate
- Write all files: `index.html`, `style.css`, `app.js`, `README.md`, `validation/handoff.json`
- Use only verified CDN URLs from Phase 2
### Phase 4: Validate (Playwright)
```bash
node .claude/skills/visual-creative-subagent/scripts/validate-visuals-playwright.mjs --pass-dir <outputDir>
```
### Phase 5: Review Screenshots
**Read the generated PNG files and visually assess them.** Check for:
- Blank/black canvas (library load failure or JS error)
- Missing primary content (visualization/animation/graphic not visible)
- Text unreadable (poor contrast, wrong font, overlap)
- Layout broken (overflow, misalignment)
- Controls missing (play/pause, regenerate, filters)
- Mobile broken (overflow, tiny text, off-screen elements)
### Phase 6: Fix and Re-validate (Loop)
If ANY failure detected:
1. Identify root cause → fix files → re-run Phase 4 → re-review Phase 5
2. **Maximum 3 fix cycles.** If still failing, document issues in `validation/handoff.json` under `unresolvedIssues`.
### Phase 7: Complete
Only after screenshots exist AND visual review passes. Write final `validation/handoff.json` with `validationPassed`, `fixCycles`, `cdnUrlsVerified`, `screenshotsReviewed` fields.
**Completion gate:** A pass is NOT complete until Playwright screenshots exist, you have visually reviewed them, and no failure conditions remain.
references/library-catalog.json
{
"version": "1.0.0",
"description": "CDN library catalog for visual/creative concept generation. Subagents select libraries based on the orchestrator's libraryDirective.",
"libraries": {
"data-visualization": {
"d3": {
"name": "D3.js",
"version": "7.9.0",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js",
"type": "script",
"rendering": ["svg", "canvas"],
"bestFor": "Custom bespoke visualizations, maximum control, force simulations, geo projections, treemaps",
"size": "~280KB"
},
"chart-js": {
"name": "Chart.js",
"version": "4.4.7",
"cdn": "https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js",
"type": "script",
"rendering": ["canvas"],
"bestFor": "Quick bar/line/pie/radar charts with minimal setup",
"size": "~200KB"
},
"echarts": {
"name": "Apache ECharts",
"version": "5.5.1",
"cdn": "https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js",
"type": "script",
"rendering": ["canvas", "svg", "webgl"],
"bestFor": "20+ chart types, large datasets, beautiful defaults, 3D charts",
"size": "~1MB"
},
"vega-lite": {
"name": "Vega-Lite",
"version": "5.21.0",
"cdn": ["https://cdn.jsdelivr.net/npm/vega@5/build/vega.min.js", "https://cdn.jsdelivr.net/npm/vega-lite@5/build/vega-lite.min.js", "https://cdn.jsdelivr.net/npm/vega-embed@6/build/vega-embed.min.js"],
"type": "script",
"rendering": ["svg", "canvas"],
"bestFor": "Declarative grammar of graphics, rapid exploration",
"size": "~350KB total"
},
"pts-js": {
"name": "Pts.js",
"version": "0.12.2",
"cdn": "https://cdn.jsdelivr.net/npm/pts@0.12.2/dist/pts.min.js",
"type": "script",
"rendering": ["canvas", "svg"],
"bestFor": "Creative data vis, artistic visualizations, point-based operations",
"size": "~100KB"
},
"rough-js": {
"name": "Rough.js",
"version": "4.6.6",
"cdn": "https://cdn.jsdelivr.net/npm/roughjs@4.6.6/bundled/rough.js",
"type": "script",
"rendering": ["svg", "canvas"],
"bestFor": "Hand-drawn sketch aesthetic overlay on any SVG/canvas",
"size": "~50KB"
}
},
"animation": {
"gsap": {
"name": "GSAP (GreenSock)",
"version": "3.12.5",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js",
"plugins": {
"motionPath": "https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/MotionPathPlugin.min.js",
"scrollTrigger": "https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js",
"morphSVG": "Club GreenSock only (not CDN)"
},
"type": "script",
"rendering": ["dom", "svg", "canvas"],
"bestFor": "Timeline-based animation, sequencing, SVG morphing, scroll-triggered effects",
"size": "~70KB"
},
"anime-js": {
"name": "Anime.js",
"version": "3.2.2",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js",
"type": "script",
"rendering": ["dom", "svg"],
"bestFor": "Lightweight CSS/SVG/DOM animation, staggering, keyframes",
"size": "~17KB"
},
"mo-js": {
"name": "Mo.js",
"version": "0.288.2",
"cdn": "https://cdn.jsdelivr.net/npm/@mojs/core@0.288.2/dist/mo.umd.js",
"type": "script",
"rendering": ["svg", "dom"],
"bestFor": "Motion graphics, burst animations, shape morphing",
"size": "~35KB"
},
"matter-js": {
"name": "Matter.js",
"version": "0.19.0",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js",
"type": "script",
"rendering": ["canvas"],
"bestFor": "2D physics simulation — gravity, collisions, constraints, ragdolls",
"size": "~90KB"
},
"lottie": {
"name": "Lottie Web",
"version": "5.12.2",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js",
"type": "script",
"rendering": ["svg", "canvas"],
"bestFor": "After Effects animation playback, JSON-based animation",
"size": "~250KB"
}
},
"graphics": {
"three-js": {
"name": "Three.js",
"version": "0.160.0",
"cdn": "https://unpkg.com/three@0.160.0/build/three.min.js",
"module": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"addons": {
"OrbitControls": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js",
"GLTFLoader": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/loaders/GLTFLoader.js"
},
"note": "v0.170.0 dropped UMD builds; use 0.160.0 for <script> tag with global THREE. Use module URL for ES module imports.",
"type": "script",
"rendering": ["webgl"],
"bestFor": "3D scenes, globes, particle clouds, interactive 3D graphics",
"size": "~600KB"
},
"p5js": {
"name": "p5.js",
"version": "1.11.3",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/p5.min.js",
"type": "script",
"rendering": ["canvas", "webgl"],
"bestFor": "Creative coding, generative art, interactive sketches, teaching",
"size": "~900KB"
},
"two-js": {
"name": "Two.js",
"version": "0.8.14",
"cdn": "https://cdn.jsdelivr.net/npm/two.js@0.8.14/build/two.min.js",
"type": "script",
"rendering": ["svg", "canvas", "webgl"],
"bestFor": "Renderer-agnostic 2D drawing, geometric patterns, clean vector art",
"size": "~120KB"
},
"paper-js": {
"name": "Paper.js",
"version": "0.12.18",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.18/paper-full.min.js",
"type": "script",
"rendering": ["canvas"],
"bestFor": "Vector graphics scripting, path operations, boolean operations",
"size": "~220KB"
},
"pixi-js": {
"name": "PixiJS",
"version": "7.3.3",
"cdn": "https://cdn.jsdelivr.net/npm/pixi.js@7.3.3/dist/pixi.min.js",
"type": "script",
"rendering": ["webgl", "canvas"],
"bestFor": "High-performance 2D rendering, sprites, particle effects, games",
"size": "~500KB"
},
"zdog": {
"name": "Zdog",
"version": "1.1.3",
"cdn": "https://cdn.jsdelivr.net/npm/zdog@1.1.3/dist/zdog.dist.min.js",
"type": "script",
"rendering": ["canvas", "svg"],
"bestFor": "Pseudo-3D illustrations, flat-shaded 3D-looking art",
"size": "~28KB"
},
"fabric-js": {
"name": "Fabric.js",
"version": "6.5.1",
"cdn": "https://cdn.jsdelivr.net/npm/fabric@6.5.1/dist/index.min.js",
"type": "script",
"rendering": ["canvas"],
"bestFor": "Canvas manipulation, SVG import/export, interactive object editing",
"size": "~300KB"
},
"snap-svg": {
"name": "Snap.svg",
"version": "0.5.1",
"cdn": "https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js",
"type": "script",
"rendering": ["svg"],
"bestFor": "SVG manipulation, animation, complex SVG scenes",
"size": "~80KB"
},
"svg-js": {
"name": "SVG.js",
"version": "3.2.4",
"cdn": "https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.2.4/dist/svg.min.js",
"type": "script",
"rendering": ["svg"],
"bestFor": "Lightweight SVG creation and manipulation",
"size": "~65KB"
}
}
}
}
scripts/validate-visuals-playwright.mjs
#!/usr/bin/env node
/**
* Visual Creative Concept — Playwright Screenshot Validation
*
* Captures screenshots of single-page visual showcases at desktop and mobile viewports.
* Unlike the frontend design validator which navigates between 10 views, this script
* captures a single showcase page per pass (the visualization, animation, or graphic).
*
* Usage:
* node validate-visuals-playwright.mjs --pass-dir <path>
* node validate-visuals-playwright.mjs --concept-root <path> [--domain <domain>] [--style <style>] [--pass <n>]
*
* Modes:
* --pass-dir Validate a single pass directory (must contain index.html)
* --concept-root Scan all pass directories under the root (default: .docs/design/concepts)
*/
import fs from "node:fs/promises";
import path from "node:path";
function arg(name, fallback = null) {
const i = process.argv.indexOf(name);
if (i === -1) return fallback;
return process.argv[i + 1] ?? fallback;
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
const passDir = arg("--pass-dir", null);
const conceptRoot = path.resolve(arg("--concept-root", ".docs/design/concepts"));
const domainFilter = arg("--domain", null);
const styleFilter = arg("--style", null);
const passFilter = arg("--pass", null);
let chromium;
try {
({ chromium } = await import("playwright"));
} catch {
console.error("Playwright not installed. Run: pnpm add -D playwright");
process.exit(2);
}
const VIEWPORTS = {
desktop: { width: 1536, height: 960 },
mobile: { width: 390, height: 844, deviceScaleFactor: 2, isMobile: true }
};
// ---------------------------------------------------------------------------
// Smart-wait helpers (reused from frontend validator)
// ---------------------------------------------------------------------------
async function dismissLoadingOverlays(page) {
await page.evaluate(() => {
const selectors = [
"#loading-overlay", "#loader", "#splash", "#preloader",
".loading-overlay", ".loader-overlay", ".splash-screen",
".loading-screen", ".preloader", ".page-loader",
"[data-loading-overlay]", "[data-loader]"
];
for (const sel of selectors) {
document.querySelectorAll(sel).forEach(el => {
el.style.display = "none";
el.style.opacity = "0";
el.style.visibility = "hidden";
el.style.pointerEvents = "none";
});
}
document.body.classList.remove("loading", "is-loading", "no-scroll", "overflow-hidden");
document.documentElement.classList.remove("loading", "is-loading", "no-scroll", "overflow-hidden");
});
}
async function waitForDomStability(page, { quietMs = 500, maxMs = 6000 } = {}) {
await page.evaluate(({ quietMs, maxMs }) => {
return new Promise(resolve => {
let timer = null;
const deadline = Date.now() + maxMs;
const observer = new MutationObserver(() => {
clearTimeout(timer);
if (Date.now() >= deadline) { observer.disconnect(); resolve(); return; }
timer = setTimeout(() => { observer.disconnect(); resolve(); }, quietMs);
});
observer.observe(document.body, {
childList: true, subtree: true, attributes: true, characterData: true
});
timer = setTimeout(() => { observer.disconnect(); resolve(); }, quietMs);
});
}, { quietMs, maxMs });
}
// ---------------------------------------------------------------------------
// Capture function for a single viewport
// ---------------------------------------------------------------------------
async function captureViewport(browser, indexPath, passPath, viewportName, viewportOpts) {
const context = await browser.newContext({ viewport: viewportOpts });
const page = await context.newPage();
const outDir = path.join(passPath, "validation", viewportName);
await fs.mkdir(outDir, { recursive: true });
const fileUrl = `file:///${indexPath.replace(/\\/g, "/")}`;
await page.goto(fileUrl, { waitUntil: "networkidle", timeout: 30000 });
// Wait for fonts
await page.evaluate(() => document.fonts?.ready).catch(() => {});
// Dismiss loading overlays
await dismissLoadingOverlays(page);
// Wait for libraries (D3, Chart.js, Three.js, p5.js, etc.) to initialize
// Give extra time since visual libraries often need longer to render
await waitForDomStability(page, { quietMs: 800, maxMs: 6000 });
// Extra wait for canvas/WebGL rendering
await page.waitForTimeout(1500);
// Full-page screenshot
const shotPath = path.join(outDir, "showcase.png");
await page.screenshot({ path: shotPath, fullPage: true });
// Also capture a viewport-only screenshot (no scroll) for animations
const viewportShotPath = path.join(outDir, "showcase-viewport.png");
await page.screenshot({ path: viewportShotPath, fullPage: false });
await context.close();
return {
screenshots: [
`validation/${viewportName}/showcase.png`,
`validation/${viewportName}/showcase-viewport.png`
]
};
}
// ---------------------------------------------------------------------------
// Process a single pass directory
// ---------------------------------------------------------------------------
async function processPass(browser, passPath) {
const indexPath = path.join(passPath, "index.html");
if (!(await exists(indexPath))) return null;
const passName = path.basename(passPath);
const styleName = path.basename(path.dirname(passPath));
const domainName = path.basename(path.dirname(path.dirname(passPath)));
console.log(`\n📸 ${domainName}/${styleName}/${passName}`);
try {
const desktop = await captureViewport(
browser, indexPath, passPath, "desktop", VIEWPORTS.desktop
);
console.log(` ✓ Desktop: ${desktop.screenshots.length} screenshots`);
const mobile = await captureViewport(
browser, indexPath, passPath, "mobile", VIEWPORTS.mobile
);
console.log(` ✓ Mobile: ${mobile.screenshots.length} screenshots`);
const report = {
domain: domainName,
style: styleName,
pass: passName,
desktop: {
viewport: VIEWPORTS.desktop,
screenshots: desktop.screenshots
},
mobile: {
viewport: VIEWPORTS.mobile,
screenshots: mobile.screenshots
},
totalScreenshots: desktop.screenshots.length + mobile.screenshots.length,
timestamp: new Date().toISOString()
};
const validationDir = path.join(passPath, "validation");
await fs.writeFile(
path.join(validationDir, "report.playwright.json"),
JSON.stringify(report, null, 2),
"utf8"
);
return report;
} catch (error) {
console.error(` ✗ Error: ${error.message}`);
return { error: error.message };
}
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
const browser = await chromium.launch({ headless: true });
const aggregate = [];
let errors = 0;
if (passDir) {
// Single-pass mode
const resolved = path.resolve(passDir);
if (!(await exists(resolved))) {
console.error(`Pass directory not found: ${resolved}`);
await browser.close();
process.exit(1);
}
const result = await processPass(browser, resolved);
if (result) {
if (result.error) errors++;
else aggregate.push(result);
}
} else {
// Scan mode: iterate over domains → styles → passes
if (!(await exists(conceptRoot))) {
console.error(`Concept root not found: ${conceptRoot}`);
await browser.close();
process.exit(1);
}
const domains = (await fs.readdir(conceptRoot, { withFileTypes: true }))
.filter(d => d.isDirectory() && !d.name.startsWith("_"))
.filter(d => !domainFilter || d.name === domainFilter);
for (const domain of domains) {
const domainPath = path.join(conceptRoot, domain.name);
const styles = (await fs.readdir(domainPath, { withFileTypes: true }))
.filter(d => d.isDirectory())
.filter(d => !styleFilter || d.name === styleFilter);
for (const style of styles) {
const stylePath = path.join(domainPath, style.name);
const passes = (await fs.readdir(stylePath, { withFileTypes: true }))
.filter(d => d.isDirectory() && d.name.startsWith("pass-"))
.filter(d => !passFilter || d.name === `pass-${passFilter}`);
for (const pass of passes) {
const passPath = path.join(stylePath, pass.name);
const result = await processPass(browser, passPath);
if (result) {
if (result.error) errors++;
else aggregate.push(result);
}
}
}
}
// Write aggregate report
await fs.writeFile(
path.join(conceptRoot, "validation-report.json"),
JSON.stringify(aggregate, null, 2),
"utf8"
);
}
await browser.close();
const totalShots = aggregate.reduce((s, r) => s + (r.totalScreenshots || 0), 0);
console.log(`\nValidated ${aggregate.length} pass folders — ${totalShots} total screenshots (${errors} errors).`);
if (errors > 0) process.exit(3);
SKILL.md
---
name: visual-creative-subagent
description: >-
Generate one isolated visual/creative concept pass as a self-contained
browser-renderable showcase using HTML/CSS/JS with domain-appropriate
libraries. Domains: data-vis (charts/graphs), animation (motion/physics),
graphic-design (generative art/3D/illustration). Each pass is written
from scratch by the AI agent, not stamped from a template.
---
# Visual Creative Subagent
Use this skill for one pass only. Do not blend with other pass outputs.
## Inputs (via Task agent prompt)
- `domain` - One of: `data-vis`, `animation`, `graphic-design`
- `styleId` - Style within the domain (e.g., "bar-chart", "particle-systems", "globe-3d")
- `pass` - Pass number
- `outputDir` - Where to write generated files
- `stylePalette` - Colors, fonts, design tokens
- `styleDirection` - Creative brief text
- `libraryDirective` - Which CDN library to use (from library-catalog.json)
- `mockData` - Sample data (for data-vis passes)
- `sceneDescription` - What to render (for animation/graphic-design passes)
- `antiRepeat` - Explicit list of things NOT to repeat from prior passes
## Hard Requirements
1. Generate a self-contained HTML showcase page (not an app with navigation).
2. The visualization/animation/graphic must render immediately on page load.
3. Each pass must be visually distinct from every other pass in the same style.
4. Use plain HTML/CSS/JS with the specified CDN library.
5. Include responsive behavior for desktop and mobile.
6. Write EVERY line of code from scratch — no shared templates.
7. The showcase must be interactive where appropriate (tooltips, controls, hover effects).
## Page Structure
Every pass produces a single showcase page with these regions:
### Header Bar
- Domain badge (e.g., "DATA VIS", "ANIMATION", "GRAPHIC DESIGN")
- Style name and pass number
- Library badge showing which library powers this pass
### Main Showcase Area
- Fills 80%+ of the viewport
- The visualization, animation, or graphic renders here
- Must be responsive — scales/adapts to viewport size
### Controls Panel
- **Data-vis**: Filter dropdowns, sort toggles, dataset switcher
- **Animation**: Play/pause button, speed slider, reset button
- **Graphic-design**: Regenerate/randomize button, parameter sliders
### Info Footer
- Technical details: library name, version, render method (Canvas/SVG/WebGL)
- Data source description (for data-vis)
- Brief technique description
## Domain-Specific Guidelines
### Data Visualization
- Use the provided `mockData` faithfully — do not invent different data
- Chart colors must use `stylePalette`, not library defaults
- Include axis labels, legend, title, and tooltips
- Show at least 2 views of the data (e.g., chart + summary table, or chart + sparklines)
- Animated entrance for chart elements
- Responsive: chart reflows on mobile, labels don't overflow
### Animation
- Animation must loop continuously or play indefinitely
- Use `requestAnimationFrame` or library animation loops
- Target 60fps — no jank or frame drops
- Include visible play/pause controls
- The animation fills the showcase area
- For physics: add click/drag interactivity
### Graphic Design
- Render a complete composition — not a sketch or placeholder
- For generative art: include a "Regenerate" button with new random seed
- For 3D: include orbit controls or auto-rotation
- For illustrations: render at high resolution
- Display the random seed or generation parameters
## Quality Standards
1. **No blank canvas**: Visible content must render immediately on load
2. **Responsive**: Works on desktop (1536px) and mobile (390px)
3. **Performance**: Smooth rendering, no memory leaks, no console errors
4. **Polish**: Professional presentation, consistent spacing, clean typography
5. **Working interactivity**: All controls and hover states must function
6. **Library loaded**: CDN script must load successfully (use valid URLs from catalog)
7. **Palette applied**: Colors must match the provided palette, not library defaults
## Files
- `index.html` - Complete HTML with CDN library tags and showcase structure
- `style.css` - Full CSS with responsive breakpoints
- `app.js` - Library initialization, rendering, interaction logic
- `README.md` - Concept overview, library usage, technique description
- `validation/handoff.json` - Domain/style/library metadata
## Mandatory Workflow: Plan → Generate → Validate → Fix Loop
**The subagent itself (NOT the orchestrator) owns the full validation lifecycle. A pass is not complete until the subagent has validated and visually reviewed its own output.**
### Phase 1: Plan
- Review all inputs (domain, style, palette, brief, library directive, anti-repeat)
- Read the library-catalog.json for correct CDN URLs
- Plan design approach
### Phase 2: Pre-validate CDN URLs
Before writing HTML, verify every CDN URL resolves:
```bash
curl -s -o /dev/null -w "%{http_code}" "<CDN_URL>"
```
- Non-200 → find alternative URL/version from catalog or CDN provider
- Check `note` fields in library-catalog.json for version-specific issues (e.g., UMD vs ESM)
### Phase 3: Generate
- Write all files with verified CDN URLs only
### Phase 4: Validate (Playwright)
The **subagent** runs the Playwright validation script (not the orchestrator):
```bash
node .claude/skills/visual-creative-subagent/scripts/validate-visuals-playwright.mjs --pass-dir <outputDir>
```
### Phase 5: Review Screenshots
The **subagent** reads the generated PNG screenshots and visually assesses them. Failure conditions:
- Blank/black canvas (library load failure or JS error)
- Missing primary content (visualization/animation/graphic not visible)
- Text unreadable (poor contrast, wrong font, overlap)
- Layout broken (overflow, misalignment)
- Controls missing
- Mobile broken
### Phase 6: Fix and Re-validate (Loop, max 3 cycles)
If failures detected → fix root cause → re-run Phase 4 → re-review Phase 5.
After 3 cycles, document remaining issues in `validation/handoff.json` under `unresolvedIssues`.
### Phase 7: Complete
Write final `validation/handoff.json` with:
- `validationPassed`: boolean
- `fixCycles`: number of fix iterations performed
- `cdnUrlsVerified`: true
- `screenshotsReviewed`: true
### Required Screenshots Per Pass
- `validation/desktop/showcase.png` — Full-page screenshot at 1536x960
- `validation/desktop/showcase-viewport.png` — Viewport-only screenshot
- `validation/mobile/showcase.png` — Full-page screenshot at 390x844 (2x scale)
- `validation/mobile/showcase-viewport.png` — Viewport-only screenshot
- `validation/report.playwright.json` — Structured report
**Completion gate:** A pass is NOT complete until screenshots exist on disk AND the subagent has visually reviewed them AND no failure conditions remain.