Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5bf1c55
Add ticket context (orchestrator)
Poliuk May 13, 2026
aca9afe
Add research report (research-lead)
Poliuk May 13, 2026
e438224
Relocate DLA bridge to tools/dla as workspace package (orchestrator)
Poliuk May 13, 2026
46d8387
Add implementation plan (planner)
Poliuk May 13, 2026
a3b2be9
Scaffold tools/dla workspace package (implementer)
Poliuk May 13, 2026
2df3944
Add data-liberation and tsx runtime deps (implementer)
Poliuk May 13, 2026
22d5144
Implement DLA MCP-stdio bridge (implementer)
Poliuk May 13, 2026
286a4c5
Wire DLA policy extension factory into pi runtime (implementer)
Poliuk May 13, 2026
aedb5e7
Wire DLA bridge bring-up and teardown into pi runtime (implementer)
Poliuk May 13, 2026
0de39aa
Add /migrate wrapper skill and fix vite prod skills-copy (implementer)
Poliuk May 13, 2026
b1bebaa
Register /migrate slash command (implementer)
Poliuk May 13, 2026
b42a828
Add standalone studio migrate CLI command (implementer)
Poliuk May 13, 2026
43a7d92
Skip Playwright Chromium download in CI build pipelines (implementer)
Poliuk May 13, 2026
7c08328
Document /migrate in CLI README (documentator)
Poliuk May 13, 2026
0cd93ca
Document DLA integration in CLI design doc (documentator)
Poliuk May 13, 2026
b948983
Add review 1 (code-reviewer)
Poliuk May 13, 2026
65ce884
Fix tsx resolution in DLA bridge (implementer)
Poliuk May 13, 2026
a835eb0
Add review 2 (code-reviewer)
Poliuk May 13, 2026
8bd351e
Add documentation review 1 and PR description (doc-reviewer)
Poliuk May 13, 2026
892493c
Fix doc accuracy issues from doc-review-1 (documentator)
Poliuk May 13, 2026
ea83c2b
Add documentation review 2 (doc-reviewer)
Poliuk May 13, 2026
f835043
Rename /migrate to /liberate (implementer)
Poliuk May 14, 2026
8d9127e
Expand Pre-merge Checklist with what's happening / why context per ga…
Poliuk May 14, 2026
88571c2
Update Gate 4 to reflect planned npm publish before merge (orchestrator)
Poliuk May 14, 2026
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
8 changes: 8 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@

# IMAGE_ID is an env var that only macOS agents need.
# Defining it at the root level propagates it too all agents, which can seem unnecessary but is a the same time convenient and DRY.
#
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 prevents the `playwright install chromium`
# postinstall (pulled in transitively by the `data-liberation` agent dep) from
# downloading ~150 MB of Chromium during every CI `npm install` / `npm ci`. The
# DLA runtime bootstraps Chromium lazily on first use of a Wix/Squarespace
# adapter, so CI builds never need it. End-user `npm install -g wp-studio` runs
# the postinstall normally and pays the cost once on first install.
env:
IMAGE_ID: $IMAGE_ID
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"

e2e_config: &e2e_config
command: bash .buildkite/commands/run-e2e-tests.sh "{{matrix.platform}}" "{{matrix.arch}}"
Expand Down
9 changes: 8 additions & 1 deletion .buildkite/release-build-and-distribute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@
# Each step checks out the release branch to ensure it builds the latest commit.
---

# Used by mac agents only
# IMAGE_ID is used by mac agents only.
#
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 prevents the `playwright install chromium`
# postinstall (pulled in transitively by the `data-liberation` agent dep) from
# downloading ~150 MB of Chromium during every release-build `npm install`. The
# DLA runtime bootstraps Chromium lazily on first use of a Wix/Squarespace
# adapter, so release builds never need it bundled.
env:
IMAGE_ID: $IMAGE_ID
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"

steps:
- group: 📦 Build for Mac
Expand Down
5 changes: 5 additions & 0 deletions .buildkite/release-pipelines/code-freeze.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
---

# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 prevents the `playwright install chromium`
# postinstall (pulled in transitively by the `data-liberation` agent dep) from
# downloading ~150 MB of Chromium during the code-freeze `npm install`. The
# DLA runtime bootstraps Chromium lazily on first use, so CI never needs it.
env:
IMAGE_ID: $IMAGE_ID
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"

steps:
- label: ":snowflake: Code Freeze"
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/publish-npm-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ permissions:
contents: read
id-token: write # Required for npm trusted publishing (OIDC)

# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 prevents the `playwright install chromium`
# postinstall (pulled in transitively by the `data-liberation` agent dep) from
# downloading ~150 MB of Chromium during `npm ci`. The DLA runtime bootstraps
# Chromium lazily on first use of a Wix/Squarespace adapter, so the publish job
# doesn't need it. End-user `npm install -g wp-studio` runs the postinstall
# normally and pays the cost once on first install.
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"

jobs:
publish:
runs-on: ubuntu-latest
Expand Down
23 changes: 23 additions & 0 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The Studio CLI lets you:
- [Quick start](#quick-start)
- [Usage](#usage)
- [Studio Code](#studio-code)
- [Migrate from a closed platform](#migrate-from-a-closed-platform)
- [Import and export](#import-and-export)
- [Sync with WordPress.com and Pressable](#sync-with-wordpresscom-and-pressable)
- [Preview sites](#preview-sites)
Expand Down Expand Up @@ -98,6 +99,28 @@ studio code sessions list
studio code sessions resume
```

## Migrate from a closed platform

The Studio CLI can move a site off a closed web platform — GoDaddy Websites & Marketing, Hostinger, HubSpot, Shopify, Squarespace, Webflow, Weebly, and Wix — into a fresh local WordPress site. The migration inspects the source, extracts its content into a WXR archive plus media, verifies the result, and lands everything in a new Studio site under `~/Studio/`. Powered by [Data Liberation Agent](https://github.com/Automattic/data-liberation-agent).

The recommended path is the `/liberate` slash command inside `studio code`. The agent walks you through detect, extract, verify, site-create, and import, confirming each heavier step before it runs. This path is gated behind a feature flag while it stabilizes — start `studio code` with `STUDIO_DLA_ENABLED=1` to enable it, then invoke the skill with or without a URL:

```bash
STUDIO_DLA_ENABLED=1 studio code
# inside the session:
# /liberate
# /liberate https://example.com
```

For headless or scripted use, run the standalone command instead. It spawns the Data Liberation Agent CLI directly with no agent in the loop, streaming progress straight to your terminal:

```bash
studio liberate https://example.com
studio liberate https://example.com --output ./out --non-interactive
```

Two source platforms need credentials before the extract step: Webflow requires `LIBERATION_TOKEN` (a Webflow site token) and Shopify requires `SHOPIFY_ADMIN_TOKEN` (a Shopify Admin API token with read access to products and orders). Set them in your shell before running either path. Installing the Studio CLI also downloads a Playwright Chromium build (~150 MB) used for the Wix and Squarespace adapters, so the initial `npm install -g wp-studio` pulls more than the base CLI does.

## Import and export

The Studio CLI allows you to import and export local backups.
Expand Down
125 changes: 117 additions & 8 deletions apps/cli/ai/runtimes/pi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SettingsManager,
type AgentSession,
type AgentSessionEvent,
type ExtensionFactory,
type SessionManager,
type ToolDefinition,
} from '@mariozechner/pi-coding-agent';
Expand All @@ -27,6 +28,12 @@ import {
type AiModelFamily,
type AiModelId,
} from '@studio/common/ai/models';
import {
createDlaPolicyFactory,
defaultPolicyBuckets,
startDlaBridge,
type DlaBridge,
} from '@studio/dla';
import { buildSystemPrompt } from 'cli/ai/system-prompt';
import { resolveStudioToolDefinitions } from 'cli/ai/tools';
import { createAskUserQuestionTool } from 'cli/ai/tools/ask-user-question';
Expand Down Expand Up @@ -203,8 +210,9 @@ async function runAgentSessionTurn(

let session: AgentSession | undefined;
let unsubscribe: ( () => void ) | undefined;
const dlaBridge = await maybeStartDlaBridge( config );
try {
session = await createStudioAgentSession( config, family, resolved.creds );
session = await createStudioAgentSession( config, family, resolved.creds, dlaBridge );
setActiveSession( session );
unsubscribe = session.subscribe( config.onEvent );

Expand All @@ -223,13 +231,61 @@ async function runAgentSessionTurn(
unsubscribe?.();
session?.dispose();
setActiveSession( undefined );
await dlaBridge?.dispose();
}
}

/**
* Bring up the Data Liberation Agent (DLA) MCP bridge for this session,
* gated on `STUDIO_DLA_ENABLED === '1'`.
*
* Failures to spawn or connect the bridge are non-fatal: a warning is
* logged and `undefined` is returned so the session still starts with the
* usual Studio tools. The bridge itself also degrades gracefully (returning
* `degraded: true` with an empty tool list) when `listTools` fails — in
* that case the bridge handle is still returned so the caller can dispose
* it during teardown.
*
* The wpcom access token, when present in the session config, is forwarded
* to the DLA child as `STUDIO_WPCOM_TOKEN` so DLA can authenticate against
* WordPress.com APIs when surfacing remote-site flows.
*
* @param config - The resolved session config for this turn.
* @returns A `DlaBridge` handle, or `undefined` when DLA is disabled or
* the bridge could not be constructed at all.
*/
async function maybeStartDlaBridge(
config: ResolvedStudioAgentTurnConfig
): Promise< DlaBridge | undefined > {
if ( config.env.STUDIO_DLA_ENABLED !== '1' ) {
return undefined;
}
try {
const bridge = await startDlaBridge( {
wpcomToken: config.wpcomAccessToken,
} );
if ( bridge.degraded ) {
console.warn(
`[studio code] DLA bridge degraded; continuing without DLA tools (${
bridge.degradationReason ?? 'unknown reason'
}).`
);
}
return bridge;
} catch ( error ) {
const reason = error instanceof Error ? error.message : String( error );
console.warn(
`[studio code] DLA bridge failed to start; continuing without DLA tools (${ reason }).`
);
return undefined;
}
}

async function createStudioAgentSession(
config: ResolvedStudioAgentTurnConfig,
family: AiModelFamily,
creds: ResolvedCredentials
creds: ResolvedCredentials,
dlaBridge?: DlaBridge
): Promise< AgentSession > {
const model = buildModel( config.model, family, creds );
const isRemoteSite = Boolean( config.activeSite?.remote && config.activeSite?.wpcomSiteId );
Expand All @@ -249,10 +305,10 @@ async function createStudioAgentSession(
: { previewSteering: isForkedByDesktop, remoteSession }
);

const tools = buildAgentTools( config, isForkedByDesktop, remoteSession );
const toolDefinitions = tools.map( toToolDefinition );
const toolDefinitions = buildAgentTools( config, isForkedByDesktop, remoteSession, dlaBridge );
const { authStorage, modelRegistry } = createModelRegistry( model, family, creds );
const settingsManager = createSettingsManager( config.env );
const extensionFactories = resolveDlaExtensionFactories( config.env );
const resourceLoader = new DefaultResourceLoader( {
cwd: STUDIO_SITES_ROOT,
agentDir: STUDIO_AGENT_DIR,
Expand All @@ -263,6 +319,7 @@ async function createStudioAgentSession(
noThemes: true,
noContextFiles: true,
systemPrompt,
extensionFactories,
} );
await resourceLoader.reload();

Expand Down Expand Up @@ -387,6 +444,33 @@ function createSettingsManager( _env: Record< string, string > ): SettingsManage
} );
}

/**
* Build the list of pi `ExtensionFactory` values to feed into
* `DefaultResourceLoader.extensionFactories` based on the session's env.
*
* Inline `extensionFactories` are loaded even when `noExtensions: true`
* is set on `DefaultResourceLoader` (verified at `resource-loader.js`'s
* `loadExtensionFactories` path), so the runtime keeps the rest of the
* extension discovery surface disabled while still mounting the DLA
* policy hook when the feature flag is on.
*
* The DLA policy factory is only mounted when
* `STUDIO_DLA_ENABLED === '1'`. v1 ships with the flag off by default;
* the same flag also gates the DLA bridge spawn in `runAgentSessionTurn`
* (handled separately) so the policy hook is a no-op when the bridge is
* inactive.
*
* @param env - The resolved process env for this session.
* @returns An array of factories to forward to `DefaultResourceLoader`.
* Empty when DLA is disabled.
*/
function resolveDlaExtensionFactories( env: Record< string, string > ): ExtensionFactory[] {
if ( env.STUDIO_DLA_ENABLED !== '1' ) {
return [];
}
return [ createDlaPolicyFactory( defaultPolicyBuckets ) ];
}

function toToolDefinition( tool: AgentToolAny ): ToolDefinition {
return {
name: tool.name,
Expand All @@ -400,11 +484,34 @@ function toToolDefinition( tool: AgentToolAny ): ToolDefinition {
};
}

/**
* Build the `customTools` list passed to pi's `createAgentSession`. The
* Studio-native tools (returned as `AgentTool` values) are converted to
* pi's `ToolDefinition` shape via `toToolDefinition`, and the bridged DLA
* tools — already `ToolDefinition[]` — are spliced into the local-site
* tool list when `dlaBridge` is supplied.
*
* The DLA tools intentionally do not appear in the remote-site branch
* because the remote flow already steers callers toward WordPress.com
* APIs (`wpcom_request`), and bridging DLA there risks recursive migration
* loops back into Studio itself.
*
* @param config - The resolved per-turn session config.
* @param enablePreviewSteering - Whether the runtime is forked by the
* desktop app (which steers users toward the Preview Site flow).
* @param remoteSession - Whether this is a remote, WPCOM-backed session.
* @param dlaBridge - Optional DLA bridge handle; when present, its
* `tools` are spliced into the local-site tool list. When absent
* (e.g. `STUDIO_DLA_ENABLED` is unset or the bridge failed to spawn)
* the result matches the legacy tool list exactly.
* @returns The fully assembled `ToolDefinition[]` for the session.
*/
function buildAgentTools(
config: ResolvedStudioAgentTurnConfig,
enablePreviewSteering: boolean,
remoteSession: boolean
): AgentToolAny[] {
remoteSession: boolean,
dlaBridge?: DlaBridge
): ToolDefinition[] {
const isRemoteSite = Boolean(
config.activeSite?.remote && config.activeSite?.wpcomSiteId && config.wpcomAccessToken
);
Expand All @@ -417,14 +524,15 @@ function buildAgentTools(
const skillTool: AgentToolAny[] = skillToolDef ? [ skillToolDef ] : [];

if ( isRemoteSite ) {
return [
const remoteTools: AgentToolAny[] = [
createWpcomRequestTool( config.wpcomAccessToken!, config.activeSite!.wpcomSiteId! ),
takeScreenshotTool,
createSiteTool,
pullSiteTool,
...askUserTool,
...skillTool,
];
return remoteTools.map( toToolDefinition );
}

const renameTool = ( tool: AgentToolAny, name: string ): AgentToolAny => ( {
Expand All @@ -442,12 +550,13 @@ function buildAgentTools(
renameTool( createFindTool( STUDIO_SITES_ROOT ), 'Glob' ),
renameTool( createLsTool( STUDIO_SITES_ROOT ), 'Ls' ),
];
return [
const studioTools: AgentToolAny[] = [
...resolveStudioToolDefinitions( { enablePreviewSteering, remoteSession } ),
...askUserTool,
...skillTool,
...piTools,
];
return [ ...studioTools.map( toToolDefinition ), ...( dlaBridge?.tools ?? [] ) ];
}

function parseJsonHeaderEnv( value: string | undefined ): Record< string, string > | undefined {
Expand Down
Loading