From a5b7958916cab97843bf9124c058b03c4c7d02f3 Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Jun 2026 17:58:12 +0530 Subject: [PATCH 1/8] Add design spec for hosting-plans-helper skill (STU-1940) --- ...06-30-hosting-plans-helper-skill-design.md | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md diff --git a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md new file mode 100644 index 0000000000..19575a72bb --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md @@ -0,0 +1,186 @@ +# Design: `hosting-plans-helper` skill + +- **Issue:** [STU-1940](https://linear.app/a8c/issue/STU-1940) (bug: [STU-1939](https://linear.app/a8c/issue/STU-1939)) +- **Date:** 2026-06-30 +- **Status:** Approved design, pending implementation plan + +## Problem + +Studio Code answers WordPress.com plan, pricing, and feature-gating questions from +stale model-training knowledge. There is no authoritative source wired into the +agent, so it recommends renamed/legacy plans and makes incorrect feature-tier +claims (e.g. wrong plugin-tier gating). STU-1940 asks for a durable fix: a skill +that gives the agent current, authoritative plan/pricing/feature data instead of +relying on memory. + +## Investigation findings (what shaped this design) + +The issue proposed "fetch live from the `/plans` v1.5 API (`features_comparison`)." +Direct testing showed this premise does not hold for what Studio Code can reach: + +- **`/plans` v1.5** (public and authenticated, v1.3/v1.5/v2, with/without the + `features_comparison` param, and site-context): returns plan **names + prices** + only. It carries **no general `features_comparison`**. Per + [SHILL-1742](https://linear.app/a8c/issue/SHILL-1742), `features_comparison` was + added **only for Woo Hosted plans in a Woo-Hosted site context** — not a general + WordPress.com plans source. +- **Plan names** returned are the legacy lineup (Free/Personal/Premium/Business/ + Commerce). Flex/Pro/Premier are **not live** — not even on the production + `/pricing` page yet — so legacy names are currently correct. +- **Prices** are live and geo/currency-localized via `/plans` `formatted_price`. + +The real source of truth for "what each tier unlocks" is the **Landpack** plugin, +which renders the production `/pricing` page from curated PHP: + +- `…/2023-pricing-grid/utilities/features.php` — `get_feature_labels()`: the + grouped catalog mapping each feature key → `title`/`subtitle`/`tooltip` + (Essential, Performance boosters, High Availability, Developer tools, Security, + Grow, …). +- `…/utilities/plan-*.php` + `plan.php` `get_plans()` — each plan's resolved + `name` (from the `/plans` store product), `slug`, feature-key lists, `storage`, + `ai_assistant_limit`, commission %, etc. + +The existing public `wpcom/v2/plans/mobile` endpoint exposes a similar but coarser, +partly-stale shadow of this (37 flat features, missing dev/host granularity like +SSH/staging/git/CDN; e.g. it reports Business = 200GB while Landpack says 50 GB). +It is not a good long-term foundation. + +**Conclusion:** names + prices come from `/plans` (live); the rich per-tier feature +data exists only in Landpack (server-side). To make it queryable, we expose Landpack +through a new, purpose-built REST endpoint. + +## Design overview + +Two coordinated pieces, in two repositories. + +``` +[Landpack PHP] get_plans() + get_feature_labels() + │ (server-side, same source the /pricing page renders) + ▼ +[wpcom] GET wpcom/v2/plans/pricing ← Piece 1 (new, public, normalized DTO) + │ + ▼ (agent fetches: curl in local mode, wpcom_request in remote mode) +[Studio] hosting-plans-helper skill ← Piece 2 (SKILL.md + system-prompt guardrail) + │ + ├─ /plans/pricing → names + per-tier features + └─ /plans → live, geo-correct prices (merged by slug) + ▼ + Agent answers from current data, never from memory +``` + +## Piece 1 — wpcom endpoint: `GET wpcom/v2/plans/pricing` + +- Added to the existing `WPCOM_REST_API_V2_Endpoint_Plans` controller + (`wp-content/rest-api-plugins/endpoints/plans.php`), alongside `/plans/mobile`. +- **Public**, no authentication (so both Studio modes can reach it). Accepts + `?locale=` (default `en`), mirroring `/plans/mobile`. +- Reads `Landpack\get_plans()` and `Landpack\get_feature_labels()` — the same + source the `/pricing` page renders — so it **cannot drift** from what users see. +- Resolves feature keys → `{ key, title, tooltip, group }` server-side and returns + a **normalized DTO**, dropping presentation-only fields (badges, CTA buttons, + icons, `features_v4/v5` card selections). +- **No prices** in this endpoint — kept single-purpose; the skill fetches `/plans` + for prices. +- **Scope:** the 5 consumer plans only — Free, Personal, Premium, Business, + Commerce. (Landpack `get_plans()` also returns VIP; excluded from v1.) + +### Response shape + +```json +{ + "source": "landpack", + "locale": "en", + "plans": [ + { + "slug": "business", + "name": "Business", + "storage": "50 GB", + "ai_assistant_limit": "Enhanced", + "features": [ + { + "key": "dev-tools-ssh", + "title": "SSH access", + "tooltip": "Securely access your site over SSH.", + "group": "Developer tools" + } + ] + } + ] +} +``` + +(Exact per-plan scalar fields — `storage`, `ai_assistant_limit`, commission — to be +finalized in the plan against what `plan_defaults()` actually exposes.) + +### Risks / to verify during implementation + +- **Loadability:** confirm Landpack pricing-section utils are `require`-able in the + REST request context (they are normally loaded for block render). The endpoint + must bootstrap them before calling `get_plans()`/`get_feature_labels()`. +- **Coupling:** the endpoint depends on Landpack internal functions; a Landpack + refactor of `get_plans()` would break it. Mitigate with a **contract test** on the + endpoint response (asserts the 5 plans + non-empty grouped features + the DTO + shape). +- **Ownership:** lives in `Automattic/wpcom` — needs a wpcom owner, review, and + deploy. This is a separate repo from Studio. + +## Piece 2 — Studio skill: `hosting-plans-helper` + +- New skill at `apps/cli/ai/skills/hosting-plans-helper/SKILL.md`, discovered by the + existing `loadSkills()` mechanism. Frontmatter: `name`, `description`, + `user-invokable: true`. +- **Skill-only** (no dedicated tool, no bundled snapshot). The agent performs the + fetch with the tools it already has. +- The SKILL.md instructs the agent, **before answering any plan/pricing/feature/ + upgrade/"what does tier X unlock" question**, to: + 1. Fetch per-tier features from `wpcom/v2/plans/pricing`. + 2. Fetch live prices from `/plans` and merge by `slug`. + 3. Answer only from the fetched data — never state names, prices, or feature-tier + gating from memory. +- **Fetch recipes for both tool environments** (the skill must work in both): + - **Local mode** (local sites): has `Bash`, no `wpcom_request`. Fetch via + `curl "https://public-api.wordpress.com/wpcom/v2/plans/pricing?locale=en"` and + `curl ".../rest/v1.5/plans?locale=en"`. + - **Remote mode** (connected WP.com site): has `wpcom_request`, no `Bash`. Fetch + via `wpcom_request` with absolute paths: `!/plans/pricing` + (`apiNamespace: "wpcom/v2"`) and `!/plans` (`apiNamespace: ""`, v1.1) — or the + equivalent the runtime supports. + +### System-prompt guardrail + +In `apps/cli/ai/system-prompt.ts`, add a guardrail directing the agent **not** to +state plan names, prices, or feature-tier gating from memory, and to load the +`hosting-plans-helper` skill (or fetch current data) first. Add it to: + +- `LOCAL_SKILL_ROUTING` (local intro), and +- the remote intro (`buildRemoteIntro`), which already gates design features by plan. + +## Sequencing & dependency + +Because v1 is **live-endpoint-only** (no bundled fallback), the Studio skill is +**non-functional until `wpcom/v2/plans/pricing` is deployed**. Implementation order: + +1. Build + test + deploy the wpcom endpoint (Piece 1). +2. Build the Studio skill + guardrail (Piece 2) against the live endpoint. + +The skill should fail gracefully if the endpoint is unreachable (tell the user it +can't verify current plan data rather than answering from memory). + +## Testing + +- **Endpoint:** contract test asserting the 5 consumer plans, non-empty grouped + features with resolved titles/tooltips, and the documented DTO shape. +- **Skill:** unit coverage that the skill is discovered/loaded (`loadSkills`) and is + `user-invokable`; the existing `system-prompt.test.ts` extended to assert the + guardrail text is present in both local and remote prompts. +- **Manual:** run a plan/feature question through Studio Code (local and remote) and + confirm answers come from the endpoint, with correct current names and live prices. + +## Out of scope (v1) + +- Pressable, WooCommerce, and VIP plans (same mechanism can extend later). +- A bundled fallback snapshot of plan data. +- A dedicated `get_hosting_plans` Studio tool (caching / uniform shape) — could be a + later robustness upgrade if skill-only fetching proves flaky. +- Sharing the mechanism with Odie/Wapuu (the endpoint is reusable by them, but that + integration is not part of this work). From d249dd048e9c06caf930d0056fa7435b315b0d90 Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Jun 2026 18:01:32 +0530 Subject: [PATCH 2/8] Add standalone spec for plans/pricing backend endpoint --- ...06-30-hosting-plans-helper-skill-design.md | 4 + ...026-06-30-plans-pricing-endpoint-design.md | 170 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md diff --git a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md index 19575a72bb..af4e44c79f 100644 --- a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md +++ b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md @@ -70,6 +70,10 @@ Two coordinated pieces, in two repositories. ## Piece 1 — wpcom endpoint: `GET wpcom/v2/plans/pricing` +> **Authoritative spec:** `2026-06-30-plans-pricing-endpoint-design.md` (standalone, +> built in the `Automattic/wpcom` repo). The summary below is for context; defer to +> that spec for the endpoint contract. + - Added to the existing `WPCOM_REST_API_V2_Endpoint_Plans` controller (`wp-content/rest-api-plugins/endpoints/plans.php`), alongside `/plans/mobile`. - **Public**, no authentication (so both Studio modes can reach it). Accepts diff --git a/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md b/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md new file mode 100644 index 0000000000..df1ba8cd68 --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md @@ -0,0 +1,170 @@ +# Design: `GET wpcom/v2/plans/pricing` endpoint + +- **Repo:** `Automattic/wpcom` (NOT the Studio repo — this is the backend piece) +- **Consumer:** Studio Code `hosting-plans-helper` skill + ([STU-1940](https://linear.app/a8c/issue/STU-1940)); see the companion Studio spec + `2026-06-30-hosting-plans-helper-skill-design.md`. +- **Date:** 2026-06-30 +- **Status:** Spec for implementation (standalone — an agent can build from this alone) + +## Goal + +Expose the WordPress.com `/pricing` plan + per-tier feature data — currently +rendered server-side by the Landpack plugin and not available through any general +API — as a small, public, normalized REST endpoint, so AI assistants (Studio Code +first; Odie/Wapuu reusable later) can fetch authoritative "what each plan unlocks" +data instead of relying on stale model knowledge. + +This endpoint deliberately carries **no prices**. Prices remain the responsibility +of the existing `/plans` API (live, geo/currency-localized). Consumers merge the two +by plan `slug`. + +## Why this source + +The existing `/plans` (v1.x) API returns plan **names + prices** but **no general +per-tier feature data** (`features_comparison` exists only for Woo Hosted plans in a +Woo-Hosted site context, per [SHILL-1742](https://linear.app/a8c/issue/SHILL-1742)). +The existing public `wpcom/v2/plans/mobile` endpoint has a coarse, partly-stale +feature list (e.g. it reports Business = 200GB; Landpack says 50 GB; it lacks +SSH/staging/git/CDN granularity). + +The authoritative source — the one the production `/pricing` page renders from — is +the Landpack plugin. Reading it directly means the endpoint **cannot drift** from +what users see on `/pricing`, with no second copy to maintain. + +## Source functions (Landpack) + +All in the Landpack plugin under: +`wp-content/plugins/landpack/src/blocks/pricing-section/themes/2023-pricing-grid/utilities/` + +- `Landpack\get_plans()` (`plan.php`) — returns a keyed array of plan configs: + `free`, `personal`, `premium`, `business`, `ecommerce`, `vip`. Each value is the + output of `Plan_*::plan_defaults()`. +- `Landpack\get_feature_labels()` (`features.php`) — the **grouped** catalog: an + array of 8 groups, each: + ```php + [ 'title' => 'Developer tools', 'features' => [ + 'dev-tools-ssh' => [ 'title' => 'SSH access', 'tooltip' => '…', /* subtitle?, jetpack?, hide_in_comparison_grid? */ ], + … ] ] + ``` +- `Landpack\get_feature_labels_ungrouped()` (`features.php`) — same data flattened to + `[ '' => [ 'title' => …, 'tooltip' => … ] ]` (loses group association). + +### Relevant fields on each `plan_defaults()` result + +- `slug` — e.g. `business`. +- `title` — display name, resolved live from the `/plans` store product + (`$store_product->product_name_short`). +- `subtitle` — short tagline. +- `features_compare_annual` / `features_compare_month` — **arrays of feature keys** + (strings) — the comprehensive comparison-grid inclusion lists. `annual` = + `month` + a few annual-only extras (free domain, support). **Use `annual` as the + canonical inclusion list.** +- `features_compare_conditional` — extra store/Woo rows (relevant to Commerce). +- `features_v4` / `features_v5` — highlighted card features (a resolved subset; not + the full inclusion list). Not needed for this endpoint. +- `storage` — e.g. `50 GB`. +- `ai_assistant_limit` — e.g. `Enhanced`. +- `standard_commission` / `woo_commission` — e.g. `2%` / `0%`. + +## Endpoint definition + +- **File:** `wp-content/rest-api-plugins/endpoints/plans.php` — add to the existing + `WPCOM_REST_API_V2_Endpoint_Plans` controller (same file/class as + `/plans/mobile`). +- **Route:** register `wpcom/v2/plans/pricing` in `register_routes()`. +- **Method:** `GET` (`WP_REST_Server::READABLE`). +- **Auth:** **public** (no auth) — Studio Code reaches it unauthenticated in both + local (curl) and remote (`wpcom_request`) modes. Matches `/plans/mobile`. +- **Args:** `locale` (default `en`), handled with `wpcom_switch_to_locale(...)` like + `get_plans_mobile()`. +- **Callback:** `get_plans_pricing( $request )`. + +## Algorithm + +1. `wpcom_switch_to_locale( $request->get_param( 'locale' ) )`. +2. Ensure Landpack pricing-section utilities are loaded (see "Loadability" below); + then build a key → label map **with group** by iterating the grouped + `Landpack\get_feature_labels()`: + ``` + foreach group: + foreach (key => label) in group['features']: + map[key] = { title: label['title'], tooltip: label['tooltip'] ?? '', group: group['title'] } + ``` +3. `$plans = Landpack\get_plans();` +4. For each of the **5 consumer plans** (`free, personal, premium, business, + ecommerce`) — exclude `vip`: + - Take `features_compare_annual` (fall back to `features_compare_month` if annual + is unset). For Commerce, also append `features_compare_conditional`. + - Resolve each key through `map`; **skip unknown keys** (a key present in a plan + list but absent from the catalog — mirror the `isset` guard in + `process_features_v4`). De-duplicate keys (a few plans list a key twice). + - Emit `{ key, title, tooltip, group }` per resolved feature, preserving the + catalog's group order. + - Carry scalar fields: `name` (= `title`), `slug`, `tagline` (= `subtitle`), + `storage`, `ai_assistant_limit`, `standard_commission`, `woo_commission`. +5. Return the DTO below. + +## Response shape (normalized DTO) + +```json +{ + "source": "landpack", + "locale": "en", + "plans": [ + { + "slug": "business", + "name": "Business", + "tagline": "Grow your business with powerful tools and priority support.", + "storage": "50 GB", + "ai_assistant_limit": "Enhanced", + "standard_commission": "2%", + "woo_commission": "0%", + "features": [ + { "key": "unlimited-pages", "title": "Unlimited pages", "tooltip": "Add as many pages as you like to your site.", "group": "Essential features" }, + { "key": "dev-tools-ssh", "title": "SSH access", "tooltip": "…", "group": "Developer tools" } + ] + } + ] +} +``` + +- Plans ordered: Free, Personal, Premium, Business, Commerce. +- No presentation fields (badges, CTA buttons, icons, `features_v4/v5`). No prices. + +## Loadability (verify during implementation) + +The Landpack util functions are normally loaded when the pricing-section block +renders, not on every request. The endpoint must make them available before calling: + +1. Prefer a guard: `if ( ! function_exists( 'Landpack\\get_plans' ) ) { require_once /plan.php; }` — `plan.php` itself `require`s `features.php` and all `plan-*.php`. +2. Confirm `plan.php`'s own relative `require`s (e.g. `../../../../../utilities/get-currency.php`) and its calls into WPCOM (`get_store_product`, `wpcom_switch_to_locale`) resolve in REST request context. +3. If direct loading proves fragile, the fallback is to replicate the small amount + of Landpack logic behind a Landpack-owned accessor — but **prefer reusing the + functions** so the data stays in lockstep with `/pricing`. + +## Error handling + +- If Landpack functions cannot be loaded or `get_plans()` returns empty, return a + `WP_Error` (HTTP 500) with a clear code (e.g. `landpack_unavailable`) rather than a + partial/empty `plans` array — so consumers can detect failure and refuse to answer + from memory rather than present wrong data. + +## Testing + +Contract test asserting: +- HTTP 200, public (works unauthenticated). +- `plans` has exactly the 5 consumer slugs in order; no `vip`. +- Each plan has non-empty `features`, every feature has non-empty `title` and a + `group`, and `key`s are unique within a plan. +- A spot-check that a known Business-only developer feature (e.g. `dev-tools-ssh` / + staging) is present on `business`/`ecommerce` and absent on `free`/`personal`. +- `locale` param is honored (a non-`en` locale changes localized titles). +- No `price`/`cost` fields anywhere in the response. + +## Scope / out of scope + +- **In:** the 5 consumer WordPress.com plans; grouped per-tier features; the scalar + fields listed above; `locale`. +- **Out:** prices (use `/plans`); VIP/Blogger/DIFM; Pressable; Woo Hosted; any + write/POST; auth/site-context personalization. From 9458673f038922beb8cf066d23b93fe91240071e Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Jun 2026 18:13:41 +0530 Subject: [PATCH 3/8] Add hosting-plans-helper skill and plan-data guardrail to Studio Code (STU-1940) --- .../ai/skills/hosting-plans-helper/SKILL.md | 64 +++++++++++++++++++ apps/cli/ai/system-prompt.ts | 9 ++- apps/cli/ai/tests/system-prompt.test.ts | 18 ++++++ ...06-30-hosting-plans-helper-skill-design.md | 20 +++--- ...026-06-30-plans-pricing-endpoint-design.md | 30 ++++++--- 5 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 apps/cli/ai/skills/hosting-plans-helper/SKILL.md diff --git a/apps/cli/ai/skills/hosting-plans-helper/SKILL.md b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md new file mode 100644 index 0000000000..8075b22207 --- /dev/null +++ b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md @@ -0,0 +1,64 @@ +--- +name: hosting-plans-helper +description: Answer WordPress.com plan, pricing, upgrade, and feature-tier questions (plan names, what each tier unlocks — plugins, themes, custom code, SSH, hosting — and current prices) from authoritative live data. Load before answering ANY plan, pricing, or feature-gating question; never answer these from memory. +user-invokable: true +--- + +# Hosting Plans Helper + +Use this skill whenever the user asks about WordPress.com (or Pressable) plans, +pricing, upgrades, or what a plan tier unlocks — for example "which plan do I need +for plugins?", "what does Business include?", "how much is Commerce?", "can I use +custom CSS on Premium?", or "should I upgrade?". + +## Hard rule: never answer from memory + +Plan names, prices, and feature-tier gating change, and your training data is stale. +You MUST fetch current data with this skill before answering. Do not state a plan +name, a price, or which tier unlocks a feature from memory — even if you are +confident. If the fetch fails, say you can't verify current plan data right now and +point the user to https://wordpress.com/pricing; do not guess. + +## Step 1: Fetch current plan data + +There is one authoritative source: the `wpcom/v2/plans/pricing` endpoint. It returns, +per plan, the current name, price, and the full list of features that tier unlocks +(grouped — Essential features, Performance boosters, High Availability, Developer +tools, Security, etc.). Fetch it once. + +Use whichever tool is available in your current environment: + +**Local sites (Bash tool available):** + +```text +curl -s "https://public-api.wordpress.com/wpcom/v2/plans/pricing?locale=en" +``` + +**Connected WordPress.com sites (wpcom_request tool available, no Bash):** + +```text +wpcom_request method=GET path="!/plans/pricing" apiNamespace="wpcom/v2" +``` + +Both reach the same public endpoint (no authentication required). Pass `?locale=` +(or the matching query param) to match the user's locale when known. + +## Step 2: Answer from the fetched data only + +- Use the exact plan **names** from the response — do not rename or substitute + legacy names. +- Quote **prices** from the response (they are localized to the response currency; + mention the currency). +- To answer "does plan X include feature Y?" or "what do I need for Y?", check the + per-plan `features` list. Each feature has a `title`, a `tooltip` (use it to + explain), and a `group`. Recommend the lowest tier whose `features` includes what + the user needs. +- When recommending an upgrade, name the specific tier and the concrete features it + unlocks for the user's stated goal. + +## Scope + +Currently covers the WordPress.com consumer plans (Free, Personal, Premium, +Business, Commerce). If the user asks about a plan or product not in the response +(e.g. Pressable, Woo Hosted, VIP, enterprise), say it's not covered by this data and +point them to https://wordpress.com/pricing rather than answering from memory. diff --git a/apps/cli/ai/system-prompt.ts b/apps/cli/ai/system-prompt.ts index 23c39595ba..767fd4972c 100644 --- a/apps/cli/ai/system-prompt.ts +++ b/apps/cli/ai/system-prompt.ts @@ -68,7 +68,8 @@ IMPORTANT: Before doing ANY work, you MUST first check the site's plan by callin - Always confirm destructive operations (deleting posts, deactivating plugins, etc.) with the user before proceeding. - When creating content, follow WordPress best practices for block-based content and the remote block content guidelines below. - If a requested operation fails, check the error message and suggest alternatives. -- Explore the API — if you're unsure about an endpoint, load the \`wpcom-remote-management\` skill and try a lightweight GET request first to discover available data.`; +- Explore the API — if you're unsure about an endpoint, load the \`wpcom-remote-management\` skill and try a lightweight GET request first to discover available data. +- ${ PLAN_DATA_GUARDRAIL }`; } function buildLocalIntro( options: { chatArtifactsEnabled: boolean } ): string { @@ -233,6 +234,8 @@ 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 PLAN_DATA_GUARDRAIL = `For any question about WordPress.com (or Pressable) plans, pricing, upgrades, or what a plan tier unlocks (plugins, themes, custom code, SSH, hosting, etc.), load the \`hosting-plans-helper\` skill before answering. Never state plan names, prices, or feature-tier gating from memory — they change and your training knowledge is stale.`; + const LOCAL_SKILL_ROUTING = `## Skill routing For any site creation, redesign, landing page, homepage, layout, style, CSS, typography, color, or motion work, load the \`visual-design\` skill before writing design files or block markup. @@ -241,4 +244,6 @@ For any page/post content, template or template-part content, block markup, bloc For verifying and polishing a built or redesigned site — checking the rendered result against intent and diagnosing layout/width, spacing, button, background, or hover issues — load the \`visual-polish\` skill and use \`inspect_design\` to root-cause from the rendered DOM before fixing. -For forms, shops/stores/ecommerce, events, LMS, galleries/slideshows, embeds, SEO/performance plugin choices, or any feature that core WordPress blocks do not cleanly provide, load the \`plugin-recommendations\` skill before installing plugins or writing plugin-provided block markup.`; +For forms, shops/stores/ecommerce, events, LMS, galleries/slideshows, embeds, SEO/performance plugin choices, or any feature that core WordPress blocks do not cleanly provide, load the \`plugin-recommendations\` skill before installing plugins or writing plugin-provided block markup. + +${ PLAN_DATA_GUARDRAIL }`; diff --git a/apps/cli/ai/tests/system-prompt.test.ts b/apps/cli/ai/tests/system-prompt.test.ts index 1abe61370b..6811f6b92f 100644 --- a/apps/cli/ai/tests/system-prompt.test.ts +++ b/apps/cli/ai/tests/system-prompt.test.ts @@ -75,6 +75,24 @@ describe( 'buildSystemPrompt', () => { expect( prompt ).not.toContain( '## Common wp/v2 Endpoints' ); } ); + it( 'guards plan/pricing/feature answers behind the hosting-plans-helper skill (local)', () => { + const prompt = buildSystemPrompt( { chatArtifactsEnabled: true } ); + + expect( prompt ).toContain( '`hosting-plans-helper` skill' ); + expect( prompt ).toContain( + 'Never state plan names, prices, or feature-tier gating from memory' + ); + } ); + + it( 'guards plan/pricing/feature answers behind the hosting-plans-helper skill (remote)', () => { + const prompt = buildSystemPrompt( { remoteSite } ); + + expect( prompt ).toContain( '`hosting-plans-helper` skill' ); + expect( prompt ).toContain( + 'Never state plan names, prices, or feature-tier gating from memory' + ); + } ); + it( 'references only bundled skills', () => { const prompts = [ buildSystemPrompt( { chatArtifactsEnabled: true } ), diff --git a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md index af4e44c79f..199da53d6f 100644 --- a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md +++ b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md @@ -137,18 +137,22 @@ finalized in the plan against what `plan_defaults()` actually exposes.) fetch with the tools it already has. - The SKILL.md instructs the agent, **before answering any plan/pricing/feature/ upgrade/"what does tier X unlock" question**, to: - 1. Fetch per-tier features from `wpcom/v2/plans/pricing`. - 2. Fetch live prices from `/plans` and merge by `slug`. - 3. Answer only from the fetched data — never state names, prices, or feature-tier + 1. Fetch current plan data (names, prices, per-tier features) from + `wpcom/v2/plans/pricing` — a **single** call (the endpoint includes prices; see + the price-source note below). + 2. Answer only from the fetched data — never state names, prices, or feature-tier gating from memory. - **Fetch recipes for both tool environments** (the skill must work in both): - **Local mode** (local sites): has `Bash`, no `wpcom_request`. Fetch via - `curl "https://public-api.wordpress.com/wpcom/v2/plans/pricing?locale=en"` and - `curl ".../rest/v1.5/plans?locale=en"`. + `curl "https://public-api.wordpress.com/wpcom/v2/plans/pricing?locale=en"`. - **Remote mode** (connected WP.com site): has `wpcom_request`, no `Bash`. Fetch - via `wpcom_request` with absolute paths: `!/plans/pricing` - (`apiNamespace: "wpcom/v2"`) and `!/plans` (`apiNamespace: ""`, v1.1) — or the - equivalent the runtime supports. + via `wpcom_request` `path="!/plans/pricing"`, `apiNamespace="wpcom/v2"`. +- **Why prices live in the endpoint, not a separate `/plans` call:** `wpcom_request` + (the only remote-mode fetch tool) supports `wp/v2` / `wpcom/v2` / v1.1 — not the + v1.5 `/plans` that carries prices; reachable alternatives don't have WP.com bundle + prices (`rest/v1.1/plans` 404s, `wpcom/v2/plans` is the Jetpack family). Folding + `price` into `/plans/pricing` lets one `wpcom/v2` call work in both modes. See the + backend spec. ### System-prompt guardrail diff --git a/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md b/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md index df1ba8cd68..3b5c1a3710 100644 --- a/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md +++ b/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md @@ -15,9 +15,14 @@ API — as a small, public, normalized REST endpoint, so AI assistants (Studio C first; Odie/Wapuu reusable later) can fetch authoritative "what each plan unlocks" data instead of relying on stale model knowledge. -This endpoint deliberately carries **no prices**. Prices remain the responsibility -of the existing `/plans` API (live, geo/currency-localized). Consumers merge the two -by plan `slug`. +This endpoint **includes a `price` per plan**. Although `/plans` (v1.5) is the usual +price source, it is unreachable from Studio Code's remote mode: the only fetch tool +there (`wpcom_request`) supports the `wp/v2`, `wpcom/v2`, and v1.1 namespaces — not +v1.5 — and the reachable alternatives don't carry WordPress.com bundle prices +(`rest/v1.1/plans` 404s; `wpcom/v2/plans` returns the Jetpack plan family, not the +hosting bundles). Since `plan_defaults()` already loads the store product (which +holds the price), exposing it here lets a single `wpcom/v2` call serve names, +features, **and** prices in both local and remote modes. ## Why this source @@ -77,7 +82,8 @@ All in the Landpack plugin under: - **Auth:** **public** (no auth) — Studio Code reaches it unauthenticated in both local (curl) and remote (`wpcom_request`) modes. Matches `/plans/mobile`. - **Args:** `locale` (default `en`), handled with `wpcom_switch_to_locale(...)` like - `get_plans_mobile()`. + `get_plans_mobile()`; `currency` (optional) for the price field, defaulting to the + request's geo/store default as `/plans` does. - **Callback:** `get_plans_pricing( $request )`. ## Algorithm @@ -102,7 +108,9 @@ All in the Landpack plugin under: - Emit `{ key, title, tooltip, group }` per resolved feature, preserving the catalog's group order. - Carry scalar fields: `name` (= `title`), `slug`, `tagline` (= `subtitle`), - `storage`, `ai_assistant_limit`, `standard_commission`, `woo_commission`. + `price` (formatted, currency-aware, from the store product — the same source + `/plans` uses; `null`/omitted for Free), `currency`, `storage`, + `ai_assistant_limit`, `standard_commission`, `woo_commission`. 5. Return the DTO below. ## Response shape (normalized DTO) @@ -116,6 +124,8 @@ All in the Landpack plugin under: "slug": "business", "name": "Business", "tagline": "Grow your business with powerful tools and priority support.", + "price": "$300", + "currency": "USD", "storage": "50 GB", "ai_assistant_limit": "Enhanced", "standard_commission": "2%", @@ -130,7 +140,7 @@ All in the Landpack plugin under: ``` - Plans ordered: Free, Personal, Premium, Business, Commerce. -- No presentation fields (badges, CTA buttons, icons, `features_v4/v5`). No prices. +- No presentation fields (badges, CTA buttons, icons, `features_v4/v5`). ## Loadability (verify during implementation) @@ -160,11 +170,11 @@ Contract test asserting: - A spot-check that a known Business-only developer feature (e.g. `dev-tools-ssh` / staging) is present on `business`/`ecommerce` and absent on `free`/`personal`. - `locale` param is honored (a non-`en` locale changes localized titles). -- No `price`/`cost` fields anywhere in the response. +- Each paid plan has a non-empty `price` and `currency`; `currency` param is honored. ## Scope / out of scope - **In:** the 5 consumer WordPress.com plans; grouped per-tier features; the scalar - fields listed above; `locale`. -- **Out:** prices (use `/plans`); VIP/Blogger/DIFM; Pressable; Woo Hosted; any - write/POST; auth/site-context personalization. + fields listed above including `price`; `locale` and `currency`. +- **Out:** VIP/Blogger/DIFM; Pressable; Woo Hosted; any write/POST; auth/site-context + personalization. From faf9debdbc0128b4fd43810e4c0ef347bd1f64cf Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Jun 2026 18:22:57 +0530 Subject: [PATCH 4/8] Source plan prices from wpcom/v2/products in hosting-plans-helper skill (STU-1940) --- .../ai/skills/hosting-plans-helper/SKILL.md | 51 ++++++++++++------- ...06-30-hosting-plans-helper-skill-design.md | 27 +++++----- ...026-06-30-plans-pricing-endpoint-design.md | 45 ++++++++-------- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/apps/cli/ai/skills/hosting-plans-helper/SKILL.md b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md index 8075b22207..8b87d90581 100644 --- a/apps/cli/ai/skills/hosting-plans-helper/SKILL.md +++ b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md @@ -19,14 +19,12 @@ name, a price, or which tier unlocks a feature from memory — even if you are confident. If the fetch fails, say you can't verify current plan data right now and point the user to https://wordpress.com/pricing; do not guess. -## Step 1: Fetch current plan data +## Step 1: Fetch plan names and features -There is one authoritative source: the `wpcom/v2/plans/pricing` endpoint. It returns, -per plan, the current name, price, and the full list of features that tier unlocks -(grouped — Essential features, Performance boosters, High Availability, Developer -tools, Security, etc.). Fetch it once. - -Use whichever tool is available in your current environment: +Fetch `wpcom/v2/plans/pricing`. It returns, per plan, the current `name`, the +`product_slug` (used to look up the price in Step 2), and the full list of `features` +that tier unlocks — grouped (Essential features, Performance boosters, High +Availability, Developer tools, Security, etc.). **Local sites (Bash tool available):** @@ -40,19 +38,38 @@ curl -s "https://public-api.wordpress.com/wpcom/v2/plans/pricing?locale=en" wpcom_request method=GET path="!/plans/pricing" apiNamespace="wpcom/v2" ``` -Both reach the same public endpoint (no authentication required). Pass `?locale=` -(or the matching query param) to match the user's locale when known. +## Step 2: Fetch current prices + +Fetch `wpcom/v2/products` for prices. It is keyed by product slug; each product has a +`cost_display` (the formatted, **already-localized** price, e.g. `"$300"`) and a +`currency_code`. + +**Local sites:** + +```text +curl -s "https://public-api.wordpress.com/wpcom/v2/products" +``` + +**Connected WordPress.com sites:** + +```text +wpcom_request method=GET path="!/products" apiNamespace="wpcom/v2" +``` + +Both endpoints are public (no authentication) and both are reachable in either +environment. -## Step 2: Answer from the fetched data only +## Step 3: Answer from the fetched data only -- Use the exact plan **names** from the response — do not rename or substitute - legacy names. -- Quote **prices** from the response (they are localized to the response currency; - mention the currency). +- Use the exact plan **names** from Step 1 — do not rename or substitute legacy + names. +- For a plan's **price**, look up `products[ plan.product_slug ].cost_display` and + quote it as-is (it is already localized; mention the currency). The free plan has + no price. - To answer "does plan X include feature Y?" or "what do I need for Y?", check the - per-plan `features` list. Each feature has a `title`, a `tooltip` (use it to - explain), and a `group`. Recommend the lowest tier whose `features` includes what - the user needs. + per-plan `features` list from Step 1. Each feature has a `title`, a `tooltip` (use + it to explain), and a `group`. Recommend the lowest tier whose `features` includes + what the user needs. - When recommending an upgrade, name the specific tier and the concrete features it unlocks for the user's stated goal. diff --git a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md index 199da53d6f..2a0994530e 100644 --- a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md +++ b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md @@ -137,22 +137,23 @@ finalized in the plan against what `plan_defaults()` actually exposes.) fetch with the tools it already has. - The SKILL.md instructs the agent, **before answering any plan/pricing/feature/ upgrade/"what does tier X unlock" question**, to: - 1. Fetch current plan data (names, prices, per-tier features) from - `wpcom/v2/plans/pricing` — a **single** call (the endpoint includes prices; see - the price-source note below). - 2. Answer only from the fetched data — never state names, prices, or feature-tier + 1. Fetch names + features + `product_slug` per plan from `wpcom/v2/plans/pricing`. + 2. Fetch prices from `wpcom/v2/products` (keyed by product slug; `cost_display` is + already localized) and join by `product_slug`. + 3. Answer only from the fetched data — never state names, prices, or feature-tier gating from memory. -- **Fetch recipes for both tool environments** (the skill must work in both): +- **Both endpoints are `wpcom/v2` and public**, so the same two calls work in both + tool environments: - **Local mode** (local sites): has `Bash`, no `wpcom_request`. Fetch via - `curl "https://public-api.wordpress.com/wpcom/v2/plans/pricing?locale=en"`. + `curl ".../wpcom/v2/plans/pricing?locale=en"` and `curl ".../wpcom/v2/products"`. - **Remote mode** (connected WP.com site): has `wpcom_request`, no `Bash`. Fetch - via `wpcom_request` `path="!/plans/pricing"`, `apiNamespace="wpcom/v2"`. -- **Why prices live in the endpoint, not a separate `/plans` call:** `wpcom_request` - (the only remote-mode fetch tool) supports `wp/v2` / `wpcom/v2` / v1.1 — not the - v1.5 `/plans` that carries prices; reachable alternatives don't have WP.com bundle - prices (`rest/v1.1/plans` 404s, `wpcom/v2/plans` is the Jetpack family). Folding - `price` into `/plans/pricing` lets one `wpcom/v2` call work in both modes. See the - backend spec. + via `wpcom_request` `path="!/plans/pricing"` and `path="!/products"`, each with + `apiNamespace="wpcom/v2"`. +- **Why `wpcom/v2/products` for prices** (not `/plans` v1.5): `wpcom_request` (the + only remote-mode fetch tool) supports `wp/v2` / `wpcom/v2` / v1.1 — not v1.5; + reachable alternatives lack WP.com bundle prices (`rest/v1.1/plans` 404s, + `wpcom/v2/plans` is the Jetpack family). `wpcom/v2/products` is reachable in both + modes and already localized. ### System-prompt guardrail diff --git a/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md b/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md index 3b5c1a3710..a3eab99c67 100644 --- a/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md +++ b/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md @@ -15,14 +15,16 @@ API — as a small, public, normalized REST endpoint, so AI assistants (Studio C first; Odie/Wapuu reusable later) can fetch authoritative "what each plan unlocks" data instead of relying on stale model knowledge. -This endpoint **includes a `price` per plan**. Although `/plans` (v1.5) is the usual -price source, it is unreachable from Studio Code's remote mode: the only fetch tool -there (`wpcom_request`) supports the `wp/v2`, `wpcom/v2`, and v1.1 namespaces — not -v1.5 — and the reachable alternatives don't carry WordPress.com bundle prices -(`rest/v1.1/plans` 404s; `wpcom/v2/plans` returns the Jetpack plan family, not the -hosting bundles). Since `plan_defaults()` already loads the store product (which -holds the price), exposing it here lets a single `wpcom/v2` call serve names, -features, **and** prices in both local and remote modes. +This endpoint carries **no prices**. It emits each plan's **`product_slug`**, and the +consumer fetches the existing public **`wpcom/v2/products`** endpoint (keyed by +product slug, with a `cost_display` that is already localized) and joins by slug. + +Why not the `/plans` v1.5 API for prices: it is unreachable from Studio Code's remote +mode — the only fetch tool there (`wpcom_request`) supports the `wp/v2`, `wpcom/v2`, +and v1.1 namespaces, not v1.5, and the reachable alternatives don't carry WP.com +bundle prices (`rest/v1.1/plans` 404s; `wpcom/v2/plans` is the Jetpack plan family). +`wpcom/v2/products` is reachable in both modes (curl locally, `wpcom_request` +`!/products` remotely) and is already localized, so prices need no work here. ## Why this source @@ -82,8 +84,7 @@ All in the Landpack plugin under: - **Auth:** **public** (no auth) — Studio Code reaches it unauthenticated in both local (curl) and remote (`wpcom_request`) modes. Matches `/plans/mobile`. - **Args:** `locale` (default `en`), handled with `wpcom_switch_to_locale(...)` like - `get_plans_mobile()`; `currency` (optional) for the price field, defaulting to the - request's geo/store default as `/plans` does. + `get_plans_mobile()`. - **Callback:** `get_plans_pricing( $request )`. ## Algorithm @@ -107,10 +108,12 @@ All in the Landpack plugin under: `process_features_v4`). De-duplicate keys (a few plans list a key twice). - Emit `{ key, title, tooltip, group }` per resolved feature, preserving the catalog's group order. - - Carry scalar fields: `name` (= `title`), `slug`, `tagline` (= `subtitle`), - `price` (formatted, currency-aware, from the store product — the same source - `/plans` uses; `null`/omitted for Free), `currency`, `storage`, - `ai_assistant_limit`, `standard_commission`, `woo_commission`. + - Carry scalar fields: `name` (= `title`), `slug`, `product_slug` (the WP.com + bundle slug from the store product, used by the consumer to join prices from + `wpcom/v2/products` — e.g. `personal-bundle`, `value_bundle` for Premium, + `business-bundle`, `ecommerce-bundle` for Commerce, `free_plan` for Free), + `tagline` (= `subtitle`), `storage`, `ai_assistant_limit`, + `standard_commission`, `woo_commission`. 5. Return the DTO below. ## Response shape (normalized DTO) @@ -123,9 +126,8 @@ All in the Landpack plugin under: { "slug": "business", "name": "Business", + "product_slug": "business-bundle", "tagline": "Grow your business with powerful tools and priority support.", - "price": "$300", - "currency": "USD", "storage": "50 GB", "ai_assistant_limit": "Enhanced", "standard_commission": "2%", @@ -140,7 +142,8 @@ All in the Landpack plugin under: ``` - Plans ordered: Free, Personal, Premium, Business, Commerce. -- No presentation fields (badges, CTA buttons, icons, `features_v4/v5`). +- No presentation fields (badges, CTA buttons, icons, `features_v4/v5`). No prices — + the consumer joins `wpcom/v2/products` on `product_slug`. ## Loadability (verify during implementation) @@ -170,11 +173,13 @@ Contract test asserting: - A spot-check that a known Business-only developer feature (e.g. `dev-tools-ssh` / staging) is present on `business`/`ecommerce` and absent on `free`/`personal`. - `locale` param is honored (a non-`en` locale changes localized titles). -- Each paid plan has a non-empty `price` and `currency`; `currency` param is honored. +- Each plan has a `product_slug` that resolves to a product in `wpcom/v2/products` + (so the consumer's price join succeeds). ## Scope / out of scope - **In:** the 5 consumer WordPress.com plans; grouped per-tier features; the scalar - fields listed above including `price`; `locale` and `currency`. -- **Out:** VIP/Blogger/DIFM; Pressable; Woo Hosted; any write/POST; auth/site-context + fields listed above including `product_slug`; `locale`. +- **Out:** prices (the consumer joins `wpcom/v2/products` by `product_slug`); + VIP/Blogger/DIFM; Pressable; Woo Hosted; any write/POST; auth/site-context personalization. From 7a9ee6ad6e7d3e3a2956ba7841d897da8f6da542 Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Jun 2026 18:26:37 +0530 Subject: [PATCH 5/8] Tell hosting-plans-helper to answer directly without citing fetched data (STU-1940) --- apps/cli/ai/skills/hosting-plans-helper/SKILL.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/cli/ai/skills/hosting-plans-helper/SKILL.md b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md index 8b87d90581..d4eeef426e 100644 --- a/apps/cli/ai/skills/hosting-plans-helper/SKILL.md +++ b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md @@ -73,6 +73,16 @@ environment. - When recommending an upgrade, name the specific tier and the concrete features it unlocks for the user's stated goal. +### Phrasing + +Answer directly and authoritatively, as plain product knowledge. Do not mention that +you fetched anything, or reference "the live data", "the data I fetched", "according +to the API", or any source or tool. The fetched data is simply the truth — state it. + +- Yes: "Plugins are supported on the Personal plan and above." +- No: "Based on the live data I just fetched, plugins are supported on Personal and + above." + ## Scope Currently covers the WordPress.com consumer plans (Free, Personal, Premium, From a84bb744eb27a7b675d9b0e5dd129ffe27cf09e9 Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Jun 2026 18:49:09 +0530 Subject: [PATCH 6/8] Rename plans/pricing endpoint to plans/features (STU-1940) --- .../ai/skills/hosting-plans-helper/SKILL.md | 6 ++--- ...06-30-hosting-plans-helper-skill-design.md | 23 ++++++++++--------- ...6-06-30-plans-features-endpoint-design.md} | 6 ++--- 3 files changed, 18 insertions(+), 17 deletions(-) rename docs/superpowers/specs/{2026-06-30-plans-pricing-endpoint-design.md => 2026-06-30-plans-features-endpoint-design.md} (98%) diff --git a/apps/cli/ai/skills/hosting-plans-helper/SKILL.md b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md index d4eeef426e..361dff0a4b 100644 --- a/apps/cli/ai/skills/hosting-plans-helper/SKILL.md +++ b/apps/cli/ai/skills/hosting-plans-helper/SKILL.md @@ -21,7 +21,7 @@ point the user to https://wordpress.com/pricing; do not guess. ## Step 1: Fetch plan names and features -Fetch `wpcom/v2/plans/pricing`. It returns, per plan, the current `name`, the +Fetch `wpcom/v2/plans/features`. It returns, per plan, the current `name`, the `product_slug` (used to look up the price in Step 2), and the full list of `features` that tier unlocks — grouped (Essential features, Performance boosters, High Availability, Developer tools, Security, etc.). @@ -29,13 +29,13 @@ Availability, Developer tools, Security, etc.). **Local sites (Bash tool available):** ```text -curl -s "https://public-api.wordpress.com/wpcom/v2/plans/pricing?locale=en" +curl -s "https://public-api.wordpress.com/wpcom/v2/plans/features?locale=en" ``` **Connected WordPress.com sites (wpcom_request tool available, no Bash):** ```text -wpcom_request method=GET path="!/plans/pricing" apiNamespace="wpcom/v2" +wpcom_request method=GET path="!/plans/features" apiNamespace="wpcom/v2" ``` ## Step 2: Fetch current prices diff --git a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md index 2a0994530e..96a521cda2 100644 --- a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md +++ b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md @@ -57,20 +57,20 @@ Two coordinated pieces, in two repositories. [Landpack PHP] get_plans() + get_feature_labels() │ (server-side, same source the /pricing page renders) ▼ -[wpcom] GET wpcom/v2/plans/pricing ← Piece 1 (new, public, normalized DTO) +[wpcom] GET wpcom/v2/plans/features ← Piece 1 (new, public, normalized DTO) │ ▼ (agent fetches: curl in local mode, wpcom_request in remote mode) [Studio] hosting-plans-helper skill ← Piece 2 (SKILL.md + system-prompt guardrail) │ - ├─ /plans/pricing → names + per-tier features - └─ /plans → live, geo-correct prices (merged by slug) + ├─ /plans/features → names + per-tier features + product_slug + └─ /products → localized prices (merged by product_slug) ▼ Agent answers from current data, never from memory ``` -## Piece 1 — wpcom endpoint: `GET wpcom/v2/plans/pricing` +## Piece 1 — wpcom endpoint: `GET wpcom/v2/plans/features` -> **Authoritative spec:** `2026-06-30-plans-pricing-endpoint-design.md` (standalone, +> **Authoritative spec:** `2026-06-30-plans-features-endpoint-design.md` (standalone, > built in the `Automattic/wpcom` repo). The summary below is for context; defer to > that spec for the endpoint contract. @@ -83,8 +83,8 @@ Two coordinated pieces, in two repositories. - Resolves feature keys → `{ key, title, tooltip, group }` server-side and returns a **normalized DTO**, dropping presentation-only fields (badges, CTA buttons, icons, `features_v4/v5` card selections). -- **No prices** in this endpoint — kept single-purpose; the skill fetches `/plans` - for prices. +- **No prices** in this endpoint. It emits each plan's `product_slug`; the skill + joins prices from `wpcom/v2/products` (already localized) by that slug. - **Scope:** the 5 consumer plans only — Free, Personal, Premium, Business, Commerce. (Landpack `get_plans()` also returns VIP; excluded from v1.) @@ -98,6 +98,7 @@ Two coordinated pieces, in two repositories. { "slug": "business", "name": "Business", + "product_slug": "business-bundle", "storage": "50 GB", "ai_assistant_limit": "Enhanced", "features": [ @@ -137,7 +138,7 @@ finalized in the plan against what `plan_defaults()` actually exposes.) fetch with the tools it already has. - The SKILL.md instructs the agent, **before answering any plan/pricing/feature/ upgrade/"what does tier X unlock" question**, to: - 1. Fetch names + features + `product_slug` per plan from `wpcom/v2/plans/pricing`. + 1. Fetch names + features + `product_slug` per plan from `wpcom/v2/plans/features`. 2. Fetch prices from `wpcom/v2/products` (keyed by product slug; `cost_display` is already localized) and join by `product_slug`. 3. Answer only from the fetched data — never state names, prices, or feature-tier @@ -145,9 +146,9 @@ finalized in the plan against what `plan_defaults()` actually exposes.) - **Both endpoints are `wpcom/v2` and public**, so the same two calls work in both tool environments: - **Local mode** (local sites): has `Bash`, no `wpcom_request`. Fetch via - `curl ".../wpcom/v2/plans/pricing?locale=en"` and `curl ".../wpcom/v2/products"`. + `curl ".../wpcom/v2/plans/features?locale=en"` and `curl ".../wpcom/v2/products"`. - **Remote mode** (connected WP.com site): has `wpcom_request`, no `Bash`. Fetch - via `wpcom_request` `path="!/plans/pricing"` and `path="!/products"`, each with + via `wpcom_request` `path="!/plans/features"` and `path="!/products"`, each with `apiNamespace="wpcom/v2"`. - **Why `wpcom/v2/products` for prices** (not `/plans` v1.5): `wpcom_request` (the only remote-mode fetch tool) supports `wp/v2` / `wpcom/v2` / v1.1 — not v1.5; @@ -167,7 +168,7 @@ state plan names, prices, or feature-tier gating from memory, and to load the ## Sequencing & dependency Because v1 is **live-endpoint-only** (no bundled fallback), the Studio skill is -**non-functional until `wpcom/v2/plans/pricing` is deployed**. Implementation order: +**non-functional until `wpcom/v2/plans/features` is deployed**. Implementation order: 1. Build + test + deploy the wpcom endpoint (Piece 1). 2. Build the Studio skill + guardrail (Piece 2) against the live endpoint. diff --git a/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md b/docs/superpowers/specs/2026-06-30-plans-features-endpoint-design.md similarity index 98% rename from docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md rename to docs/superpowers/specs/2026-06-30-plans-features-endpoint-design.md index a3eab99c67..bfaed353e3 100644 --- a/docs/superpowers/specs/2026-06-30-plans-pricing-endpoint-design.md +++ b/docs/superpowers/specs/2026-06-30-plans-features-endpoint-design.md @@ -1,4 +1,4 @@ -# Design: `GET wpcom/v2/plans/pricing` endpoint +# Design: `GET wpcom/v2/plans/features` endpoint - **Repo:** `Automattic/wpcom` (NOT the Studio repo — this is the backend piece) - **Consumer:** Studio Code `hosting-plans-helper` skill @@ -79,13 +79,13 @@ All in the Landpack plugin under: - **File:** `wp-content/rest-api-plugins/endpoints/plans.php` — add to the existing `WPCOM_REST_API_V2_Endpoint_Plans` controller (same file/class as `/plans/mobile`). -- **Route:** register `wpcom/v2/plans/pricing` in `register_routes()`. +- **Route:** register `wpcom/v2/plans/features` in `register_routes()`. - **Method:** `GET` (`WP_REST_Server::READABLE`). - **Auth:** **public** (no auth) — Studio Code reaches it unauthenticated in both local (curl) and remote (`wpcom_request`) modes. Matches `/plans/mobile`. - **Args:** `locale` (default `en`), handled with `wpcom_switch_to_locale(...)` like `get_plans_mobile()`. -- **Callback:** `get_plans_pricing( $request )`. +- **Callback:** `get_plans_features( $request )`. ## Algorithm From 85fe8a4f3da42aab169178edb03afe91f2626fae Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Jun 2026 18:51:56 +0530 Subject: [PATCH 7/8] Remove design specs from branch (STU-1940) --- ...06-30-hosting-plans-helper-skill-design.md | 196 ------------------ ...26-06-30-plans-features-endpoint-design.md | 185 ----------------- 2 files changed, 381 deletions(-) delete mode 100644 docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md delete mode 100644 docs/superpowers/specs/2026-06-30-plans-features-endpoint-design.md diff --git a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md b/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md deleted file mode 100644 index 96a521cda2..0000000000 --- a/docs/superpowers/specs/2026-06-30-hosting-plans-helper-skill-design.md +++ /dev/null @@ -1,196 +0,0 @@ -# Design: `hosting-plans-helper` skill - -- **Issue:** [STU-1940](https://linear.app/a8c/issue/STU-1940) (bug: [STU-1939](https://linear.app/a8c/issue/STU-1939)) -- **Date:** 2026-06-30 -- **Status:** Approved design, pending implementation plan - -## Problem - -Studio Code answers WordPress.com plan, pricing, and feature-gating questions from -stale model-training knowledge. There is no authoritative source wired into the -agent, so it recommends renamed/legacy plans and makes incorrect feature-tier -claims (e.g. wrong plugin-tier gating). STU-1940 asks for a durable fix: a skill -that gives the agent current, authoritative plan/pricing/feature data instead of -relying on memory. - -## Investigation findings (what shaped this design) - -The issue proposed "fetch live from the `/plans` v1.5 API (`features_comparison`)." -Direct testing showed this premise does not hold for what Studio Code can reach: - -- **`/plans` v1.5** (public and authenticated, v1.3/v1.5/v2, with/without the - `features_comparison` param, and site-context): returns plan **names + prices** - only. It carries **no general `features_comparison`**. Per - [SHILL-1742](https://linear.app/a8c/issue/SHILL-1742), `features_comparison` was - added **only for Woo Hosted plans in a Woo-Hosted site context** — not a general - WordPress.com plans source. -- **Plan names** returned are the legacy lineup (Free/Personal/Premium/Business/ - Commerce). Flex/Pro/Premier are **not live** — not even on the production - `/pricing` page yet — so legacy names are currently correct. -- **Prices** are live and geo/currency-localized via `/plans` `formatted_price`. - -The real source of truth for "what each tier unlocks" is the **Landpack** plugin, -which renders the production `/pricing` page from curated PHP: - -- `…/2023-pricing-grid/utilities/features.php` — `get_feature_labels()`: the - grouped catalog mapping each feature key → `title`/`subtitle`/`tooltip` - (Essential, Performance boosters, High Availability, Developer tools, Security, - Grow, …). -- `…/utilities/plan-*.php` + `plan.php` `get_plans()` — each plan's resolved - `name` (from the `/plans` store product), `slug`, feature-key lists, `storage`, - `ai_assistant_limit`, commission %, etc. - -The existing public `wpcom/v2/plans/mobile` endpoint exposes a similar but coarser, -partly-stale shadow of this (37 flat features, missing dev/host granularity like -SSH/staging/git/CDN; e.g. it reports Business = 200GB while Landpack says 50 GB). -It is not a good long-term foundation. - -**Conclusion:** names + prices come from `/plans` (live); the rich per-tier feature -data exists only in Landpack (server-side). To make it queryable, we expose Landpack -through a new, purpose-built REST endpoint. - -## Design overview - -Two coordinated pieces, in two repositories. - -``` -[Landpack PHP] get_plans() + get_feature_labels() - │ (server-side, same source the /pricing page renders) - ▼ -[wpcom] GET wpcom/v2/plans/features ← Piece 1 (new, public, normalized DTO) - │ - ▼ (agent fetches: curl in local mode, wpcom_request in remote mode) -[Studio] hosting-plans-helper skill ← Piece 2 (SKILL.md + system-prompt guardrail) - │ - ├─ /plans/features → names + per-tier features + product_slug - └─ /products → localized prices (merged by product_slug) - ▼ - Agent answers from current data, never from memory -``` - -## Piece 1 — wpcom endpoint: `GET wpcom/v2/plans/features` - -> **Authoritative spec:** `2026-06-30-plans-features-endpoint-design.md` (standalone, -> built in the `Automattic/wpcom` repo). The summary below is for context; defer to -> that spec for the endpoint contract. - -- Added to the existing `WPCOM_REST_API_V2_Endpoint_Plans` controller - (`wp-content/rest-api-plugins/endpoints/plans.php`), alongside `/plans/mobile`. -- **Public**, no authentication (so both Studio modes can reach it). Accepts - `?locale=` (default `en`), mirroring `/plans/mobile`. -- Reads `Landpack\get_plans()` and `Landpack\get_feature_labels()` — the same - source the `/pricing` page renders — so it **cannot drift** from what users see. -- Resolves feature keys → `{ key, title, tooltip, group }` server-side and returns - a **normalized DTO**, dropping presentation-only fields (badges, CTA buttons, - icons, `features_v4/v5` card selections). -- **No prices** in this endpoint. It emits each plan's `product_slug`; the skill - joins prices from `wpcom/v2/products` (already localized) by that slug. -- **Scope:** the 5 consumer plans only — Free, Personal, Premium, Business, - Commerce. (Landpack `get_plans()` also returns VIP; excluded from v1.) - -### Response shape - -```json -{ - "source": "landpack", - "locale": "en", - "plans": [ - { - "slug": "business", - "name": "Business", - "product_slug": "business-bundle", - "storage": "50 GB", - "ai_assistant_limit": "Enhanced", - "features": [ - { - "key": "dev-tools-ssh", - "title": "SSH access", - "tooltip": "Securely access your site over SSH.", - "group": "Developer tools" - } - ] - } - ] -} -``` - -(Exact per-plan scalar fields — `storage`, `ai_assistant_limit`, commission — to be -finalized in the plan against what `plan_defaults()` actually exposes.) - -### Risks / to verify during implementation - -- **Loadability:** confirm Landpack pricing-section utils are `require`-able in the - REST request context (they are normally loaded for block render). The endpoint - must bootstrap them before calling `get_plans()`/`get_feature_labels()`. -- **Coupling:** the endpoint depends on Landpack internal functions; a Landpack - refactor of `get_plans()` would break it. Mitigate with a **contract test** on the - endpoint response (asserts the 5 plans + non-empty grouped features + the DTO - shape). -- **Ownership:** lives in `Automattic/wpcom` — needs a wpcom owner, review, and - deploy. This is a separate repo from Studio. - -## Piece 2 — Studio skill: `hosting-plans-helper` - -- New skill at `apps/cli/ai/skills/hosting-plans-helper/SKILL.md`, discovered by the - existing `loadSkills()` mechanism. Frontmatter: `name`, `description`, - `user-invokable: true`. -- **Skill-only** (no dedicated tool, no bundled snapshot). The agent performs the - fetch with the tools it already has. -- The SKILL.md instructs the agent, **before answering any plan/pricing/feature/ - upgrade/"what does tier X unlock" question**, to: - 1. Fetch names + features + `product_slug` per plan from `wpcom/v2/plans/features`. - 2. Fetch prices from `wpcom/v2/products` (keyed by product slug; `cost_display` is - already localized) and join by `product_slug`. - 3. Answer only from the fetched data — never state names, prices, or feature-tier - gating from memory. -- **Both endpoints are `wpcom/v2` and public**, so the same two calls work in both - tool environments: - - **Local mode** (local sites): has `Bash`, no `wpcom_request`. Fetch via - `curl ".../wpcom/v2/plans/features?locale=en"` and `curl ".../wpcom/v2/products"`. - - **Remote mode** (connected WP.com site): has `wpcom_request`, no `Bash`. Fetch - via `wpcom_request` `path="!/plans/features"` and `path="!/products"`, each with - `apiNamespace="wpcom/v2"`. -- **Why `wpcom/v2/products` for prices** (not `/plans` v1.5): `wpcom_request` (the - only remote-mode fetch tool) supports `wp/v2` / `wpcom/v2` / v1.1 — not v1.5; - reachable alternatives lack WP.com bundle prices (`rest/v1.1/plans` 404s, - `wpcom/v2/plans` is the Jetpack family). `wpcom/v2/products` is reachable in both - modes and already localized. - -### System-prompt guardrail - -In `apps/cli/ai/system-prompt.ts`, add a guardrail directing the agent **not** to -state plan names, prices, or feature-tier gating from memory, and to load the -`hosting-plans-helper` skill (or fetch current data) first. Add it to: - -- `LOCAL_SKILL_ROUTING` (local intro), and -- the remote intro (`buildRemoteIntro`), which already gates design features by plan. - -## Sequencing & dependency - -Because v1 is **live-endpoint-only** (no bundled fallback), the Studio skill is -**non-functional until `wpcom/v2/plans/features` is deployed**. Implementation order: - -1. Build + test + deploy the wpcom endpoint (Piece 1). -2. Build the Studio skill + guardrail (Piece 2) against the live endpoint. - -The skill should fail gracefully if the endpoint is unreachable (tell the user it -can't verify current plan data rather than answering from memory). - -## Testing - -- **Endpoint:** contract test asserting the 5 consumer plans, non-empty grouped - features with resolved titles/tooltips, and the documented DTO shape. -- **Skill:** unit coverage that the skill is discovered/loaded (`loadSkills`) and is - `user-invokable`; the existing `system-prompt.test.ts` extended to assert the - guardrail text is present in both local and remote prompts. -- **Manual:** run a plan/feature question through Studio Code (local and remote) and - confirm answers come from the endpoint, with correct current names and live prices. - -## Out of scope (v1) - -- Pressable, WooCommerce, and VIP plans (same mechanism can extend later). -- A bundled fallback snapshot of plan data. -- A dedicated `get_hosting_plans` Studio tool (caching / uniform shape) — could be a - later robustness upgrade if skill-only fetching proves flaky. -- Sharing the mechanism with Odie/Wapuu (the endpoint is reusable by them, but that - integration is not part of this work). diff --git a/docs/superpowers/specs/2026-06-30-plans-features-endpoint-design.md b/docs/superpowers/specs/2026-06-30-plans-features-endpoint-design.md deleted file mode 100644 index bfaed353e3..0000000000 --- a/docs/superpowers/specs/2026-06-30-plans-features-endpoint-design.md +++ /dev/null @@ -1,185 +0,0 @@ -# Design: `GET wpcom/v2/plans/features` endpoint - -- **Repo:** `Automattic/wpcom` (NOT the Studio repo — this is the backend piece) -- **Consumer:** Studio Code `hosting-plans-helper` skill - ([STU-1940](https://linear.app/a8c/issue/STU-1940)); see the companion Studio spec - `2026-06-30-hosting-plans-helper-skill-design.md`. -- **Date:** 2026-06-30 -- **Status:** Spec for implementation (standalone — an agent can build from this alone) - -## Goal - -Expose the WordPress.com `/pricing` plan + per-tier feature data — currently -rendered server-side by the Landpack plugin and not available through any general -API — as a small, public, normalized REST endpoint, so AI assistants (Studio Code -first; Odie/Wapuu reusable later) can fetch authoritative "what each plan unlocks" -data instead of relying on stale model knowledge. - -This endpoint carries **no prices**. It emits each plan's **`product_slug`**, and the -consumer fetches the existing public **`wpcom/v2/products`** endpoint (keyed by -product slug, with a `cost_display` that is already localized) and joins by slug. - -Why not the `/plans` v1.5 API for prices: it is unreachable from Studio Code's remote -mode — the only fetch tool there (`wpcom_request`) supports the `wp/v2`, `wpcom/v2`, -and v1.1 namespaces, not v1.5, and the reachable alternatives don't carry WP.com -bundle prices (`rest/v1.1/plans` 404s; `wpcom/v2/plans` is the Jetpack plan family). -`wpcom/v2/products` is reachable in both modes (curl locally, `wpcom_request` -`!/products` remotely) and is already localized, so prices need no work here. - -## Why this source - -The existing `/plans` (v1.x) API returns plan **names + prices** but **no general -per-tier feature data** (`features_comparison` exists only for Woo Hosted plans in a -Woo-Hosted site context, per [SHILL-1742](https://linear.app/a8c/issue/SHILL-1742)). -The existing public `wpcom/v2/plans/mobile` endpoint has a coarse, partly-stale -feature list (e.g. it reports Business = 200GB; Landpack says 50 GB; it lacks -SSH/staging/git/CDN granularity). - -The authoritative source — the one the production `/pricing` page renders from — is -the Landpack plugin. Reading it directly means the endpoint **cannot drift** from -what users see on `/pricing`, with no second copy to maintain. - -## Source functions (Landpack) - -All in the Landpack plugin under: -`wp-content/plugins/landpack/src/blocks/pricing-section/themes/2023-pricing-grid/utilities/` - -- `Landpack\get_plans()` (`plan.php`) — returns a keyed array of plan configs: - `free`, `personal`, `premium`, `business`, `ecommerce`, `vip`. Each value is the - output of `Plan_*::plan_defaults()`. -- `Landpack\get_feature_labels()` (`features.php`) — the **grouped** catalog: an - array of 8 groups, each: - ```php - [ 'title' => 'Developer tools', 'features' => [ - 'dev-tools-ssh' => [ 'title' => 'SSH access', 'tooltip' => '…', /* subtitle?, jetpack?, hide_in_comparison_grid? */ ], - … ] ] - ``` -- `Landpack\get_feature_labels_ungrouped()` (`features.php`) — same data flattened to - `[ '' => [ 'title' => …, 'tooltip' => … ] ]` (loses group association). - -### Relevant fields on each `plan_defaults()` result - -- `slug` — e.g. `business`. -- `title` — display name, resolved live from the `/plans` store product - (`$store_product->product_name_short`). -- `subtitle` — short tagline. -- `features_compare_annual` / `features_compare_month` — **arrays of feature keys** - (strings) — the comprehensive comparison-grid inclusion lists. `annual` = - `month` + a few annual-only extras (free domain, support). **Use `annual` as the - canonical inclusion list.** -- `features_compare_conditional` — extra store/Woo rows (relevant to Commerce). -- `features_v4` / `features_v5` — highlighted card features (a resolved subset; not - the full inclusion list). Not needed for this endpoint. -- `storage` — e.g. `50 GB`. -- `ai_assistant_limit` — e.g. `Enhanced`. -- `standard_commission` / `woo_commission` — e.g. `2%` / `0%`. - -## Endpoint definition - -- **File:** `wp-content/rest-api-plugins/endpoints/plans.php` — add to the existing - `WPCOM_REST_API_V2_Endpoint_Plans` controller (same file/class as - `/plans/mobile`). -- **Route:** register `wpcom/v2/plans/features` in `register_routes()`. -- **Method:** `GET` (`WP_REST_Server::READABLE`). -- **Auth:** **public** (no auth) — Studio Code reaches it unauthenticated in both - local (curl) and remote (`wpcom_request`) modes. Matches `/plans/mobile`. -- **Args:** `locale` (default `en`), handled with `wpcom_switch_to_locale(...)` like - `get_plans_mobile()`. -- **Callback:** `get_plans_features( $request )`. - -## Algorithm - -1. `wpcom_switch_to_locale( $request->get_param( 'locale' ) )`. -2. Ensure Landpack pricing-section utilities are loaded (see "Loadability" below); - then build a key → label map **with group** by iterating the grouped - `Landpack\get_feature_labels()`: - ``` - foreach group: - foreach (key => label) in group['features']: - map[key] = { title: label['title'], tooltip: label['tooltip'] ?? '', group: group['title'] } - ``` -3. `$plans = Landpack\get_plans();` -4. For each of the **5 consumer plans** (`free, personal, premium, business, - ecommerce`) — exclude `vip`: - - Take `features_compare_annual` (fall back to `features_compare_month` if annual - is unset). For Commerce, also append `features_compare_conditional`. - - Resolve each key through `map`; **skip unknown keys** (a key present in a plan - list but absent from the catalog — mirror the `isset` guard in - `process_features_v4`). De-duplicate keys (a few plans list a key twice). - - Emit `{ key, title, tooltip, group }` per resolved feature, preserving the - catalog's group order. - - Carry scalar fields: `name` (= `title`), `slug`, `product_slug` (the WP.com - bundle slug from the store product, used by the consumer to join prices from - `wpcom/v2/products` — e.g. `personal-bundle`, `value_bundle` for Premium, - `business-bundle`, `ecommerce-bundle` for Commerce, `free_plan` for Free), - `tagline` (= `subtitle`), `storage`, `ai_assistant_limit`, - `standard_commission`, `woo_commission`. -5. Return the DTO below. - -## Response shape (normalized DTO) - -```json -{ - "source": "landpack", - "locale": "en", - "plans": [ - { - "slug": "business", - "name": "Business", - "product_slug": "business-bundle", - "tagline": "Grow your business with powerful tools and priority support.", - "storage": "50 GB", - "ai_assistant_limit": "Enhanced", - "standard_commission": "2%", - "woo_commission": "0%", - "features": [ - { "key": "unlimited-pages", "title": "Unlimited pages", "tooltip": "Add as many pages as you like to your site.", "group": "Essential features" }, - { "key": "dev-tools-ssh", "title": "SSH access", "tooltip": "…", "group": "Developer tools" } - ] - } - ] -} -``` - -- Plans ordered: Free, Personal, Premium, Business, Commerce. -- No presentation fields (badges, CTA buttons, icons, `features_v4/v5`). No prices — - the consumer joins `wpcom/v2/products` on `product_slug`. - -## Loadability (verify during implementation) - -The Landpack util functions are normally loaded when the pricing-section block -renders, not on every request. The endpoint must make them available before calling: - -1. Prefer a guard: `if ( ! function_exists( 'Landpack\\get_plans' ) ) { require_once /plan.php; }` — `plan.php` itself `require`s `features.php` and all `plan-*.php`. -2. Confirm `plan.php`'s own relative `require`s (e.g. `../../../../../utilities/get-currency.php`) and its calls into WPCOM (`get_store_product`, `wpcom_switch_to_locale`) resolve in REST request context. -3. If direct loading proves fragile, the fallback is to replicate the small amount - of Landpack logic behind a Landpack-owned accessor — but **prefer reusing the - functions** so the data stays in lockstep with `/pricing`. - -## Error handling - -- If Landpack functions cannot be loaded or `get_plans()` returns empty, return a - `WP_Error` (HTTP 500) with a clear code (e.g. `landpack_unavailable`) rather than a - partial/empty `plans` array — so consumers can detect failure and refuse to answer - from memory rather than present wrong data. - -## Testing - -Contract test asserting: -- HTTP 200, public (works unauthenticated). -- `plans` has exactly the 5 consumer slugs in order; no `vip`. -- Each plan has non-empty `features`, every feature has non-empty `title` and a - `group`, and `key`s are unique within a plan. -- A spot-check that a known Business-only developer feature (e.g. `dev-tools-ssh` / - staging) is present on `business`/`ecommerce` and absent on `free`/`personal`. -- `locale` param is honored (a non-`en` locale changes localized titles). -- Each plan has a `product_slug` that resolves to a product in `wpcom/v2/products` - (so the consumer's price join succeeds). - -## Scope / out of scope - -- **In:** the 5 consumer WordPress.com plans; grouped per-tier features; the scalar - fields listed above including `product_slug`; `locale`. -- **Out:** prices (the consumer joins `wpcom/v2/products` by `product_slug`); - VIP/Blogger/DIFM; Pressable; Woo Hosted; any write/POST; auth/site-context - personalization. From 7bd780ab0680644d918ad111495547c2df76e741 Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Wed, 1 Jul 2026 11:26:06 +0530 Subject: [PATCH 8/8] Strengthen plan-data guardrail so the agent never answers plan questions from memory (STU-1940) --- apps/cli/ai/system-prompt.ts | 11 +++++------ apps/cli/ai/tests/system-prompt.test.ts | 10 ++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/cli/ai/system-prompt.ts b/apps/cli/ai/system-prompt.ts index 767fd4972c..ee8309f0f9 100644 --- a/apps/cli/ai/system-prompt.ts +++ b/apps/cli/ai/system-prompt.ts @@ -47,6 +47,7 @@ function buildRemoteIntro( site: RemoteSiteContext ): string { IMPORTANT: The active site is a remote WordPress.com site: "${ site.name }" (ID: ${ site.id }) at ${ site.url }. IMPORTANT: You MUST use the wpcom_request tool to manage this site. Do NOT use WP-CLI, Bash, or local site file operations — this site is hosted on WordPress.com and cannot be modified through the local filesystem. You may use local Read/Write/Edit/Ls for temporary working files within Studio app data; those files do not affect the remote site until passed to wpcom_request. IMPORTANT: Before doing ANY work, you MUST first check the site's plan by calling \`GET /\` (apiNamespace: \`""\`). The \`plan.product_slug\` field indicates the plan. If the site is on a free plan (e.g. \`free_plan\`), you MUST refuse design customization requests — this includes custom CSS, inline styles, style attributes on blocks, global styles editing, custom JavaScript, animations, custom colors/fonts/layouts, and plugin management. Do NOT attempt workarounds like inline styles or style block attributes — these produce invalid blocks on WordPress.com. Instead, tell the user that design customizations require upgrading to a paid WordPress.com plan and STOP. Do not proceed with the design task. +IMPORTANT: ${ PLAN_DATA_GUARDRAIL } ## Available Tools @@ -68,8 +69,7 @@ IMPORTANT: Before doing ANY work, you MUST first check the site's plan by callin - Always confirm destructive operations (deleting posts, deactivating plugins, etc.) with the user before proceeding. - When creating content, follow WordPress best practices for block-based content and the remote block content guidelines below. - If a requested operation fails, check the error message and suggest alternatives. -- Explore the API — if you're unsure about an endpoint, load the \`wpcom-remote-management\` skill and try a lightweight GET request first to discover available data. -- ${ PLAN_DATA_GUARDRAIL }`; +- Explore the API — if you're unsure about an endpoint, load the \`wpcom-remote-management\` skill and try a lightweight GET request first to discover available data.`; } function buildLocalIntro( options: { chatArtifactsEnabled: boolean } ): string { @@ -96,6 +96,7 @@ ${ getStudioWidgetPromptManifest() }` 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 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: ${ PLAN_DATA_GUARDRAIL } IMPORTANT: For any generated content for the site, these three principles are mandatory: - 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. @@ -234,7 +235,7 @@ 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 PLAN_DATA_GUARDRAIL = `For any question about WordPress.com (or Pressable) plans, pricing, upgrades, or what a plan tier unlocks (plugins, themes, custom code, SSH, hosting, etc.), load the \`hosting-plans-helper\` skill before answering. Never state plan names, prices, or feature-tier gating from memory — they change and your training knowledge is stale.`; +const PLAN_DATA_GUARDRAIL = `For ANY question about WordPress.com or Pressable plans, pricing, upgrades, or what a plan tier includes (plugins, themes, custom code, SSH, hosting, storage, etc.), you MUST load the \`hosting-plans-helper\` skill and answer only from the data it fetches. Do NOT answer from memory: your training knowledge of plan names, prices, and feature-tier gating is stale and frequently wrong. In particular, do not claim a tier lacks a feature (e.g. that Personal or Premium cannot install plugins) based on memory — check the fetched per-tier feature list, which is the only source of truth. If you cannot fetch the data, say you cannot verify current plan details and point the user to https://wordpress.com/pricing; never guess.`; const LOCAL_SKILL_ROUTING = `## Skill routing @@ -244,6 +245,4 @@ For any page/post content, template or template-part content, block markup, bloc For verifying and polishing a built or redesigned site — checking the rendered result against intent and diagnosing layout/width, spacing, button, background, or hover issues — load the \`visual-polish\` skill and use \`inspect_design\` to root-cause from the rendered DOM before fixing. -For forms, shops/stores/ecommerce, events, LMS, galleries/slideshows, embeds, SEO/performance plugin choices, or any feature that core WordPress blocks do not cleanly provide, load the \`plugin-recommendations\` skill before installing plugins or writing plugin-provided block markup. - -${ PLAN_DATA_GUARDRAIL }`; +For forms, shops/stores/ecommerce, events, LMS, galleries/slideshows, embeds, SEO/performance plugin choices, or any feature that core WordPress blocks do not cleanly provide, load the \`plugin-recommendations\` skill before installing plugins or writing plugin-provided block markup.`; diff --git a/apps/cli/ai/tests/system-prompt.test.ts b/apps/cli/ai/tests/system-prompt.test.ts index 6811f6b92f..430671f167 100644 --- a/apps/cli/ai/tests/system-prompt.test.ts +++ b/apps/cli/ai/tests/system-prompt.test.ts @@ -79,18 +79,16 @@ describe( 'buildSystemPrompt', () => { const prompt = buildSystemPrompt( { chatArtifactsEnabled: true } ); expect( prompt ).toContain( '`hosting-plans-helper` skill' ); - expect( prompt ).toContain( - 'Never state plan names, prices, or feature-tier gating from memory' - ); + expect( prompt ).toContain( 'Do NOT answer from memory' ); + expect( prompt ).toContain( 'Personal or Premium cannot install plugins' ); } ); it( 'guards plan/pricing/feature answers behind the hosting-plans-helper skill (remote)', () => { const prompt = buildSystemPrompt( { remoteSite } ); expect( prompt ).toContain( '`hosting-plans-helper` skill' ); - expect( prompt ).toContain( - 'Never state plan names, prices, or feature-tier gating from memory' - ); + expect( prompt ).toContain( 'Do NOT answer from memory' ); + expect( prompt ).toContain( 'Personal or Premium cannot install plugins' ); } ); it( 'references only bundled skills', () => {