Skip to content

Agent evals: Add a pull-requests skill and a suite that checks it - #82028

Draft
jeryj wants to merge 16 commits into
trunkfrom
ai/improve-pr-description
Draft

jeryj wants to merge 16 commits into
trunkfrom
ai/improve-pr-description

Conversation

@jeryj

@jeryj jeryj commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Important

I haven't reviewed this yet - an idea that I don't want to forget about

Follow up to #80812, which it is based on. Review that first.

What?

Adds a pull-requests agent skill, sharpens the PR template it points at, and adds an eval suite that checks the skill is found and followed.

Why?

PR descriptions are the first thing a reviewer reads and a common source of back-and-forth. The skill records what a good one contains; the eval checks an agent actually uses it, rather than assuming.

How?

  • .agents/skills/pull-requests/SKILL.md covers writing style, describing the committed diff, testing instructions that name where a result is observable, and the AI-tools disclosure.
  • .github/PULL_REQUEST_TEMPLATE.md asks for distinct things in What and Why, skips checks CI already enforces, and asks which model was used.
  • The suite applies a patch fixture at HEAD, commits it as Change under review, and asks for the description. The fixture is a small real bug fix — getFilename not decoding percent-encoded filenames — kept in the repository so it can be reviewed and regenerated.
  • Grading is structural only (template sections present and in order, AI-tools section filled in) plus an agent-rubric that judges the description against the committed diff. Checks that need judgement are listed in the grader with the reason each is left to the rubric.

Testing Instructions

  1. npm --workspace @wordpress/agent-skill-evals run test:grader — confirm 4 passing grader tests, no model calls.
  2. npm run test:agent-evals -- --config specs/pull-request-skill/pr-description.test.js
  3. npm --workspace @wordpress/agent-skill-evals run view — confirm metrics for invoking the skill, reading the template, the structural checks, and the reviewer-usefulness rubric.

Step 2 makes real model calls against your own Claude quota. Results vary between runs; the agent does not always invoke the skill.

Testing Instructions for Keyboard

Not applicable; this PR does not change the user interface.

Screenshots or screencast

Not applicable.

Use of AI Tools

Claude Code (Opus) was used to implement and document this, and Codex (GPT-5) earlier in the series. All output was reviewed and verified by running the harness.

jeryj added 7 commits August 24, 2026 10:51
Adds `test/ai-development/evals`, a standalone workspace that runs coding
agents (Claude and Codex) against isolated temporary checkouts and grades
the result, so we can measure whether the skills in this repository are
discovered and followed.

Promptfoo drives the prompt x provider x test matrix. Lifecycle hooks in
`lib/workspace-extension.mjs` create a clean Git workspace from HEAD before
each row and delete it afterwards; shared provider and assertion defaults
live in `lib/`, and each suite under `specs/` supplies its own cases.

The workspace also regenerates `.claude/skills` from `.agents/skills`. A real
checkout gets that from `npm run agents:setup` via postinstall, but the
generated directory is ignored by Git and the workspace never runs npm — so
without it the repository's skills would sit there as files nothing announces,
and every suite would be measuring a broken environment rather than the
guidance under test.

The first suite covers testing-skill routing. `npm run test:grader`
unit-tests any suite's deterministic grader without spending agent tokens.

Run the evals with `npm run test:agent-evals`.
The Claude provider listed neither the skills to enable nor the tool that
invokes them, so the repository's skills were never offered to the model. Runs
looked like an agent ignoring its guidance when it had never been shown any.

Two settings are needed and they do different jobs. `skills` lists skills to
the model; omitting it is not "skills off" but no SDK configuration at all.
`custom_allowed_tools` replaces the allowed tool list outright rather than
extending it, so `Skill` has to appear there for the listed skills to be
invocable.

With both in place the agent invokes the testing skill and follows it to the
e2e reference, leaving the Jest and PHPUnit references unread.
Two corrections to the testing-skill-routing suite, both found by running it.

The skill assertion matched a shell command reading `SKILL.md`, so it could
only pass if the agent opened the file by hand. Claude invokes the skill
natively instead, which left the assertion failing on a run where the skill was
used correctly. Promptfoo's `skill-used` matches the invocation itself.

The rubric also required asserting serialized content without `expect.poll()`.
Nothing in the testing skill says that, and the contributor documentation
teaches the opposite: `docs/contributors/code/e2e/overusing-snapshots.md`
presents `expect.poll( editor.getBlocks )` as the recommended pattern, and the
suite uses `expect.poll( editor.getEditedPostContent )` 164 times across 14
spec files. Playwright only auto-retries locator assertions, so awaiting the
getter first collapses it to a one-shot comparison — the weaker form of the
two. The clause failed correct work, so it is gone.

Skill invocation is not deterministic: across three completed runs on the fixed
configuration the agent invoked the skill twice. Single runs cannot support
conclusions about this suite.
The Codex provider had never been run. Its binary is not on a normal
non-interactive PATH, so the documented command failed for anyone who tried it,
and an untested provider in the matrix reads as a broken harness rather than an
unfinished one.

Removing it leaves the plain command working with no provider filter. The
agent-rubric grader moves to Claude as well, since it was configured to grade
through Codex.

The shared layout still expects more than one agent — providers live in their
own file and assertions match Promptfoo's provider-independent command
trajectory steps — so a second agent can be added once someone has actually
run it.
The task asked for a test proving a typed `&` stays `&` rather than becoming
`&` in serialized content. That is not how the editor behaves, and should
not be: `escapeAmpersand` in `@wordpress/escape-html` escapes a bare ampersand,
so correct output is `<p>&amp;</p>`. The rubric demanded an assertion that
would fail against correct code, and the agent that noticed and asserted on the
rendered DOM instead was marked wrong for being right.

Centring a paragraph has no such ambiguity. The rubric names the attribute the
editor actually writes — `{"style":{"typography":{"textAlign":"center"}}}` —
and calls out the legacy `{"align":"center"}` form, which is only parsed as
input and is easy to copy from the block fixtures.

Both runs now pass the rubric, leaving skill invocation as the only varying
signal.
The package sat at `test/ai-development/evals`, one level below the `test/*`
workspace glob, so a root `npm install` skipped it and it carried the only
nested `package-lock.json` in the repository. Anyone running the evals had to
discover a second install step first.

Moving it to `test/ai-development` matches the other test workspaces, resolves
it through the root lockfile, and lets the root script address it by name.

The cost is that promptfoo's dependencies now install for every contributor —
about 640 additional entries in the root lockfile. They are devDependencies, so
nothing reaches a published package or plugin build.
The task moved from ampersand encoding to paragraph centre alignment, but the
case kept its old description, which is the label shown in results.
@jeryj
jeryj requested a review from desrosj as a code owner August 25, 2026 13:35
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jeryj <jeryj@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 7.9 MB

compressed-size-action

@jeryj
jeryj marked this pull request as draft August 25, 2026 13:43
@jeryj jeryj self-assigned this Aug 25, 2026
@jeryj jeryj added the [Type] Automated Testing Testing infrastructure changes impacting the execution of end-to-end (E2E) and/or unit tests. label Aug 25, 2026
jeryj added 2 commits August 25, 2026 09:05
The sandbox options claimed more than they delivered. A probe run showed the
agent could read anything on disk — including this repository's checkout, where
the eval's own assertions and rubrics live. Removing the evaluation directory
from the workspace copy does not help when the original is still readable.

`allowRead` only widens access, and the `allowManaged*Only` locks bind solely
when passed as managedSettings, where they are filtered through a
restrictive-key allowlist; as plain sandbox options they did nothing. So the
source root is now denied explicitly, which a repeat of the probe confirms:
reading a spec file returns "Operation not permitted" rather than its contents.

`failIfUnavailable` is dropped as well — it already defaults to true whenever
the sandbox is enabled.

What remains is verified in force: network egress fails, writes stay inside the
workspace, and the suite still passes.
Three settings were being applied through our own mechanisms rather than the
documented ones.

The per-test timeout lived in an environment variable inside the npm script, so
it was invisible to anyone reading a spec, could not vary per suite, and was
lost when running `promptfoo eval` directly. `evaluateOptions.timeoutMs` does
the same job in the spec, verified by watching a deliberately short value abort
a run with no environment variable set.

`options.bustCache` only suppressed cache reads while still writing entries.
`evaluateOptions.cache` covers both: `initializeAgenticCache` returns early
when caching is disabled, so the per-test option is redundant.

Tracing keeps its `$ref`. Promptfoo resolves JSON schema references through
`@apidevtools/json-schema-ref-parser` and documents them for reusable blocks,
so this is its own sharing mechanism rather than something incidental.
jeryj added 7 commits August 25, 2026 11:41
The extension reproduced what `tools/agents/setup-skills.mjs` does rather than
calling it, so the workspace would silently stop matching a real checkout if
that script ever did more than copy a directory. It exports `setupSkills` and
takes the repository root as an argument, so the workspace can be passed
directly; only its CLI wrapper hardcodes the root.

A missing source directory now fails instead of returning quietly. A workspace
without the generated view still runs, and every suite would report an agent
ignoring guidance it was never offered — the most misleading result this
harness can produce.
The repository writes American English; the task prompt and rubric had crept
into British forms.
Tracing, extensions, providers, defaultTest, evaluateOptions and outputPath are
the same for every suite, but each spec declared its own. A second suite could
quietly run with a different timeout, repeat count or cache setting than the
first, and the difference would not be visible from either file.

They now live in `lib/promptfooconfig.yaml` and are pulled in by reference, so a
spec carries only what is genuinely its own: what it asks for and what it
asserts. Promptfoo resolves JSON schema references, and `file://` paths inside a
referenced value still resolve relative to the spec, so the provider and
defaultTest files load as before.

Results are written to one file per run rather than per suite. Promptfoo's own
store is the history that `npm run view` reads; this is a convenience dump.
Promptfoo accepts `.js` configs alongside `.yaml`, and this repository is
JavaScript nearly everywhere. More usefully, JavaScript composes: a spec spreads
`lib/base.js` and adds only what is its own, where the YAML version repeated a
reference per shared key and could not express partial overrides at all.

The `file://` indirection for providers and default test options goes too —
those are now plain imports.
The YAML carried a `yaml-language-server` schema comment, which editors used to
check the config as it was written. Moving to JavaScript dropped that. Promptfoo
exports its config types, so a JSDoc annotation restores it, matching how the
repository types other configuration — `packages/stylelint-config/index.js` does
the same against stylelint's.

It earns its place immediately: `acceptFormats: [ 'json' ]` widened to
`string[]` where the schema wants `('json' | 'protobuf')[]`, which nothing would
have caught before.
Adds `.agents/skills/pull-requests/SKILL.md`, covering writing style, the
requirement to describe the committed diff rather than the working tree,
testing instructions that name where each result is observable, and filling
in the AI-tools disclosure.

Also tightens the prompts in `.github/PULL_REQUEST_TEMPLATE.md` so the What
and Why sections ask for distinct things, testing instructions skip checks
CI already enforces, and the AI-tools note asks which model was used.
Adds a second suite that asks the agent to author a PR description for a
committed change and grades the result.

The suite needs something to describe, so the workspace extension gains a
`patchFile` var: the patch is applied at HEAD and committed. The fixture is a
small real bug fix kept in the repository, where it can be reviewed and
regenerated, and it is committed as `Change under review` — a real subject line
would hand over most of the description being asked for.

Structural rules are graded deterministically; everything needing judgement is
left to the agent-rubric. `npm run test:grader` checks the grader's own rules
against canned descriptions without spending agent tokens.
@jeryj
jeryj force-pushed the ai/improve-pr-description branch from b2ba50d to 344a7cc Compare August 25, 2026 18:42
@ciampo
ciampo force-pushed the add/promptfoo branch 6 times, most recently from f740176 to 12711be Compare August 31, 2026 18:23
@ciampo
ciampo force-pushed the add/promptfoo branch 2 times, most recently from ac47589 to 059432e Compare August 31, 2026 18:45
Base automatically changed from add/promptfoo to trunk September 4, 2026 21:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Automated Testing Testing infrastructure changes impacting the execution of end-to-end (E2E) and/or unit tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant