Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/cli/ai/skills/block-content/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,4 @@ Do not use `--post_content-file=<host path>`. `wp_cli` runs inside the PHP-WASM

## Validation

- Run `validate_html_blocks` after every write or edit that creates or changes block content. If it reports invalid `core/html` blocks, rewrite only those blocks as editable core or plugin blocks, then call `validate_html_blocks` again before editor validation.
- Run `validate_and_fix_blocks` after `validate_html_blocks` passes. Call it with `filePath` whenever the content lives in a file; safe editor serialization fixes are applied directly to that file. If it says an auto-fix was applied, do not manually replace markup or call validation again unless you intentionally change block markup afterward. Use the diff only to inspect structural changes for CSS impact. Classes added or removed by the validator can affect layout and styling.
- Run `validate_blocks` after every write or edit that creates or changes block content. Call it with `filePath` whenever the content lives in a file. It first runs a static `core/html` policy check: if that reports invalid `core/html` blocks, editor validation is skipped — rewrite only those blocks as editable core or plugin blocks, then call `validate_blocks` again. Once the policy passes it validates in the live editor and applies safe serialization fixes directly to the file. If it says an auto-fix was applied, do not manually replace markup or call validation again unless you intentionally change block markup afterward. Use the diff only to inspect structural changes for CSS impact. Classes added or removed by the validator can affect layout and styling.
4 changes: 2 additions & 2 deletions apps/cli/ai/skills/plugin-recommendations/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ wp_cli eval 'foreach (\WP_Block_Type_Registry::get_instance()->get_all_registere

4. If you expect a plugin block but it is missing, check whether the plugin uses modules or feature flags, then activate the relevant module.
5. Use the registered block in editable block markup.
6. Validate generated block markup with `validate_html_blocks`, then `validate_and_fix_blocks` with `filePath` when the content lives in a file so safe editor fixes are applied automatically.
6. Validate generated block markup with `validate_blocks`, passing `filePath` when the content lives in a file so safe editor fixes are applied automatically.

## WooCommerce Shop Sites

Expand Down Expand Up @@ -164,4 +164,4 @@ Then activate the needed module:
wp_cli jetpack module activate <slug>
```

3. Use the registered block in page markup and validate with `validate_html_blocks`, then `validate_and_fix_blocks` with `filePath` when possible.
3. Use the registered block in page markup and validate with `validate_blocks`, passing `filePath` when possible.
4 changes: 2 additions & 2 deletions apps/cli/ai/skills/visual-polish/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ user-invokable: true

# Visual Polish

Use this skill to verify a built or redesigned site and fix the design issues that make generated sites feel unpolished. The generated block markup, the editor serialization fixes from `validate_and_fix_blocks`, and WordPress's own injected layout classes mean the rendered page often differs from what you intended. This skill closes that gap.
Use this skill to verify a built or redesigned site and fix the design issues that make generated sites feel unpolished. The generated block markup, the editor serialization fixes from `validate_blocks`, and WordPress's own injected layout classes mean the rendered page often differs from what you intended. This skill closes that gap.

The core method is **diagnose from evidence, not from memory**. Do not guess why something looks wrong from the screenshot alone — the rendered DOM usually differs from the markup you wrote. Read the real DOM with `inspect_design`, find the actual cause, then fix it.

Expand Down Expand Up @@ -38,7 +38,7 @@ Do not make a single edit until you have diagnosed every section and listed ever

### Phase 2 — Fix the whole batch

Work through the plan with targeted `Edit` calls (one `Write`/`Edit` per turn, per the system prompt cadence — never batch files into one turn). Do **not** screenshot between edits. If an edit changes block markup (not just CSS), re-run `validate_and_fix_blocks` on that file and re-check its diff, since the serializer can change classes again.
Work through the plan with targeted `Edit` calls (one `Write`/`Edit` per turn, per the system prompt cadence — never batch files into one turn). Do **not** screenshot between edits. If an edit changes block markup (not just CSS), re-run `validate_blocks` on that file and re-check its diff, since the serializer can change classes again.

### Phase 3 — Verify and loop

Expand Down
9 changes: 4 additions & 5 deletions apps/cli/ai/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ IMPORTANT: For any generated content for the site, these three principles are ma

- Gorgeous design: Load the \`visual-design\` skill for site creation, redesign, layout, style, CSS, typography, color, or motion work. To verify and polish the rendered result, load the \`visual-polish\` skill.
- Editable block content: Load the \`block-content\` skill before writing page, post, template, template-part, or other block markup.
- Valid blocks: Use validate_html_blocks first to catch invalid core/html usage, then use validate_and_fix_blocks to validate in the live editor. When called with filePath, validate_and_fix_blocks applies safe editor-serialization fixes directly to that file and returns a CSS-review diff.
- Valid blocks: Use validate_blocks. It first runs a static core/html policy check and, only once that passes, validates in the live editor. When called with filePath, it applies safe editor-serialization fixes directly to that file and returns a CSS-review diff.

## Workflow

Expand All @@ -118,8 +118,8 @@ Then continue with:
1. **Get site details**: Use site_info to get the site path, URL, and credentials.
2. **Plan the design**: Before writing any code, review the site spec (from the \`site-spec\` skill) and load the \`visual-design\` skill to plan the visual direction: layout, colors, typography, and spacing.
3. **Write theme/plugin files**: For a brand new theme, call \`scaffold_theme\` first — it drops an unopinionated block-theme baseline (style.css with only the theme header, theme.json with appearanceTools only, functions.php with frontend + editor style enqueue, default templates and parts, empty assets/fonts and patterns dirs) and activates it by default. Then use Write and Edit to fill the scaffold (one part/template/file per turn). For plugins or for editing an existing theme, use Write and Edit directly under the site's wp-content/themes/ or wp-content/plugins/ directory.
4. **Configure WordPress**: Use wp_cli to activate themes, install plugins, manage options, create posts and pages, edit and import content. The site must be running. Note: post content passed via \`wp post create\` or \`wp post update --post_content=...\` need to be pre-validated for editability, follow the \`block-content\` skill, checked with validate_html_blocks, and validated/fixed with validate_and_fix_blocks. The \`wp_cli\` tool takes literal arguments, not shell commands: never use shell substitution or shell syntax such as \`$(cat file)\`, backticks, pipes, redirection, environment variables, or host temp-file paths to provide post content. Pass the literal content directly in \`--post_content=...\`, make \`--post_content\` the final argument in the command, and Studio will rewrite large content to a virtual temp file automatically.
5. **Check and fix block validity**: Run validate_html_blocks on block content first. If it reports invalid core/html blocks, rewrite only those blocks as editable core or plugin blocks and call validate_html_blocks again. Then call validate_and_fix_blocks with filePath whenever the content lives in a file. If validate_and_fix_blocks says an auto-fix was applied, the file already contains the fixed block content; do not manually replace markup or call validation again unless you intentionally change block markup afterward. Use the diff only to inspect class/nesting changes and update CSS selectors if needed. For inline content, use any returned fixed block content exactly as the replacement content.
4. **Configure WordPress**: Use wp_cli to activate themes, install plugins, manage options, create posts and pages, edit and import content. The site must be running. Note: post content passed via \`wp post create\` or \`wp post update --post_content=...\` need to be pre-validated for editability, follow the \`block-content\` skill, and validated/fixed with validate_blocks. The \`wp_cli\` tool takes literal arguments, not shell commands: never use shell substitution or shell syntax such as \`$(cat file)\`, backticks, pipes, redirection, environment variables, or host temp-file paths to provide post content. Pass the literal content directly in \`--post_content=...\`, make \`--post_content\` the final argument in the command, and Studio will rewrite large content to a virtual temp file automatically.
5. **Check and fix block validity**: Run validate_blocks on block content, with filePath whenever the content lives in a file. It first runs a static core/html policy check: if that reports invalid core/html blocks, editor validation is skipped — rewrite only those blocks as editable core or plugin blocks and call validate_blocks again. Once the policy passes it validates in the live editor. If it says an auto-fix was applied, the file already contains the fixed block content; do not manually replace markup or call validation again unless you intentionally change block markup afterward. Use the diff only to inspect class/nesting changes and update CSS selectors if needed. For inline content, use any returned fixed block content exactly as the replacement content.
6. **Check and polish the result**: Load the \`visual-polish\` skill and run it to polish the design. The design must match your original expectations.

## Working cadence
Expand All @@ -146,8 +146,7 @@ For long CSS or page-content files (>~200 lines), load the \`block-content\` ski
- preview_delete: Delete a hosted WordPress.com preview by hostname
- wp_cli: Run WP-CLI commands on a running site
- scaffold_theme: Scaffold a minimal block theme (style.css, theme.json, functions.php with frontend + editor enqueue, default templates and parts, empty assets/fonts and patterns dirs) into a site and activate it. Use as the first step when starting a new custom theme; the agent fills design-specific content afterwards. Block themes only.
- validate_html_blocks: Check core/html blocks for misuse. Call before live editor validation; rewrite reported invalid HTML blocks as editable core or plugin blocks and call this again until it passes.
- validate_and_fix_blocks: Validate block content in the running site's real block editor. With filePath, applies safe editor fixes directly to the file and returns a CSS-review diff. With inline content, returns exact fixed block content plus the diff. Requires a site name or path. Call after every file write/edit that contains block content.
- validate_blocks: Validate block content in two stages and return a combined report. First a static core/html policy check; if it finds invalid core/html blocks it returns only those (rewrite them as editable core or plugin blocks and call again) and skips the editor. Once it passes, validates in the running site's real block editor: with filePath, applies safe editor fixes directly to the file and returns a CSS-review diff; with inline content, returns exact fixed block content plus the diff. Requires a site name or path. Call after every file write/edit that contains block content.
- take_screenshot: Take a full-page screenshot of a URL (supports desktop, mobile, or \`viewport: "all"\` for both). Use this to visually check the site after building it.
- inspect_design: Inspect the rendered DOM and computed styles of a page by CSS selector to root-cause visual issues. Pair with take_screenshot when verifying or polishing a design.
- need_for_speed: Measure frontend performance metrics (TTFB, FCP, LCP, CLS, page weight, DOM size, JS/CSS/image/font asset breakdown) for a running site. Use this to identify performance bottlenecks and guide optimization.
Expand Down
11 changes: 7 additions & 4 deletions apps/cli/ai/tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,18 @@ describe( 'Studio AI MCP tools', () => {
);
} );

it( 'reports invalid core/html blocks', async () => {
const result = await getTool( 'validate_html_blocks' ).rawHandler( {
it( 'reports invalid core/html blocks and skips editor validation', async () => {
const result = await getTool( 'validate_blocks' ).rawHandler( {
nameOrPath: 'My Site',
content:
'<!-- wp:html --><form><label>Email<input type="email" /></label></form><!-- /wp:html -->',
} as never );

const text = getTextContent( result );
expect( text ).toContain( 'HTML block policy: 1/1 core/html blocks invalid' );
expect( text ).toContain( '<form>' );
// The HTML policy gate short-circuits before the live editor runs.
expect( validateBlocks ).not.toHaveBeenCalled();
} );

it( 'returns fixed inline block content', async () => {
Expand All @@ -202,7 +205,7 @@ describe( 'Studio AI MCP tools', () => {
'<!-- wp:paragraph {"align":"center"} -->\n<p class="has-text-align-center">Hello</p>\n<!-- /wp:paragraph -->';
mockValidatedFix( fixedContent );

const result = await getTool( 'validate_and_fix_blocks' ).rawHandler( {
const result = await getTool( 'validate_blocks' ).rawHandler( {
nameOrPath: 'My Site',
content: originalContent,
} as never );
Expand All @@ -227,7 +230,7 @@ describe( 'Studio AI MCP tools', () => {
mockValidatedFix( fixedContent, 'core/separator' );

try {
const result = await getTool( 'validate_and_fix_blocks' ).rawHandler( {
const result = await getTool( 'validate_blocks' ).rawHandler( {
nameOrPath: 'My Site',
filePath,
} as never );
Expand Down
6 changes: 2 additions & 4 deletions apps/cli/ai/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ import { stopSiteTool } from './stop-site';
import { studioPresentTool } from './studio-present';
import { takeScreenshotTool } from './take-screenshot';
import { updatePreviewTool } from './update-preview';
import { validateAndFixBlocksTool } from './validate-and-fix-blocks';
import { validateHtmlBlocksTool } from './validate-html-blocks';
import { validateBlocksTool } from './validate-blocks';
import { waitForAnnotationsTool } from './wait-for-annotations';
import { runWpCliTool } from './wp-cli';
import type { AnyStudioAgentTool } from './define-tool';
Expand All @@ -44,8 +43,7 @@ export const studioToolDefinitions: AnyStudioAgentTool[] = [
deletePreviewTool,
runWpCliTool,
scaffoldThemeTool,
validateHtmlBlocksTool,
validateAndFixBlocksTool,
validateBlocksTool,
takeScreenshotTool,
inspectDesignTool,
shareScreenshotTool,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { readFile, writeFile } from 'fs/promises';
import { Type } from 'typebox';
import { validateHtmlBlockPolicy } from 'cli/ai/block-content-policy';
import { validateBlocks, type ValidationReportBase } from 'cli/ai/block-validator';
import { createUnifiedDiff } from 'cli/ai/content-diff';
import { getSiteUrl } from 'cli/lib/cli-config/sites';
import { emitProgress } from 'cli/logger';
import { defineTool } from './define-tool';
import { resolveSite, textResult } from './utils';

function formatPreview( content: string ): string {
const compact = content.replace( /\s+/g, ' ' ).trim();
return compact.length > 500 ? compact.slice( 0, 500 ) + '…' : compact;
}

function formatInvalidBlocks( report: ValidationReportBase ): string[] {
const lines: string[] = [];
for ( const result of report.results ) {
Expand All @@ -33,9 +39,9 @@ function formatMarkdownFence( language: string, content: string ): string {
return `${ fence }${ language }\n${ content }\n${ fence }`;
}

export const validateAndFixBlocksTool = defineTool(
'validate_and_fix_blocks',
"Validates WordPress block content in the site's real block editor. When filePath is provided, applies safe live-editor serialization fixes directly to the file and returns a diff for CSS impact review. For inline content, returns exact fixed block content plus the diff. The site must be running.",
export const validateBlocksTool = defineTool(
'validate_blocks',
"Validates WordPress block content in two stages and returns a combined report. First runs a static core/html block policy check; if it finds invalid core/html blocks, it returns only those (rewrite them as editable core or plugin blocks and call again) without touching the editor. Once the policy check passes, it validates the content in the site's real block editor: with filePath it applies safe live-editor serialization fixes directly to the file and returns a CSS-review diff; with inline content it returns the exact fixed block content plus the diff. The site must be running.",
{
nameOrPath: Type.String( {
description: 'The site name or file system path — the site must be running',
Expand Down Expand Up @@ -67,6 +73,37 @@ export const validateAndFixBlocksTool = defineTool(
throw new Error( 'Either content or filePath must be provided.' );
}

// Stage 1: static core/html policy check. Acts as a gate — if it
// fails we stop here instead of paying the live-editor round-trip on
// content we already know needs rewriting.
emitProgress( `Checking HTML blocks in ${ fileName }…` );
const htmlReport = validateHtmlBlockPolicy( blockContent );

if ( htmlReport.invalidHtmlBlocks.length > 0 ) {
emitProgress(
`${ fileName }: ${ htmlReport.invalidHtmlBlocks.length }/${ htmlReport.totalHtmlBlocks } core/html blocks invalid`
);
const lines = [
`HTML block policy: ${ htmlReport.invalidHtmlBlocks.length }/${ htmlReport.totalHtmlBlocks } core/html blocks invalid`,
'',
'Invalid HTML blocks:',
...htmlReport.invalidHtmlBlocks.flatMap( ( block ) => [
` - #${ block.blockNumber } line ${ block.line }`,
...block.issues.map( ( issue ) => ` ${ issue }` ),
` Content: ${ formatPreview( block.content ) }`,
] ),
'',
'Rewrite each invalid core/html block as editable core or plugin blocks, then call validate_blocks again. Editor validation was skipped until the HTML policy passes.',
];
return textResult( lines.join( '\n' ) );
}

const htmlSummary =
htmlReport.totalHtmlBlocks === 0
? 'HTML block policy: no core/html blocks found.'
: `HTML block policy: all ${ htmlReport.totalHtmlBlocks } core/html blocks within policy.`;

// Stage 2: validate (and fix) in the site's real block editor.
emitProgress( `Validating and fixing blocks in ${ fileName }…` );

const site = await resolveSite( args.nameOrPath );
Expand All @@ -82,6 +119,7 @@ export const validateAndFixBlocksTool = defineTool(
emitProgress( `${ fileName }: all ${ report.totalBlocks } blocks valid` );
return textResult(
[
htmlSummary,
`Validation: ${ report.validBlocks }/${ report.totalBlocks } blocks valid`,
'No editor serialization fixes needed.',
].join( '\n' )
Expand All @@ -95,6 +133,7 @@ export const validateAndFixBlocksTool = defineTool(
emitProgress( `${ fileName }: ${ report.invalidBlocks } invalid (${ invalidNames })` );

const lines = [
htmlSummary,
`Validation: ${ report.validBlocks }/${ report.totalBlocks } blocks valid`,
'',
'Invalid blocks:',
Expand Down
Loading