@ai-kits/wp-ag-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/ANTIGRAVITY-README.md +47 -0
  2. package/CONTRIBUTING.md +122 -0
  3. package/README.md +135 -0
  4. package/STRUCTURE.md +200 -0
  5. package/agents/wordpress-expert.md +36 -0
  6. package/agents/wp-frontend-expert.md +21 -0
  7. package/bin/antigravity-agent.js +159 -0
  8. package/docs/authoring-guide.md +56 -0
  9. package/docs/compatibility-policy.md +18 -0
  10. package/docs/packaging.md +26 -0
  11. package/docs/principles.md +7 -0
  12. package/docs/skill-set-v1.md +21 -0
  13. package/docs/upstream-sync.md +52 -0
  14. package/package.json +47 -0
  15. package/rules/GEMINI.md +273 -0
  16. package/shared/references/.gitkeep +1 -0
  17. package/shared/references/gutenberg-releases.json +155 -0
  18. package/shared/references/wordpress-core-versions.json +208 -0
  19. package/shared/references/wp-gutenberg-version-map.json +886 -0
  20. package/shared/scripts/ai-generate-updates.mjs +458 -0
  21. package/shared/scripts/scaffold-skill.mjs +62 -0
  22. package/shared/scripts/skillpack-build.mjs +165 -0
  23. package/shared/scripts/skillpack-install.mjs +275 -0
  24. package/shared/scripts/update-upstream-indices.mjs +173 -0
  25. package/skills/wordpress-router/SKILL.md +51 -0
  26. package/skills/wordpress-router/references/decision-tree.md +55 -0
  27. package/skills/wp-abilities-api/SKILL.md +95 -0
  28. package/skills/wp-abilities-api/references/php-registration.md +67 -0
  29. package/skills/wp-abilities-api/references/rest-api.md +13 -0
  30. package/skills/wp-block-development/SKILL.md +174 -0
  31. package/skills/wp-block-development/references/attributes-and-serialization.md +22 -0
  32. package/skills/wp-block-development/references/block-json.md +49 -0
  33. package/skills/wp-block-development/references/creating-new-blocks.md +46 -0
  34. package/skills/wp-block-development/references/debugging.md +36 -0
  35. package/skills/wp-block-development/references/deprecations.md +24 -0
  36. package/skills/wp-block-development/references/dynamic-rendering.md +23 -0
  37. package/skills/wp-block-development/references/inner-blocks.md +25 -0
  38. package/skills/wp-block-development/references/registration.md +30 -0
  39. package/skills/wp-block-development/references/supports-and-wrappers.md +18 -0
  40. package/skills/wp-block-development/references/tooling-and-testing.md +21 -0
  41. package/skills/wp-block-development/scripts/list_blocks.mjs +121 -0
  42. package/skills/wp-block-themes/SKILL.md +116 -0
  43. package/skills/wp-block-themes/references/creating-new-block-theme.md +37 -0
  44. package/skills/wp-block-themes/references/debugging.md +24 -0
  45. package/skills/wp-block-themes/references/patterns.md +18 -0
  46. package/skills/wp-block-themes/references/style-variations.md +14 -0
  47. package/skills/wp-block-themes/references/templates-and-parts.md +16 -0
  48. package/skills/wp-block-themes/references/theme-json.md +59 -0
  49. package/skills/wp-block-themes/scripts/detect_block_themes.mjs +117 -0
  50. package/skills/wp-interactivity-api/SKILL.md +179 -0
  51. package/skills/wp-interactivity-api/references/debugging.md +29 -0
  52. package/skills/wp-interactivity-api/references/directives-quickref.md +30 -0
  53. package/skills/wp-interactivity-api/references/server-side-rendering.md +310 -0
  54. package/skills/wp-performance/SKILL.md +146 -0
  55. package/skills/wp-performance/references/autoload-options.md +24 -0
  56. package/skills/wp-performance/references/cron.md +20 -0
  57. package/skills/wp-performance/references/database.md +20 -0
  58. package/skills/wp-performance/references/http-api.md +15 -0
  59. package/skills/wp-performance/references/measurement.md +21 -0
  60. package/skills/wp-performance/references/object-cache.md +24 -0
  61. package/skills/wp-performance/references/query-monitor-headless.md +38 -0
  62. package/skills/wp-performance/references/server-timing.md +22 -0
  63. package/skills/wp-performance/references/wp-cli-doctor.md +24 -0
  64. package/skills/wp-performance/references/wp-cli-profile.md +32 -0
  65. package/skills/wp-performance/scripts/perf_inspect.mjs +128 -0
  66. package/skills/wp-phpstan/SKILL.md +97 -0
  67. package/skills/wp-phpstan/references/configuration.md +52 -0
  68. package/skills/wp-phpstan/references/third-party-classes.md +76 -0
  69. package/skills/wp-phpstan/references/wordpress-annotations.md +124 -0
  70. package/skills/wp-phpstan/scripts/phpstan_inspect.mjs +263 -0
  71. package/skills/wp-playground/SKILL.md +101 -0
  72. package/skills/wp-playground/references/blueprints.md +36 -0
  73. package/skills/wp-playground/references/cli-commands.md +39 -0
  74. package/skills/wp-playground/references/debugging.md +16 -0
  75. package/skills/wp-plugin-development/SKILL.md +112 -0
  76. package/skills/wp-plugin-development/references/data-and-cron.md +19 -0
  77. package/skills/wp-plugin-development/references/debugging.md +19 -0
  78. package/skills/wp-plugin-development/references/lifecycle.md +33 -0
  79. package/skills/wp-plugin-development/references/security.md +29 -0
  80. package/skills/wp-plugin-development/references/settings-api.md +22 -0
  81. package/skills/wp-plugin-development/references/structure.md +16 -0
  82. package/skills/wp-plugin-development/scripts/detect_plugins.mjs +122 -0
  83. package/skills/wp-project-triage/SKILL.md +38 -0
  84. package/skills/wp-project-triage/references/triage.schema.json +143 -0
  85. package/skills/wp-project-triage/scripts/detect_wp_project.mjs +592 -0
  86. package/skills/wp-rest-api/SKILL.md +114 -0
  87. package/skills/wp-rest-api/references/authentication.md +18 -0
  88. package/skills/wp-rest-api/references/custom-content-types.md +20 -0
  89. package/skills/wp-rest-api/references/discovery-and-params.md +20 -0
  90. package/skills/wp-rest-api/references/responses-and-fields.md +30 -0
  91. package/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
  92. package/skills/wp-rest-api/references/schema.md +22 -0
  93. package/skills/wp-wpcli-and-ops/SKILL.md +123 -0
  94. package/skills/wp-wpcli-and-ops/references/automation.md +30 -0
  95. package/skills/wp-wpcli-and-ops/references/cron-and-cache.md +23 -0
  96. package/skills/wp-wpcli-and-ops/references/debugging.md +17 -0
  97. package/skills/wp-wpcli-and-ops/references/multisite.md +22 -0
  98. package/skills/wp-wpcli-and-ops/references/packages-and-updates.md +22 -0
  99. package/skills/wp-wpcli-and-ops/references/safety.md +30 -0
  100. package/skills/wp-wpcli-and-ops/references/search-replace.md +40 -0
  101. package/skills/wp-wpcli-and-ops/scripts/wpcli_inspect.mjs +90 -0
  102. package/skills/wpds/SKILL.md +58 -0
  103. package/workflows/create-block.md +27 -0
  104. package/workflows/wp-lint.md +27 -0
@@ -0,0 +1,275 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+
5
+ function usage() {
6
+ process.stderr.write(
7
+ [
8
+ "Usage:",
9
+ " node shared/scripts/skillpack-install.mjs --dest=<repo-root> [options]",
10
+ "",
11
+ "Options:",
12
+ " --dest=<path> Destination repo root (required, unless using --global)",
13
+ " --from=<path> Source directory (default: dist)",
14
+ " --targets=<list> Comma-separated targets: codex, vscode, claude, claude-global (default: codex,vscode)",
15
+ " --skills=<list> Comma-separated skill names to install (default: all)",
16
+ " --mode=<mode> 'replace' (default) or 'merge'",
17
+ " --global Shorthand for --targets=claude-global (installs to ~/.claude/skills)",
18
+ " --dry-run Show what would be installed without making changes",
19
+ " --list List available skills and exit",
20
+ "",
21
+ "Targets:",
22
+ " codex Install to <dest>/.codex/skills/",
23
+ " vscode Install to <dest>/.github/skills/",
24
+ " claude Install to <dest>/.claude/skills/ (project-level)",
25
+ " claude-global Install to ~/.claude/skills/ (user-level, ignores --dest)",
26
+ "",
27
+ "Examples:",
28
+ " # Build and install to a WordPress project",
29
+ " node shared/scripts/skillpack-build.mjs --clean",
30
+ " node shared/scripts/skillpack-install.mjs --dest=../my-wp-repo --targets=codex,vscode,claude",
31
+ "",
32
+ " # Install globally for Claude Code (all skills)",
33
+ " node shared/scripts/skillpack-install.mjs --global",
34
+ "",
35
+ " # Install specific skills globally",
36
+ " node shared/scripts/skillpack-install.mjs --global --skills=wp-playground,wp-block-development",
37
+ "",
38
+ " # Install to project with specific skills",
39
+ " node shared/scripts/skillpack-install.mjs --dest=../my-repo --targets=claude --skills=wp-wpcli-and-ops",
40
+ "",
41
+ ].join("\n")
42
+ );
43
+ }
44
+
45
+ function parseArgs(argv) {
46
+ const args = {
47
+ from: "dist",
48
+ dest: null,
49
+ targets: ["codex", "vscode"],
50
+ skills: [],
51
+ mode: "replace",
52
+ dryRun: false,
53
+ global: false,
54
+ list: false,
55
+ };
56
+
57
+ for (const a of argv) {
58
+ if (a === "--help" || a === "-h") args.help = true;
59
+ else if (a === "--dry-run") args.dryRun = true;
60
+ else if (a === "--global") args.global = true;
61
+ else if (a === "--list") args.list = true;
62
+ else if (a.startsWith("--from=")) args.from = a.slice("--from=".length);
63
+ else if (a.startsWith("--dest=")) args.dest = a.slice("--dest=".length);
64
+ else if (a.startsWith("--targets=")) args.targets = a.slice("--targets=".length).split(",").filter(Boolean);
65
+ else if (a.startsWith("--skills=")) args.skills = a.slice("--skills=".length).split(",").filter(Boolean);
66
+ else if (a.startsWith("--mode=")) args.mode = a.slice("--mode=".length);
67
+ else {
68
+ process.stderr.write(`Unknown arg: ${a}\n`);
69
+ args.help = true;
70
+ }
71
+ }
72
+
73
+ // --global is shorthand for --targets=claude-global
74
+ if (args.global) {
75
+ args.targets = ["claude-global"];
76
+ }
77
+
78
+ return args;
79
+ }
80
+
81
+ function assert(condition, message) {
82
+ if (!condition) throw new Error(message);
83
+ }
84
+
85
+ function isSymlink(p) {
86
+ try {
87
+ return fs.lstatSync(p).isSymbolicLink();
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ function copyFileSyncPreserveMode(src, dest) {
94
+ const st = fs.statSync(src);
95
+ fs.copyFileSync(src, dest);
96
+ fs.chmodSync(dest, st.mode);
97
+ }
98
+
99
+ function copyDir({ srcDir, destDir }) {
100
+ if (isSymlink(srcDir)) throw new Error(`Refusing to copy symlink dir: ${srcDir}`);
101
+ fs.mkdirSync(destDir, { recursive: true });
102
+
103
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
104
+ for (const ent of entries) {
105
+ if (ent.name === ".DS_Store") continue;
106
+ const src = path.join(srcDir, ent.name);
107
+ const dest = path.join(destDir, ent.name);
108
+
109
+ if (isSymlink(src)) throw new Error(`Refusing to copy symlink: ${src}`);
110
+
111
+ if (ent.isDirectory()) {
112
+ copyDir({ srcDir: src, destDir: dest });
113
+ continue;
114
+ }
115
+ if (ent.isFile()) {
116
+ copyFileSyncPreserveMode(src, dest);
117
+ continue;
118
+ }
119
+ }
120
+ }
121
+
122
+ function listSkillDirs(skillsRoot) {
123
+ if (!fs.existsSync(skillsRoot)) return [];
124
+ return fs
125
+ .readdirSync(skillsRoot, { withFileTypes: true })
126
+ .filter((d) => d.isDirectory())
127
+ .map((d) => path.join(skillsRoot, d.name))
128
+ .filter((d) => fs.existsSync(path.join(d, "SKILL.md")));
129
+ }
130
+
131
+ const VALID_TARGETS = ["codex", "vscode", "claude", "claude-global"];
132
+
133
+ // Map target to source subdirectory in dist
134
+ function getSourceDir(fromDir, target) {
135
+ // claude-global uses the same source as claude
136
+ const sourceTarget = target === "claude-global" ? "claude" : target;
137
+ const targetDirMap = {
138
+ codex: path.join(fromDir, "codex", ".codex", "skills"),
139
+ vscode: path.join(fromDir, "vscode", ".github", "skills"),
140
+ claude: path.join(fromDir, "claude", ".claude", "skills"),
141
+ };
142
+ return targetDirMap[sourceTarget];
143
+ }
144
+
145
+ // Map target to destination directory
146
+ function getDestDir(destRepoRoot, target) {
147
+ // claude-global doesn't need destRepoRoot
148
+ if (target === "claude-global") {
149
+ return path.join(os.homedir(), ".claude", "skills");
150
+ }
151
+
152
+ // Other targets require destRepoRoot
153
+ const destDirMap = {
154
+ codex: path.join(destRepoRoot, ".codex", "skills"),
155
+ vscode: path.join(destRepoRoot, ".github", "skills"),
156
+ claude: path.join(destRepoRoot, ".claude", "skills"),
157
+ };
158
+ return destDirMap[target];
159
+ }
160
+
161
+ function installTarget({ fromDir, destRepoRoot, target, skillsFilter, mode, dryRun }) {
162
+ const srcSkillsRoot = getSourceDir(fromDir, target);
163
+ const destSkillsRoot = getDestDir(destRepoRoot, target);
164
+
165
+ assert(srcSkillsRoot, `Unknown target: ${target}`);
166
+ assert(fs.existsSync(srcSkillsRoot), `Missing source skillpack dir: ${srcSkillsRoot}. Did you run skillpack-build.mjs first?`);
167
+
168
+ let skillDirs = listSkillDirs(srcSkillsRoot);
169
+ assert(skillDirs.length > 0, `No skills found in: ${srcSkillsRoot}`);
170
+
171
+ // Filter skills if requested
172
+ if (skillsFilter.length > 0) {
173
+ const requested = new Set(skillsFilter);
174
+ const available = skillDirs.map((d) => path.basename(d));
175
+
176
+ // Validate requested skills exist
177
+ for (const s of requested) {
178
+ assert(available.includes(s), `Unknown skill: ${s}. Available: ${available.join(", ")}`);
179
+ }
180
+
181
+ skillDirs = skillDirs.filter((d) => requested.has(path.basename(d)));
182
+ }
183
+
184
+ if (dryRun) {
185
+ process.stdout.write(`[DRY-RUN] Would install ${skillDirs.length} skill(s) to ${destSkillsRoot}:\n`);
186
+ for (const d of skillDirs) {
187
+ process.stdout.write(` - ${path.basename(d)}\n`);
188
+ }
189
+ return;
190
+ }
191
+
192
+ fs.mkdirSync(destSkillsRoot, { recursive: true });
193
+
194
+ for (const srcSkillDir of skillDirs) {
195
+ const name = path.basename(srcSkillDir);
196
+ const destSkillDir = path.join(destSkillsRoot, name);
197
+
198
+ if (mode === "replace") {
199
+ fs.rmSync(destSkillDir, { recursive: true, force: true });
200
+ }
201
+
202
+ copyDir({ srcDir: srcSkillDir, destDir: destSkillDir });
203
+ }
204
+
205
+ const location = target === "claude-global" ? destSkillsRoot : path.relative(destRepoRoot, destSkillsRoot) || ".";
206
+ process.stdout.write(`OK: installed ${skillDirs.length} skill(s) to ${location}\n`);
207
+ }
208
+
209
+ function listAvailableSkills(fromDir) {
210
+ // Check all possible target sources
211
+ const sources = ["codex", "vscode", "claude"].map((t) => getSourceDir(fromDir, t)).filter((p) => fs.existsSync(p));
212
+
213
+ if (sources.length === 0) {
214
+ process.stderr.write("No built skills found. Run skillpack-build.mjs first.\n");
215
+ process.exit(1);
216
+ }
217
+
218
+ const skillDirs = listSkillDirs(sources[0]);
219
+ process.stdout.write("Available skills:\n");
220
+ for (const d of skillDirs) {
221
+ process.stdout.write(` - ${path.basename(d)}\n`);
222
+ }
223
+ }
224
+
225
+ function main() {
226
+ const args = parseArgs(process.argv.slice(2));
227
+ if (args.help) {
228
+ usage();
229
+ process.exit(2);
230
+ }
231
+
232
+ const repoRoot = process.cwd();
233
+ const fromDir = path.isAbsolute(args.from) ? args.from : path.join(repoRoot, args.from);
234
+
235
+ // Handle --list
236
+ if (args.list) {
237
+ listAvailableSkills(fromDir);
238
+ process.exit(0);
239
+ }
240
+
241
+ // Validate targets
242
+ const targets = [...new Set(args.targets)];
243
+ for (const t of targets) {
244
+ assert(VALID_TARGETS.includes(t), `Invalid target: ${t}. Valid targets: ${VALID_TARGETS.join(", ")}`);
245
+ }
246
+
247
+ // --dest is required unless only using claude-global
248
+ const needsDest = targets.some((t) => t !== "claude-global");
249
+ if (needsDest && !args.dest) {
250
+ process.stderr.write("Error: --dest is required for non-global targets.\n\n");
251
+ usage();
252
+ process.exit(1);
253
+ }
254
+
255
+ const destRepoRoot = args.dest
256
+ ? path.isAbsolute(args.dest)
257
+ ? args.dest
258
+ : path.join(repoRoot, args.dest)
259
+ : null;
260
+
261
+ assert(args.mode === "replace" || args.mode === "merge", "mode must be 'replace' or 'merge'");
262
+
263
+ for (const target of targets) {
264
+ installTarget({
265
+ fromDir,
266
+ destRepoRoot,
267
+ target,
268
+ skillsFilter: args.skills,
269
+ mode: args.mode,
270
+ dryRun: args.dryRun,
271
+ });
272
+ }
273
+ }
274
+
275
+ main();
@@ -0,0 +1,173 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const SOURCES = {
5
+ wordpressCoreVersionCheck: "https://api.wordpress.org/core/version-check/1.7/",
6
+ gutenbergReleases: "https://api.github.com/repos/WordPress/gutenberg/releases?per_page=50",
7
+ wpGutenbergMapDoc:
8
+ "https://developer.wordpress.org/block-editor/contributors/versions-in-wordpress/",
9
+ };
10
+
11
+ function mkdirp(dirPath) {
12
+ fs.mkdirSync(dirPath, { recursive: true });
13
+ }
14
+
15
+ function writeJson(filePath, value) {
16
+ mkdirp(path.dirname(filePath));
17
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
18
+ }
19
+
20
+ function stripTags(html) {
21
+ return html
22
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
23
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
24
+ .replace(/<[^>]+>/g, " ")
25
+ .replace(/\s+/g, " ")
26
+ .trim();
27
+ }
28
+
29
+ function decodeHtml(text) {
30
+ return text
31
+ .replace(/&nbsp;/g, " ")
32
+ .replace(/&amp;/g, "&")
33
+ .replace(/&lt;/g, "<")
34
+ .replace(/&gt;/g, ">")
35
+ .replace(/&quot;/g, "\"")
36
+ .replace(/&#39;/g, "'")
37
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
38
+ }
39
+
40
+ async function fetchText(url) {
41
+ const res = await fetch(url, {
42
+ headers: {
43
+ "user-agent": "wp-agent-skills-upstream-sync/0.1",
44
+ accept: "text/html,application/json",
45
+ },
46
+ });
47
+ if (!res.ok) throw new Error(`Fetch failed ${res.status} for ${url}`);
48
+ return await res.text();
49
+ }
50
+
51
+ async function fetchJson(url) {
52
+ const text = await fetchText(url);
53
+ try {
54
+ return JSON.parse(text);
55
+ } catch {
56
+ throw new Error(`Expected JSON from ${url}, got non-JSON response`);
57
+ }
58
+ }
59
+
60
+ function parseWpGutenbergMapFromHtml(html) {
61
+ // Best-effort HTML table parsing without dependencies:
62
+ // 1) find the first <table> that contains "WordPress Version" and "Gutenberg Versions"
63
+ // 2) extract rows, then extract cells
64
+ const tables = [...html.matchAll(/<table[\s\S]*?<\/table>/gi)].map((m) => m[0]);
65
+ const target = tables.find((t) => /WordPress\s*Version/i.test(t) && /Gutenberg\s*Versions/i.test(t));
66
+ if (!target) return { rows: [], note: "table-not-found" };
67
+
68
+ const rowHtml = [...target.matchAll(/<tr[\s\S]*?<\/tr>/gi)].map((m) => m[0]);
69
+ const rows = [];
70
+
71
+ for (const r of rowHtml) {
72
+ const cellHtml = [...r.matchAll(/<(td|th)[^>]*>([\s\S]*?)<\/\1>/gi)].map((m) => m[2]);
73
+ if (cellHtml.length < 2) continue;
74
+
75
+ const cells = cellHtml.map((c) => decodeHtml(stripTags(c)));
76
+ const wp = cells[0];
77
+ const gb = cells[1];
78
+
79
+ if (/WordPress\s*Version/i.test(wp) || /Gutenberg\s*Versions/i.test(gb)) continue;
80
+ if (!/^\d+\.\d+/.test(wp)) continue;
81
+
82
+ rows.push({ wordpress: wp, gutenberg: gb });
83
+ }
84
+
85
+ return { rows, note: null };
86
+ }
87
+
88
+ function normalizeWpVersionCheckPayload(payload) {
89
+ // https://api.wordpress.org/core/version-check/1.7/ returns something like:
90
+ // { offers: [...], translations: [...] }
91
+ const offers = Array.isArray(payload?.offers) ? payload.offers : [];
92
+
93
+ const candidates = offers
94
+ .map((o) => ({
95
+ version: typeof o?.version === "string" ? o.version : null,
96
+ current: typeof o?.current === "string" ? o.current : null,
97
+ download: typeof o?.download === "string" ? o.download : null,
98
+ phpVersion: typeof o?.php_version === "string" ? o.php_version : null,
99
+ mysqlVersion: typeof o?.mysql_version === "string" ? o.mysql_version : null,
100
+ response: typeof o?.response === "string" ? o.response : null,
101
+ locale: typeof o?.locale === "string" ? o.locale : null,
102
+ }))
103
+ .filter((o) => o.version || o.current);
104
+
105
+ // Keep a small, stable subset; prioritize "upgrade" offers.
106
+ const byVersion = new Map();
107
+ for (const o of candidates) {
108
+ const v = o.version ?? o.current;
109
+ if (!v) continue;
110
+ if (!byVersion.has(v)) byVersion.set(v, o);
111
+ }
112
+
113
+ const versions = [...byVersion.keys()].sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
114
+ return {
115
+ latest: versions[0] ?? null,
116
+ recent: versions.slice(0, 20),
117
+ offers: versions.slice(0, 20).map((v) => byVersion.get(v)),
118
+ };
119
+ }
120
+
121
+ function normalizeGutenbergReleases(payload) {
122
+ const releases = Array.isArray(payload) ? payload : [];
123
+ const stable = releases
124
+ .filter((r) => r && !r.draft && !r.prerelease && typeof r.tag_name === "string")
125
+ .map((r) => ({
126
+ tag: r.tag_name,
127
+ name: typeof r.name === "string" ? r.name : null,
128
+ publishedAt: typeof r.published_at === "string" ? r.published_at : null,
129
+ url: typeof r.html_url === "string" ? r.html_url : null,
130
+ }));
131
+ return {
132
+ latest: stable[0] ?? null,
133
+ recent: stable.slice(0, 30),
134
+ };
135
+ }
136
+
137
+ async function main() {
138
+ const repoRoot = process.cwd();
139
+ const outDir = path.join(repoRoot, "shared", "references");
140
+
141
+ const [wpVersionPayload, gbReleasesPayload, mapHtml] = await Promise.all([
142
+ fetchJson(SOURCES.wordpressCoreVersionCheck),
143
+ fetchJson(SOURCES.gutenbergReleases),
144
+ fetchText(SOURCES.wpGutenbergMapDoc),
145
+ ]);
146
+
147
+ const wordpress = normalizeWpVersionCheckPayload(wpVersionPayload);
148
+ const gutenberg = normalizeGutenbergReleases(gbReleasesPayload);
149
+ const map = parseWpGutenbergMapFromHtml(mapHtml);
150
+
151
+ writeJson(path.join(outDir, "wordpress-core-versions.json"), {
152
+ source: SOURCES.wordpressCoreVersionCheck,
153
+ ...wordpress,
154
+ });
155
+
156
+ writeJson(path.join(outDir, "gutenberg-releases.json"), {
157
+ source: SOURCES.gutenbergReleases,
158
+ ...gutenberg,
159
+ });
160
+
161
+ writeJson(path.join(outDir, "wp-gutenberg-version-map.json"), {
162
+ source: SOURCES.wpGutenbergMapDoc,
163
+ note: map.note,
164
+ rows: map.rows,
165
+ });
166
+
167
+ process.stdout.write("OK: updated shared/references/* upstream indices\n");
168
+ }
169
+
170
+ main().catch((err) => {
171
+ process.stderr.write(`${err?.stack || String(err)}\n`);
172
+ process.exit(1);
173
+ });
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: wordpress-router
3
+ description: "Use when the user asks about WordPress codebases (plugins, themes, block themes, Gutenberg blocks, WP core checkouts) and you need to quickly classify the repo and route to the correct workflow/skill (blocks, theme.json, REST API, WP-CLI, performance, security, testing, release packaging)."
4
+ compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI."
5
+ ---
6
+
7
+ # WordPress Router
8
+
9
+ ## When to use
10
+
11
+ Use this skill at the start of most WordPress tasks to:
12
+
13
+ - identify what kind of WordPress codebase this is (plugin vs theme vs block theme vs WP core checkout vs full site),
14
+ - pick the right workflow and guardrails,
15
+ - delegate to the most relevant domain skill(s).
16
+
17
+ ## Inputs required
18
+
19
+ - Repo root (current working directory).
20
+ - The user’s intent (what they want changed) and any constraints (WP version targets, WP.com specifics, release requirements).
21
+
22
+ ## Procedure
23
+
24
+ 1. Run the project triage script:
25
+ - `node skills/wp-project-triage/scripts/detect_wp_project.mjs`
26
+ 2. Read the triage output and classify:
27
+ - primary project kind(s),
28
+ - tooling available (PHP/Composer, Node, @wordpress/scripts),
29
+ - tests present (PHPUnit, Playwright, wp-env),
30
+ - any version hints.
31
+ 3. Route to domain workflows based on user intent + repo kind:
32
+ - For the decision tree, read: `skills/wordpress-router/references/decision-tree.md`.
33
+ 4. Apply guardrails before making changes:
34
+ - Confirm any version constraints if unclear.
35
+ - Prefer the repo’s existing tooling and conventions for builds/tests.
36
+
37
+ ## Verification
38
+
39
+ - Re-run the triage script if you create or restructure significant files.
40
+ - Run the repo’s lint/test/build commands that the triage output recommends (if available).
41
+
42
+ ## Failure modes / debugging
43
+
44
+ - If triage reports `kind: unknown`, inspect:
45
+ - root `composer.json`, `package.json`, `style.css`, `block.json`, `theme.json`, `wp-content/`.
46
+ - If the repo is huge, consider narrowing scanning scope or adding ignore rules to the triage script.
47
+
48
+ ## Escalation
49
+
50
+ - If routing is ambiguous, ask one question:
51
+ - “Is this intended to be a WordPress plugin, a theme (classic/block), or a full site repo?”
@@ -0,0 +1,55 @@
1
+ # Router decision tree (v1)
2
+
3
+ This is a lightweight routing guide. It assumes you can run `wp-project-triage` first.
4
+
5
+ ## Step 1: classify repo kind (from triage)
6
+
7
+ Use `triage.project.kind` and the strongest signals:
8
+
9
+ - `wp-core` → treat as WordPress core checkout work (core patches, PHPUnit, build tools).
10
+ - `wp-site` → treat as a full site repo (wp-content present; changes might be theme + plugins).
11
+ - `wp-block-theme` → theme.json/templates/patterns workflows.
12
+ - `wp-theme` → classic theme workflows (templates PHP, `functions.php`, `style.css`).
13
+ - `wp-block-plugin` → Gutenberg block development in a plugin (block.json, build pipeline).
14
+ - `wp-plugin` / `wp-mu-plugin` → plugin workflows (hooks, admin, settings, cron, REST, security).
15
+ - `gutenberg` → Gutenberg monorepo workflows (packages, tooling, docs).
16
+
17
+ If multiple kinds match, prefer the most specific:
18
+ `gutenberg` > `wp-core` > `wp-site` > `wp-block-theme` > `wp-block-plugin` > `wp-theme` > `wp-plugin`.
19
+
20
+ ## Step 2: route by user intent (keywords)
21
+
22
+ Route by intent even if repo kind is broad (like `wp-site`):
23
+
24
+ - **Interactivity API / data-wp-* directives / @wordpress/interactivity / viewScriptModule**
25
+ - Route → `wp-interactivity-api`.
26
+ - **Abilities API / wp_register_ability / wp-abilities/v1 / @wordpress/abilities**
27
+ - Route → `wp-abilities-api`.
28
+ - **Playground / run-blueprint / build-snapshot / @wp-playground/cli / playground.wordpress.net**
29
+ - Route → `wp-playground`.
30
+ - **Blocks / block.json / registerBlockType / attributes / save serialization**
31
+ - Route → `wp-block-development`.
32
+ - **theme.json / Global Styles / templates/*.html / patterns/**
33
+ - Route → `wp-block-themes`.
34
+ - **Plugins / hooks / activation hook / uninstall / Settings API / admin pages**
35
+ - Route → `wp-plugin-development`.
36
+ - **REST endpoint / register_rest_route / permission_callback**
37
+ - Route → `wp-rest-api`.
38
+ - **WP-CLI / wp-cli.yml / commands**
39
+ - Route → `wp-wpcli-and-ops`.
40
+ - **Build tooling / @wordpress/scripts / webpack / Vite / npm scripts**
41
+ - Route → `wp-build-tooling` (planned).
42
+ - **Testing / PHPUnit / wp-env / Playwright**
43
+ - Route → `wp-testing` (planned).
44
+ - **PHPStan / static analysis / phpstan.neon / phpstan-baseline.neon**
45
+ - Route → `wp-phpstan`.
46
+ - **Performance / caching / query profiling / editor slowness**
47
+ - Route → `wp-performance`.
48
+ - **Security / nonces / capabilities / sanitization/escaping / uploads**
49
+ - Route → `wp-security` (planned).
50
+
51
+ ## Step 3: guardrails checklist (always)
52
+
53
+ - Verify detected tooling before suggesting commands (Composer vs npm/yarn/pnpm).
54
+ - Prefer existing lint/test scripts if present.
55
+ - If version constraints aren’t detectable, ask for target WP core and PHP versions.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: wp-abilities-api
3
+ description: "Use when working with the WordPress Abilities API (wp_register_ability, wp_register_ability_category, /wp-json/wp-abilities/v1/*, @wordpress/abilities) including defining abilities, categories, meta, REST exposure, and permissions checks for clients."
4
+ compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI."
5
+ ---
6
+
7
+ # WP Abilities API
8
+
9
+ ## When to use
10
+
11
+ Use this skill when the task involves:
12
+
13
+ - registering abilities or ability categories in PHP,
14
+ - exposing abilities to clients via REST (`wp-abilities/v1`),
15
+ - consuming abilities in JS (notably `@wordpress/abilities`),
16
+ - diagnosing “ability doesn’t show up” / “client can’t see ability” / “REST returns empty”.
17
+
18
+ ## Inputs required
19
+
20
+ - Repo root (run `wp-project-triage` first if you haven’t).
21
+ - Target WordPress version(s) and whether this is WP core or a plugin/theme.
22
+ - Where the change should live (plugin vs theme vs mu-plugin).
23
+
24
+ ## Procedure
25
+
26
+ ### 1) Confirm availability and version constraints
27
+
28
+ - If this is WP core work, check `signals.isWpCoreCheckout` and `versions.wordpress.core`.
29
+ - If the project targets WP < 6.9, you may need the Abilities API plugin/package rather than relying on core.
30
+
31
+ ### 2) Find existing Abilities usage
32
+
33
+ Search for these in the repo:
34
+
35
+ - `wp_register_ability(`
36
+ - `wp_register_ability_category(`
37
+ - `wp_abilities_api_init`
38
+ - `wp_abilities_api_categories_init`
39
+ - `wp-abilities/v1`
40
+ - `@wordpress/abilities`
41
+
42
+ If none exist, decide whether you’re introducing Abilities API fresh (new registrations + client consumption) or only consuming.
43
+
44
+ ### 3) Register categories (optional)
45
+
46
+ If you need a logical grouping, register an ability category early (see `references/php-registration.md`).
47
+
48
+ ### 4) Register abilities (PHP)
49
+
50
+ Implement the ability in PHP registration with:
51
+
52
+ - stable `id` (namespaced),
53
+ - `label`/`description`,
54
+ - `category`,
55
+ - `meta`:
56
+ - add `readonly: true` when the ability is informational,
57
+ - set `show_in_rest: true` for abilities you want visible to clients.
58
+
59
+ Use the documented init hooks for Abilities API registration so they load at the right time (see `references/php-registration.md`).
60
+
61
+ ### 5) Confirm REST exposure
62
+
63
+ - Verify the REST endpoints exist and return expected results (see `references/rest-api.md`).
64
+ - If the client still can’t see the ability, confirm `meta.show_in_rest` is enabled and you’re querying the right endpoint.
65
+
66
+ ### 6) Consume from JS (if needed)
67
+
68
+ - Prefer `@wordpress/abilities` APIs for client-side access and checks.
69
+ - Ensure build tooling includes the dependency and the project’s build pipeline bundles it.
70
+
71
+ ## Verification
72
+
73
+ - `wp-project-triage` indicates `signals.usesAbilitiesApi: true` after your change (if applicable).
74
+ - REST check (in a WP environment): endpoints under `wp-abilities/v1` return your ability and category when expected.
75
+ - If the repo has tests, add/update coverage near:
76
+ - PHP: ability registration and meta exposure
77
+ - JS: ability consumption and UI gating
78
+
79
+ ## Failure modes / debugging
80
+
81
+ - Ability never appears:
82
+ - registration code not running (wrong hook / file not loaded),
83
+ - missing `meta.show_in_rest`,
84
+ - incorrect category/ID mismatch.
85
+ - REST shows ability but JS doesn’t:
86
+ - wrong REST base/namespace,
87
+ - JS dependency not bundled,
88
+ - caching (object/page caches) masking changes.
89
+
90
+ ## Escalation
91
+
92
+ - If you’re uncertain about version support, confirm target WP core versions and whether Abilities API is expected from core or as a plugin.
93
+ - For canonical details, consult:
94
+ - `references/rest-api.md`
95
+ - `references/php-registration.md`