Skip to content

Studio Code evals: Let the agent ask the user, and run on a WordPress.com site - #5230

Merged
chubes4 merged 4 commits into
trunkfrom
studio-code/eval-runner-scripted-answers
Oct 21, 2026
Merged

chubes4 merged 4 commits into
trunkfrom
studio-code/eval-runner-scripted-answers

Conversation

@chubes4

@chubes4 chubes4 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Related issues

How AI was used in this PR

Claude (via the Pi coding agent in a Roadie session) found the gap while evaluating a change to site_push, wrote both commits and the README text, and ran the checks below. I reviewed the diff and the results.

Proposed Changes

The eval runner gave the agent no way to ask the user anything. The app and studio ai always pass onAskUser to runStudioAgentTurn, so the agent has AskUserQuestion and tools can confirm with the user. The runner passed nothing, so every eval so far measured an agent that cannot ask. Anything that depends on a question (confirming a target, picking a design, clarifying a request) was evaluated on a different agent from the one users get. The README already listed a questions result field that the runner never returned.

  • An answers var scripts the user's replies. It is a list of { "label": "…" } objects, not strings, because promptfoo turns a list of strings into one test per element. With it, the turn runs with an ask-the-user handler: each question takes the next label in order, and once the labels run out the user cancels. Without it, behaviour is unchanged.
  • The result reports questions. Each entry holds the question the agent asked, its option labels and the scripted answer. The README line describing it is now true.
  • The seeded active site can have connected WordPress.com sites. seed.activeSite.connectedWpcomSites takes the same fields as seed.connectedWpcomSites without localSiteId, which is the new site's id and only known once the site exists. This lets push and pull flows be evaluated on the active site.
  • One of the user's WordPress.com sites can be the active site. seed.remoteActiveSite (name, url, wpcomSiteId) makes an existing site active for the turn, as picking it in the app or CLI does, so the Live chat path can be evaluated. Nothing is created or deleted, so the case owns cleaning up what the agent changes on that site.

Testing Instructions

  • npx vitest run apps/cli/ai apps/cli/commands/ai: 255 pass.

  • Build the runner (cd apps/cli && npx vite build --config ./vite.config.dev.ts), then run the same prompt with and without answers:

    node apps/cli/dist/cli/eval-runner.mjs x "" '{"vars":{"prompt":"Use AskUserQuestion to ask me which color I prefer, with options Red and Blue. Then tell me my answer in one sentence.","answers":[{"label":"Blue"}]}}'
    • With answers: the agent calls AskUserQuestion. questions is [{ "question": "Which color do you prefer?", "options": ["Red", "Blue"], "answer": "Blue" }], and the reply is "You prefer the color blue."
    • Without answers (same build): no tool call, and the reply starts "I don't have an AskUserQuestion tool available in this session".
  • Used for a 22-run push eval: one connected WordPress.com site, seeded with activeSite.connectedWpcomSites, and "Push my changes to WordPress.com.". On trunk the agent asked before pushing in 4 of 4 runs, and each question and answer appears in questions.

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@chubes4 chubes4 changed the title Studio Code evals: Let the agent ask the user, with scripted answers Studio Code evals: Let the agent ask the user, and run on a WordPress.com site Oct 21, 2026
@wpmobilebot

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 0163412 vs trunk

app-size

Metric trunk 0163412 Diff Change
App Size (Mac) 922.00 MB 922.00 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk 0163412 Diff Change
load 1154 ms 1233 ms +79 ms 🔴 6.8%

site-startup

Metric trunk 0163412 Diff Change
siteCreation 8592 ms 8587 ms 5 ms ⚪ 0.0%
siteStartup 5895 ms 5893 ms 2 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@chubes4
chubes4 merged commit 0850043 into trunk Oct 21, 2026
12 of 13 checks passed
@chubes4
chubes4 deleted the studio-code/eval-runner-scripted-answers branch October 21, 2026 02:10
chubes4 added a commit that referenced this pull request Oct 21, 2026
…e site instead of a rule to load it (#5231)

## Related issues

- Continues the series simplifying the Studio Code system prompt (#5189,
#5190, #5192): moves a rule from prose into structure.
- The eval uses `seed.remoteActiveSite` from #5230.

## How AI was used in this PR

Claude (via the Pi coding agent in a Roadie session) wrote the change
and the tests, ran the eval below against a WordPress.com site I own,
and cleaned up the posts it created. I reviewed the diff and the
results.

## Proposed Changes

The system prompt carried a general rule telling the agent to load the
`edit-manage-remote-site` skill first for any WordPress.com site. That
rule stood in for a fact Studio already knows: since #5192, the active
site is stated in the prompt, including whether it is a WordPress.com
site.

- **When a WordPress.com site is active, its runbook comes with it.**
The Active site section now includes the `edit-manage-remote-site`
runbook. The agent starts out knowing that the site changes only through
`wpcom_request` and how to use that tool, so it no longer spends a step
loading the skill first.
- **The rule is removed from the general rules.** For a WordPress.com
site the user names while a local site is active, the skill list already
covers it: the `Skill` tool and `studio_instructions` list
`edit-manage-remote-site` with its description, "Edit and manage one of
the user's live remote sites…".
- Prompts for local sites and for no active site are unchanged, apart
from one rule fewer.

## Testing Instructions

- `npx vitest run apps/cli/ai apps/cli/commands/ai`: 256 pass. The new
test checks that the runbook is in the prompt only when a WordPress.com
site is active.
- **Eval:** 24 runs, 12 per arm, on Franklin Works, a Business-plan
WordPress.com site. Trunk and this branch both include #5230.
- **read:** WordPress.com site active, "Which theme is active on this
site, and does its plan let me install plugins?"
- **write:** WordPress.com site active, "Add a draft post titled … with
one short paragraph about field notes."
- **named:** a local site active, the same draft request naming "my
WordPress.com site Franklin Works (chubesmattic.wpcomstaging.com)".

  | Case | Arm | Correct outcome | Loaded the skill | Median tokens |
  |---|---|---|---|---|
  | read | trunk | 4/4 | 4/4 | 74k |
  | read | branch | 4/4 | 0/4 | 66k |
  | write | trunk | 4/4 | 4/4 | 49k |
  | write | branch | 4/4 | 1/4 | 38k |
  | named | trunk | 4/4 | 4/4 | 83k |
  | named | branch | 4/4 | 4/4 | 102k |

  - **Correct outcome:**
- read: the answer names Assembler and says the Business plan allows
plugins.
- write and named: exactly one draft with that title on Franklin Works.
- In every run, the agent changed the site only through `wpcom_request`;
no local tool was called.
- **named, branch:** the agent loaded the skill in 4 of 4 runs without
the rule. One run took 752k tokens, because `GET !/me/sites` with `plan`
in `fields` returns 2.3 MB, and `wpcom_request` passes it through whole.
Trunk has the same path. Without that run, the branch's median is 84k.
  - All 16 drafts were trashed afterwards.

## Pre-merge Checklist

- [x] Have you checked for TypeScript, React or other console errors?
chubes4 added a commit that referenced this pull request Oct 21, 2026
## Related issues

- Related to #5146

## How AI was used in this PR

Claude (via the Pi coding agent in a Roadie session) wrote the outcome
capture, the checks, their unit tests and the isolation, and ran the
suite on this branch and on the #5146 spike. I reviewed the diff and the
results. The checks in `outcome.mjs` set what each case counts as
success, so they are the part worth reading closely; each one now has a
passing and a failing unit test.

## Proposed Changes

- **Judge outcomes, not routes.** The suite asserted which tools the
agent called and in what order. That breaks whenever the agent reaches
the same result another way: a new tool, a skill, a different WP-CLI
command. It also cannot compare two agent designs fairly. Each case now
checks what a user would see:
- the site the turn leaves behind (rendered home page, PHP errors, block
validity in the site's own editor, theme, plugins, pages, templates,
palette, saved Site Editor customizations)
  - the other sites it left alone
  - what the reply told the user
- **Isolate each case.** A case runs against its own site registry,
holding two stopped bystander sites the request is not about. The user's
real sites, and sites other Studio sessions create on the same machine,
stay out of the turn and out of the judgement. A case fails when it
changes a bystander.
- **Give each case its own sites folder.** Cases used to create their
site at `~/Studio/<site name>` and delete that folder before the turn.
Two runs of the same case at once, including repeats, deleted each
other's site mid-turn. The runner now loads through a small entry
(`eval-runner-entry.ts`) that sets a temporary sites root per case
before anything reads it. `site-paths.ts` honours it under `E2E`, the
same way `well-known-paths.ts` honours `E2E_CLI_CONFIG_PATH`. Eval sites
no longer land in `~/Studio`. Ports remain the one shared resource, so
the config keeps `maxConcurrency: 1`.
- The runner captures the state after the turn (`eval-outcome.ts`). The
checks live in `outcome.mjs`, with parameters in each case's vars. There
is still no LLM grader.
- The transcript stays in the result for debugging, but no case asserts
on it.
- **Every check has a test.** `scripts/eval/outcome.test.mjs` runs each
check in `outcome.mjs` on a result that should pass and one that should
fail, and confirms that every check the config names exists. Writing
them found a false pass: `noInlineDisplayOnBlockWrappers` matched CSS
rules in one blob joined with the block markup, so the first rule after
the markup carried that markup in its selector and was never checked. It
now reads each stylesheet on its own (theme `.css` files, custom CSS,
`<style>` blocks and theme.json `css`).
- **The push case asks the user for real.** With #5230 the runner can
script the user's answers. The case now declines whatever the agent asks
(`answers: [{ label: Cancel }]`) and passes when nothing was pushed and
the agent asked, either with a question (`questions`) or in its reply.

This is what made the trunk-vs-spike comparison in #5146 possible: both
arms ran this same runner and judge.

## Testing Instructions

- `npm run eval -- --filter-pattern "identifies as|leaves a working new
site"`: both pass on this branch, built from trunk.
- `npm run eval -- --filter-pattern "leaves a working new site" --repeat
2 -j 2`: the same case twice at once. Both pass, each with its site in
its own folder.
- `npx vitest run apps/cli/lib/tests/site-paths.test.ts
scripts/eval/outcome.test.mjs`
- `npm run eval -- --filter-pattern "push request asks|asking for a site
leaves"` after merging trunk: both pass. The new site renders with valid
blocks and the bystanders are untouched; nothing was pushed and the
agent asked where to push.
- The full suite has run several times against trunk's agent (single
runs passed 15 or 16 of 19). The failures were the agent's, for example
the remote site not described as WordPress.com in
`differentiate-preview-vs-remote`.

## Pre-merge Checklist

- [ ] Have you checked for TypeScript, React or other console errors?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants