A development environment an agent can drive
Giving the Agent Errors to Read
Why an agent that stubs wp_kses_post() and tests against its own stub reports green, and how to wire real WordPress errors, WPCS and PHPStan into the loop.
Here is the failure this lesson exists to prevent, and it happened in a real recorded run.
An agent built a Gutenberg block plugin, then verified it. It ran php -l on every
PHP file and reported no syntax errors. It ran its own JavaScript and CSS linters, which passed.
Then it wrote a test harness containing hand-rolled versions of wp_kses_post(),
get_block_wrapper_attributes() and esc_attr__(), tested its plugin
against those, and reported that escaping worked, that a hostile CSS value fell back to the
default, and that empty input returned an empty string.
Every one of those results was correct — verified afterwards against real WordPress. The method was still worthless, and it is worth understanding exactly why before you build anything. The agent tested its code against its own beliefs about WordPress. If a belief was wrong, the stub encoded the same wrong belief, the test passed, and the report said green. The harness could not fail in the direction that mattered.
PHP's default feedback is almost nothing
PHP is forgiving in a way that makes a confident agent dangerous. Three examples, run on PHP 8.5.6 while writing this lesson.
A file that calls wp_kses_post() with no WordPress loaded passes
php -l cleanly. Run it and you get a fatal error naming the line and the function.
The syntax checker had nothing to say because the syntax is fine; the program is what is
broken.
$ php -l nocore.php
No syntax errors detected in nocore.php
$ php nocore.php
PHP Fatal error: Uncaught Error: Call to undefined function add_filter() in nocore.php:2
Stack trace:
#0 {main}
thrown in nocore.php on line 2
That pair is the whole argument for a real install. The first command is the one an agent runs by default, and it produces the word an agent likes best: no errors. The second command is the one that tells the truth, and it requires WordPress to be present — or at least it requires you to be honest about the fact that it is not.
The third example is quieter and worse. A hook name with a typo is valid PHP. So is a
callback registered with the wrong number of arguments, a capability check that uses a
capability nobody has, and an esc_html() call wrapped around a value that needed
esc_attr(). None of them are syntax errors, and none of them are type errors in a
language with no type checker in the default workflow. They are semantic errors, and PHP will
run them happily.
Give the agent the surfaces where errors land
The fix is unglamorous. Turn things on so that failures are loud, then require the agent to read them.
Errors on screen. In a local install, WordPress's debug constants make notices, warnings and deprecations visible instead of silent. The important one for agent work is the log, because a command-line agent reads files more reliably than it screenshots pages. The reference for the exact constant names is the WordPress developer documentation rather than a tutorial, and the rule to give the agent is simple: after any change, check the debug log and report anything new in it.
The command's own output. wp commands return non-zero exit
codes on failure and print the reason to standard error. An agent that runs
wp plugin activate acme-notice and gets
Error: Plugin 'acme-notice' could not be activated. has something to act on. An
agent that runs wp plugin activate and reports "activation succeeded" without
showing you the output has done nothing you can trust.
The browser, for the parts that need it. Some failures exist only in a rendered editor: a block that fails to register, a control that throws when the Inspector opens, a REST request that only happens from JavaScript. Playwright or the WordPress Playground skill's browser path closes that gap, and the research run for this course explicitly did not test the editor in a browser, which is why its result is stated as a plugin that installs and renders rather than a plugin that is finished.
Install the gates that can actually fail
WordPress Coding Standards enforced by PHP_CodeSniffer, and PHPStan for static analysis, are the two tools that turn invisible semantic problems into reported lines. They are also the two tools most likely to be missing, including in the environment used to write this course: neither Composer nor PHPCS was installed there, so no configuration in this lesson was executed. Treat the commands as documented rather than demonstrated, and check them on your machine.
The commands come straight from WordPress's own repositories, which is the strongest
endorsement available. WP-CLI's AGENTS.md prescribes, for any PHP file,
composer phpcs -- <file> and composer phpstan. Gutenberg's
AGENTS.md names vendor/bin/phpcs to check and
vendor/bin/phpcbf to fix, which is the distinction to teach an agent: one command
reports, the other rewrites. Let the agent run the fixer on a file you have committed, then make
you read the diff.
The same files carry the warnings that make the tooling usable by an agent. WP-CLI sets
NO_COLOR=1 and WP_CLI_TEST_QUIET=1 so tool output stays compact
instead of drawing progress bars into a log an agent has to parse. It also states that a green
composer test is not the same as a green CI, because other checks run separately —
a sentence worth copying into your rules file, because agents generalise success.
A wrong belief survives a green build and a green lint
The single most useful sentence to put in front of an agent is that a passing check proves only what the check tests. Two concrete cases from the WordPress ecosystem make it stick.
First, npm run build on a block plugin externalises the
@wordpress/* packages to browser globals, so webpack never verifies that the
export you imported still exists. In the recorded run, an agent imported a component from
@wordpress/block-editor, the build succeeded, and the component did still exist in
the version being tested. Nothing in the toolchain would have caught it if it had been removed.
An agent can hand you a block that builds perfectly and white-screens the editor.
Second, the generated files disagree with the generated configuration in ways nothing
checks. In that same run, the plugin header declared one minimum WordPress version, the
phpcs.xml.dist matched it, and readme.txt declared a different tested
version — while WordPress was several major releases newer. Cosmetic on its own. The same class
of defect as a wrong hook signature: locally plausible, globally wrong, invisible to every gate
in the chain.
The rule that stops an agent inventing WordPress
Everything above becomes one instruction, and it belongs in AGENTS.md where the
agent reads it before every task.
## Verification
- Run the real command. If a WordPress function is unavailable because no
installation is reachable, say so and stop.
- Never write a replacement for a WordPress function and test against it.
A stub encodes your assumptions; a passing stub test proves only that the
assumptions are self-consistent.
- After each change: `composer phpcs -- <file>`, `composer phpstan`,
then the smallest `wp` command that exercises the change.
- Quote the command and its output in your summary. A summary without the
output is not evidence.
- A green build or a green lint is not evidence about WordPress. Say which
check you ran and what it does not cover.
The wording matters less than the enforcement. If you cannot check whether the agent ran the command, the rule is decoration — so ask for the transcript, not the conclusion.
Build the smallest possible demonstration that a green check can be wrong, then fix it.
- In a plugin file, call a WordPress function that does not exist. Write one made-up name; do not use a deprecated function, because that is a different lesson.
- Run
php -lon the file and keep the output. Then run the file and keep that output. - Now wire the real check: from your WordPress root, use
wp evalwithfunction_exists()to test the made-up name and a real function side by side. Keep the output. - Ask the agent to find the bug, and require it to show every command it ran. Then ask it to fix the call and prove the fix by running the plugin's CLI command or activating the plugin.
- Write down which of the five checks actually detected the problem, and which reported success.
Show the expected outputs and what each check can and cannot prove
Steps 2 and 3 produce a contrast that is worth keeping in your notes.
$ php -l acme-badge.php
No syntax errors detected in acme-badge.php
$ php acme-badge.php
PHP Fatal error: Uncaught Error: Call to undefined function wp_kses_post() in acme-badge.php:3
$ wp eval 'echo function_exists( "acme_sanitize_notice_message" ) ? "EXISTS" : "MISSING";'
MISSING
Four checks, four different levels of proof. php -l proves the file parses,
which is necessary and close to useless on its own. Running the file proves the code path
reaches a missing function, and it works here only because the file was written without
WordPress loaded — in a plugin, WordPress suppresses the fatal instead and gives you a white
screen. wp eval with function_exists() answers the question the
agent actually has, in the environment where the answer is authoritative. Activating the
plugin or running its CLI command proves the change did not break the plugin's own surface.
Step 5 is the interesting answer, and it usually surprises people. The check that caught
the bug is not the one an agent runs by default. php -l reported success on a
file that cannot run, and a stub harness would also have reported success, because a stub for
a made-up function is as easy to write as a stub for a real one. That is the whole
mechanism: stubbing removes the only step where a wrong belief about WordPress has to collide
with WordPress.
Two alternative fixes are defensible, and the choice depends on what you are doing. If the
function genuinely does not exist, the fix is to call the real one, and the proof is
function_exists() returning true inside a live install followed by an actual
render. If the call was meant to be a plugin helper, then the function is missing rather than
the call, and the fix belongs in the plugin with a unit test that does not touch WordPress —
PHPUnit with WordPress's test suite, not hand-rolled stubs of core functions. The wrong fix in
both cases is to write the missing function into a test harness and let the harness pass.
One honest limit on this whole approach: none of these gates covers the editor's runtime
behaviour, and the research behind this course did not test it in a browser. A plugin can pass
php -l, pass WPCS, activate cleanly, render correctly over HTTP and still throw
the moment someone opens the block in the inserter. If your change touches editor JavaScript,
the browser step is not optional, and a green command-line loop is not evidence that you can
skip it.
Official reference
The WordPress developer reference for what this lesson uses.
Progress
Mark this lesson done and it stays marked in this browser.