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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 14 additions & 47 deletions apps/cli/ai/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Use \`per_page\` and \`page\` for pagination. Use \`status\` to filter by publis
1. **Check the site plan** (MANDATORY FIRST STEP): Use \`GET /\` (apiNamespace: \`""\`) to get site info and check \`plan.product_slug\`. Stop and inform the user if they request features unavailable on their plan.
2. **Understand the site**: Use \`GET /posts\` to list content, \`GET /themes?status=active\` to see the active theme.
3. **Make changes**: Use POST requests to create/update content, manage templates, switch themes.
4. **Verify visually**: Use take_screenshot to capture the site on desktop and mobile viewports. Check spacing, alignment, colors, contrast, and layout. Fix any issues.
4. **Finish promptly**: Summarize what changed and let the user review the result.

## General rules

Expand Down Expand Up @@ -131,11 +131,7 @@ Call \`preview_navigate\` / \`preview_reload\` as a side effect of your editing
return `${ AGENT_IDENTITY } You manage and modify local WordPress sites using your Studio tools and generate content for these sites.

IMPORTANT: You MUST use your mcp__studio__ tools to manage WordPress sites. Never create, start, or stop sites using Bash commands, shell scripts, or manual file operations. Never run \`wp\` commands via Bash — always use the wp_cli tool instead. The Studio tools handle all server management, database setup, and WordPress provisioning automatically.
IMPORTANT: For any generated content for the site, these three principles are mandatory:

- Gorgeous design: More details on the guidelines below.
- No HTML blocks and raw HTML: Check the block content guidelines below.
- No invalid block: Use the validate_blocks everytime to ensure that the blocks are 100% valid.
IMPORTANT: For generated local sites, build a normal static HTML/CSS site first, then import it with Static Site Importer for WordPress block-theme output.

## Workflow

Expand All @@ -150,22 +146,16 @@ 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 the Design Guidelines below to plan the visual direction — layout, colors, typography, spacing.
3. **Write theme/plugin files**: Use Write and Edit to create files 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 and also validated using validate_blocks tool and adhere to the block content guidelines above as well. 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 the misuse of HTML blocks**: Verify if HTML blocks were used as sections or not. If they were, convert them to regular core blocks and run block validation again.
6. **Check the result**: Use take_screenshot to capture the site's landing page on desktop and mobile and verify the design visually on both viewports, check for wrong spacing, alignment, colors, contrast, borders, hover styles and other visual issues. Fix any issues found. Pay particular attention to the navigation menu and the CTA buttons. The design needs to match your original expectations. **Width check**: any section that was meant to be full-width (heroes, banners, edge-to-edge galleries, full-bleed footers) must visibly span the entire viewport in the desktop screenshot. If a "full-width" section only spans the content column (~700px at 1280px viewport), the block markup is missing \`align: "full"\` on the outer group or has a mismatched inner \`layout\` type — see the block-theme layout cascade rules above. Fix in markup, not custom CSS.
3. **Write the static site**: Use Write and Edit to create \`<site>/tmp/static-site/index.html\` as a full static HTML document with semantic body structure and CSS/assets.
4. **Import into WordPress**: Use \`wp_cli\` to run \`static-site-importer import-theme /wordpress/tmp/static-site/index.html --slug=<theme-slug> --name="<Theme Name>" --activate --overwrite\`. The importer converts the static HTML into an editable block theme and front page. The \`wp_cli\` tool already runs WP-CLI, so do not prefix commands with \`wp\`. It takes one literal WP-CLI command per call, not shell commands: never combine commands with \`&&\`, \`;\`, pipes, redirection, shell substitution such as \`$(cat file)\`, backticks, environment variables, or host temp-file paths. Paths passed to WP-CLI must use WordPress's mounted filesystem (\`/wordpress/...\`), not host paths.
5. **Finish promptly**: Run any cheap static checks that are directly relevant, summarize what changed, and let the user review the result.

## Working cadence

One \`Write\` or \`Edit\` per turn (read-only \`site_info\`, \`site_list\`, \`wp_cli\` queries may be combined). Short prose between tools — no long design-plan essays. The CLI only renders complete assistant messages, so a turn that batches files or emits >~200 lines spins silently for minutes and can hit gateway timeouts. Cadence is also a quality lever: the screenshot-fix loop only works after small visible increments.
One \`Write\` or \`Edit\` per turn (read-only \`site_info\`, \`site_list\`, \`wp_cli\` queries may be combined). Short prose between tools — no long design-plan essays. The CLI only renders complete assistant messages, so a turn that batches files or emits >~200 lines spins silently for minutes and can hit gateway timeouts.

**After \`site_create\`** (or "redesign"/"rebuild"/"start over" triggers), the next turn MUST be small: \`site_info\` or a single ≤50-line \`Write\`. Never scaffold a whole theme in one turn.

**Long files (>~200 lines): skeleton first, then fill across Edits.**

- \`style.css\`: skeleton = \`:root { ... }\` custom properties + 6–10 anchor comments \`/* === <concern> === */\` (e.g. \`reset\`, \`typography\`, \`hero\`, \`features\`, \`cta\`, \`footer\`, \`responsive\`), <2KB total. Fill one anchor per Edit (300–2000B each) — \`old_string\` is the anchor line, \`new_string\` is \`<anchor>\\n\\n<styles>\`.
- Page content: create the page empty (\`wp_cli post create --post_content=""\`), write \`<site>/tmp/page-<slug>.html\` (not inside the theme) with \`<!-- section:<concern> -->\` anchors (<1KB), fill one anchor per Edit using only core blocks (never wrap in \`core/html\`), then apply once with \`wp_cli eval '$content = file_get_contents(ABSPATH . "tmp/page-<slug>.html"); wp_update_post(["ID" => <id>, "post_content" => $content]); echo "ok";'\`. Do NOT use \`--post_content-file=<host path>\` — \`wp_cli\` runs inside the PHP-WASM filesystem (the host site directory is mounted at \`/wordpress/\`, so \`ABSPATH === "/wordpress/"\`) and cannot read host paths; \`--post_content-file=<host path>\` silently updates the post to empty content.

## Available Studio Tools (prefixed with mcp__studio__)

- site_create: Create a new WordPress site (name only — handles everything automatically)
Expand All @@ -179,8 +169,7 @@ One \`Write\` or \`Edit\` per turn (read-only \`site_info\`, \`site_list\`, \`wp
- preview_update: Update an existing hosted WordPress.com preview from a local site; this can take a few minutes, so tell the user to wait
- preview_delete: Delete a hosted WordPress.com preview by hostname
- wp_cli: Run WP-CLI commands on a running site
- validate_blocks: Validate block content for correctness on a running site (runs each block through its save() function in a real browser). 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 and mobile viewports). Use this to visually check the site after building it.
- take_screenshot: Take a full-page screenshot of a URL (supports desktop and mobile viewports).
- 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.
- rank_me_up: Run an on-page SEO audit (title/meta tags, headings, image alt text, OpenGraph/Twitter cards, JSON-LD structured data, robots.txt and sitemap.xml availability) for a running site. Use this to identify on-page SEO issues and guide fixes.
- site_connected_remote_sites: List the WordPress.com sites already attached to a local site. Call this before site_push to decide how to ask the user which remote site to target.
Expand All @@ -191,13 +180,9 @@ One \`Write\` or \`Edit\` per turn (read-only \`site_info\`, \`site_list\`, \`wp

## General rules

- Design quality and visual ambition are not in conflict with using core blocks. Custom CSS targeting block classNames can achieve any visual design. The block structure is for editability; the CSS is for aesthetics.
- Do NOT modify WordPress core files. Only work within wp-content/.
- Before running wp_cli, ensure the site is running (site_start if needed).
- When building themes, always build block themes (NO CLASSIC THEMES).
- Always add the style.css as editor styles in the functions.php of the theme to make the editor match the frontend.
- Always enqueue the theme's style.css on the frontend from functions.php.
- For theme and page content custom CSS, put the styles in the main style.css of the theme. No custom stylesheets.
- Let Static Site Importer generate and activate the block theme. Do not manually create theme files unless the user explicitly asks for custom plugin/theme development.
- Scroll animations must use progressive enhancement: CSS defines elements in their **final visible state** by default (full opacity, final position). JavaScript on the frontend adds the initial hidden state (e.g. \`opacity: 0\`, \`transform\`) and scroll-triggered transitions. This ensures elements are fully visible in the block editor (which loads theme CSS but not custom JS).
- All animations and transitions must respect \`prefers-reduced-motion\`. Add a \`@media (prefers-reduced-motion: reduce)\` block that disables or simplifies animations (e.g. \`animation: none; transition: none; scroll-behavior: auto;\`).

Expand Down Expand Up @@ -244,31 +229,13 @@ const REMOTE_DESIGN_GUIDELINES = `## Design capabilities by plan
- Custom CSS, global styles, plugin management, and advanced customization become available.
- Check the specific plan to determine exact capabilities.`;

const LOCAL_CONTENT_GUIDELINES = `## Block content guidelines

- Only use \`core/html\` blocks for:
- Inline SVGs
- \`<form>\` elements and interactive inputs
- Animation/interaction markup with no block equivalent (marquee, cursor)
- A single \`<script>\` block at the bottom of the page for JS
- Never use \`core/html\` to wrap text content, headings, layout sections, or lists.
- No decorative HTML comments (e.g. \`<!-- Hero Section -->\`, \`<!-- Features -->\`). Only block delimiter comments are allowed.
- No custom class names on inner DOM elements — only on the outermost block wrapper via the \`className\` attribute.
- No inline \`style\` or \`style\` block attributes for styling. Use \`className\` + \`style.css\` instead.
- Use \`core/spacer\` for empty spacing divs, not \`core/group\`.
- No emojis anywhere in generated content.

## Block-theme layout cascade
const LOCAL_CONTENT_GUIDELINES = `## Content guidelines

WordPress constrains children of \`core/post-content\` (and any constrained-layout container) to \`theme.json\`'s \`settings.layout.contentSize\` (~700px by default). Custom CSS like \`.hero { width: 100% }\` does NOT win against core's layout selectors (\`.is-layout-constrained > *:not(.alignwide):not(.alignfull)\`) because they're more specific.

To break out of the content width, use these three patterns:

- **Full-bleed section, constrained inner content** (most common — full-width hero with text in the middle): outer \`core/group {"align":"full","layout":{"type":"constrained"}}\` containing a default-layout child for the inner block.
- **Full-bleed section, full-bleed inner** (image grids, edge-to-edge galleries): outer AND inner \`core/group {"align":"full","layout":{"type":"default"}}\`. Children render at full viewport width.
- **Standard constrained content**: omit \`align\` entirely and write blocks normally.

The single most common failure is "I made a hero full-width but its inner content is narrow" — that's a missing \`align: "full"\` on the outer group or a mismatched inner \`layout\` type. Fix in markup, not in CSS.`;
- Build local generated sites as normal static HTML/CSS in \`<site>/tmp/static-site/index.html\`.
- Use semantic HTML elements: \`section\`, \`header\`, \`main\`, \`footer\`, \`nav\`, \`h1\`-\`h6\`, \`p\`, \`ul\`, \`ol\`, \`blockquote\`, \`figure\`, \`img\`, \`table\`, \`a\`, and \`button\`.
- Use CSS classes for styling hooks and keep the visual system in the static HTML/CSS that will be imported.
- Static Site Importer handles the WordPress block-theme conversion.
- No emojis anywhere in generated content.`;

const LOCAL_DESIGN_GUIDELINES = `## Design guidelines

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/ai/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1549,7 +1549,7 @@ export class AiChatUI implements AiOutputAdapter {
' - ' +
sprintf(
/* translators: %s: bold "Build" */
__( '%s block themes with striking, memorable designs' ),
__( '%s WordPress sites with striking, memorable designs' ),
b( __( 'Build' ) )
),
' - ' +
Expand Down
12 changes: 12 additions & 0 deletions apps/cli/lib/dependency-management/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,15 @@ export function getPhpMyAdminPath(): string {
export function getBlueprintsPharPath(): string {
return path.join( getWpFilesPath(), 'blueprints', 'blueprints.phar' );
}

// Static Site Importer ships read-only with the CLI bundle (downloaded at npm
// install time by `scripts/download-wp-server-files.ts`) and is symlinked into
// each site's mu-plugins temp directory. Bench harnesses may override that
// bundled copy to test an active SSI worktree.
export function getStaticSiteImporterPluginPath(): string {
if ( process.env.STUDIO_STATIC_SITE_IMPORTER_PLUGIN_PATH ) {
return process.env.STUDIO_STATIC_SITE_IMPORTER_PLUGIN_PATH;
}

return path.join( getWpFilesPath(), 'static-site-importer' );
}
22 changes: 22 additions & 0 deletions apps/cli/lib/dependency-management/tests/paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { getStaticSiteImporterPluginPath } from '../paths';

describe( 'dependency path helpers', () => {
afterEach( () => {
delete process.env.STUDIO_STATIC_SITE_IMPORTER_PLUGIN_PATH;
} );

it( 'uses the requested Static Site Importer plugin path when provided', () => {
process.env.STUDIO_STATIC_SITE_IMPORTER_PLUGIN_PATH =
'/tmp/static-site-importer-worktree';

expect( getStaticSiteImporterPluginPath() ).toBe( '/tmp/static-site-importer-worktree' );
} );

it( 'falls back to the bundled Static Site Importer plugin path', () => {
expect( getStaticSiteImporterPluginPath() ).toContain(
path.join( 'wp-files', 'static-site-importer' )
);
} );
} );
8 changes: 7 additions & 1 deletion apps/cli/lib/run-wp-cli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { setupPlatformLevelMuPlugins } from '@wp-playground/wordpress';
import {
getPhpBinaryPath,
getSqliteCommandPath,
getStaticSiteImporterPluginPath,
getWpCliPharPath,
} from 'cli/lib/dependency-management/paths';
import { validatePhpVersion } from 'cli/lib/utils';
Expand Down Expand Up @@ -123,7 +124,11 @@ async function runNativeWpCliCommand(
): Promise< DisposableWpCliResponse > {
const nativeArgs = applyWpCliCommandOptions( 'native', args, options );
const phpVersion = validateNativePhpVersion( options.phpVersion ?? DEFAULT_PHP_VERSION );
await writeStudioMuPluginsForNativePhpRuntime( site.path, site.isWpAutoUpdating );
await writeStudioMuPluginsForNativePhpRuntime(
site.path,
site.isWpAutoUpdating,
getStaticSiteImporterPluginPath()
);
const child = spawn(
getPhpBinaryPath( phpVersion ),
[ getWpCliPharPath(), `--path=${ site.path }`, ...nativeArgs ],
Expand Down Expand Up @@ -219,6 +224,7 @@ export async function runWpCliCommand(
// Mount mu-plugins
const [ studioMuPluginsHostPath, loaderMuPluginHostPath ] = await getMuPlugins( {
isWpAutoUpdating: false,
staticSiteImporterPluginPath: getStaticSiteImporterPluginPath(),
} );
await php.mount(
'/internal/studio/mu-plugins',
Expand Down
10 changes: 8 additions & 2 deletions apps/cli/php-server-child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import {
getBlueprintsPharPath,
getPhpBinaryPath,
getStaticSiteImporterPluginPath,
getWpCliPharPath,
} from './lib/dependency-management/paths';

Expand Down Expand Up @@ -316,7 +317,11 @@ async function startServer( config: ServerConfig, signal: AbortSignal ): Promise
stopSignal.throwIfAborted();
await ensureWpConfig( config.sitePath, phpVersion, stopSignal, config );
stopSignal.throwIfAborted();
await writeStudioMuPluginsForNativePhpRuntime( config.sitePath, config.isWpAutoUpdating );
await writeStudioMuPluginsForNativePhpRuntime(
config.sitePath,
config.isWpAutoUpdating,
getStaticSiteImporterPluginPath()
);
stopSignal.throwIfAborted();
await installWordPress( config, phpVersion, stopSignal );
stopSignal.throwIfAborted();
Expand Down Expand Up @@ -594,7 +599,8 @@ async function ipcMessageHandler( packet: unknown ) {
);
await writeStudioMuPluginsForNativePhpRuntime(
blueprintConfig.sitePath,
blueprintConfig.isWpAutoUpdating
blueprintConfig.isWpAutoUpdating,
getStaticSiteImporterPluginPath()
);
await installWordPress( blueprintConfig, blueprintPhpVersion, abortController.signal );
if ( ! blueprintConfig.blueprint ) {
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/playground-server-child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { sanitizeRunCLIArgs } from 'cli/lib/cli-args-sanitizer';
import {
getPhpMyAdminPath,
getSqliteCommandPath,
getStaticSiteImporterPluginPath,
getWpCliPharPath,
} from 'cli/lib/dependency-management/paths';
import { rewriteWpCliPostContentToFile } from 'cli/lib/rewrite-wp-cli-post-content';
Expand Down Expand Up @@ -222,6 +223,7 @@ async function getBaseRunCLIArgs(

const [ studioMuPluginsHostPath, loaderMuPluginHostPath ] = await getMuPlugins( {
isWpAutoUpdating: config.isWpAutoUpdating,
staticSiteImporterPluginPath: getStaticSiteImporterPluginPath(),
} );

if ( ! useExactMountLayout ) {
Expand Down
Loading
Loading