Skip to content
System nominalUTC --:--:--Articles 085
MODULE 05 Lesson 13 / 18 30 MIN

Working on a site someone else built

Reading an Unfamiliar Codebase With an Agent

How to make an agent map a WordPress codebase you did not write, and how to check its reading against the running site before you believe a word of it.

The site arrives as a zip file with a theme, four plugins and a note that says "the footer is custom". You open the theme, find eleven PHP files, and within a minute you are reading someone else's abstraction over a framework you know well. This is the work most WordPress developers actually do, and it is the work an agent is worst at doing unsupervised, because the shape of a codebase that somebody else built is exactly the thing a model will guess at.

Point an agent at an unfamiliar repository and ask what it does, and you will get a fluent summary of the file names. The summary is not reading. The agent has no memory of running this site, no idea which plugin is active, and no way to tell a function that is called on every page from one that was abandoned in 2019. Those are the facts that decide whether your change works, and they are available to you in about ten minutes of commands.

The method in this lesson is a loop. You build the map yourself with cheap commands, you ask the agent to extend and explain it, and then you settle every disagreement by asking the running install. The install is the only party in that conversation that cannot be wrong.

Read the codebase in order of what can be reached

File order is arbitrary. The order that matters is reachability: WordPress calls your code through hooks, and a function that no hook and no template can reach might as well not exist. Almost every WordPress codebase announces its entry points in a form you can grep.

cd wp-content/themes/acme-studio
find . -type f | sort
grep -rn "add_action\|add_filter" --include=*.php .
grep -rn "do_action\|apply_filters" --include=*.php .

On a small agency child theme that produced this:

$ find . -type f | sort
./functions.php
./inc/brand.php
./style.css
./theme.json

$ grep -rn "add_action\|add_filter" --include=*.php .
./functions.php:10:add_action( 'wp_footer', 'acme_studio_footer_note' );
./inc/brand.php:6:add_action( 'wp_enqueue_scripts', 'acme_studio_brand_styles' );

Four files and two registrations. You now know more about this theme than an agent will tell you from a prompt, because you know how little there is. The second grep is the one people skip: a theme that consumes a filter is telling you which plugin is supposed to be feeding it, and a theme that fires its own action is telling you where a future change is expected to attach.

A callback name is a claim about a function that may not exist

Here is the part agents get wrong quietly. Registrations reference callbacks by string, and PHP resolves those strings at call time. Nothing in the file that registers acme_studio_footer_note proves that a function with that name is ever defined, because the definition could be in a file that is never included, in a plugin that is not active, or nowhere at all.

In the theme above, functions.php includes one file and registers one hook. inc/brand.php defines both the palette function and the callback it registers. But the codebase also references acme_studio_register_notice_block and acme_studio_append_notice in a file that has since been deleted, which is the normal way a real site accumulates dead references. Cross-checking every callback name against every definition is tedious by hand and trivial as a probe.

<?php
/**
 * Probe file: run with wp eval-file on the site you are reading.
 */
require_once ABSPATH . 'wp-admin/includes/plugin.php';

$symbols = array(
	'acme_studio_brand_styles',
	'acme_studio_brand_palette',
	'acme_studio_register_notice_block',
	'acme_studio_append_notice',
	'acme_brand_get_config',
);

foreach ( $symbols as $symbol ) {
	printf( "%-34s %s\n", $symbol, function_exists( $symbol ) ? 'defined' : 'NOT DEFINED' );
}

printf( "active plugins: %s\n", implode( ', ', (array) get_option( 'active_plugins' ) ) ?: '(none)' );
printf( "stylesheet: %s, template: %s\n", get_stylesheet(), get_template() );

Run against the install this lesson was written on, that file printed the following, and the two lines it did not print are the two names that matter.

$ wp eval-file /tmp/m5-inventory.php
acme_studio_brand_styles           defined
acme_studio_brand_palette          defined
acme_studio_register_notice_block  NOT DEFINED
acme_studio_append_notice          NOT DEFINED
acme_brand_get_config              NOT DEFINED
active plugins: (none)
stylesheet: acme-studio, template: twentytwentyfive

Three findings, all of which change how you work. The notice block registration refers to a function that does not exist anywhere, so that file is dead or the contribution is incomplete. The theme is a child of Twenty Twenty-Five, so any parent template you touch is overwritten by the next parent update. And there are no active plugins, which means every function_exists() guard that checks for a plugin is currently taking the negative branch.

Probe the site, not the source you were handed

The probe above is worth generalising, because it answers the four questions that come up on every unfamiliar site. What is the active theme, and does it have a parent? Which plugins are active? Which of the names this code calls actually exist right now? What state is in the database that the code reads?

WP-CLI answers the first three directly, and the answers are cheap enough to collect before you read a single line of logic.

wp theme list --status=active
wp plugin list --status=active
wp eval-file /tmp/m5-inventory.php
wp option get acme_studio_notice
$ wp plugin list --status=active
name	status	update	version
(no output)

$ wp option get acme_studio_notice
(no output, exit code 1)

The empty wp plugin list --status=active output is a fact about the site, not a failed command. So is the missing option. Both are the kind of thing an agent will narrate as "the plugin provides brand configuration" if you let it summarise the code without running it, and both are the reason a later change breaks: the guard clause in the theme is currently the only branch that executes.

One caution about which tool answers which question. wp eval-file loads WordPress first, so a theme that fatals during load takes the probe down with it. That is useful information, but it means a probe is not a substitute for fetching the site. If the probe dies, you have found the first thing to fix and you have not yet read any application logic.

Hand the agent the map, then give it one task at a time

With the inventory in hand, an agent becomes useful, and the way to keep it useful is to decide what it is allowed to do. Reading a codebase is a task where the agent has no way to check itself, so the useful instruction is not "explain this theme" but "here are the five registrations I found and the two symbols that do not resolve; tell me which files can reach each callback, and mark anything you cannot trace to a definition as unknown".

The distinction that carries the weight is between explaining and changing. An agent asked to explain produces paragraphs you can check against the inventory. An agent asked to fix produces a diff on a site you have not finished reading, which is how a two-line change becomes a migration. If you want both, ask for the explanation first and make the edit a separate, reviewed step with its own commit.

Write the site facts down before you touch anything

One paragraph, committed to the repository, listing the active theme and its parent, the active plugins, the WordPress version, and the option or table that holds the data your change will read. Every later question in this module is answered against that paragraph, and the agent can be given it as context instead of being asked to rediscover it.

Lab 30 minutes

Pick a WordPress site you did not build. It can be a site of your own that you have not opened in a year, a plugin from the directory, or the theme installed on a client site you have approval to inspect. Do the following without editing anything.

  1. List the files, then grep for add_action, add_filter, do_action and apply_filters. Keep both outputs.
  2. Write a probe file that loops over every callback name you found plus every helper those callbacks call, and prints defined or NOT DEFINED for each. Run it with wp eval-file.
  3. Collect the active theme, its parent, the active plugins and the WordPress version.
  4. Find one symbol that is referenced but not defined, or one option that the code reads and the database does not have. Write down which of the two it is.

Then write the one-paragraph site summary, and ask the agent to answer this question from the inventory alone: which callbacks run on a normal front-end page, and which do not?

Show the reasoning and the answer to the question

The inventory is the whole answer, and the reason it works is that WordPress only runs a callback if two things are true: the hook fires on that request, and the callback name resolves to a function. Your probe settled the second. The first is settled by knowing which hooks fire on which kind of request, and that does not need an agent.

On the theme in this lesson, wp_enqueue_scripts fires on every front-end request, so acme_studio_brand_styles runs. wp_footer fires on every front-end request that reaches the footer, so acme_studio_footer_note runs, and it is the callback that calls acme_studio_brand_config(), which returns null while the agency toolkit plugin is inactive. That function is the site's latent fatal, and no amount of reading tells you that; running it does.

The two NOT DEFINED names do not run at all. acme_studio_notice is absent from the database, the block is never registered, and the filter that would append the notice is never attached. A change that assumed the notice feature existed would be built against a surface that does not execute.

$ wp eval 'var_export( acme_studio_brand_config() );'
NULL

Two answers to the question are defensible, and they differ in what they assume about the environment. If you answer from the code alone, every registered callback whose function exists runs, and everything else is dead code. If you answer against the running site, you can add that one of the reachable callbacks currently produces a fatal because its dependency is missing. The second answer is the one worth keeping, because it is the one that survives contact with the site.

The wrong instinct is to ask the agent which callbacks run. That is a question about the runtime, and the runtime is a command away. An agent asked to guess will produce a confident answer that is right about init and wrong about everything specific to this install.

Official reference

The WordPress developer reference for what this lesson uses.

Progress

Mark this lesson done and it stays marked in this browser.