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

12 KiB

name, description
name description
wordpress-performance-diagnostics 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:

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):

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:

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:

... 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:

... 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 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 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.