@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.
- package/ANTIGRAVITY-README.md +47 -0
- package/CONTRIBUTING.md +122 -0
- package/README.md +135 -0
- package/STRUCTURE.md +200 -0
- package/agents/wordpress-expert.md +36 -0
- package/agents/wp-frontend-expert.md +21 -0
- package/bin/antigravity-agent.js +159 -0
- package/docs/authoring-guide.md +56 -0
- package/docs/compatibility-policy.md +18 -0
- package/docs/packaging.md +26 -0
- package/docs/principles.md +7 -0
- package/docs/skill-set-v1.md +21 -0
- package/docs/upstream-sync.md +52 -0
- package/package.json +47 -0
- package/rules/GEMINI.md +273 -0
- package/shared/references/.gitkeep +1 -0
- package/shared/references/gutenberg-releases.json +155 -0
- package/shared/references/wordpress-core-versions.json +208 -0
- package/shared/references/wp-gutenberg-version-map.json +886 -0
- package/shared/scripts/ai-generate-updates.mjs +458 -0
- package/shared/scripts/scaffold-skill.mjs +62 -0
- package/shared/scripts/skillpack-build.mjs +165 -0
- package/shared/scripts/skillpack-install.mjs +275 -0
- package/shared/scripts/update-upstream-indices.mjs +173 -0
- package/skills/wordpress-router/SKILL.md +51 -0
- package/skills/wordpress-router/references/decision-tree.md +55 -0
- package/skills/wp-abilities-api/SKILL.md +95 -0
- package/skills/wp-abilities-api/references/php-registration.md +67 -0
- package/skills/wp-abilities-api/references/rest-api.md +13 -0
- package/skills/wp-block-development/SKILL.md +174 -0
- package/skills/wp-block-development/references/attributes-and-serialization.md +22 -0
- package/skills/wp-block-development/references/block-json.md +49 -0
- package/skills/wp-block-development/references/creating-new-blocks.md +46 -0
- package/skills/wp-block-development/references/debugging.md +36 -0
- package/skills/wp-block-development/references/deprecations.md +24 -0
- package/skills/wp-block-development/references/dynamic-rendering.md +23 -0
- package/skills/wp-block-development/references/inner-blocks.md +25 -0
- package/skills/wp-block-development/references/registration.md +30 -0
- package/skills/wp-block-development/references/supports-and-wrappers.md +18 -0
- package/skills/wp-block-development/references/tooling-and-testing.md +21 -0
- package/skills/wp-block-development/scripts/list_blocks.mjs +121 -0
- package/skills/wp-block-themes/SKILL.md +116 -0
- package/skills/wp-block-themes/references/creating-new-block-theme.md +37 -0
- package/skills/wp-block-themes/references/debugging.md +24 -0
- package/skills/wp-block-themes/references/patterns.md +18 -0
- package/skills/wp-block-themes/references/style-variations.md +14 -0
- package/skills/wp-block-themes/references/templates-and-parts.md +16 -0
- package/skills/wp-block-themes/references/theme-json.md +59 -0
- package/skills/wp-block-themes/scripts/detect_block_themes.mjs +117 -0
- package/skills/wp-interactivity-api/SKILL.md +179 -0
- package/skills/wp-interactivity-api/references/debugging.md +29 -0
- package/skills/wp-interactivity-api/references/directives-quickref.md +30 -0
- package/skills/wp-interactivity-api/references/server-side-rendering.md +310 -0
- package/skills/wp-performance/SKILL.md +146 -0
- package/skills/wp-performance/references/autoload-options.md +24 -0
- package/skills/wp-performance/references/cron.md +20 -0
- package/skills/wp-performance/references/database.md +20 -0
- package/skills/wp-performance/references/http-api.md +15 -0
- package/skills/wp-performance/references/measurement.md +21 -0
- package/skills/wp-performance/references/object-cache.md +24 -0
- package/skills/wp-performance/references/query-monitor-headless.md +38 -0
- package/skills/wp-performance/references/server-timing.md +22 -0
- package/skills/wp-performance/references/wp-cli-doctor.md +24 -0
- package/skills/wp-performance/references/wp-cli-profile.md +32 -0
- package/skills/wp-performance/scripts/perf_inspect.mjs +128 -0
- package/skills/wp-phpstan/SKILL.md +97 -0
- package/skills/wp-phpstan/references/configuration.md +52 -0
- package/skills/wp-phpstan/references/third-party-classes.md +76 -0
- package/skills/wp-phpstan/references/wordpress-annotations.md +124 -0
- package/skills/wp-phpstan/scripts/phpstan_inspect.mjs +263 -0
- package/skills/wp-playground/SKILL.md +101 -0
- package/skills/wp-playground/references/blueprints.md +36 -0
- package/skills/wp-playground/references/cli-commands.md +39 -0
- package/skills/wp-playground/references/debugging.md +16 -0
- package/skills/wp-plugin-development/SKILL.md +112 -0
- package/skills/wp-plugin-development/references/data-and-cron.md +19 -0
- package/skills/wp-plugin-development/references/debugging.md +19 -0
- package/skills/wp-plugin-development/references/lifecycle.md +33 -0
- package/skills/wp-plugin-development/references/security.md +29 -0
- package/skills/wp-plugin-development/references/settings-api.md +22 -0
- package/skills/wp-plugin-development/references/structure.md +16 -0
- package/skills/wp-plugin-development/scripts/detect_plugins.mjs +122 -0
- package/skills/wp-project-triage/SKILL.md +38 -0
- package/skills/wp-project-triage/references/triage.schema.json +143 -0
- package/skills/wp-project-triage/scripts/detect_wp_project.mjs +592 -0
- package/skills/wp-rest-api/SKILL.md +114 -0
- package/skills/wp-rest-api/references/authentication.md +18 -0
- package/skills/wp-rest-api/references/custom-content-types.md +20 -0
- package/skills/wp-rest-api/references/discovery-and-params.md +20 -0
- package/skills/wp-rest-api/references/responses-and-fields.md +30 -0
- package/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
- package/skills/wp-rest-api/references/schema.md +22 -0
- package/skills/wp-wpcli-and-ops/SKILL.md +123 -0
- package/skills/wp-wpcli-and-ops/references/automation.md +30 -0
- package/skills/wp-wpcli-and-ops/references/cron-and-cache.md +23 -0
- package/skills/wp-wpcli-and-ops/references/debugging.md +17 -0
- package/skills/wp-wpcli-and-ops/references/multisite.md +22 -0
- package/skills/wp-wpcli-and-ops/references/packages-and-updates.md +22 -0
- package/skills/wp-wpcli-and-ops/references/safety.md +30 -0
- package/skills/wp-wpcli-and-ops/references/search-replace.md +40 -0
- package/skills/wp-wpcli-and-ops/scripts/wpcli_inspect.mjs +90 -0
- package/skills/wpds/SKILL.md +58 -0
- package/workflows/create-block.md +27 -0
- package/workflows/wp-lint.md +27 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered skill update generator
|
|
3
|
+
*
|
|
4
|
+
* Analyzes upstream changes and generates updates to affected skills.
|
|
5
|
+
* Designed to run in GitHub Actions with ANTHROPIC_API_KEY env var.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node shared/scripts/ai-generate-updates.mjs
|
|
9
|
+
*
|
|
10
|
+
* Environment:
|
|
11
|
+
* ANTHROPIC_API_KEY - Required
|
|
12
|
+
* AI_DRY_RUN - Set to "true" to skip writing files
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = process.cwd();
|
|
20
|
+
const REFERENCES_DIR = path.join(REPO_ROOT, "shared", "references");
|
|
21
|
+
const SKILLS_DIR = path.join(REPO_ROOT, "skills");
|
|
22
|
+
const STATE_FILE = path.join(REPO_ROOT, ".github", "state", "last-sync.json");
|
|
23
|
+
|
|
24
|
+
// Model selection: Sonnet 4 for balanced cost/performance
|
|
25
|
+
const MODEL = "claude-sonnet-4-20250514";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load JSON file safely
|
|
29
|
+
*/
|
|
30
|
+
function loadJson(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write JSON file with directory creation
|
|
40
|
+
*/
|
|
41
|
+
function writeJson(filePath, data) {
|
|
42
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
43
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get current upstream state hash for comparison
|
|
48
|
+
*/
|
|
49
|
+
function getUpstreamStateHash(indices) {
|
|
50
|
+
const state = {
|
|
51
|
+
wpLatest: indices.wordpress?.latest ?? null,
|
|
52
|
+
gbLatest: indices.gutenberg?.latest?.tag ?? null,
|
|
53
|
+
gbRecentCount: indices.gutenberg?.recent?.length ?? 0,
|
|
54
|
+
mapRowCount: indices.map?.rows?.length ?? 0,
|
|
55
|
+
};
|
|
56
|
+
// Simple hash: JSON stringify and take first 16 chars of base64
|
|
57
|
+
const hash = Buffer.from(JSON.stringify(state)).toString("base64").slice(0, 16);
|
|
58
|
+
return { hash, state };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load all current upstream indices
|
|
63
|
+
*/
|
|
64
|
+
function loadUpstreamIndices() {
|
|
65
|
+
return {
|
|
66
|
+
wordpress: loadJson(path.join(REFERENCES_DIR, "wordpress-core-versions.json")),
|
|
67
|
+
gutenberg: loadJson(path.join(REFERENCES_DIR, "gutenberg-releases.json")),
|
|
68
|
+
map: loadJson(path.join(REFERENCES_DIR, "wp-gutenberg-version-map.json")),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Load last sync state
|
|
74
|
+
*/
|
|
75
|
+
function loadLastSyncState() {
|
|
76
|
+
return loadJson(STATE_FILE) ?? { hash: null, state: null, lastSync: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Save current sync state
|
|
81
|
+
*/
|
|
82
|
+
function saveSyncState(hash, state, changes) {
|
|
83
|
+
writeJson(STATE_FILE, {
|
|
84
|
+
hash,
|
|
85
|
+
state,
|
|
86
|
+
lastSync: new Date().toISOString(),
|
|
87
|
+
lastChanges: changes,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect what changed between last sync and current state
|
|
93
|
+
*/
|
|
94
|
+
function detectChanges(lastState, currentState) {
|
|
95
|
+
const changes = [];
|
|
96
|
+
|
|
97
|
+
if (!lastState.state) {
|
|
98
|
+
changes.push({
|
|
99
|
+
type: "initial-sync",
|
|
100
|
+
description: "First sync - no previous state",
|
|
101
|
+
riskLevel: "low",
|
|
102
|
+
});
|
|
103
|
+
return changes;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const last = lastState.state;
|
|
107
|
+
const current = currentState.state;
|
|
108
|
+
|
|
109
|
+
// WordPress version change
|
|
110
|
+
if (last.wpLatest !== current.wpLatest) {
|
|
111
|
+
changes.push({
|
|
112
|
+
type: "wordpress-release",
|
|
113
|
+
description: `WordPress updated: ${last.wpLatest} → ${current.wpLatest}`,
|
|
114
|
+
oldVersion: last.wpLatest,
|
|
115
|
+
newVersion: current.wpLatest,
|
|
116
|
+
riskLevel: "medium",
|
|
117
|
+
affectedSkills: [
|
|
118
|
+
"wp-block-themes",
|
|
119
|
+
"wp-block-development",
|
|
120
|
+
"wp-plugin-development",
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Gutenberg version change
|
|
126
|
+
if (last.gbLatest !== current.gbLatest) {
|
|
127
|
+
changes.push({
|
|
128
|
+
type: "gutenberg-release",
|
|
129
|
+
description: `Gutenberg updated: ${last.gbLatest} → ${current.gbLatest}`,
|
|
130
|
+
oldVersion: last.gbLatest,
|
|
131
|
+
newVersion: current.gbLatest,
|
|
132
|
+
riskLevel: "medium",
|
|
133
|
+
affectedSkills: [
|
|
134
|
+
"wp-interactivity-api",
|
|
135
|
+
"wp-abilities-api",
|
|
136
|
+
"wp-block-development",
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Map table updated (new mappings added)
|
|
142
|
+
if (last.mapRowCount !== current.mapRowCount) {
|
|
143
|
+
changes.push({
|
|
144
|
+
type: "version-map-update",
|
|
145
|
+
description: `WP↔Gutenberg mapping updated: ${last.mapRowCount} → ${current.mapRowCount} entries`,
|
|
146
|
+
riskLevel: "low",
|
|
147
|
+
affectedSkills: ["wordpress-router"],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return changes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Load a skill's SKILL.md content
|
|
156
|
+
*/
|
|
157
|
+
function loadSkillContent(skillName) {
|
|
158
|
+
const skillPath = path.join(SKILLS_DIR, skillName, "SKILL.md");
|
|
159
|
+
try {
|
|
160
|
+
return fs.readFileSync(skillPath, "utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Load all reference docs for a skill
|
|
168
|
+
*/
|
|
169
|
+
function loadSkillReferences(skillName) {
|
|
170
|
+
const refsDir = path.join(SKILLS_DIR, skillName, "references");
|
|
171
|
+
const refs = {};
|
|
172
|
+
try {
|
|
173
|
+
const files = fs.readdirSync(refsDir);
|
|
174
|
+
for (const file of files) {
|
|
175
|
+
if (file.endsWith(".md")) {
|
|
176
|
+
refs[file] = fs.readFileSync(path.join(refsDir, file), "utf8");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// No references dir
|
|
181
|
+
}
|
|
182
|
+
return refs;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build the prompt for AI analysis
|
|
187
|
+
*/
|
|
188
|
+
function buildAnalysisPrompt(changes, indices) {
|
|
189
|
+
const changesSummary = changes
|
|
190
|
+
.map((c) => `- ${c.type}: ${c.description} (risk: ${c.riskLevel})`)
|
|
191
|
+
.join("\n");
|
|
192
|
+
|
|
193
|
+
return `You are analyzing upstream changes to WordPress/Gutenberg to determine if skills need updates.
|
|
194
|
+
|
|
195
|
+
## Detected Changes
|
|
196
|
+
${changesSummary}
|
|
197
|
+
|
|
198
|
+
## Current Upstream State
|
|
199
|
+
- WordPress latest: ${indices.wordpress?.latest ?? "unknown"}
|
|
200
|
+
- Gutenberg latest: ${indices.gutenberg?.latest?.tag ?? "unknown"}
|
|
201
|
+
- Gutenberg release URL: ${indices.gutenberg?.latest?.url ?? "unknown"}
|
|
202
|
+
|
|
203
|
+
## Task
|
|
204
|
+
Analyze these changes and determine:
|
|
205
|
+
1. Which skills are likely affected and why
|
|
206
|
+
2. What specific updates might be needed (procedures, references, examples)
|
|
207
|
+
3. Risk assessment for each potential update
|
|
208
|
+
|
|
209
|
+
Respond in JSON format:
|
|
210
|
+
{
|
|
211
|
+
"analysis": "Brief overall analysis",
|
|
212
|
+
"skillUpdates": [
|
|
213
|
+
{
|
|
214
|
+
"skill": "skill-name",
|
|
215
|
+
"reason": "Why this skill is affected",
|
|
216
|
+
"suggestedChanges": ["List of specific changes to make"],
|
|
217
|
+
"riskLevel": "low|medium|high",
|
|
218
|
+
"priority": 1-5
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
"skipUpdate": true/false,
|
|
222
|
+
"skipReason": "If skipping, explain why no updates are needed"
|
|
223
|
+
}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Build prompt for generating skill updates
|
|
228
|
+
*/
|
|
229
|
+
function buildUpdatePrompt(skillName, skillContent, references, changes, indices) {
|
|
230
|
+
const relevantChanges = changes.filter(
|
|
231
|
+
(c) => c.affectedSkills?.includes(skillName)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const refsSummary = Object.entries(references)
|
|
235
|
+
.map(([name, content]) => `### ${name}\n${content.slice(0, 2000)}...`)
|
|
236
|
+
.join("\n\n");
|
|
237
|
+
|
|
238
|
+
return `You are updating a WordPress development skill based on upstream changes.
|
|
239
|
+
|
|
240
|
+
## Skill: ${skillName}
|
|
241
|
+
|
|
242
|
+
## Current SKILL.md
|
|
243
|
+
${skillContent}
|
|
244
|
+
|
|
245
|
+
## Current References (truncated)
|
|
246
|
+
${refsSummary || "(no references)"}
|
|
247
|
+
|
|
248
|
+
## Relevant Changes
|
|
249
|
+
${relevantChanges.map((c) => `- ${c.description}`).join("\n")}
|
|
250
|
+
|
|
251
|
+
## Upstream Context
|
|
252
|
+
- WordPress latest: ${indices.wordpress?.latest ?? "unknown"}
|
|
253
|
+
- Gutenberg latest: ${indices.gutenberg?.latest?.tag ?? "unknown"}
|
|
254
|
+
|
|
255
|
+
## Instructions
|
|
256
|
+
1. Review the current skill content
|
|
257
|
+
2. Identify what needs to change based on the upstream updates
|
|
258
|
+
3. Generate updated content that:
|
|
259
|
+
- Preserves the existing structure and tone
|
|
260
|
+
- Updates version references if needed
|
|
261
|
+
- Adds notes about new features/changes if relevant
|
|
262
|
+
- Does NOT remove existing content unless it's deprecated
|
|
263
|
+
|
|
264
|
+
Respond in JSON format:
|
|
265
|
+
{
|
|
266
|
+
"skillUpdated": true/false,
|
|
267
|
+
"changes": [
|
|
268
|
+
{
|
|
269
|
+
"file": "SKILL.md or references/filename.md",
|
|
270
|
+
"description": "What changed",
|
|
271
|
+
"newContent": "Full new file content (only if changed)"
|
|
272
|
+
}
|
|
273
|
+
],
|
|
274
|
+
"summary": "Brief summary of changes for PR description",
|
|
275
|
+
"noChangeReason": "If no changes needed, explain why"
|
|
276
|
+
}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Call Claude API
|
|
281
|
+
*/
|
|
282
|
+
async function callClaude(client, prompt, systemPrompt = null) {
|
|
283
|
+
const messages = [{ role: "user", content: prompt }];
|
|
284
|
+
|
|
285
|
+
const response = await client.messages.create({
|
|
286
|
+
model: MODEL,
|
|
287
|
+
max_tokens: 8192,
|
|
288
|
+
system: systemPrompt ?? "You are a technical writer maintaining WordPress development skills documentation. Always respond with valid JSON.",
|
|
289
|
+
messages,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const text = response.content
|
|
293
|
+
.filter((b) => b.type === "text")
|
|
294
|
+
.map((b) => b.text)
|
|
295
|
+
.join("");
|
|
296
|
+
|
|
297
|
+
// Extract JSON from response (handle markdown code blocks)
|
|
298
|
+
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/) ||
|
|
299
|
+
text.match(/```\s*([\s\S]*?)\s*```/) ||
|
|
300
|
+
[null, text];
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
return JSON.parse(jsonMatch[1] || text);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
console.error("Failed to parse Claude response as JSON:", text.slice(0, 500));
|
|
306
|
+
throw new Error(`Invalid JSON response from Claude: ${e.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Main execution
|
|
312
|
+
*/
|
|
313
|
+
async function main() {
|
|
314
|
+
const dryRun = process.env.AI_DRY_RUN === "true";
|
|
315
|
+
|
|
316
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
317
|
+
console.error("ERROR: ANTHROPIC_API_KEY environment variable is required");
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const client = new Anthropic();
|
|
322
|
+
|
|
323
|
+
console.log("=== AI Skill Update Generator ===\n");
|
|
324
|
+
|
|
325
|
+
// 1. Load current state
|
|
326
|
+
console.log("Loading upstream indices...");
|
|
327
|
+
const indices = loadUpstreamIndices();
|
|
328
|
+
const currentState = getUpstreamStateHash(indices);
|
|
329
|
+
const lastState = loadLastSyncState();
|
|
330
|
+
|
|
331
|
+
console.log(`Current state hash: ${currentState.hash}`);
|
|
332
|
+
console.log(`Last sync hash: ${lastState.hash ?? "(none)"}\n`);
|
|
333
|
+
|
|
334
|
+
// 2. Detect changes
|
|
335
|
+
console.log("Detecting changes...");
|
|
336
|
+
const changes = detectChanges(lastState, currentState);
|
|
337
|
+
|
|
338
|
+
if (changes.length === 0) {
|
|
339
|
+
console.log("No changes detected. Exiting.");
|
|
340
|
+
process.exit(0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(`Found ${changes.length} change(s):`);
|
|
344
|
+
for (const c of changes) {
|
|
345
|
+
console.log(` - ${c.type}: ${c.description}`);
|
|
346
|
+
}
|
|
347
|
+
console.log();
|
|
348
|
+
|
|
349
|
+
// 3. AI analysis of impact
|
|
350
|
+
console.log("Analyzing impact with AI...");
|
|
351
|
+
const analysisPrompt = buildAnalysisPrompt(changes, indices);
|
|
352
|
+
const analysis = await callClaude(client, analysisPrompt);
|
|
353
|
+
|
|
354
|
+
console.log(`Analysis: ${analysis.analysis}\n`);
|
|
355
|
+
|
|
356
|
+
if (analysis.skipUpdate) {
|
|
357
|
+
console.log(`Skipping updates: ${analysis.skipReason}`);
|
|
358
|
+
saveSyncState(currentState.hash, currentState.state, changes);
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 4. Generate updates for affected skills
|
|
363
|
+
const updates = [];
|
|
364
|
+
const skillsToUpdate = analysis.skillUpdates
|
|
365
|
+
.filter((s) => s.priority >= 3 || s.riskLevel !== "low")
|
|
366
|
+
.sort((a, b) => b.priority - a.priority);
|
|
367
|
+
|
|
368
|
+
console.log(`Skills to update: ${skillsToUpdate.map((s) => s.skill).join(", ")}\n`);
|
|
369
|
+
|
|
370
|
+
for (const skillUpdate of skillsToUpdate) {
|
|
371
|
+
const { skill } = skillUpdate;
|
|
372
|
+
console.log(`Processing ${skill}...`);
|
|
373
|
+
|
|
374
|
+
const skillContent = loadSkillContent(skill);
|
|
375
|
+
if (!skillContent) {
|
|
376
|
+
console.log(` Skill ${skill} not found, skipping.`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const references = loadSkillReferences(skill);
|
|
381
|
+
const updatePrompt = buildUpdatePrompt(
|
|
382
|
+
skill,
|
|
383
|
+
skillContent,
|
|
384
|
+
references,
|
|
385
|
+
changes,
|
|
386
|
+
indices
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const updateResult = await callClaude(client, updatePrompt);
|
|
390
|
+
|
|
391
|
+
if (updateResult.skillUpdated && updateResult.changes?.length > 0) {
|
|
392
|
+
updates.push({
|
|
393
|
+
skill,
|
|
394
|
+
...updateResult,
|
|
395
|
+
});
|
|
396
|
+
console.log(` ${updateResult.changes.length} file(s) to update`);
|
|
397
|
+
} else {
|
|
398
|
+
console.log(` No changes needed: ${updateResult.noChangeReason}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 5. Write updates
|
|
403
|
+
if (updates.length === 0) {
|
|
404
|
+
console.log("\nNo skill updates generated.");
|
|
405
|
+
saveSyncState(currentState.hash, currentState.state, changes);
|
|
406
|
+
process.exit(0);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.log(`\n=== Writing ${updates.length} skill update(s) ===\n`);
|
|
410
|
+
|
|
411
|
+
for (const update of updates) {
|
|
412
|
+
for (const change of update.changes) {
|
|
413
|
+
const filePath = path.join(SKILLS_DIR, update.skill, change.file);
|
|
414
|
+
console.log(`Writing: ${filePath}`);
|
|
415
|
+
console.log(` ${change.description}`);
|
|
416
|
+
|
|
417
|
+
if (!dryRun && change.newContent) {
|
|
418
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
419
|
+
fs.writeFileSync(filePath, change.newContent, "utf8");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 6. Save state
|
|
425
|
+
saveSyncState(currentState.hash, currentState.state, changes);
|
|
426
|
+
|
|
427
|
+
// 7. Output summary for GitHub Actions
|
|
428
|
+
const summary = updates
|
|
429
|
+
.map((u) => `- **${u.skill}**: ${u.summary}`)
|
|
430
|
+
.join("\n");
|
|
431
|
+
|
|
432
|
+
console.log("\n=== Summary ===");
|
|
433
|
+
console.log(summary);
|
|
434
|
+
|
|
435
|
+
// Write summary to file for GitHub Actions to pick up
|
|
436
|
+
const summaryFile = path.join(REPO_ROOT, ".github", "state", "update-summary.md");
|
|
437
|
+
writeJson(summaryFile.replace(".md", ".json"), {
|
|
438
|
+
timestamp: new Date().toISOString(),
|
|
439
|
+
changes,
|
|
440
|
+
analysis: analysis.analysis,
|
|
441
|
+
updates: updates.map((u) => ({
|
|
442
|
+
skill: u.skill,
|
|
443
|
+
summary: u.summary,
|
|
444
|
+
files: u.changes.map((c) => c.file),
|
|
445
|
+
})),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (dryRun) {
|
|
449
|
+
console.log("\n(Dry run - no files were written)");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log("\nDone.");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
main().catch((err) => {
|
|
456
|
+
console.error("Fatal error:", err);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function usage() {
|
|
5
|
+
process.stderr.write(
|
|
6
|
+
[
|
|
7
|
+
"Usage:",
|
|
8
|
+
' node shared/scripts/scaffold-skill.mjs <skill-name> "<description>"',
|
|
9
|
+
"",
|
|
10
|
+
"Notes:",
|
|
11
|
+
"- <skill-name> must be lowercase unicode letters/digits with hyphens (no leading/trailing hyphen, no --).",
|
|
12
|
+
"- Creates skills/<skill-name>/SKILL.md and eval/scenarios/<skill-name>.md",
|
|
13
|
+
"",
|
|
14
|
+
].join("\n")
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assert(condition, message) {
|
|
19
|
+
if (!condition) throw new Error(message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validateSkillName(name) {
|
|
23
|
+
if (!name || typeof name !== "string") return "Missing skill name";
|
|
24
|
+
if (name.length > 64) return `Skill name exceeds 64 chars (${name.length})`;
|
|
25
|
+
if (name !== name.toLowerCase()) return "Skill name must be lowercase";
|
|
26
|
+
if (name.startsWith("-") || name.endsWith("-")) return "Skill name cannot start or end with hyphen";
|
|
27
|
+
if (name.includes("--")) return "Skill name cannot contain consecutive hyphens";
|
|
28
|
+
const ok = /^[\p{Ll}\p{Nd}]+(?:-[\p{Ll}\p{Nd}]+)*$/u.test(name);
|
|
29
|
+
if (!ok) return "Skill name contains invalid characters";
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function main() {
|
|
34
|
+
const [, , skillName, description] = process.argv;
|
|
35
|
+
if (!skillName || !description) {
|
|
36
|
+
usage();
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nameError = validateSkillName(skillName);
|
|
41
|
+
assert(!nameError, nameError);
|
|
42
|
+
assert(description.length > 0 && description.length <= 1024, "Description must be 1-1024 characters");
|
|
43
|
+
|
|
44
|
+
const repoRoot = process.cwd();
|
|
45
|
+
const skillDir = path.join(repoRoot, "skills", skillName);
|
|
46
|
+
const skillMd = path.join(skillDir, "SKILL.md");
|
|
47
|
+
const scenarioPath = path.join(repoRoot, "eval", "scenarios", `${skillName}.md`);
|
|
48
|
+
|
|
49
|
+
assert(!fs.existsSync(skillDir), `Skill directory already exists: ${path.relative(repoRoot, skillDir)}`);
|
|
50
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const skillBody = `---\nname: ${skillName}\ndescription: ${description}\ncompatibility: Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node.\n---\n\n# ${skillName}\n\n## When to use\n\n## Inputs required\n\n## Procedure\n\n## Verification\n\n## Failure modes / debugging\n\n## Escalation\n`;
|
|
53
|
+
fs.writeFileSync(skillMd, skillBody, "utf8");
|
|
54
|
+
|
|
55
|
+
fs.mkdirSync(path.dirname(scenarioPath), { recursive: true });
|
|
56
|
+
const scenario = `# Scenario: ${skillName}\n\n## Prompt\n\n## Expected behavior\n\n- Uses \`${skillName}\` when the prompt matches its description.\n- Follows the skill procedure and verifies results.\n`;
|
|
57
|
+
fs.writeFileSync(scenarioPath, scenario, "utf8");
|
|
58
|
+
|
|
59
|
+
process.stdout.write(`OK: created ${path.relative(repoRoot, skillMd)} and ${path.relative(repoRoot, scenarioPath)}\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function usage() {
|
|
5
|
+
process.stderr.write(
|
|
6
|
+
[
|
|
7
|
+
"Usage:",
|
|
8
|
+
" node shared/scripts/skillpack-build.mjs [--out=dist] [--targets=codex,vscode,claude] [--skills=skill1,skill2] [--clean]",
|
|
9
|
+
"",
|
|
10
|
+
"Outputs:",
|
|
11
|
+
" - <out>/codex/.codex/skills/<skill>/SKILL.md",
|
|
12
|
+
" - <out>/vscode/.github/skills/<skill>/SKILL.md",
|
|
13
|
+
" - <out>/claude/.claude/skills/<skill>/SKILL.md",
|
|
14
|
+
"",
|
|
15
|
+
"Options:",
|
|
16
|
+
" --targets Comma-separated list of targets (codex, vscode, claude). Default: codex,vscode,claude",
|
|
17
|
+
" --skills Comma-separated list of skill names to build. Default: all skills",
|
|
18
|
+
" --clean Remove target directories before building",
|
|
19
|
+
"",
|
|
20
|
+
"Notes:",
|
|
21
|
+
"- Avoids symlinks (Codex ignores symlinked directories).",
|
|
22
|
+
"",
|
|
23
|
+
].join("\n")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const args = { out: "dist", targets: ["codex", "vscode", "claude"], skills: [], clean: false };
|
|
29
|
+
for (const a of argv) {
|
|
30
|
+
if (a === "--help" || a === "-h") args.help = true;
|
|
31
|
+
else if (a === "--clean") args.clean = true;
|
|
32
|
+
else if (a.startsWith("--out=")) args.out = a.slice("--out=".length);
|
|
33
|
+
else if (a.startsWith("--targets=")) args.targets = a.slice("--targets=".length).split(",").filter(Boolean);
|
|
34
|
+
else if (a.startsWith("--skills=")) args.skills = a.slice("--skills=".length).split(",").filter(Boolean);
|
|
35
|
+
else {
|
|
36
|
+
process.stderr.write(`Unknown arg: ${a}\n`);
|
|
37
|
+
args.help = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assert(condition, message) {
|
|
44
|
+
if (!condition) throw new Error(message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isSymlink(p) {
|
|
48
|
+
try {
|
|
49
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function copyFileSyncPreserveMode(src, dest) {
|
|
56
|
+
const st = fs.statSync(src);
|
|
57
|
+
fs.copyFileSync(src, dest);
|
|
58
|
+
fs.chmodSync(dest, st.mode);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function copyDir({ srcDir, destDir }) {
|
|
62
|
+
assert(!isSymlink(srcDir), `Refusing to copy symlink dir: ${srcDir}`);
|
|
63
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
66
|
+
for (const ent of entries) {
|
|
67
|
+
if (ent.name === ".DS_Store") continue;
|
|
68
|
+
const src = path.join(srcDir, ent.name);
|
|
69
|
+
const dest = path.join(destDir, ent.name);
|
|
70
|
+
|
|
71
|
+
if (isSymlink(src)) {
|
|
72
|
+
throw new Error(`Refusing to copy symlink: ${src}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (ent.isDirectory()) {
|
|
76
|
+
copyDir({ srcDir: src, destDir: dest });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ent.isFile()) {
|
|
80
|
+
copyFileSyncPreserveMode(src, dest);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Ignore sockets, devices, etc.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function listSkillDirs(skillsRoot) {
|
|
88
|
+
if (!fs.existsSync(skillsRoot)) return [];
|
|
89
|
+
const dirs = fs
|
|
90
|
+
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
91
|
+
.filter((d) => d.isDirectory())
|
|
92
|
+
.map((d) => path.join(skillsRoot, d.name));
|
|
93
|
+
|
|
94
|
+
return dirs.filter((d) => fs.existsSync(path.join(d, "SKILL.md")));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildTarget({ repoRoot, outDir, target, skillDirs }) {
|
|
98
|
+
const rootByTarget = {
|
|
99
|
+
codex: path.join(outDir, "codex", ".codex", "skills"),
|
|
100
|
+
vscode: path.join(outDir, "vscode", ".github", "skills"),
|
|
101
|
+
claude: path.join(outDir, "claude", ".claude", "skills"),
|
|
102
|
+
};
|
|
103
|
+
const destSkillsRoot = rootByTarget[target];
|
|
104
|
+
assert(destSkillsRoot, `Unknown target: ${target}`);
|
|
105
|
+
|
|
106
|
+
fs.mkdirSync(destSkillsRoot, { recursive: true });
|
|
107
|
+
|
|
108
|
+
for (const srcSkillDir of skillDirs) {
|
|
109
|
+
const name = path.basename(srcSkillDir);
|
|
110
|
+
const destSkillDir = path.join(destSkillsRoot, name);
|
|
111
|
+
copyDir({ srcDir: srcSkillDir, destDir: destSkillDir });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rel = path.relative(repoRoot, destSkillsRoot);
|
|
115
|
+
process.stdout.write(`OK: built ${target} skillpack at ${rel}\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const VALID_TARGETS = ["codex", "vscode", "claude"];
|
|
119
|
+
|
|
120
|
+
function main() {
|
|
121
|
+
const args = parseArgs(process.argv.slice(2));
|
|
122
|
+
if (args.help) {
|
|
123
|
+
usage();
|
|
124
|
+
process.exit(2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const repoRoot = process.cwd();
|
|
128
|
+
const skillsRoot = path.join(repoRoot, "skills");
|
|
129
|
+
const outDir = path.isAbsolute(args.out) ? args.out : path.join(repoRoot, args.out);
|
|
130
|
+
|
|
131
|
+
let skillDirs = listSkillDirs(skillsRoot);
|
|
132
|
+
assert(skillDirs.length > 0, "No skills found under ./skills");
|
|
133
|
+
|
|
134
|
+
// Filter skills if --skills was specified
|
|
135
|
+
if (args.skills.length > 0) {
|
|
136
|
+
const requestedSkills = new Set(args.skills);
|
|
137
|
+
const availableSkills = skillDirs.map((d) => path.basename(d));
|
|
138
|
+
|
|
139
|
+
// Validate requested skills exist
|
|
140
|
+
for (const s of requestedSkills) {
|
|
141
|
+
assert(availableSkills.includes(s), `Unknown skill: ${s}. Available: ${availableSkills.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
skillDirs = skillDirs.filter((d) => requestedSkills.has(path.basename(d)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const targets = [...new Set(args.targets)];
|
|
148
|
+
for (const t of targets) {
|
|
149
|
+
assert(VALID_TARGETS.includes(t), `Invalid target: ${t}. Valid targets: ${VALID_TARGETS.join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (args.clean) {
|
|
153
|
+
for (const t of targets) {
|
|
154
|
+
const targetDir = path.join(outDir, t);
|
|
155
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const target of targets) {
|
|
160
|
+
buildTarget({ repoRoot, outDir, target, skillDirs });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main();
|
|
165
|
+
|