Skip to content
Closed
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
2 changes: 1 addition & 1 deletion apps/cli/ai/inspector/page-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const INSPECTOR_PAGE_SCRIPT =

/* --------------------------------------------------------------------
* Element identification — build a CSS selector that's specific enough
* for the agent to find the element again via wp_cli/theme files.
* for the agent to find the element again via WP-CLI or theme files.
* ------------------------------------------------------------------ */
function buildSelector( el ) {
if ( ! el || el.nodeType !== 1 ) return '';
Expand Down
21 changes: 20 additions & 1 deletion apps/cli/ai/runtimes/pi/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'fs';
import path from 'path';
import { type AgentTool } from '@earendil-works/pi-agent-core';
import {
type Credential,
Expand Down Expand Up @@ -50,6 +51,11 @@ import { STUDIO_SITES_ROOT } from 'cli/lib/site-paths';
import { getFileToolPrompt } from './file-tool-prompts';
import { getPendingWork, type PendingWork } from './pending-work';
import { stripStaleImagesFromContext } from './strip-stale-images';
import {
ensureStudioWpShimDirectory,
getStudioWpShimScript,
prependToPath,
} from './studio-wp-shim';
import {
getIncompleteToolCallReason,
getPayloadLimitViolation,
Expand Down Expand Up @@ -603,6 +609,15 @@ function toToolDefinition(
};
}

// Every chunk of the CLI bundle sits next to `main.mjs`, so it is this
// module's sibling; `STUDIO_CLI_BIN` points elsewhere in development.
function getStudioWpSpawnHook() {
const cliPath = process.env.STUDIO_CLI_BIN ?? path.join( import.meta.dirname, 'main.mjs' );
return prependToPath(
ensureStudioWpShimDirectory( getStudioWpShimScript( process.execPath, cliPath ) )
);
}

function buildAgentTools(
config: ResolvedStudioAgentTurnConfig,
chatArtifactsEnabled: boolean,
Expand Down Expand Up @@ -637,13 +652,17 @@ function buildAgentTools(
renameTool( createReadTool( STUDIO_SITES_ROOT ), 'Read' ),
designSystemPreview( renameTool( createWriteTool( STUDIO_SITES_ROOT ), 'Write' ) ),
designSystemPreview( renameTool( createEditTool( STUDIO_SITES_ROOT ), 'Edit' ) ),
renameTool( createBashTool( STUDIO_SITES_ROOT ), 'Bash' ),
renameTool(
createBashTool( STUDIO_SITES_ROOT, { spawnHook: getStudioWpSpawnHook() } ),
'Bash'
),
renameTool( createGrepTool( STUDIO_SITES_ROOT ), 'Grep' ),
renameTool( createFindTool( STUDIO_SITES_ROOT ), 'Glob' ),
renameTool( createLsTool( STUDIO_SITES_ROOT ), 'Ls' ),
];
const studioTools = resolveStudioToolDefinitions( {
emitChatArtifacts: chatArtifactsEnabled,
shellRunsStudioWp: true,
imageGeneration: imageGenerationEnabled,
visionEnabled,
canAskUser: Boolean( config.onAskUser ),
Expand Down
51 changes: 51 additions & 0 deletions apps/cli/ai/runtimes/pi/studio-wp-shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';
import { getConfigDirectory } from '@studio/common/lib/well-known-paths';
import type { BashSpawnHook } from '@earendil-works/pi-coding-agent';

function shellQuote( value: string ): string {
return `'${ value.replace( /'/g, `'\\''` ) }'`;
}

// `wp` in the agent's shell runs `studio wp`: WP-CLI on Studio's PHP binary for the site,
// with Studio's SQLite integration loaded. A system `wp`, where one is installed at all,
// runs on whatever PHP the machine has and cannot open a Studio site's database.
export function getStudioWpShimScript( nodePath: string, cliPath: string ): string {
return `#!/bin/sh\nexec ${ shellQuote( nodePath ) } ${ shellQuote( cliPath ) } wp "$@"\n`;
}

// Writes the shim into a directory named after its content, so concurrent Studio
// processes (or different installs) never overwrite each other's script.
export function ensureStudioWpShimDirectory(
script: string,
root: string = path.join( getConfigDirectory(), 'tmp', 'agent-bin' )
): string {
const directory = path.join(
root,
createHash( 'sha256' ).update( script ).digest( 'hex' ).slice( 0, 12 )
);
const shimPath = path.join( directory, 'wp' );
if ( ! fs.existsSync( shimPath ) ) {
fs.mkdirSync( directory, { recursive: true } );
const temporaryPath = `${ shimPath }.${ process.pid }`;
fs.writeFileSync( temporaryPath, script, { mode: 0o755 } );
fs.renameSync( temporaryPath, shimPath );
}
return directory;
}

export function prependToPath( directory: string ): BashSpawnHook {
return ( context ) => {
const pathKey =
Object.keys( context.env ).find( ( key ) => key.toUpperCase() === 'PATH' ) ?? 'PATH';
const current = context.env[ pathKey ];
return {
...context,
env: {
...context.env,
[ pathKey ]: current ? `${ directory }${ path.delimiter }${ current }` : directory,
},
};
};
}
4 changes: 2 additions & 2 deletions apps/cli/ai/skills/annotate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ If the user wants to re-annotate, point them back to the open browser and call `
For each annotation:
1. **Identify what to change:**
- Use the CSS selector to find the element in theme templates or stylesheets
- Use `wp_cli` with `post list --post_type=wp_template --format=json` to check if it's in a template override
- Use `wp_cli` with `eval "echo wp_get_custom_css();"` to check existing custom CSS
- Use `wp post list --post_type=wp_template --format=json` to check if it's in a template override
- Use `wp eval "echo wp_get_custom_css();"` to check existing custom CSS
2. **Apply the change using the right approach:**
- For style changes (colors, sizes, spacing): use Global Styles custom CSS with the selector from the annotation
- For content changes (text, headings, block structure): edit the template or post content via WP-CLI
Expand Down
12 changes: 6 additions & 6 deletions apps/cli/ai/skills/block-content/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Pick one of the two per page:
- **Designed page** whose hero carries its own heading (home, landing, anything built from sections): assign the no-title template. Themes from `scaffold_theme` ship `templates/page-no-title.html`, registered in `theme.json` under `customTemplates`. Assign it to the page after creating it:

```text
wp_cli post meta update <id> _wp_page_template page-no-title
wp post meta update <id> _wp_page_template page-no-title
```

Do **not** solve this by deleting `core/post-title` from `page.html` — every ordinary page then renders untitled, and a missing `h1` is worse than a duplicated one. In a theme with no no-title template, add one (a copy of `page.html` without the title group) and register it in `customTemplates` rather than stripping the title.
Expand Down Expand Up @@ -146,7 +146,7 @@ For long page content:
1. Create the page empty:

```text
wp_cli post create --post_content=""
wp post create --post_content=""
```

2. Write `<site>/tmp/page-<slug>.html`, not a file inside the theme, with small section anchors:
Expand All @@ -158,16 +158,16 @@ wp_cli post create --post_content=""
```

3. Fill the anchors with editable blocks, several per `Edit` call: one `edits[]` entry per section, under ~8KB of new text per call. Never wrap a section in `core/html`.
4. **Validate before applying (mandatory gate).** Once all anchors are filled, you MUST call `validate_blocks` with `filePath` pointing at `<site>/tmp/page-<slug>.html` and get a passing result — the core/html policy passes and editor validation reports all blocks valid. This is not optional and not a step you can defer to after `wp_cli eval`: the scratch file is block content, so it must be validated as a file while it still lives in a file. If validation reports invalid blocks, fix them in the file and call `validate_blocks` again until it passes. Never apply block content you have not validated.
4. **Validate before applying (mandatory gate).** Once all anchors are filled, you MUST call `validate_blocks` with `filePath` pointing at `<site>/tmp/page-<slug>.html` and get a passing result — the core/html policy passes and editor validation reports all blocks valid. This is not optional and not a step you can defer to after `wp eval`: the scratch file is block content, so it must be validated as a file while it still lives in a file. If validation reports invalid blocks, fix them in the file and call `validate_blocks` again until it passes. Never apply block content you have not validated.
5. Apply the validated content once:

```text
wp_cli eval '$content = file_get_contents(ABSPATH . "tmp/page-<slug>.html"); wp_update_post(["ID" => <id>, "post_content" => $content]); echo "ok";'
wp 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=<path>`: it is not a WP-CLI option, so WP-CLI never reads the file and the post content is not updated.

## Validation

- Validation is a mandatory gate, not a cleanup step. You MUST call `validate_blocks` and get a passing result for any block content you generate **before** that content reaches the live site — before `wp_cli post create/update`, before `wp_cli eval`, and before importing a scratch file. Never apply, import, or save block content you have not validated. A build that skips validation is incomplete, even if the page renders.
- 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 — including scratch files such as `<site>/tmp/page-<slug>.html` that you later import with `wp_cli eval`. The scratch file is the block content; validate the file, not just the eventual post. Theme `templates/*.html` and `parts/*.html` files count too — they are live as soon as they are written, so validate each one right after every write or edit. 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.
- Validation is a mandatory gate, not a cleanup step. You MUST call `validate_blocks` and get a passing result for any block content you generate **before** that content reaches the live site — before `wp post create/update`, before `wp eval`, and before importing a scratch file. Never apply, import, or save block content you have not validated. A build that skips validation is incomplete, even if the page renders.
- 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 — including scratch files such as `<site>/tmp/page-<slug>.html` that you later import with `wp eval`. The scratch file is the block content; validate the file, not just the eventual post. Theme `templates/*.html` and `parts/*.html` files count too — they are live as soon as they are written, so validate each one right after every write or edit. 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.
2 changes: 1 addition & 1 deletion apps/cli/ai/skills/build-site/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ With the Site Spec stated, start small: call `scaffold_theme` on its own, and ne

## 4. Provision the site

Use wp_cli to activate the theme, install and activate the plugins the Site Spec lists, and set options. Do this before validating — the live editor only recognizes the active theme and registered plugin blocks. The site must be running.
Use WP-CLI (`wp`) to activate the theme, install and activate the plugins the Site Spec lists, and set options. Do this before validating — the live editor only recognizes the active theme and registered plugin blocks. The site must be running.

## 5. Validate and publish the content

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/ai/skills/imagery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The look is picked on one image and the layout on a set, both from the `build-si

When `generate_images` is not available but you have an image tool of your own (such as `image_gen`), make the same images with it, including the look image and the site's set below: plan them exactly as above, and write what you would pass as `siteContext`, `pageContext`, `style` and `imageGrade` into each image's prompt, with no text in the image. Then put each file where its destination says:

- **Content imagery**: copy the files into the site's `tmp/` folder and import them in one `wp_cli` call, `media import tmp/<a>.png tmp/<b>.png --porcelain`, which prints their attachment IDs in order; `post list --post_type=attachment --post__in=<ids> --fields=ID,guid` gives their URLs.
- **Content imagery**: copy the files into the site's `tmp/` folder and import them in one call, `wp media import tmp/<a>.png tmp/<b>.png --porcelain`, which prints their attachment IDs in order; `post list --post_type=attachment --post__in=<ids> --fields=ID,guid` gives their URLs.
- **Theme imagery**: copy the file to its path in the theme's `assets/images`.

For the look image, pass the file where your tool saved it as each look's `image`; import it only if the site uses it.
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/ai/skills/need-for-speed/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Run a performance audit on a WordPress site to measure Core Web Vitals and page

Based on the metrics, suggest specific actions:

- **High TTFB**: Enable page caching (e.g., `wp_cli: plugin install wp-super-cache --activate`), check for slow database queries, consider object caching.
- **High TTFB**: Enable page caching (e.g., `wp plugin install wp-super-cache --activate`), check for slow database queries, consider object caching.
- **High LCP / FCP**: Check for render-blocking CSS/JS, add lazy loading for below-the-fold images, defer non-critical scripts.
- **Large JS payload**: Identify heavy plugins from the scripts URL list, suggest deactivating unused plugins, check for jQuery dependency chains.
- **Large CSS payload**: Look for unused theme stylesheets, check for multiple Google Fonts loads.
Expand Down
Loading
Loading