Files
agent-skills/skills/wordpress-performance-diagnostics/SKILL.md
T
MalinandClaude Sonnet 5 6fa011a9a6 feat: add wordpress-performance-diagnostics skill
Paired skills.sh/autoskills.sh catalog scans (2026-08-15, reports in
granja/_temp/codex-logs/) independently flagged wordpress/agent-skills'
wp-performance module as the best net-new find of the whole scan: a
measurement-first, backend-only WP diagnostic workflow (WP-CLI
doctor/profile, headless Query Monitor, autoload/object-cache/cron
checks) — a strong fit since this fleet's 13+ WordPress sites are all
headless/jailed with no browser-first profiling access.

Adapted from github.com/wordpress/agent-skills (skills/wp-performance)
rather than installed verbatim: swapped the upstream's local/SSH
assumptions for the fleet's actual `bastille cmd ... su -m www`
remote-execution pattern (reusing wordpress-cli-remote-execution's
convention exactly), added a jail/site selector step against the real
granja/staging/gringo inventory, narrowed the always-collect baseline to
four metrics (TTFB, autoload size, object-cache presence, cron health)
with fleet-specific context for each, added redaction guidance for
Valkey/DB credentials that diagnostic output can surface, and replaced
the upstream's hardcoded "WordPress 7.0+" compatibility claim with
per-site version verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:26:58 +02:00

254 lines
12 KiB
Markdown

---
name: wordpress-performance-diagnostics
description: Use when investigating why a WordPress site in the fleet is slow (frontend TTFB, wp-admin, a REST route, WP-Cron) or before proposing any performance remediation on one. Backend-only, measurement-first diagnostic workflow for headless/jailed WordPress sites reached via `bastille cmd` — establishes baseline evidence (TTFB, autoloaded-options size, object-cache presence, cron health) before any write/remediation action.
---
# WordPress Performance Diagnostics
Adapted from wordpress/agent-skills' `wp-performance` module
(`github.com/wordpress/agent-skills`, `skills/wp-performance`) —
identified 2026-08-15 via a paired skills.sh/autoskills.sh catalog scan;
both scans independently flagged it as the single best net-new find of
the whole scan, since this fleet's WordPress estate is entirely
headless/jailed with no convenient browser-first profiling access. This
version keeps the upstream's measurement-first structure but replaces its
local/SSH assumptions with the fleet's actual `bastille cmd`
remote-execution pattern, adds a jail/site selector step (13+ sites
across granja/staging/gringo), and narrows the always-collect baseline to
four metrics with fleet-specific context for each.
## When to use
- A WordPress site (or several) is reported slow — frontend TTFB,
wp-admin, a REST route, or WP-Cron.
- Before proposing ANY performance remediation on a fleet site — collect
the baseline evidence first, always.
- Comparing before/after a change (theme deploy, plugin update,
object-cache rollout) to confirm it actually helped.
Every site in this fleet is a jailed, headless WordPress install with no
browser-based profiling access — WP-CLI, `curl`, and log inspection are
the only tools available. Treat this as backend-only by default.
## Step 0 — read-only-first, always
This skill's job is to produce evidence, not to remediate. Until the user
has explicitly asked for a fix (and you've already captured a baseline to
measure against), do not:
- flush any cache (`wp cache flush`, `wp redis flush`/`wp redis enable`)
- install or activate a plugin
- change `wp-config.php` or any debug constant (`WP_DEBUG`, `SAVEQUERIES`)
- restart php-fpm/nginx/valkey
If a fix is later proposed, re-run the exact same measurements from Step
3 afterward and report the delta — see Verification below. Flushing or
restarting anything *before* you have a baseline destroys the thing you
needed to compare against.
## Step 1 — identify the jail (site selector)
The fleet runs 13+ WordPress jails across three hosts. Domain names do
not map to jail names by a fixed rule (jail names are typically the
domain with punctuation stripped, but always confirm, don't guess) — look
it up in the per-host inventory before running anything:
- `docs/server-granja.md` — granja's "Site jails" table: 13 WordPress
jails, e.g. `menorca.pt` → jail `menorcapt` (`10.20.0.19`), `elrumano.es`
→ jail `elrumanoro` (`10.20.0.12`).
- `docs/server-staging.md` — staging's "Site jails" table, e.g. the
`bauxa` jail, which serves Cala Verd restaurant's WordPress site
alongside four other, non-WordPress services in the same jail.
- `docs/server-gringo.md` — gringo's site jails, e.g. `bauxa-app`
(`10.20.0.76`) — the same Cala Verd WordPress site, migrated here
2026-08-13 but not yet cut over in DNS/NPM; both copies are currently
live, so confirm which one you actually mean before diagnosing.
If a domain isn't in any of those tables, don't guess a jail name — ask,
or `ssh root@<host> "bastille list"` and cross-reference `wp option get
siteurl` per jail (the same method used for the 2026-08-13 granja
re-audit) rather than trusting an older or stale note.
Once you have `<host>`, `<jail>`, and the site's on-disk path, confirm the
path rather than assuming a fixed convention — e.g. `bastille cmd <jail>
ls /usr/local/www/`. Multi-service jails (like `bauxa-app`, which serves
five separate things behind one jail IP) don't have one obvious WordPress
docroot; guessing wrong here silently profiles the wrong document root.
## Step 2 — remote WP-CLI wrapper
Reuse the fleet's established remote-execution pattern exactly — see the
`wordpress-cli-remote-execution` skill for the full gotcha list
(jail-exec output-prefix stripping, `error_reporting(0)` bootstraps
swallowing errors, shell globbing failing silently under some remote
shells). Do not invent a parallel SSH-direct or local-WP-CLI pattern;
every site in this fleet is reached the same way:
```bash
ssh root@<host> "bastille cmd <jail> sh -c 'cd <site-path> && su -m www -c \"wp <command> --path=<site-path>\"'"
```
- Run as the jail's web user (`www`) via `su -m www -c`, not as root —
matches the fleet convention and avoids the `wp-content`
ownership-drift problem documented in `wordpress-plugin-conventions`
(files created as root over SSH have already caused a real
WordPress-core-update failure on this fleet — `menorca.ro`, 2026-07-10).
- Always pass `--path=` explicitly, don't rely on `cd` propagating.
- Strip the `bastille cmd` wrapper's own `[<jail>]:` prefix line before
parsing any captured output programmatically.
## Step 3 — the four core baseline metrics
Collect all four before proposing any fix, even when the symptom seems to
point at one obvious cause — a slow site is frequently more than one
thing at once, and an isolated fix without a baseline can't be verified
afterward.
**1. TTFB** (time to first byte, measured from outside the jail):
```bash
curl -s -o /dev/null -w 'ttfb=%{time_starttransfer}s total=%{time_total}s http_code=%{http_code}\n' -H 'Host: <domain>' http://<jail-ip>/
```
Hit the jail's own IP directly with the right `Host:` header (per the
`bastille-jail-provisioning` skill's testing gotcha) unless the site's
public vhost is already fully wired — hitting the host's `127.0.0.1` with
a `Host:` header before that vhost exists silently falls through to a
different site's `server_name` and produces a misleadingly "fast" TTFB
for the wrong page entirely. Take at least 3 samples and use the median;
a single run is noise.
**2. Autoloaded-options size:**
```bash
ssh root@<host> "bastille cmd <jail> sh -c 'cd <site-path> && su -m www -c \"wp option list --autoload=on --format=total_bytes --path=<site-path>\"'"
```
No fixed "too big" threshold is asserted here — compare against the
site's own history or a healthy sibling jail rather than trusting one
specific byte count as universally bad. To find the biggest individual
offenders, add `--fields=option_name,size_bytes` and sort the output.
**3. Object-cache presence:**
Don't assume every site has a persistent object cache. As of the most
recent audit (`docs/server-granja.md`, `docs/server-cerebro.md`), only
`menorca.pt`/`menorca.ro` on granja have the Redis Object Cache plugin
wired against cerebro's Valkey instance — the other granja jails, and
Cala Verd's MariaDB-only setup, do not. Check, don't assume:
```bash
... wp eval 'echo wp_using_ext_object_cache() ? "yes" : "no";' --path=<site-path>
```
plus confirm the drop-in file exists: `bastille cmd <jail> test -f
<site-path>/wp-content/object-cache.php && echo present`. If present, `wp
redis status` (Redis Object Cache plugin) reports connection health —
Valkey lives on `cerebro`, with per-site ACL credentials; see the
Redaction section below before pasting any of that output verbatim
anywhere.
**4. Cron health:**
```bash
... wp cron event list --path=<site-path>
... wp cron test --path=<site-path>
```
Look for: events stuck far in the past (missed spawns), duplicate events
on the same hook, and any heavy-looking hook (import/sync/content-agent
jobs — several granja sites are fed by hourly RSS/content-agent crons per
`docs/server-granja.md`) that might be running inline on a page load
instead of via a real WP-Cron spawn.
## Step 4 — deeper diagnostics (only if the four baseline metrics don't explain it)
Load [`references/diagnostic-tools.md`](references/diagnostic-tools.md)
for `wp doctor`, `wp profile stage/hook/eval`, and headless Query Monitor
usage via REST response headers — all still read-only, still WP-CLI/curl
based, no browser required.
Load [`references/fix-patterns.md`](references/fix-patterns.md) once a
dominant bottleneck category is identified (database/queries, autoload,
object cache, cron, remote HTTP calls) — condensed fix-pattern guidance
per category, still gated by the same read-only-first / measure-before-
and-after discipline as everything above.
## Redaction — before pasting ANY of this into a report
Diagnostic output on this fleet can surface real secrets. Before quoting
command output verbatim in a report, chat message, or committed doc,
redact:
- `wp config list` / raw `wp-config.php` contents — `DB_PASSWORD`, auth
keys/salts, `WP_REDIS_USERNAME`/`WP_REDIS_PASSWORD` (Valkey ACL
credentials; per `docs/server-cerebro.md`, per-site passwords live at
`/root/.granja_valkey_creds` on cerebro, the admin password at
`/root/.valkey_admin_pw` — never paste these values themselves either).
- `wp option list` output that happens to include API keys stored as
plugin options (payment-gateway keys, mail-relay credentials, etc.).
- Any Application Password generated while setting up headless Query
Monitor REST auth (see `references/diagnostic-tools.md`).
- Full stack traces / DB error output that can echo a connection string.
Follow the `<REDACTED>` convention used elsewhere in this library (see
the `diagnosing-bugs` skill): replace the secret value in place, keep
everything else so the diagnostic signal survives. When in doubt whether
a value is sensitive, redact it — the cost of over-redacting is a
follow-up question; the cost of under-redacting is a leaked credential in
a transcript or a committed doc.
## Verification
- Baseline and after-fix numbers were captured with the *same* method
(Step 3, same site, same `--path`/`--url`, ideally the same time of
day — don't compare a cold-cache run to a warm one).
- `wp cron test` / `wp doctor check` (if installed) are clean or
improved.
- No new PHP errors in the jail's PHP-FPM/WordPress debug log after the
change.
- If the fix required a cache flush, that flush is the *last* step
taken, not a diagnostic shortcut — flushing before measuring erases the
baseline you needed to compare against.
## Failure modes
- **"No change" after a fix** — check you're hitting the same jail IP /
domain (`Host:` header) as the baseline, and that PHP's opcache isn't
serving a stale compiled version (clearing it, or restarting php-fpm,
is itself a diagnostic action worth noting in the report, not a routine
step to take silently).
- **Noisy TTFB/profiling numbers** — an hourly content-agent cron job on
granja sites, or a plugin update check, landing mid-sample can skew a
single `curl` run. Take multiple samples.
- **Object-cache check says "no" but you expected "yes"** — confirm which
site this actually is; only two of granja's 13 jails currently have
Redis Object Cache wired up. Check the inventory before treating that
as a bug.
## Escalation
- Do not install plugins (including the `wp doctor`/`wp profile`
packages themselves), enable `SAVEQUERIES`/`WP_DEBUG` in a way that
could linger, or flush any shared cache without explicit approval —
cerebro's Valkey DB 0 is genuinely shared across sites, and `wp redis
enable`'s `FLUSHDB` call can transiently clear a *different* site's
cache too (confirmed in `docs/server-cerebro.md`). Treat this as
production-adjacent even on a "staging" host — the `staging` host has
hosted real production traffic before (see `docs/server-staging.md`).
- If system-level profiling (an APM, a PHP profiler extension) is needed
beyond WP-CLI's own `profile` package, that's an infra change — flag it
rather than installing it unilaterally on a jail.
## Version note
Do not assume a fleet-wide WordPress version. The upstream `wp-performance`
skill asserts "WordPress 7.0+, PHP 7.4.0+" as its own compatibility
target — treat that as the upstream author's target, not a verified fact
about this fleet. Confirm per-site with `wp core version --path=<site-path>`.
As one dated data point: granja's 13 WordPress jails were confirmed on
core 7.0.4 during the 2026-08-12 fleet-wide security rollout
(`docs/server-granja.md`) — that's a specific fact about one host at one
point in time, not a floor to assume for staging, gringo, or any future
site.