
Wp Project Triage
- 2.9k installs
- 1.9k repo stars
- Updated July 27, 2026
- wordpress/agent-skills
wp-project-triage is a WordPress skill that detects repo kind, tooling, tests, and version hints via a JSON triage script.
About
WP Project Triage produces a structured JSON report describing what kind of WordPress repository you are in and which commands or conventions to follow before editing code. Compatibility targets WordPress 6.9 plus with PHP 7.2.24 minimum, using a filesystem-based agent with bash and node, and some workflows require WP-CLI. The procedure runs node skills/wp-project-triage/scripts/detect_wp_project.mjs from the repo root, optionally reads triage.schema.json for the output contract, and uses the report to select workflow guardrails for project kind, PHP and Node tooling, tests present, and version hints with sources. Verification requires parseable JSON including project.kind, signals, and tooling keys, with a re-run after structural changes like adding theme.json or block.json. Failure modes include unknown project kind when the repo root is wrong and slow scans that need extended ignore directories in the detector script. Agents should update the detector when signals are missing rather than guessing project type.
- detect_wp_project.mjs prints JSON triage to stdout.
- Reports project kind, tooling, tests, and version hint signals.
- triage.schema.json defines the output contract.
- Re-run after theme.json, block.json, or build config changes.
- Update detector script instead of guessing missing signals.
Wp Project Triage by the numbers
- 2,885 all-time installs (skills.sh)
- +180 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #177 of 3,301 Productivity & Planning skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
wp-project-triage capabilities & compatibility
- Capabilities
- deterministic wp project kind detection · tooling and test presence signals · version hint extraction with sources · json schema output contract · detector update guidance for missing signals
- Use cases
- research · project management
- Pricing
- Free
What wp-project-triage says it does
If the report is missing signals you need, update the detector rather than guessing.
npx skills add https://github.com/wordpress/agent-skills --skill wp-project-triageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.9k |
|---|---|
| repo stars | ★ 1.9k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | wordpress/agent-skills ↗ |
How do I quickly tell if this repo is a plugin, theme, block theme, or core fork before editing?
Deterministically inspect a WordPress repo to detect project kind, tooling, tests, and version hints before making changes.
Who is it for?
Agents or developers entering unfamiliar WordPress repositories who need deterministic project classification.
Skip if: Skip for non-WordPress repos or live production WP-CLI maintenance without code changes.
When should I use this skill?
User needs WordPress repo triage, project kind detection, or tooling discovery before changes.
What you get
Parsed triage JSON with project.kind, signals, tooling, and selected workflow guardrails.
- Structured JSON triage report
- Repo type classification
- Tooling and test command hints
By the numbers
- Targets WordPress 6.9+ and PHP 7.2.24+
- Detects 6 repository types: plugin, theme, block theme, core, Gutenberg, full-site
Files
WP Project Triage
When to use
Use this skill to quickly understand what kind of WordPress repo you’re in and what commands/conventions to follow before making changes.
Inputs required
- Repo root (current working directory).
Procedure
1. Run the detector (prints JSON to stdout):
node skills/wp-project-triage/scripts/detect_wp_project.mjs
2. If you need the exact output contract, read:
skills/wp-project-triage/references/triage.schema.json
3. Use the report to select workflow guardrails:
- project kind(s)
- PHP/Node tooling present
- tests present
- version hints and sources
4. If the report is missing signals you need, update the detector rather than guessing.
Verification
- The JSON should parse and include:
project.kind,signals, andtooling. - Re-run after changes that affect structure/tooling (adding
theme.json,block.json, build config).
Failure modes / debugging
- If it reports
unknown, check whether the repo root is correct. - If scanning is slow, add/extend ignore directories in the script.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://agentskills.local/wp-project-triage/triage.schema.json",
"title": "WP Project Triage Report",
"type": "object",
"required": ["tool", "project", "signals", "tooling"],
"properties": {
"tool": {
"type": "object",
"required": ["name", "version"],
"properties": {
"name": { "type": "string", "const": "detect_wp_project" },
"version": { "type": "string" }
},
"additionalProperties": true
},
"project": {
"type": "object",
"required": ["kind"],
"properties": {
"kind": {
"type": "array",
"items": {
"type": "string",
"enum": [
"unknown",
"wp-plugin",
"wp-mu-plugin",
"wp-theme",
"wp-block-theme",
"wp-block-plugin",
"wp-site",
"wp-core",
"gutenberg"
]
}
},
"primary": { "type": "string" },
"notes": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
},
"signals": {
"type": "object",
"required": ["paths"],
"properties": {
"paths": {
"type": "object",
"properties": {
"repoRoot": { "type": "string" },
"wpContent": { "type": "string" },
"pluginsDir": { "type": "string" },
"themesDir": { "type": "string" }
},
"additionalProperties": true
}
,
"usesInteractivityApi": { "type": "boolean" },
"usesAbilitiesApi": { "type": "boolean" },
"usesInnerBlocks": { "type": "boolean" },
"usesWpCli": { "type": "boolean" },
"performanceHints": { "type": "object", "additionalProperties": true },
"interactivityHints": { "type": "object", "additionalProperties": true },
"abilitiesHints": { "type": "object", "additionalProperties": true },
"innerBlocksHints": { "type": "object", "additionalProperties": true },
"wpCliHints": { "type": "object", "additionalProperties": true }
},
"additionalProperties": true
},
"tooling": {
"type": "object",
"required": ["php", "node", "tests"],
"properties": {
"php": {
"type": "object",
"properties": {
"hasComposerJson": { "type": "boolean" },
"hasVendorDir": { "type": "boolean" },
"phpunitXml": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
},
"node": {
"type": "object",
"properties": {
"hasPackageJson": { "type": "boolean" },
"packageManager": { "type": ["string", "null"], "enum": ["npm", "yarn", "pnpm", "bun", null] },
"usesWordpressScripts": { "type": "boolean" }
},
"additionalProperties": true
},
"tests": {
"type": "object",
"properties": {
"hasPhpUnit": { "type": "boolean" },
"hasWpEnv": { "type": "boolean" },
"hasPlaywright": { "type": "boolean" },
"hasJest": { "type": "boolean" }
},
"additionalProperties": true
}
},
"additionalProperties": true
},
"versions": {
"type": "object",
"properties": {
"wordpress": {
"type": "object",
"properties": {
"core": {
"type": "object",
"properties": {
"value": { "type": ["string", "null"] },
"source": { "type": ["string", "null"] }
},
"additionalProperties": true
}
},
"additionalProperties": true
},
"gutenberg": {
"type": "object",
"properties": {
"value": { "type": ["string", "null"] },
"source": { "type": ["string", "null"] }
},
"additionalProperties": true
}
},
"additionalProperties": true
},
"recommendations": {
"type": "object",
"properties": {
"commands": { "type": "array", "items": { "type": "string" } },
"notes": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
}
},
"additionalProperties": true
}
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const TOOL_VERSION = "0.1.0";
const DEFAULT_IGNORES = new Set([
".git",
"node_modules",
"vendor",
"dist",
"build",
"coverage",
".next",
".turbo",
]);
function statSafe(p) {
try {
return fs.statSync(p);
} catch {
return null;
}
}
function readFileSafe(p, maxBytes = 256 * 1024) {
try {
const buf = fs.readFileSync(p);
if (buf.byteLength > maxBytes) return buf.subarray(0, maxBytes).toString("utf8");
return buf.toString("utf8");
} catch {
return null;
}
}
function scanForTokens(repoRoot, { tokens, exts, maxFiles = 2500, maxDepth = 8 }) {
const loweredTokens = tokens.map((t) => t.toLowerCase());
const matches = new Map();
const { results: files, truncated } = findFilesRecursive(
repoRoot,
(p) => {
const ext = path.extname(p).toLowerCase();
return exts.includes(ext);
},
{ maxFiles, maxDepth }
);
for (const filePath of files) {
const contents = readFileSafe(filePath, 128 * 1024);
if (!contents) continue;
const haystack = contents.toLowerCase();
for (let i = 0; i < loweredTokens.length; i += 1) {
const token = loweredTokens[i];
if (matches.has(token)) continue;
if (haystack.includes(token)) matches.set(token, path.relative(repoRoot, filePath));
}
if (matches.size === loweredTokens.length) break;
}
return {
truncated,
matches: Object.fromEntries([...matches.entries()]),
};
}
function existsFile(p) {
const st = statSafe(p);
return Boolean(st && st.isFile());
}
function existsDir(p) {
const st = statSafe(p);
return Boolean(st && st.isDirectory());
}
function detectPackageManager(repoRoot) {
const hasPnpm = existsFile(path.join(repoRoot, "pnpm-lock.yaml"));
const hasYarn = existsFile(path.join(repoRoot, "yarn.lock"));
const hasNpm = existsFile(path.join(repoRoot, "package-lock.json"));
const hasBun = existsFile(path.join(repoRoot, "bun.lockb")) || existsFile(path.join(repoRoot, "bun.lock"));
if (hasPnpm) return "pnpm";
if (hasYarn) return "yarn";
if (hasBun) return "bun";
if (hasNpm) return "npm";
return null;
}
function findFilesRecursive(repoRoot, predicate, { maxFiles = 6000, maxDepth = 8 } = {}) {
const results = [];
const queue = [{ dir: repoRoot, depth: 0 }];
let visited = 0;
while (queue.length > 0) {
const { dir, depth } = queue.shift();
if (depth > maxDepth) continue;
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
continue;
}
for (const ent of entries) {
const fullPath = path.join(dir, ent.name);
if (ent.isDirectory()) {
if (DEFAULT_IGNORES.has(ent.name)) continue;
queue.push({ dir: fullPath, depth: depth + 1 });
continue;
}
if (!ent.isFile()) continue;
visited += 1;
if (visited > maxFiles) return { results, truncated: true };
if (predicate(fullPath)) results.push(fullPath);
}
}
return { results, truncated: false };
}
function detectPluginHeaderFromPhpFile(filePath) {
const contents = readFileSafe(filePath, 128 * 1024);
if (!contents) return null;
const headerMatch = contents.match(/^\s*Plugin Name:\s*(.+)\s*$/im);
if (!headerMatch) return null;
return headerMatch[1].trim();
}
function detectThemeHeaderFromStyleCss(filePath) {
const contents = readFileSafe(filePath, 128 * 1024);
if (!contents) return null;
const headerMatch = contents.match(/^\s*Theme Name:\s*(.+)\s*$/im);
if (!headerMatch) return null;
return headerMatch[1].trim();
}
function guessWpCoreVersionFromCheckout(repoRoot) {
const versionPhp = path.join(repoRoot, "wp-includes", "version.php");
if (!existsFile(versionPhp)) return { value: null, source: null };
const contents = readFileSafe(versionPhp, 64 * 1024);
if (!contents) return { value: null, source: null };
const match = contents.match(/\$wp_version\s*=\s*'([^']+)'/);
if (!match) return { value: null, source: "wp-includes/version.php" };
return { value: match[1], source: "wp-includes/version.php" };
}
function guessGutenbergVersion(repoRoot) {
const gutenbergPackageJson = path.join(repoRoot, "packages", "plugins", "package.json");
const rootPackageJson = path.join(repoRoot, "package.json");
for (const candidate of [gutenbergPackageJson, rootPackageJson]) {
if (!existsFile(candidate)) continue;
const txt = readFileSafe(candidate);
if (!txt) continue;
try {
const pkg = JSON.parse(txt);
if (pkg?.name === "@wordpress/plugins" && typeof pkg?.version === "string") {
return { value: pkg.version, source: path.relative(repoRoot, candidate) };
}
if (pkg?.name === "gutenberg" && typeof pkg?.version === "string") {
return { value: pkg.version, source: path.relative(repoRoot, candidate) };
}
} catch {
// ignore
}
}
return { value: null, source: null };
}
function parsePackageJson(repoRoot) {
const p = path.join(repoRoot, "package.json");
if (!existsFile(p)) return null;
const txt = readFileSafe(p);
if (!txt) return null;
try {
return JSON.parse(txt);
} catch {
return null;
}
}
function parseComposerJson(repoRoot) {
const p = path.join(repoRoot, "composer.json");
if (!existsFile(p)) return null;
const txt = readFileSafe(p);
if (!txt) return null;
try {
return JSON.parse(txt);
} catch {
return null;
}
}
function detectConfigConstants(repoRoot) {
const { results: configFiles } = findFilesRecursive(repoRoot, (p) => path.basename(p) === "wp-config.php", {
maxFiles: 4000,
maxDepth: 4,
});
const configPath = configFiles[0] ?? null;
if (!configPath) {
return { source: null, constants: {} };
}
const contents = readFileSafe(configPath, 256 * 1024);
if (!contents) return { source: path.relative(repoRoot, configPath), constants: {} };
const c = contents;
const enabled = (name) =>
new RegExp(`define\\(\\s*['"]${name}['"]\\s*,\\s*(true|1)\\s*\\)`, "i").test(c) ||
new RegExp(`\\b${name}\\b\\s*=\\s*(true|1)`, "i").test(c);
const mentioned = (name) => new RegExp(`\\b${name}\\b`, "i").test(c);
return {
source: path.relative(repoRoot, configPath),
constants: {
savequeriesMentioned: mentioned("SAVEQUERIES"),
savequeriesEnabled: enabled("SAVEQUERIES"),
wpDebugMentioned: mentioned("WP_DEBUG"),
wpDebugEnabled: enabled("WP_DEBUG"),
disableWpCronMentioned: mentioned("DISABLE_WP_CRON"),
disableWpCronEnabled: enabled("DISABLE_WP_CRON"),
},
};
}
function detectKinds(repoRoot, signals) {
const kinds = new Set();
if (signals.isGutenbergRepo) kinds.add("gutenberg");
if (signals.isWpCoreCheckout) kinds.add("wp-core");
if (signals.hasWpContentDir) kinds.add("wp-site");
if (signals.detectedThemeName) kinds.add(signals.isBlockTheme ? "wp-block-theme" : "wp-theme");
if (signals.detectedPluginName) kinds.add(signals.isBlockPlugin ? "wp-block-plugin" : "wp-plugin");
if (signals.hasMuPluginsDir) kinds.add("wp-mu-plugin");
if (kinds.size === 0) kinds.add("unknown");
const priority = [
"gutenberg",
"wp-core",
"wp-site",
"wp-block-theme",
"wp-block-plugin",
"wp-theme",
"wp-mu-plugin",
"wp-plugin",
"unknown",
];
let primary = "unknown";
for (const k of priority) {
if (kinds.has(k)) {
primary = k;
break;
}
}
return { kind: [...kinds], primary };
}
function buildRecommendations({ repoRoot, primaryKind, packageManager, packageJson, composerJson, tooling, signals }) {
const commands = [];
const notes = [];
if (tooling.node.hasPackageJson) {
const pm = packageManager ?? "npm";
const run = pm === "yarn" ? "yarn" : `${pm} run`;
const hasScript = (name) => Boolean(packageJson?.scripts && Object.prototype.hasOwnProperty.call(packageJson.scripts, name));
if (hasScript("lint")) commands.push(`${run} lint`);
if (hasScript("test")) commands.push(`${run} test`);
if (hasScript("build")) commands.push(`${run} build`);
if (hasScript("start")) commands.push(`${run} start`);
if (tooling.node.usesWordpressScripts) notes.push("Detected @wordpress/scripts usage; prefer its standard lint/build/test scripts.");
}
if (tooling.php.hasComposerJson) {
commands.push("composer install");
if (tooling.php.phpunitXml.length > 0) commands.push("vendor/bin/phpunit");
}
if (tooling.tests.hasWpEnv) notes.push("Detected wp-env; E2E workflows may rely on Docker.");
if (signals.scanTruncated) notes.push("Scan truncated due to file limit; some signals may be missing.");
if (primaryKind === "unknown") notes.push("Could not confidently classify repo; inspect root for plugin/theme headers or wp-content structure.");
return { commands, notes };
}
function main() {
const repoRoot = process.cwd();
const wpContent = path.join(repoRoot, "wp-content");
const pluginsDir = path.join(wpContent, "plugins");
const muPluginsDir = path.join(wpContent, "mu-plugins");
const themesDir = path.join(wpContent, "themes");
const isWpCoreCheckout = existsFile(path.join(repoRoot, "wp-includes", "version.php"));
const isGutenbergRepo =
existsDir(path.join(repoRoot, "packages")) &&
(existsDir(path.join(repoRoot, "packages", "block-editor")) || existsDir(path.join(repoRoot, "packages", "components")));
const packageJson = parsePackageJson(repoRoot);
const composerJson = parseComposerJson(repoRoot);
const packageManager = detectPackageManager(repoRoot);
const usesWordpressScripts = Boolean(
packageJson?.devDependencies?.["@wordpress/scripts"] ||
packageJson?.dependencies?.["@wordpress/scripts"] ||
packageJson?.scripts?.build?.includes("wp-scripts") ||
packageJson?.scripts?.start?.includes("wp-scripts") ||
packageJson?.scripts?.test?.includes("wp-scripts") ||
packageJson?.scripts?.lint?.includes("wp-scripts")
);
const pkgHasInteractivity = Boolean(
packageJson?.devDependencies?.["@wordpress/interactivity"] || packageJson?.dependencies?.["@wordpress/interactivity"]
);
const pkgHasAbilities = Boolean(
packageJson?.devDependencies?.["@wordpress/abilities"] || packageJson?.dependencies?.["@wordpress/abilities"]
);
const hasWpContentDir = existsDir(wpContent);
const hasPluginsDir = existsDir(pluginsDir);
const hasThemesDir = existsDir(themesDir);
const hasMuPluginsDir = existsDir(muPluginsDir);
const config = detectConfigConstants(repoRoot);
const pluginCandidates = [];
const themeCandidates = [];
// Root-level plugin/theme detection (common when repo root is the plugin/theme).
for (const entry of fs.readdirSync(repoRoot, { withFileTypes: true })) {
if (!entry.isFile()) continue;
if (entry.name.toLowerCase().endsWith(".php")) pluginCandidates.push(path.join(repoRoot, entry.name));
if (entry.name === "style.css") themeCandidates.push(path.join(repoRoot, entry.name));
}
let detectedPluginName = null;
for (const phpFile of pluginCandidates) {
detectedPluginName = detectPluginHeaderFromPhpFile(phpFile);
if (detectedPluginName) break;
}
let detectedThemeName = null;
for (const styleCss of themeCandidates) {
detectedThemeName = detectThemeHeaderFromStyleCss(styleCss);
if (detectedThemeName) break;
}
const { results: blockJsonFiles, truncated: scanTruncated } = findFilesRecursive(
repoRoot,
(p) => path.basename(p) === "block.json",
{ maxFiles: 6000, maxDepth: 8 }
);
const { results: themeJsonFiles } = findFilesRecursive(repoRoot, (p) => path.basename(p) === "theme.json", {
maxFiles: 6000,
maxDepth: 8,
});
const templatesDirCandidates = [
path.join(repoRoot, "templates"),
path.join(repoRoot, "parts"),
path.join(repoRoot, "patterns"),
];
const isBlockTheme = themeJsonFiles.length > 0 && templatesDirCandidates.some((p) => existsDir(p));
const isBlockPlugin = blockJsonFiles.length > 0;
const interactivityScan = scanForTokens(repoRoot, {
tokens: ["data-wp-interactive", "@wordpress/interactivity", "viewScriptModule"],
exts: [".php", ".js", ".ts", ".tsx", ".json", ".html"],
maxFiles: 2500,
maxDepth: 8,
});
const abilitiesScan = scanForTokens(repoRoot, {
tokens: [
"wp_register_ability(",
"wp_register_ability_category(",
"wp_abilities_api_init",
"wp_abilities_api_categories_init",
"wp-abilities/v1",
"@wordpress/abilities",
],
exts: [".php", ".js", ".ts", ".tsx"],
maxFiles: 2500,
maxDepth: 8,
});
const innerBlocksScan = scanForTokens(repoRoot, {
tokens: ["InnerBlocks", "useInnerBlocksProps", "InnerBlocks.Content"],
exts: [".js", ".ts", ".tsx"],
maxFiles: 2500,
maxDepth: 8,
});
const wpCliConfigBasenames = new Set([
"wp-cli.yml",
"wp-cli.yaml",
"wp-cli.local.yml",
"wp-cli.local.yaml",
".wp-cli.yml",
".wp-cli.yaml",
]);
const { results: wpCliConfigFiles, truncated: wpCliConfigTruncated } = findFilesRecursive(
repoRoot,
(p) => wpCliConfigBasenames.has(path.basename(p)),
{ maxFiles: 6000, maxDepth: 6 }
);
const composerRequire = composerJson?.require && typeof composerJson.require === "object" ? composerJson.require : {};
const composerRequireDev =
composerJson?.["require-dev"] && typeof composerJson["require-dev"] === "object" ? composerJson["require-dev"] : {};
const composerHasWpCli = Boolean(
composerRequire["wp-cli/wp-cli"] ||
composerRequireDev["wp-cli/wp-cli"] ||
composerRequire["wp-cli/wp-cli-bundle"] ||
composerRequireDev["wp-cli/wp-cli-bundle"]
);
const wpCliTokenScan = scanForTokens(repoRoot, {
tokens: [
"wp search-replace",
"wp db export",
"wp db import",
"wp cron event",
"wp cache flush",
"wp rewrite flush",
"wp plugin update",
"wp theme update",
],
exts: [".sh", ".yml", ".yaml", ".js", ".ts", ".php", ".json"],
maxFiles: 2500,
maxDepth: 8,
});
const usesInteractivityApi = pkgHasInteractivity || Object.keys(interactivityScan.matches).length > 0;
const usesAbilitiesApi = pkgHasAbilities || Object.keys(abilitiesScan.matches).length > 0;
const usesInnerBlocks = Object.keys(innerBlocksScan.matches).length > 0;
const usesWpCli = composerHasWpCli || wpCliConfigFiles.length > 0 || Object.keys(wpCliTokenScan.matches).length > 0;
const wpContentRoot = path.join(repoRoot, "wp-content");
const hasObjectCacheDropin = existsFile(path.join(wpContentRoot, "object-cache.php"));
const hasAdvancedCacheDropin = existsFile(path.join(wpContentRoot, "advanced-cache.php"));
const hasDbDropin = existsFile(path.join(wpContentRoot, "db.php"));
const hasSunriseDropin = existsFile(path.join(wpContentRoot, "sunrise.php"));
const hasQueryMonitorPlugin = existsDir(path.join(wpContentRoot, "plugins", "query-monitor"));
const hasPerformanceLabPlugin = existsDir(path.join(wpContentRoot, "plugins", "performance-lab"));
const phpunitXml = [];
for (const candidate of ["phpunit.xml", "phpunit.xml.dist"]) {
const full = path.join(repoRoot, candidate);
if (existsFile(full)) phpunitXml.push(candidate);
}
const hasWpEnv =
existsFile(path.join(repoRoot, ".wp-env.json")) ||
existsFile(path.join(repoRoot, ".wp-env.override.json")) ||
Boolean(packageJson?.devDependencies?.["@wordpress/env"] || packageJson?.dependencies?.["@wordpress/env"]);
const hasPlaywright = Boolean(
packageJson?.devDependencies?.["@playwright/test"] ||
packageJson?.dependencies?.["@playwright/test"] ||
packageJson?.devDependencies?.["@wordpress/e2e-test-utils-playwright"] ||
packageJson?.dependencies?.["@wordpress/e2e-test-utils-playwright"]
);
const hasJest = Boolean(
packageJson?.devDependencies?.jest ||
packageJson?.dependencies?.jest ||
packageJson?.devDependencies?.["@wordpress/jest-preset-default"] ||
packageJson?.dependencies?.["@wordpress/jest-preset-default"]
);
const hasPhpUnit = phpunitXml.length > 0 || Boolean(composerJson?.requireDev?.phpunit || composerJson?.["require-dev"]?.phpunit);
const signals = {
paths: {
repoRoot,
wpContent: hasWpContentDir ? wpContent : null,
pluginsDir: hasPluginsDir ? pluginsDir : null,
themesDir: hasThemesDir ? themesDir : null,
muPluginsDir: hasMuPluginsDir ? muPluginsDir : null,
},
isWpCoreCheckout,
isGutenbergRepo,
hasWpContentDir,
hasPluginsDir,
hasThemesDir,
hasMuPluginsDir,
detectedPluginName,
detectedThemeName,
isBlockPlugin,
isBlockTheme,
usesInteractivityApi,
usesAbilitiesApi,
usesInnerBlocks,
usesWpCli,
performanceHints: {
wpConfig: config.source,
constants: config.constants,
dropins: {
objectCache: hasObjectCacheDropin,
advancedCache: hasAdvancedCacheDropin,
db: hasDbDropin,
sunrise: hasSunriseDropin,
},
plugins: {
queryMonitor: hasQueryMonitorPlugin,
performanceLab: hasPerformanceLabPlugin,
},
},
interactivityHints: {
packageJson: pkgHasInteractivity,
matches: interactivityScan.matches,
scanTruncated: interactivityScan.truncated,
},
abilitiesHints: {
packageJson: pkgHasAbilities,
matches: abilitiesScan.matches,
scanTruncated: abilitiesScan.truncated,
},
innerBlocksHints: {
matches: innerBlocksScan.matches,
scanTruncated: innerBlocksScan.truncated,
},
wpCliHints: {
configFiles: wpCliConfigFiles.map((p) => path.relative(repoRoot, p)).slice(0, 50),
configScanTruncated: wpCliConfigTruncated,
composerJson: composerHasWpCli,
matches: wpCliTokenScan.matches,
scanTruncated: wpCliTokenScan.truncated,
},
blockJsonFiles: blockJsonFiles.map((p) => path.relative(repoRoot, p)).slice(0, 50),
themeJsonFiles: themeJsonFiles.map((p) => path.relative(repoRoot, p)).slice(0, 50),
scanTruncated,
};
const { kind, primary } = detectKinds(repoRoot, signals);
const versions = {
wordpress: {
core: guessWpCoreVersionFromCheckout(repoRoot),
},
gutenberg: guessGutenbergVersion(repoRoot),
};
const tooling = {
php: {
hasComposerJson: existsFile(path.join(repoRoot, "composer.json")),
hasVendorDir: existsDir(path.join(repoRoot, "vendor")),
phpunitXml,
},
node: {
hasPackageJson: existsFile(path.join(repoRoot, "package.json")),
packageManager,
usesWordpressScripts,
},
tests: {
hasPhpUnit,
hasWpEnv,
hasPlaywright,
hasJest,
},
};
const recommendations = buildRecommendations({
repoRoot,
primaryKind: primary,
packageManager,
packageJson,
composerJson,
tooling,
signals,
});
const report = {
tool: { name: "detect_wp_project", version: TOOL_VERSION },
project: { kind, primary, notes: [] },
signals,
tooling,
versions,
recommendations,
};
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
}
main();
Related skills
How it compares
Use wp-project-triage before any WordPress edit skill when repo type and test commands are unknown; skip it on well-documented internal projects.
FAQ
What command produces the triage JSON?
Run node skills/wp-project-triage/scripts/detect_wp_project.mjs from the repo root.
What fields must the JSON include?
It should include project.kind, signals, and tooling at minimum.
What if kind is unknown?
Confirm the repo root is correct and extend ignore dirs or detector rules rather than guessing.
Is Wp Project Triage safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.