Initial commit

This commit is contained in:
Samraaj Bath
2026-06-29 15:11:48 -07:00
commit 66dfdcc58d
404 changed files with 45970 additions and 0 deletions
+632
View File
@@ -0,0 +1,632 @@
---
name: agent-browser
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
user-invocable: false
allowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*)
---
# Browser Automation with agent-browser
## Core Workflow
Every browser automation follows this pattern:
1. **Navigate**: `agent-browser open <url>`
2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)
3. **Interact**: Use refs to click, fill, select
4. **Re-snapshot**: After navigation or DOM changes, get fresh refs
```bash
agent-browser open https://example.com/form
agent-browser snapshot -i
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"
agent-browser fill @e1 "user@example.com"
agent-browser fill @e2 "password123"
agent-browser click @e3
agent-browser wait --load networkidle
agent-browser snapshot -i # Check result
```
## Command Chaining
Commands can be chained with `&&` in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls.
```bash
# Chain open + wait + snapshot in one call
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
# Chain multiple interactions
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "password123" && agent-browser click @e3
# Navigate and capture
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
```
**When to chain:** Use `&&` when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs).
## Handling Authentication
When automating a site that requires login, choose the approach that fits:
**Option 1: Import auth from the user's browser (fastest for one-off tasks)**
```bash
# Connect to the user's running Chrome (they're already logged in)
agent-browser --auto-connect state save ./auth.json
# Use that auth state
agent-browser --state ./auth.json open https://app.example.com/dashboard
```
State files contain session tokens in plaintext -- add to `.gitignore` and delete when no longer needed. Set `AGENT_BROWSER_ENCRYPTION_KEY` for encryption at rest.
**Option 2: Persistent profile (simplest for recurring tasks)**
```bash
# First run: login manually or via automation
agent-browser --profile ~/.myapp open https://app.example.com/login
# ... fill credentials, submit ...
# All future runs: already authenticated
agent-browser --profile ~/.myapp open https://app.example.com/dashboard
```
**Option 3: Session name (auto-save/restore cookies + localStorage)**
```bash
agent-browser --session-name myapp open https://app.example.com/login
# ... login flow ...
agent-browser close # State auto-saved
# Next time: state auto-restored
agent-browser --session-name myapp open https://app.example.com/dashboard
```
**Option 4: Auth vault (credentials stored encrypted, login by name)**
```bash
echo "$PASSWORD" | agent-browser auth save myapp --url https://app.example.com/login --username user --password-stdin
agent-browser auth login myapp
```
**Option 5: State file (manual save/load)**
```bash
# After logging in:
agent-browser state save ./auth.json
# In a future session:
agent-browser state load ./auth.json
agent-browser open https://app.example.com/dashboard
```
See [references/authentication.md](references/authentication.md) for OAuth, 2FA, cookie-based auth, and token refresh patterns.
## Essential Commands
```bash
# Navigation
agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser close # Close browser
# Snapshot
agent-browser snapshot -i # Interactive elements with refs (recommended)
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer)
agent-browser snapshot -s "#selector" # Scope to CSS selector
# Interaction (use @refs from snapshot)
agent-browser click @e1 # Click element
agent-browser click @e1 --new-tab # Click and open in new tab
agent-browser fill @e2 "text" # Clear and type text
agent-browser type @e2 "text" # Type without clearing
agent-browser select @e1 "option" # Select dropdown option
agent-browser check @e1 # Check checkbox
agent-browser press Enter # Press key
agent-browser keyboard type "text" # Type at current focus (no selector)
agent-browser keyboard inserttext "text" # Insert without key events
agent-browser scroll down 500 # Scroll page
agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container
# Get information
agent-browser get text @e1 # Get element text
agent-browser get url # Get current URL
agent-browser get title # Get page title
agent-browser get cdp-url # Get CDP WebSocket URL
# Wait
agent-browser wait @e1 # Wait for element
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --url "**/page" # Wait for URL pattern
agent-browser wait 2000 # Wait milliseconds
# Downloads
agent-browser download @e1 ./file.pdf # Click element to trigger download
agent-browser wait --download ./output.zip # Wait for any download to complete
agent-browser --download-path ./downloads open <url> # Set default download directory
# Viewport & Device Emulation
agent-browser set viewport 1920 1080 # Set viewport size (default: 1280x720)
agent-browser set viewport 1920 1080 2 # 2x retina (same CSS size, higher res screenshots)
agent-browser set device "iPhone 14" # Emulate device (viewport + user agent)
# Capture
agent-browser screenshot # Screenshot to temp dir
agent-browser screenshot --full # Full page screenshot (AVOID: can exceed 8000px API image limit — use scroll + multiple viewport screenshots instead)
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser pdf output.pdf # Save as PDF
# Diff (compare page states)
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
agent-browser diff screenshot --baseline before.png # Visual pixel diff
agent-browser diff url <url1> <url2> # Compare two pages
agent-browser diff url <url1> <url2> --wait-until networkidle # Custom wait strategy
agent-browser diff url <url1> <url2> --selector "#main" # Scope to element
```
## Common Patterns
### Form Submission
```bash
agent-browser open https://example.com/signup
agent-browser snapshot -i
agent-browser fill @e1 "Jane Doe"
agent-browser fill @e2 "jane@example.com"
agent-browser select @e3 "California"
agent-browser check @e4
agent-browser click @e5
agent-browser wait --load networkidle
```
### Authentication with Auth Vault (Recommended)
```bash
# Save credentials once (encrypted with AGENT_BROWSER_ENCRYPTION_KEY)
# Recommended: pipe password via stdin to avoid shell history exposure
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
# Login using saved profile (LLM never sees password)
agent-browser auth login github
# List/show/delete profiles
agent-browser auth list
agent-browser auth show github
agent-browser auth delete github
```
### Authentication with State Persistence
```bash
# Login once and save state
agent-browser open https://app.example.com/login
agent-browser snapshot -i
agent-browser fill @e1 "$USERNAME"
agent-browser fill @e2 "$PASSWORD"
agent-browser click @e3
agent-browser wait --url "**/dashboard"
agent-browser state save auth.json
# Reuse in future sessions
agent-browser state load auth.json
agent-browser open https://app.example.com/dashboard
```
### Session Persistence
```bash
# Auto-save/restore cookies and localStorage across browser restarts
agent-browser --session-name myapp open https://app.example.com/login
# ... login flow ...
agent-browser close # State auto-saved to ~/.agent-browser/sessions/
# Next time, state is auto-loaded
agent-browser --session-name myapp open https://app.example.com/dashboard
# Encrypt state at rest
export AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32)
agent-browser --session-name secure open https://app.example.com
# Manage saved states
agent-browser state list
agent-browser state show myapp-default.json
agent-browser state clear myapp
agent-browser state clean --older-than 7
```
### Data Extraction
```bash
agent-browser open https://example.com/products
agent-browser snapshot -i
agent-browser get text @e5 # Get specific element text
agent-browser get text body > page.txt # Get all page text
# JSON output for parsing
agent-browser snapshot -i --json
agent-browser get text @e1 --json
```
### Parallel Sessions
```bash
agent-browser --session site1 open https://site-a.com
agent-browser --session site2 open https://site-b.com
agent-browser --session site1 snapshot -i
agent-browser --session site2 snapshot -i
agent-browser session list
```
### Connect to Existing Chrome
```bash
# Auto-discover running Chrome with remote debugging enabled
agent-browser --auto-connect open https://example.com
agent-browser --auto-connect snapshot
# Or with explicit CDP port
agent-browser --cdp 9222 snapshot
```
### Color Scheme (Dark Mode)
```bash
# Persistent dark mode via flag (applies to all pages and new tabs)
agent-browser --color-scheme dark open https://example.com
# Or via environment variable
AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com
# Or set during session (persists for subsequent commands)
agent-browser set media dark
```
### Viewport & Responsive Testing
```bash
# Set a custom viewport size (default is 1280x720)
agent-browser set viewport 1920 1080
agent-browser screenshot desktop.png
# Test mobile-width layout
agent-browser set viewport 375 812
agent-browser screenshot mobile.png
# Retina/HiDPI: same CSS layout at 2x pixel density
# Screenshots stay at logical viewport size, but content renders at higher DPI
agent-browser set viewport 1920 1080 2
agent-browser screenshot retina.png
# Device emulation (sets viewport + user agent in one step)
agent-browser set device "iPhone 14"
agent-browser screenshot device.png
```
The `scale` parameter (3rd argument) sets `window.devicePixelRatio` without changing CSS layout. Use it when testing retina rendering or capturing higher-resolution screenshots.
### Visual Browser (Debugging)
```bash
agent-browser --headed open https://example.com
agent-browser highlight @e1 # Highlight element
agent-browser inspect # Open Chrome DevTools for the active page
agent-browser record start demo.webm # Record session
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop trace.json # Stop and save profile (path optional)
```
Use `AGENT_BROWSER_HEADED=1` to enable headed mode via environment variable. Browser extensions work in both headed and headless mode.
### Local Files (PDFs, HTML)
```bash
# Open local files with file:// URLs
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
agent-browser screenshot output.png
```
### iOS Simulator (Mobile Safari)
```bash
# List available iOS simulators
agent-browser device list
# Launch Safari on a specific device
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
# Same workflow as desktop - snapshot, interact, re-snapshot
agent-browser -p ios snapshot -i
agent-browser -p ios tap @e1 # Tap (alias for click)
agent-browser -p ios fill @e2 "text"
agent-browser -p ios swipe up # Mobile-specific gesture
# Take screenshot
agent-browser -p ios screenshot mobile.png
# Close session (shuts down simulator)
agent-browser -p ios close
```
**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`)
**Real devices:** Works with physical iOS devices if pre-configured. Use `--device "<UDID>"` where UDID is from `xcrun xctrace list devices`.
## Security
All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output.
### Content Boundaries (Recommended for AI Agents)
Enable `--content-boundaries` to wrap page-sourced output in markers that help LLMs distinguish tool output from untrusted page content:
```bash
export AGENT_BROWSER_CONTENT_BOUNDARIES=1
agent-browser snapshot
# Output:
# --- AGENT_BROWSER_PAGE_CONTENT nonce=<hex> origin=https://example.com ---
# [accessibility tree]
# --- END_AGENT_BROWSER_PAGE_CONTENT nonce=<hex> ---
```
### Domain Allowlist
Restrict navigation to trusted domains. Wildcards like `*.example.com` also match the bare domain `example.com`. Sub-resource requests, WebSocket, and EventSource connections to non-allowed domains are also blocked. Include CDN domains your target pages depend on:
```bash
export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com"
agent-browser open https://example.com # OK
agent-browser open https://malicious.com # Blocked
```
### Action Policy
Use a policy file to gate destructive actions:
```bash
export AGENT_BROWSER_ACTION_POLICY=./policy.json
```
Example `policy.json`:
```json
{ "default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"] }
```
Auth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies.
### Output Limits
Prevent context flooding from large pages:
```bash
export AGENT_BROWSER_MAX_OUTPUT=50000
```
## Diffing (Verifying Changes)
Use `diff snapshot` after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session.
```bash
# Typical workflow: snapshot -> action -> diff
agent-browser snapshot -i # Take baseline snapshot
agent-browser click @e2 # Perform action
agent-browser diff snapshot # See what changed (auto-compares to last snapshot)
```
For visual regression testing or monitoring:
```bash
# Save a baseline screenshot, then compare later
agent-browser screenshot baseline.png
# ... time passes or changes are made ...
agent-browser diff screenshot --baseline baseline.png
# Compare staging vs production
agent-browser diff url https://staging.example.com https://prod.example.com --screenshot
```
`diff snapshot` output uses `+` for additions and `-` for removals, similar to git diff. `diff screenshot` produces a diff image with changed pixels highlighted in red, plus a mismatch percentage.
## Timeouts and Slow Pages
The default Playwright timeout is 25 seconds for local browsers. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout:
```bash
# Wait for network activity to settle (best for slow pages)
agent-browser wait --load networkidle
# Wait for a specific element to appear
agent-browser wait "#content"
agent-browser wait @e1
# Wait for a specific URL pattern (useful after redirects)
agent-browser wait --url "**/dashboard"
# Wait for a JavaScript condition
agent-browser wait --fn "document.readyState === 'complete'"
# Wait a fixed duration (milliseconds) as a last resort
agent-browser wait 5000
```
When dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait <selector>` or `wait @ref`.
## Session Management and Cleanup
When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:
```bash
# Each agent gets its own isolated session
agent-browser --session agent1 open site-a.com
agent-browser --session agent2 open site-b.com
# Check active sessions
agent-browser session list
```
Always close your browser session when done to avoid leaked processes:
```bash
agent-browser close # Close default session
agent-browser --session agent1 close # Close specific session
```
If a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work.
## Ref Lifecycle (Important)
Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after:
- Clicking links or buttons that navigate
- Form submissions
- Dynamic content loading (dropdowns, modals)
```bash
agent-browser click @e5 # Navigates to new page
agent-browser snapshot -i # MUST re-snapshot
agent-browser click @e1 # Use new refs
```
## Annotated Screenshots (Vision Mode)
Use `--annotate` to take a screenshot with numbered labels overlaid on interactive elements. Each label `[N]` maps to ref `@eN`. This also caches refs, so you can interact with elements immediately without a separate snapshot.
In native mode, this currently works on the CDP-backed browser path (Chromium/Lightpanda). The Safari/WebDriver backend does not yet support `--annotate`.
```bash
agent-browser screenshot --annotate
# Output includes the image path and a legend:
# [1] @e1 button "Submit"
# [2] @e2 link "Home"
# [3] @e3 textbox "Email"
agent-browser click @e2 # Click using ref from annotated screenshot
```
Use annotated screenshots when:
- The page has unlabeled icon buttons or visual-only elements
- You need to verify visual layout or styling
- Canvas or chart elements are present (invisible to text snapshots)
- You need spatial reasoning about element positions
## Semantic Locators (Alternative to Refs)
When refs are unavailable or unreliable, use semantic locators:
```bash
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find role button click --name "Submit"
agent-browser find placeholder "Search" type "query"
agent-browser find testid "submit-btn" click
```
## JavaScript Evaluation (eval)
Use `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues.
```bash
# Simple expressions work with regular quoting
agent-browser eval 'document.title'
agent-browser eval 'document.querySelectorAll("img").length'
# Complex JS: use --stdin with heredoc (RECOMMENDED)
agent-browser eval --stdin <<'EVALEOF'
JSON.stringify(
Array.from(document.querySelectorAll("img"))
.filter(i => !i.alt)
.map(i => ({ src: i.src.split("/").pop(), width: i.width }))
)
EVALEOF
# Alternative: base64 encoding (avoids all shell escaping issues)
agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)"
```
**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely.
**Rules of thumb:**
- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine
- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`
- Programmatic/generated scripts -> use `eval -b` with base64
## Configuration File
Create `agent-browser.json` in the project root for persistent settings:
```json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data"
}
```
Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.json` < env vars < CLI flags. Use `--config <path>` or `AGENT_BROWSER_CONFIG` env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., `--executable-path` -> `"executablePath"`). Boolean flags accept `true`/`false` values (e.g., `--headed false` overrides config). Extensions from user and project configs are merged, not replaced.
## Deep-Dive Documentation
| Reference | When to Use |
| -------------------------------------------------------------------- | --------------------------------------------------------- |
| [references/commands.md](references/commands.md) | Full command reference with all options |
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis |
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
## Experimental: Native Mode
agent-browser has an experimental native Rust daemon that communicates with Chrome directly via CDP, bypassing Node.js and Playwright entirely. It is opt-in and not recommended for production use yet.
```bash
# Enable via flag
agent-browser --native open example.com
# Enable via environment variable (avoids passing --native every time)
export AGENT_BROWSER_NATIVE=1
agent-browser open example.com
```
The native daemon supports Chromium and Safari (via WebDriver). Firefox and WebKit are not yet supported. All core commands (navigate, snapshot, click, fill, screenshot, cookies, storage, tabs, eval, etc.) work identically in native mode. Use `agent-browser close` before switching between native and default mode within the same session.
## Browser Engine Selection
Use `--engine` to choose a local browser engine. The default is `chrome`.
```bash
# Use Lightpanda (fast headless browser, requires separate install)
agent-browser --engine lightpanda open example.com
# Via environment variable
export AGENT_BROWSER_ENGINE=lightpanda
agent-browser open example.com
# With custom binary path
agent-browser --engine lightpanda --executable-path /path/to/lightpanda open example.com
```
Supported engines:
- `chrome` (default) -- Chrome/Chromium via CDP
- `lightpanda` -- Lightpanda headless browser via CDP (10x faster, 10x less memory than Chrome)
Lightpanda does not support `--extension`, `--profile`, `--state`, or `--allow-file-access`. Install Lightpanda from https://lightpanda.io/docs/open-source/installation.
## Ready-to-Use Templates
| Template | Description |
| ------------------------------------------------------------------------ | ----------------------------------- |
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
```bash
./templates/form-automation.sh https://example.com/form
./templates/authenticated-session.sh https://app.example.com/login
./templates/capture-workflow.sh https://example.com ./output
```
@@ -0,0 +1,178 @@
---
name: animation-translation
description: Library mapping from source-site animation primitives (CSS keyframes, CSS transitions, GSAP tweens/timelines/ScrollTrigger, Framer Motion, Lottie, Three.js, IntersectionObserver reveals) to target primitives in the cloned Next.js project. Rules on when to translate vs keep source library. Load this in Stage 2 (CSS portion) and Stage 3 (JS portion).
user-invocable: false
---
# Animation Translation
Rule of thumb: prefer declarative (Framer Motion, Tailwind transitions) over imperative (GSAP, manual `requestAnimationFrame`) when the translation is clean. Keep imperative when translation loses fidelity — e.g. a GSAP timeline with 15 chained steps.
## Mapping table
| Source | Target | Stage | Notes |
| ----------------------------------------- | -------------------------------------------------- | ----- | ----------------------------------------------------------- |
| CSS keyframes (`@keyframes`) | CSS keyframes kept in `globals.css` | 2 | No reason to translate |
| CSS transition | Tailwind `transition-*` / arbitrary | 2 | `transition-colors duration-200 ease-in-out` |
| GSAP tween (single property) | Framer Motion `animate` prop | 3 | `<motion.div animate={{ opacity: 1 }} transition={{...}}/>` |
| GSAP timeline (complex, chained) | Keep GSAP + `@gsap/react` `useGSAP` | 3 | Do not force bad Framer translation |
| GSAP ScrollTrigger (simple) | Framer `useScroll` + `useTransform` | 3 | Only when trigger + 1-2 output props |
| GSAP ScrollTrigger (complex) | Keep GSAP + ScrollTrigger | 3 | Timeline > 3 steps, pinned, scrub |
| Framer Motion (source already) | Framer Motion direct | 3 | Copy `variants`, `transition`, `whileInView` verbatim |
| Lottie | `lottie-react` + captured `animationData` | 3 | Component is `"use client"` |
| Three.js | `@react-three/fiber` + `drei` | 4 | Handled by `clone-advanced` |
| Custom GLSL shaders | `<shaderMaterial>` with extracted GLSL | 4 | Handled by `clone-advanced` |
| `IntersectionObserver` reveal | Framer `whileInView` + `viewport: { once: true }` | 3 | Rarely worth keeping imperative |
| Raw `<canvas>` 2D loop | Keep the `<canvas>` + `"use client"` + `useEffect` | 4 | If reconstruction fails, video fallback |
| WAAPI (`Element.animate()`) | Translate to Framer `animate` | 3 | Similar semantics |
| Parallax (`background-attachment: fixed`) | CSS kept | 2 | Browsers support it directly |
## Stage 2 (CSS animation)
### Keyframes
Source:
```css
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: none;
}
}
.hero-title {
animation: fadeUp 0.6s ease-out forwards;
}
```
Target — in `globals.css`:
```css
@layer utilities {
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: none;
}
}
.animate-fade-up {
animation: fade-up 0.6s ease-out forwards;
}
}
```
Or, per `css-to-tailwind`, add to `tailwind.config.ts`:
```ts
theme: { extend: {
keyframes: { "fade-up": { from: { opacity: "0", transform: "translateY(12px)" }, to: { opacity: "1", transform: "none" } } },
animation: { "fade-up": "fade-up 0.6s ease-out forwards" },
}}
```
Then the component just has `className="animate-fade-up"`.
### Transitions
Source: `transition: transform 400ms cubic-bezier(0.22,1,0.36,1);`
Target: `className="transition-transform duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)]"`. If that tuple repeats, promote the easing to `theme.extend.transitionTimingFunction`.
## Stage 3 (JS animation)
### Framer Motion (common path)
The capture hook dump gives you the component's `variants`, `transition`, `animate`, `initial`, `whileInView`, `whileHover`, `whileTap` verbatim. Reproduce them:
```tsx
'use client';
import { motion } from 'framer-motion';
const variants = { hidden: { opacity: 0, y: 12 }, visible: { opacity: 1, y: 0 } };
export default function Hero() {
return (
<motion.section
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
variants={variants}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
...
</motion.section>
);
}
```
### GSAP ScrollTrigger kept
When the source has a pinned, scrubbed, multi-step timeline, translating to Framer would flatten it. Keep GSAP:
```tsx
'use client';
import { useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGSAP } from '@gsap/react';
gsap.registerPlugin(ScrollTrigger);
export default function PinnedHero() {
const ref = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const tl = gsap.timeline({
scrollTrigger: { trigger: ref.current, start: 'top top', end: '+=1200', pin: true, scrub: 1 },
});
tl.to('.layer-a', { yPercent: -30 }).to('.layer-b', { yPercent: -50 }, '<');
},
{ scope: ref }
);
return <div ref={ref}>...</div>;
}
```
### Lottie
```tsx
'use client';
import Lottie from 'lottie-react';
import animationData from '@/public/assets/cloned/lottie/<hash>.json';
export default function LottieIcon() {
return <Lottie animationData={animationData} loop autoplay style={{ width: 64, height: 64 }} />;
}
```
Prefer imports over `fetch()` so the JSON ships in the bundle and there is no layout shift on first play.
## Respecting `prefers-reduced-motion`
The Stage 1 gate runs with reduced motion forced, so your Stage 1 output should render a static final-state snapshot when motion is reduced.
In Framer Motion, use the `MotionConfig` with `reducedMotion="user"` in `layout.tsx`, or gate `animate` / `initial` on a `useReducedMotion()` hook.
For CSS animations, wrap the rule:
```css
@media (prefers-reduced-motion: no-preference) {
.animate-fade-up {
animation: fade-up 0.6s ease-out forwards;
}
}
```
## Do not
- Do not add animations that are not in the source. The clone target is fidelity, not flair.
- Do not approximate a GSAP timeline with a Framer `animate` sequence if the timeline has overlaps, labels, or scrubbing — keep GSAP.
- Do not use `requestAnimationFrame` by hand if Framer covers the case. You will burn React lifecycle bugs.
- Do not import full `gsap` bundle if only `ScrollTrigger` is needed — it is fine to `import { gsap } from "gsap"` because tree-shaking handles it, but check the bundle size if multiple sections use GSAP.
+169
View File
@@ -0,0 +1,169 @@
---
name: asset-pipeline
description: Rules for how the clone pipeline names, organizes, and references downloaded assets. Covers folder layout under public/assets/cloned, hash-based filename convention, font strategy (self-host via next/font/local, fall back to system stack if licensing unclear), SVG inline-vs-file threshold, video format, and import paths. Load this when downloading assets and when referencing them in JSX.
user-invocable: false
---
# Asset Pipeline
All cloned assets live under the generated project's `public/assets/cloned/`. Never outside this tree. Never in the workspace `.clone-workspace` beyond the capture bundle.
## Layout
```
<project>/public/assets/cloned/
images/ # jpg, png, webp, avif
videos/ # mp4 (only)
fonts/ # woff2 (preferred), woff, ttf
svg/ # .svg files when not inlined
lottie/ # .json animation data
```
## Naming
Every file is renamed to `<sha1-8>-<sanitized-original-name>`. The sha1 is of the source URL (not content), truncated to 8 hex chars. Sanitization: lowercase, spaces → `-`, strip everything not in `[a-z0-9.-]`.
Examples:
- `https://cdn.site.com/hero-bg.jpg``a1b2c3d4-hero-bg.jpg`
- `https://fonts.site.com/inter-display-500.woff2``e5f67890-inter-display-500.woff2`
Collisions are effectively impossible at 8 chars for any one site. If they happen, extend to 10.
The manifest records the full `local_path` for each asset — the generate agent reads that field, never reconstructs the path.
## Fonts
**Preferred: self-hosted via `next/font/local`.** In `src/app/layout.tsx`:
```ts
import localFont from 'next/font/local';
const display = localFont({
src: [
{ path: '../../public/assets/cloned/fonts/e5f67890-inter-display-400.woff2', weight: '400', style: 'normal' },
{ path: '../../public/assets/cloned/fonts/abcd1234-inter-display-500.woff2', weight: '500', style: 'normal' },
],
variable: '--font-display',
display: 'swap',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={display.variable}>
<body className="font-display">{children}</body>
</html>
);
}
```
Then in `tailwind.config.ts`:
```ts
theme: {
extend: {
fontFamily: {
display: ['var(--font-display)', 'sans-serif'];
}
}
}
```
**If licensing is unclear** (manifest marks `license_hint: "licensed"` or `"unclear"`), do NOT ship the font file. Substitute with the closest free alternative from a system stack and flag in the final report:
| Source font | Fallback |
| -------------------------------- | ------------------------------------------------------ |
| Any licensed sans | Inter from Google Fonts (via `next/font/google`) |
| Any licensed serif | `ui-serif, Georgia, Cambria, "Times New Roman", serif` |
| Any licensed mono | `ui-monospace, SFMono-Regular, Menlo, monospace` |
| Variable display font (licensed) | Inter or Manrope via `next/font/google` |
Record the substitution in the manifest's `skipped[]` with `reason: "licensed_font_unclear"` and log to `logs/skipped.md`.
## SVGs
**Inline as React component** when all hold:
- Source size < 5KB
- SVG is used in-flow, not as a CSS `background-image`
- SVG has no `<script>` tags (never inline external scripts)
Use `@svgr/webpack` or inline the markup by hand in a small component file `src/components/icons/<Name>.tsx`. Prefer hand-written components for simple icons — `@svgr` adds tooling complexity that rarely pays back.
**Keep as file** when:
- SVG ≥ 5KB
- SVG is used as `background-image` (referenced from CSS)
- SVG contains references to `<filter>` or `<pattern>` that make it complex to inline
For file-kept SVGs, reference as `<img src="/assets/cloned/svg/<hash>.svg" alt="" />`. `next/image` does not accept SVGs without the `dangerouslyAllowSVG` flag — prefer raw `<img>` over enabling that flag.
## Videos
Only MP4. Capture normalizes HLS/DASH to MP4 when possible; if the source is DRM-protected (Widevine / FairPlay / PlayReady signatures in HAR), skip with `reason: "drm"` in the manifest.
Embed as:
```tsx
<video
src="/assets/cloned/videos/<hash>.mp4"
autoPlay
loop
muted
playsInline
poster="/assets/cloned/images/<hash>-poster.jpg"
/>
```
## Images
`next/image` is preferred. Configure no remote domains — everything is local under `/assets/cloned/`, which Next.js serves as static assets automatically.
```tsx
import Image from 'next/image';
<Image
src="/assets/cloned/images/abc1234-hero.jpg"
alt=""
width={1920}
height={1080}
priority={isAboveTheFold}
sizes="(max-width: 768px) 100vw, 1200px"
/>;
```
- `alt=""` for decorative images. Preserve source `alt` when present.
- `priority` on above-the-fold images (hero bg, logo).
- `sizes` matches the responsive layout — omit if the image is fixed-width.
For background images (CSS `background-image`), keep as CSS with arbitrary Tailwind value:
```tsx
<div className="bg-[url('/assets/cloned/images/abc1234-bg.jpg')] bg-cover bg-center" />
```
## Lottie
Lottie JSON files go under `lottie/`. Import them as ES modules, not fetch at runtime — this eliminates flash and is under 100KB in nearly every case:
```tsx
import animationData from '@/public/assets/cloned/lottie/<hash>.json';
```
If a Lottie is > 1MB, consider dynamic import with a `<Suspense>` boundary to defer.
## Import paths
In Next.js, the public directory is served at the root. Use absolute paths starting with `/`:
-`<img src="/assets/cloned/svg/logo.svg" />`
-`import data from "@/public/assets/cloned/lottie/x.json"` (for ES import of JSON)
- ❌ Never `../../../public/...` from components
- ❌ Never `https://source-site.com/...` — those were downloaded
## Flagging failures
If an asset download fails (404, timeout, CORS, auth), the orchestrator records it in `logs/skipped.md` with the URL and reason. Generation should render a placeholder of the captured dimensions rather than a broken `<img>` for any section that referenced the missing asset.
## Cleanup rule
Do not delete assets the capture downloaded "just in case they are unused". The analyze agent records `assets[].referenced_by[]`; if the array is empty, leave the file in place — it might be referenced in a stage that has not run yet. Orphan cleanup is a post-run task, not part of generation.
+113
View File
@@ -0,0 +1,113 @@
---
name: clone-staging
description: The four-stage taxonomy for the clone pipeline (Foundation, CSS & Static Interactivity, JS Animation, WebGL/3D), with gate thresholds. In staged-mode runs, gates are hard — sections must pass each stage before progressing. In default-mode runs, the staging is descriptive: it tells the generate agent which features go in early vs late and what's deferrable. Load this before starting any cloning work — the orchestrator, generate, and validate agents all reference these definitions.
user-invocable: false
---
# Clone Staging
The clone pipeline organizes work into four ordered stages by capability. The same taxonomy serves two roles depending on orchestrator mode:
- **Staged-mode (`--staged`):** gates are hard. A section never enters stage N until stage N-1 has passed for that section. Prevents burning tokens on WebGL translation when the layout is still broken. Used for sites where surgical iteration matters.
- **Default-mode:** stages are descriptive guidance for the generate agent — what goes in foundation, what's deferred to animation, what's webgl-only. The orchestrator does NOT enforce gates between stages; it generates everything in parallel batches per section, then iterates on visible failures. Used for marketing/landing pages where the manifest signals are usually enough to carry fidelity through one generation pass.
Either way, the stage classifications below are the source of truth for what work belongs where.
## Stage 0 — Capture (always, once)
Not a generation stage. The capture agent runs once and produces the bundle under `<workspace>/capture/`. Generation only begins after capture is complete and the manifest is written.
## Stage 1 — Foundation
**Scope:**
- Design tokens in `tailwind.config.ts` + `globals.css` (colors, typography, spacing, shadows, radii, breakpoints)
- Asset pipeline: all images, videos, fonts, SVGs, lottie JSONs downloaded to `/public/assets/cloned/`
- Font loading via `next/font/local` (self-hosted `@font-face`)
- Layout primitives: `Container`, `Section`, base typography in `globals.css`
- Static page structure — DOM skeleton per section, no interactivity
**Gate:**
- Per-section diff at 1280px viewport with `prefers-reduced-motion` forced: `diff_pct < 5%`
- All viewports (375 / 768 / 1280 / 1920) render without cumulative layout shift
- No 404s in the network log for local assets
## Stage 2 — CSS & Static Interactivity
**Scope:**
- Gradients, filters, `backdrop-filter`, masks, clip-paths
- SVGs inlined or imported as components
- CSS animations: `@keyframes`, `transition`
- Hover, focus, focus-visible states
- `prefers-color-scheme: dark` styles if detected in capture
**Gate:**
- Per-section diff at rest: `diff_pct < 3%`
- Hover/focus state screenshots match the captured variants (delta `< 4%`)
## Stage 3 — JS Animation (only if Stage 2 passes)
**Scope:**
- Scroll-linked animations — Framer Motion `useScroll` / `useTransform` if simple, keep GSAP ScrollTrigger if timeline is complex
- Framer Motion variants for entrance / exit / `whileInView`
- Lottie animations via `lottie-react`
- IntersectionObserver-driven reveals → Framer `whileInView`
**Gate:**
- Per-section diff averaged across 5 sampled scroll positions (0%, 25%, 50%, 75%, 100%): `diff_pct < 8%`
## Stage 4 — WebGL / 3D (only if Stage 3 passes)
**Scope:**
- Three.js scenes reconstructed from the captured scene graph via `@react-three/fiber`
- Custom shaders reinstated from extracted GLSL
- Custom `<canvas>` 2D animation if detected
**Gate:**
- Best-effort. `diff_pct < 15%` on sampled frames is a pass.
- Two reconstruction attempts; if both fail, fall back to the captured MP4 embedded as `<video autoplay loop muted playsinline>` with matching dimensions. Fallback is a valid pass — flag in report.
## Tie-breakers
**A section straddles stages.** Classify by the _highest_ stage required for any element in it.
Example: a hero section is mostly static CSS but has one Lottie icon → `max_stage_required: 3`. The generate agent still builds the static part in Stage 1, adds CSS flourishes in Stage 2, and only wires up the Lottie in Stage 3. The Stage 1 gate is applied with the Lottie slot rendered as a placeholder of matching dimensions.
**Placeholder rule during earlier stages.** For elements whose implementation is deferred to a later stage, render a positioned placeholder `<div style={{ width, height }}>` so layout diffs at the earlier-stage gate are not polluted. Replace in the later stage.
**Conservative staging.** When in doubt about whether a section needs Stage 3 (e.g. subtle fade-in), mark it Stage 2. If Stage 2 validation fails because the animation is material to layout, promote at that point. Cheap to promote upward; expensive to demote.
## Non-goals (skip, do not attempt)
When any of these is detected, do not try to clone. Replace with a sized placeholder and comment:
- DRM'd video/audio (Widevine, FairPlay, PlayReady)
- Third-party widgets: Intercom, Drift, Typeform, Calendly, HubSpot forms, chat widgets, cookie consent banners
- Authenticated or personalized content behind login
- Obfuscated WASM modules
- Real-time data feeds (stock tickers, live chat)
- Analytics pixels / tag managers (no value to clone; actively harmful)
Pattern:
```tsx
<div style={{ width: W, height: H }}>{/* CLONE: skipped third-party widget — <vendor> */}</div>
```
The captured bounding box supplies W/H so layout does not shift.
## Iteration caps
- **Staged mode:** 3 iterations per section per stage. After three failed attempts at the same section at the same stage, log to `logs/manual-review.md` with the current component code, diff images, and most recent issues list; move on. A partial clone is better than a stuck clone.
- **Default mode:** 2 targeted-fix iterations per section AFTER the initial parallel-batch generation. Same logging rule on exhaustion.
## Default-mode "advisory" gates
When the orchestrator does the visual-review pass in default mode, the gate thresholds above are useful guidance but not enforced. A section showing 12% pixel diff dominated by font-rendering or video-frame timing is fine. A section showing 12% pixel diff with a visibly missing element is not. The orchestrator's judgment call — informed by reading the screenshots — is what triggers a targeted-fix call.
+128
View File
@@ -0,0 +1,128 @@
---
name: css-to-tailwind
description: Rules for converting resolved computed CSS from the capture into Tailwind v3 classes on Next.js components. Decides when to use arbitrary values vs extending tailwind.config.ts, how to handle gradients / shadows / filters / clamp / variable fonts / container queries, and when to bail to globals.css. Load this in every Stage 1 or Stage 2 generation run.
user-invocable: false
---
# CSS → Tailwind
The generate agent receives a DOM fragment with computed styles already resolved per node. Your job is to turn those styles into Tailwind classes on the JSX output — no cascade reasoning required.
## Core decision: utility vs arbitrary vs config vs CSS file
| Situation | Approach |
| ---------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Value matches a default Tailwind scale step (e.g. `p-4`, `gap-8`) | Use the utility |
| Value is used 1-2 times site-wide and does not match scale | Arbitrary: `w-[347px]` |
| Value is used 3+ times site-wide | Extend `tailwind.config.ts`, then use the token |
| Cannot be expressed in Tailwind (complex `filter`, `@property`, `@supports`, dynamic `attr()`) | Write to `globals.css` |
The manifest's `design_tokens` object is already deduplicated by frequency — use it as the source of truth for what goes into `tailwind.config.ts`.
## Specific rules
### Heights and widths — use intent over resolved px
The capture's computed styles resolve `vh`/`vw`/`%`/`calc()` to literal pixel values at the capture viewport (1280×720). **Do not blindly transcribe these.** The manifest gives you better signals:
1. **`section.vh_relative === true`**: the section's height is viewport-height-derived. Map `section.vh_value` to Tailwind:
- `vh_value` ≈ 100 → `min-h-screen` (or `h-screen` if the source had `height` not `min-height`)
- `vh_value` between 50 and 99 → `min-h-[<vh>vh]` arbitrary, e.g. `min-h-[88vh]`
- `vh_value` < 50 → still `min-h-[<vh>vh]` but consider whether content alone determines the height (if so, omit the height entirely and let content size it)
2. **`section.full_width === true`**: use `w-full` (or `w-screen` for true full-bleed elements that escape parent padding). Never write `w-[1280px]`.
3. **`capture/css-rules/<vp>.json`** has the source intent verbatim. When in doubt for a specific element, read the rule that matched its selector. Look for `100vh`, `100%`, `calc(...)`, `clamp(...)`, `aspect-ratio` — these are signals to NOT use the literal px value.
Heuristic for elements not in `vh_relative_elements` but suspected vh-relative: if `bounding_box.height` is close to `viewport_height * N` for `N ∈ {0.5, 0.75, 1.0}` and the element is a top-level section with no other content driving height, prefer the vh form. Cheaper to over-flag here than to leave a brittle px height.
### Colors
- Every color in `manifest.design_tokens.colors` gets promoted into `theme.extend.colors` under its semantic name.
- Reference as `bg-primary`, `text-primary`, `border-accent-1`, etc. in class output.
- Unseen one-off colors: arbitrary, e.g. `text-[#3a3a3a]`.
### Typography
- Font families: promote to `theme.extend.fontFamily`. Use `next/font/local` in `layout.tsx` with the downloaded woff2 files and expose via CSS variable, then reference as `['var(--font-display)']` etc.
- Font sizes: promote clusters to `theme.extend.fontSize` as named keys (`display`, `h1`, `body`, `small`). Value is `[fontSize, { lineHeight, letterSpacing, fontWeight }]`.
- One-off sizes: `text-[32px]/[1.2]` arbitrary.
### Spacing
- Infer the scale from `manifest.design_tokens.spacing[]`. Often 4px / 8px / 16px / 24px ... but match what the site actually uses.
- Extend Tailwind's spacing scale only for values not already close to a default step.
- Use `gap-*`, `p-*`, `m-*`, `space-y-*` utilities.
### Gradients
- Linear: `bg-gradient-to-r from-[#...] via-[#...] to-[#...]` for simple 2-3 stops. For more stops or angled variants, use arbitrary `bg-[linear-gradient(135deg,#a_0%,#b_50%,#c_100%)]`.
- Radial: `bg-[radial-gradient(...)]` arbitrary.
- Conic: `bg-[conic-gradient(...)]` arbitrary.
- Mesh / multi-layer: wrap in `bg-[image:...]` syntax with escaped commas.
### Shadows
- Single-layer shadows that match scale: `shadow-md`, `shadow-xl`.
- Multi-layer shadows: arbitrary with escaped commas: `shadow-[0_1px_2px_rgba(0,0,0,0.05),_0_4px_8px_rgba(0,0,0,0.08)]`.
- Promote 3+ occurrences of the same shadow to `theme.extend.boxShadow`.
### Filters and backdrop-filter
- Built-in: `blur-*`, `brightness-*`, `contrast-*`, `grayscale-*`, `backdrop-blur-*`, `backdrop-saturate-*`.
- Arbitrary: `filter-[blur(16px)_saturate(140%)]`, `backdrop-filter-[blur(20px)_saturate(140%)]`.
- Complex filter chains (4+ functions): promote to a CSS utility class in `globals.css` under `@layer utilities`.
### clamp() / min() / max()
- Preserve verbatim in arbitrary: `text-[clamp(24px,4vw,48px)]`, `w-[min(100%,1200px)]`.
- Do not try to approximate with `md:` breakpoints — the whole point of `clamp()` is continuous scaling.
### Variable fonts
- `font-variation-settings: "wght" 420, "ital" 0` → arbitrary `[font-variation-settings:'wght'_420,'ital'_0]`.
- If the site uses variable weights fluidly (e.g. on hover), pair with `transition-[font-variation-settings]`.
### Container queries
- Use the `@tailwindcss/container-queries` plugin. Add to `tailwind.config.ts` plugins.
- Mark the container: `@container`.
- Query: `@md:grid-cols-2`, `@lg:text-lg`, etc.
### clip-path and mask
- Built-in utilities cover basic cases (`rounded-full`). Anything custom: arbitrary.
- `clip-path: polygon(...)``[clip-path:polygon(0_0,100%_0,100%_80%,0_100%)]`.
- `mask-image``[mask-image:linear-gradient(...)]` + matching `[-webkit-mask-image:...]` if Safari support needed.
### Transforms
- Scale / rotate / translate cover common cases. Chained transforms: Tailwind's `transform` + individual utilities, or arbitrary `[transform:perspective(800px)_rotateY(12deg)]`.
### Transitions
- Simple: `transition-colors`, `duration-200`, `ease-in-out`.
- Multi-property transitions: arbitrary `[transition:transform_400ms_cubic-bezier(0.22,1,0.36,1),_opacity_300ms]`.
### Keyframes / `@keyframes`
- Always bail to `globals.css` under `@layer utilities` or a named keyframe in `tailwind.config.ts` `theme.extend.keyframes` + `animation`.
- Then reference in JSX: `animate-fade-up`.
## When to bail to `globals.css`
Write rules to `src/app/globals.css` (not to the component) when:
- The rule is `@property` — Tailwind cannot express it.
- The rule is `@supports` — conditional CSS outside Tailwind's reach.
- The rule is `@font-face` — prefer `next/font/local`, but if the font is served from a remote URL that cannot be downloaded, `@font-face` in `globals.css` is acceptable.
- A selector you cannot express as a class (e.g. `:has()`, `:nth-child(odd) > :first-of-type`).
- Complex multi-rule animations (keyframes + state variants + JS hooks).
Keep `globals.css` under 200 lines. If it grows beyond, group related rules into separate files imported from `globals.css`.
## Do not
- Do not re-derive cascade from the source stylesheets. The computed styles on each node already resolve cascade. Copy those.
- Do not use `!important`. If a style is not applying, you have a selector specificity problem — fix it by structuring the JSX, not by nuking the cascade.
- Do not add classes that are not represented in the captured computed styles. If the source does not have `box-shadow`, do not add one "for polish".
- Do not add `@apply` in component files. `@apply` only belongs in `globals.css` when consolidating a repeated utility cluster.
- Do not leave TODO comments in component files — the cloner is meant to be done work, not a scaffold.
+140
View File
@@ -0,0 +1,140 @@
---
name: dom-to-jsx
description: Rules for converting captured DOM fragments into Next.js App Router JSX components. Covers component naming, when to split, Server vs Client classification, hydration-safe patterns, image handling (next/image vs raw img), and prop extraction for repeated patterns. Load this in every generation run from Stage 1 onward.
user-invocable: false
---
# DOM → JSX
The generate agent receives a DOM fragment per section. Convert it into idiomatic Next.js 14+ App Router JSX.
## Component naming
- PascalCase. Section-scoped: `HeroSection`, `FeatureGrid`, `TestimonialCard`, `PricingTable`, `CTAStrip`.
- File path: `src/components/sections/<Name>.tsx` for top-level sections, `src/components/cards/<Name>.tsx` for repeated card components, `src/components/ui/<Name>.tsx` for primitives (button, badge).
- Default export matches the file name. Named exports are fine for sub-components.
## When to split
Split a component when any of these is true:
- File exceeds 150 lines of JSX
- JSX nesting exceeds 3 levels
- A repeated pattern appears 3+ times within the section
When splitting for a repeat, extract a component with a `data` prop (array of items) or a single-item prop shape matching the manifest's `candidate_component` proposal.
Example — manifest says a `FeatureCard` appears 6 times with `{title, description, icon}`:
```tsx
// src/components/cards/FeatureCard.tsx
type Props = { title: string; description: string; icon: string };
export default function FeatureCard({ title, description, icon }: Props) { ... }
// src/components/sections/Features.tsx
import FeatureCard from "@/components/cards/FeatureCard";
const ITEMS = [{ title: "...", description: "...", icon: "..." }, ...];
export default function Features() {
return <div className="grid grid-cols-3 gap-8">{ITEMS.map(i => <FeatureCard key={i.title} {...i} />)}</div>;
}
```
Extract `ITEMS` as a local `const` in the section file for Stage 1. If the data is clearly content-managed (12+ items, or visually heterogeneous), move to `src/content/<section>.ts` as a typed export.
## Server vs Client
Default: **Server Component**. Add `"use client"` at the top of a file only if any of these is true:
- Uses `useState`, `useEffect`, `useRef`, or any other hook
- Uses event handlers (`onClick`, `onChange`, etc.) — interactive buttons that do more than `<a href>`-style navigation
- Uses `framer-motion`'s `motion.*` components
- Uses `lottie-react` or any other client-only lib
- Uses browser APIs (`window`, `document`, `localStorage`)
Do not mark parents client unnecessarily. Keep `"use client"` at the leaf — a client leaf can live inside a server parent, not vice versa.
## Hydration safety
- **No** `Date.now()` / `Math.random()` / `new Date()` at render. If needed, generate in `useEffect` and store in state. At render time the first pass must be deterministic.
- **No** reading `window` / `document` at module scope. Wrap in `useEffect` or guard with `typeof window !== "undefined"`.
- `suppressHydrationWarning` is a last resort — only on a single element where the server/client divergence is intentional and minimal (e.g. a `<time>` element).
- Do not pass `new Date()` or live content into a Server Component from within a client wrapper at first paint — you will get a mismatch.
## Image handling
**Default: `next/image`** when all of these hold:
- The image has known dimensions (the manifest captured them)
- The image is a raster (jpg, png, webp, avif)
- The image is content, not purely decorative
Example:
```tsx
import Image from 'next/image';
<Image src="/assets/cloned/images/abc1234-hero.jpg" alt="" width={1920} height={1080} />;
```
**Raw `<img>`** for:
- SVGs (use native `<img src="/assets/cloned/svg/xyz.svg" alt="" />` or inline the SVG as a component if <5KB and decorative)
- Images with unknown dimensions at clone time
- Decorative blur-ups where LCP is not a concern
**Inline SVG** when:
- The SVG is <5KB in the source
- The SVG is used in-flow (not as a background-image)
- Animated parts need to be controlled from JS
Configure `next.config.js` to allow loading from `/assets/cloned` paths — no external loader needed since everything is already local.
## Video
```tsx
<video src="/assets/cloned/videos/<hash>.mp4" autoPlay loop muted playsInline className="..." />
```
Always `playsInline` (iOS won't inline autoplay without it). Always `muted` for autoplay (browsers require it).
## Links
- Internal: `<Link href="/...">` from `next/link`.
- External: `<a href="..." target="_blank" rel="noopener noreferrer">`.
- Anchor in page: plain `<a href="#id">` is fine — `<Link>` with a hash works too.
## Accessibility
Keep what the source had — do not invent. If the source has `aria-label` or `alt=""`, preserve it. If the source is inaccessible, the clone is inaccessible. This is a fidelity tool, not a correction tool.
That said — do not _remove_ accessibility attributes the source had.
## Third-party widgets (skip per `clone-staging`)
When the manifest flags a subtree as a third-party widget, replace with:
```tsx
<div style={{ width: W, height: H }}>{/* CLONE: skipped third-party widget — <vendor> */}</div>
```
Pull `W` and `H` from the manifest's bounding box for the widget's container. This preserves layout so later sections land at the right Y offset.
## Forms
The clone is a static marketing-page mirror. Forms should render visually but the `onSubmit` handler is `(e) => e.preventDefault()` — no backend wiring.
If the source had client-side validation that is material to the layout (error states, spinners), keep it as a visual-only `useState` example. Do not call real APIs.
## Attribute translation
- `class``className`
- `for``htmlFor`
- `tabindex``tabIndex`
- Hyphenated attrs on SVG: most become camelCase (`stroke-width``strokeWidth`), but `data-*` and `aria-*` stay hyphenated.
- `style="..."``style={{ ... }}` with camelCase keys. Only inline style that is computed / dynamic; static style always goes to Tailwind per `css-to-tailwind`.
## Content
- Copy text verbatim from the DOM fragment. Do not paraphrase. Do not translate. Do not "improve" grammar.
- Preserve whitespace-sensitive content (`<pre>`, `<code>`) exactly.
- Keep HTML entities as-is (`&mdash;`, `&nbsp;`, `&hellip;`) but JSX renders them when placed inside string literals — use `{"—"}` or `{"\u00a0"}` for non-breaking spaces when adjacency matters.
+177
View File
@@ -0,0 +1,177 @@
---
name: site-manifest
description: Schema and rules for manifest.json — the single blueprint artifact the analyze agent produces and the generate + validate agents consume. Defines field shapes, stable id conventions, token naming conventions, and what goes where. Load this in the analyze agent before writing manifest.json and in any agent that reads it.
user-invocable: false
---
# site-manifest
Everything the generate + validate agents need lives in `manifest.json` at `<workspace>/manifest.json`. The capture bundle is the raw source; the manifest is the refined, structured index.
## Top-level schema
```ts
type Manifest = {
source_url: string; // absolute URL
captured_at: string; // ISO 8601
viewports: number[]; // e.g. [375, 768, 1280, 1920]
design_tokens: {
colors: Record<string, string>; // semantic_name -> hex (e.g. "primary" -> "#1A1A1A")
typography: {
families: Record<string, string>; // semantic -> family (e.g. "display" -> "'Inter Display', sans-serif")
scales: Array<{
name: string; // "display" | "h1" | "body" | "small" | ...
size: string; // "48px"
lineHeight: string; // "1.1"
weight: number; // 500
letterSpacing: string; // "-0.02em"
}>;
};
spacing: number[]; // the scale in px — e.g. [4, 8, 12, 16, 24, 32, 48, 64]
radii: Record<string, string>; // "sm" -> "4px"
shadows: Record<string, string>; // "md" -> "0 1px 2px ..." (full CSS value)
breakpoints: Record<string, number>; // "md" -> 768
};
fonts: Array<{
family: string; // "Inter Display"
weights: number[]; // [400, 500, 700]
styles: Array<'normal' | 'italic'>;
source_urls: string[]; // as seen in @font-face
local_paths: string[]; // where they were downloaded
license_hint?: 'licensed' | 'unclear' | 'open';
}>;
sections: Array<{
id: string; // slug — stable across reruns
name: string; // "Hero" | "Feature Grid" | ...
dom_path: string; // relative to capture_dir — JSON file with DOM + computed styles
screenshot_paths: Record<number, string>; // viewport -> relative path in capture_dir
section_anchor?: string; // CSS selector for this section's root element (e.g. "#hero", ".elementor-element-fe377c0"). Used by validate's dom-diff `root_selector`. REQUIRED when present in capture.
bounding_box: { x: number; y: number; width: number; height: number }; // at canonical 1280×720 capture
vh_relative?: boolean; // true if the height is viewport-height-derived (see vh-flags.json)
vh_value?: number; // implied vh percentage (e.g. 88 for ~min-height: 88vh) when vh_relative is true
full_width?: boolean; // true if width matched viewport width at capture time AND nominal source CSS targets full-bleed (use w-full / w-screen, not the literal px value)
max_stage_required: 1 | 2 | 3 | 4;
detected_libs: string[]; // ["gsap", "framer-motion", "lottie", "three"]
third_party_widgets: Array<{ vendor: string; selector: string; bbox: BBox }>;
repeated_patterns: Array<{
selector: string; // pattern selector in the captured DOM
count: number; // 3+
candidate_component: {
name: string; // PascalCase
props: string[]; // field names extracted from pattern variance
};
}>;
}>;
assets: Array<{
type: 'image' | 'video' | 'font' | 'svg' | 'lottie';
source_url: string; // absolute
local_path: string; // "public/assets/cloned/images/<hash>-<name>"
dimensions?: { width: number; height: number };
from_css?: boolean; // true if discovered from a CSS url() rather than a HAR response
referenced_by: string[]; // section ids where this asset appears
}>;
// Optional — populated when capture.py finds vh-relative elements (compares
// canonical 1280×720 capture against a 1280×1080 alt-height capture).
vh_relative_elements?: Array<{
path: string; // structural path produced by detect_vh_flags()
tag: string;
id?: string | null;
class?: string | null;
primary_h: number; // height at 1280×720
alt_h: number; // height at 1280×1080
ratio: number; // alt_h / primary_h — should ~= 1.5 for vh-relative
vh: number; // implied vh % (= primary_h / 720 * 100)
}>;
skipped: Array<{
reason: 'drm' | 'third_party_widget' | 'auth_required' | 'wasm_obfuscated' | 'licensed_font_unclear';
element?: string; // selector
original_url?: string;
vendor?: string;
}>;
};
```
## Id conventions
- `section.id` is a slug from `section.name`, suffixed with a stable counter when the same semantic name appears twice: `features-0`, `features-1`. Order is DOM order top-to-bottom.
- Asset `local_path` uses an 8-char sha1 hash of the source URL + the original filename: `abc12345-hero-background.jpg`. This guarantees no collisions and is stable across reruns.
- `repeated_patterns[].candidate_component.name` is PascalCase, singular: `FeatureCard` not `FeatureCards`.
## Determinism
The analyze agent MUST produce the same manifest on the same capture input. This means:
- No timestamps inside section ids (`captured_at` at the top is fine — it is read-once)
- No random numbers, no iterator-order-dependent ids
- Sort arrays by semantic key (sections by DOM order, assets by local_path, skipped by reason then element)
## Promotion rule for tokens
A value goes into `design_tokens` only if it is used 3+ times in the capture. One-off values stay inline in the DOM fragment and become arbitrary Tailwind values in generation.
Exception: the page-level background color, primary text color, and primary font family always go into `design_tokens` regardless of count — downstream agents need them to set globals.
## Capture artifacts the analyze agent should read
In addition to per-section `dom_path` files, several capture-level artifacts are now available:
- `<capture_dir>/sections/<viewport>.json``__CLONE_LIST_SECTIONS__` candidate sections per viewport, with bbox and selector. Use these as scaffolding when decomposing into manifest sections — the smarter scroll loop already used these to time the capture, so each candidate has a clean DOM dump.
- `<capture_dir>/section-shots/<viewport>/section-NN.png` — pre-cropped per-section screenshots. Map these to manifest sections by index; the validate agent uses them as the diff reference.
- `<capture_dir>/css-rules/<viewport>.json` — every same-origin CSS rule, plus extracted `url()` asset references. Use this to recover source intent (e.g. `min-height: 100vh`, `width: 100%`, `aspect-ratio: 16/9`) that resolved computed styles erase. Also lets you recover backgrounds/masks/cursors that were not GET-requested at capture time.
- `<capture_dir>/dom-alt/1280-1080/step-00.json` — DOM at canonical width with alt height. Used internally to derive `vh-flags.json`; you don't need to read this directly.
- `<workspace>/vh-flags.json` (next to manifest.json) — pre-computed list of vh-relative element candidates. Cross-reference against your section bbox.y values to populate `section.vh_relative` + `section.vh_value`.
## How to populate vh_relative on a section
1. Load `vh-flags.json`.
2. For each entry, check whether its `path` resolves to an element inside or equal to one of your section roots.
3. If a flagged entry IS the section root, set `section.vh_relative = true` and `section.vh_value = entry.vh`.
4. If a flagged entry is a child but the child's height equals the section's height, the section itself is vh-relative — same flag.
5. The generate agent uses this to emit `min-h-[<vh>vh]` (Tailwind arbitrary) or `min-h-screen` when vh ≈ 100. See `css-to-tailwind` for the rule.
## Reading the DOM fragment
Each `section.dom_path` points to a JSON file with this shape:
```ts
type DomNode = {
tag: string;
attrs: Record<string, string>; // already translated: class, for, tabindex names
text?: string; // text content if leaf or pre-text
computed: {
// resolved computed styles — only non-default values
color?: string;
backgroundColor?: string;
fontFamily?: string;
fontSize?: string;
// ... any property the node actually uses
pseudo?: {
'::before'?: Record<string, string>;
'::after'?: Record<string, string>;
':hover'?: Record<string, string>;
':focus-visible'?: Record<string, string>;
};
};
children: DomNode[];
};
```
The generate agent walks this tree and emits JSX with Tailwind classes derived from `computed`. Because the styles are already resolved, no cascade reasoning is required.
## Validation
After writing `manifest.json`, self-check:
- `viewports.length >= 1`
- Every `section.screenshot_paths` has a key for each viewport
- Every `asset.local_path` starts with `public/assets/cloned/`
- No section has `max_stage_required > 4` or `< 1`
- `design_tokens.colors.primary` exists (fall back to `#000` if none inferred — noisy fallback is better than missing key)
If a check fails, fix the manifest — do not write a broken one.
+161
View File
@@ -0,0 +1,161 @@
---
name: validation-loop
description: How the validate agent runs the two diff channels — screenshot pixel-diff AND structural DOM diff — against captured originals, ranks issues, and feeds them back to the generate agent. Defines thresholds, tool usage, diff report format, and the 3-iteration hard cap per section per stage. Load this in the validate agent every run.
user-invocable: false
---
# Validation Loop
The validate agent closes the loop: render → screenshot + DOM dump → diff against captured originals → decide pass/fail → return a report the generate agent can act on.
## Two diff channels
Both run on every fail. Combine the signals.
### 1. Pixel diff — `screenshot-diff`
Wraps `scripts/diff.py` (pixelmatch + scipy connected components).
Inputs: `before` (captured PNG), `after` (rendered PNG), `threshold` (default 0.1).
Outputs: `diff_pct`, `diff_image_path`, `worst_regions[]`.
Prefer per-section pre-cropped captures when available:
- `capture/section-shots/<viewport>/section-<idx>.png` — if the analyze agent matched this section to a candidate index.
- Otherwise the closest scroll-step PNG from `capture/screenshots/<viewport>/step-NN.png`.
### 2. Structural diff — `dom-diff`
Wraps `scripts/dom-diff.py`. Compares two DOM JSON files produced by `__CLONE_DUMP_COMPUTED__` (the rich walker the capture script and `dump-rendered` both use). Returns concrete property-level mismatches the generate agent can fix without eyeballing pixels.
Inputs:
- `captured``manifest.sections[id].dom_path` from the source capture.
- `rendered` — JSON from `dump-rendered` against `localhost:3000`.
- `root_selector``#id` or `.class` to scope to the section. Use the manifest's `section_anchor` field; fall back to `#<section_id>`.
Outputs:
```json
{
"matched": 76,
"counts": { "matched": 76, "missing": 2, "extra": 1, "tag_mismatch": 0, "style": 5, "size": 3 },
"issues": [
"section.hero > div[1]: missing in rendered (expected <img> #data-overlay, ~620x340)",
"section.hero > h1[0]: fontSize is 48px, should be 56px",
"section.hero: size 1280x634, should be 1280x792 (Δ22.4%)"
],
"structured_issues": [...]
}
```
The `issues` array is already ranked and human-readable. Use it verbatim in the validate report.
## Capturing the rendered clone
For pixel: see `agent-browser-shot`. For structural:
```
dumpRendered({
url: "http://localhost:3000/",
output: "<workspace>/rendered-dom/<section_id>-<vp>.json",
viewport: <width>,
scroll_y: <bbox.y - 80>,
reduce_motion: <stage===1>
})
```
The walker captures the same property set as the source, so the comparison is apples-to-apples.
## Gate thresholds
From `clone-staging`:
| Stage | Pixel | Structural extra check |
| ----- | ---------------------------------------------------------------- | ------------------------------------------------ |
| 1 | `diff_pct < 5%` at 1280px, reduced motion. Layout shift = 0. | counts.missing == 0 AND counts.tag_mismatch == 0 |
| 2 | `diff_pct < 3%` at rest. Hover/focus delta < 4%. | counts.style + counts.size < 5 |
| 3 | mean `diff_pct < 8%` across 5 scroll samples. | (animations dominate; structural less reliable) |
| 4 | `diff_pct < 15%` on sampled frames. Fallback to MP4 also passes. | n/a |
A pass requires BOTH pixel and structural to pass at Stages 1 and 2. A pixel pass with `counts.missing > 0` is a fail because the diff distributed across pixels; an early-stage section with a missing 600x400 element will sometimes squeak under a 5% pixel threshold and that is the regression we are guarding against.
## Diff report format
Pass:
```json
{
"status": "pass",
"section_id": "hero",
"stage": 2,
"viewports": {
"375": { "diff_pct": 1.8, "structural": { "matched": 60, "missing": 0 } },
"1280": { "diff_pct": 1.5, "structural": { "matched": 87, "missing": 0 } }
}
}
```
Fail:
```json
{
"status": "fail",
"section_id": "hero",
"stage": 2,
"viewport": 1280,
"diff_pct": 7.4,
"structural": { "matched": 76, "missing": 2, "tag_mismatch": 0, "style": 5, "size": 3 },
"worst_regions": [{ "x": 120, "y": 88, "width": 340, "height": 60 }],
"issues": [
"section.hero > div[1]: missing in rendered (expected <img> #data-overlay, ~620x340)",
"section.hero > h1[0]: fontSize is 48px, should be 56px",
"section.hero: size 1280x634, should be 1280x792 (Δ22.4%) — vh-relative; use min-h-screen"
],
"structured_issues": [...],
"diff_image_path": "<workspace>/diffs/hero-1280.png"
}
```
The `issues` array is the key payload. 3-6 items, most-impactful first. Each item is a single concrete change.
## Issue ranking
`dom-diff` already ranks: missing > tag_mismatch > size_mismatch > style_mismatch > extra. Style mismatches further weight `backgroundImage`, `fontSize`, `color`, `backgroundColor`, `fontFamily` higher than other properties.
Only override the order if pixel-diff caught something structural missed (color gradients, image content shift, blur/filter changes that don't show in computed styles).
## Special cases
### Section flagged as vh_relative
If `manifest.sections[id].vh_relative === true`, the captured `bounding_box.height` reflects a viewport-height-derived value at capture time. When the rendered clone is wider/taller than the capture viewport, structural size mismatches on the section itself are expected unless the clone uses `min-h-screen`/`h-screen`. **Report the size mismatch with the hint `"vh-relative; use min-h-screen"` so generate knows the fix shape.**
### Cumulative layout shift
A Stage 1 failure even if `diff_pct` is below threshold. Detect by comparing the rendered viewport at multiple scroll positions — if elements jump > 4px between initial paint and post-network-idle paint, fail with `issues: ["Layout shift detected: hero image reflows 120px after load"]`.
### Font swap flicker
If captured shows the web font and the clone's first paint shows fallback, `next/font/local` is mis-wired. Report — usually fix is `display: "swap"` + correct `preload`.
### Dark mode
If the manifest records dark-mode styles, diff both modes. Run with `document.documentElement.classList.add("dark")` (or the captured toggle mechanism) and diff both screenshots and DOMs.
### Hover / focus states
At Stage 2, hover interactive elements flagged in capture, wait 200ms, screenshot, diff. Structural diff isn't useful here — pixel only.
## Iteration cap
**3 iterations per section per stage.** Orchestrator-enforced. Each call is one-shot: snapshot + diff + report.
After third failure, orchestrator writes a `manual-review.md` entry with: section id, stage, final component file, final diff image, last issues list. Continue to next section.
## Do not
- Do not edit component files. Report, do not fix.
- Do not restart the dev server mid-validation — flushes state.
- Do not widen `threshold` to make a diff pass.
- Do not report "no issues found" with a failing `diff_pct` — if the gate fails, list issues. If structural matched cleanly but pixel still failed: that's a non-DOM divergence (canvas content, video frame, font rendering); say so explicitly: `["Pixel diff fails despite clean structural match — likely canvas/video/font-rendering divergence; consider re-rendering at higher DPR"]`.