@massa-ai/cursor-plugin 1.27.0 → 1.29.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/.cursor-plugin/plugin.json +1 -1
- package/agent-profiles/balanced/massa-ai-judge.md +1 -2
- package/agent-profiles/cheap/massa-ai-judge.md +1 -2
- package/agent-profiles/heavy/massa-ai-judge.md +1 -2
- package/agent-profiles/home/massa-ai-judge.md +1 -2
- package/agent-profiles/work/massa-ai-judge.md +1 -2
- package/agents/massa-ai-judge.md +1 -2
- package/install.sh +35 -0
- package/package.json +1 -1
- package/skills/agents/judge/SKILL.md +3 -3
- package/skills/massa-ai/SKILL.md +5 -1
- package/skills/massa-ai/references/agent-orchestration.md +1 -1
- package/skills/massa-ai/references/coding-guidelines.md +67 -0
- package/skills/massa-ai/references/skill-architect/examples.md +256 -0
- package/skills/massa-ai/references/skill-architect/patterns.md +317 -0
- package/skills/massa-ai/references/skill-architect/quality-checklist.md +70 -0
- package/skills/massa-ai/scripts/validate_skill.ts +364 -0
- package/skills/massa-ai/workflows/skill-architect.md +393 -0
- package/skills/massa-ai/workflows/to-prd.md +81 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Validate a skill folder against Skill Architect requirements.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun scripts/validate_skill.ts <path-to-skill-folder>
|
|
7
|
+
* bun scripts/validate_skill.ts <path-to-skill-folder> --format json
|
|
8
|
+
* bun scripts/validate_skill.ts <path-to-skill-folder> --json-out /tmp/skill-report.json
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 = pass (warnings allowed)
|
|
12
|
+
* 1 = fail (at least one error)
|
|
13
|
+
*
|
|
14
|
+
* Token-efficient workflow: run once with --json-out, then reuse the saved
|
|
15
|
+
* JSON for feedback/review without re-running validation.
|
|
16
|
+
*
|
|
17
|
+
* TypeScript port of the former validate_skill.py (Skill Architect,
|
|
18
|
+
* Useful-Agent-Skills). Frontmatter is parsed with Bun's built-in real YAML
|
|
19
|
+
* parser (`Bun.YAML.parse`) — same precedent as
|
|
20
|
+
* scripts/__tests__/workflow-bun-cache.test.ts — so the Python version's
|
|
21
|
+
* PyYAML/stdlib fallback split is gone.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
25
|
+
import path from "path";
|
|
26
|
+
|
|
27
|
+
type Severity = "error" | "warning";
|
|
28
|
+
|
|
29
|
+
interface Check {
|
|
30
|
+
name: string;
|
|
31
|
+
passed: boolean;
|
|
32
|
+
message: string;
|
|
33
|
+
severity: Severity;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Results {
|
|
37
|
+
path: string;
|
|
38
|
+
checks: Check[];
|
|
39
|
+
passed: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
warnings: number;
|
|
42
|
+
parser_mode: string;
|
|
43
|
+
next_steps: string[];
|
|
44
|
+
summary?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
48
|
+
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
49
|
+
|
|
50
|
+
export function validateSkill(skillPath: string): Results {
|
|
51
|
+
const results: Results = {
|
|
52
|
+
path: skillPath,
|
|
53
|
+
checks: [],
|
|
54
|
+
passed: 0,
|
|
55
|
+
failed: 0,
|
|
56
|
+
warnings: 0,
|
|
57
|
+
parser_mode: "unknown",
|
|
58
|
+
next_steps: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const addCheck = (name: string, passed: boolean, message: string, severity: Severity = "error") => {
|
|
62
|
+
results.checks.push({ name, passed, message, severity });
|
|
63
|
+
if (passed) results.passed += 1;
|
|
64
|
+
else if (severity === "warning") results.warnings += 1;
|
|
65
|
+
else results.failed += 1;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// --- Check 1: Folder exists ---
|
|
69
|
+
if (!existsSync(skillPath) || !statSync(skillPath).isDirectory()) {
|
|
70
|
+
addCheck("folder_exists", false, `Path is not a directory: ${skillPath}`);
|
|
71
|
+
results.summary = "FAIL — folder not found";
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
addCheck("folder_exists", true, "Skill folder exists");
|
|
75
|
+
|
|
76
|
+
// --- Check 2: Folder name is kebab-case ---
|
|
77
|
+
const folderName = path.basename(path.normalize(skillPath));
|
|
78
|
+
const isKebab = KEBAB_RE.test(folderName);
|
|
79
|
+
addCheck("folder_kebab_case", isKebab, `Folder name '${folderName}' ${isKebab ? "is" : "is NOT"} kebab-case`);
|
|
80
|
+
|
|
81
|
+
// --- Check 3: SKILL.md exists (exact casing) ---
|
|
82
|
+
const entries = readdirSync(skillPath);
|
|
83
|
+
const hasSkillMd = entries.includes("SKILL.md");
|
|
84
|
+
addCheck("skill_md_exists", hasSkillMd, hasSkillMd ? "SKILL.md exists" : "SKILL.md not found (case-sensitive)");
|
|
85
|
+
|
|
86
|
+
const wrongCasings = entries.filter((e) => e.toLowerCase() === "skill.md" && e !== "SKILL.md");
|
|
87
|
+
if (wrongCasings.length > 0) {
|
|
88
|
+
addCheck("skill_md_casing", false, `Found wrong casing: ${wrongCasings[0]} (must be exactly SKILL.md)`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!hasSkillMd) {
|
|
92
|
+
results.summary = "FAIL — SKILL.md not found";
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Check 4: No README.md ---
|
|
97
|
+
const hasReadme = entries.some((e) => e.toLowerCase() === "readme.md");
|
|
98
|
+
addCheck(
|
|
99
|
+
"no_readme",
|
|
100
|
+
!hasReadme,
|
|
101
|
+
hasReadme ? "README.md found — remove it (skills are for agents, not humans)" : "No README.md in skill folder",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// --- Check 5: Parse frontmatter ---
|
|
105
|
+
const content = readFileSync(path.join(skillPath, "SKILL.md"), "utf8");
|
|
106
|
+
const fmMatch = FRONTMATTER_RE.exec(content);
|
|
107
|
+
if (!fmMatch) {
|
|
108
|
+
addCheck("frontmatter_delimiters", false, "Missing or malformed --- delimiters in frontmatter");
|
|
109
|
+
results.summary = "FAIL — frontmatter parse error";
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
addCheck("frontmatter_delimiters", true, "YAML frontmatter delimiters present");
|
|
113
|
+
|
|
114
|
+
let fm: Record<string, unknown>;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = Bun.YAML.parse(fmMatch[1]);
|
|
117
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
118
|
+
throw new Error("Frontmatter is not a YAML mapping");
|
|
119
|
+
}
|
|
120
|
+
fm = parsed as Record<string, unknown>;
|
|
121
|
+
results.parser_mode = "bun-yaml";
|
|
122
|
+
addCheck("frontmatter_valid_yaml", true, "Frontmatter is valid YAML (parsed with Bun.YAML)");
|
|
123
|
+
} catch (e) {
|
|
124
|
+
addCheck("frontmatter_valid_yaml", false, `YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
125
|
+
results.summary = "FAIL — YAML parse error";
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Check 6: name field ---
|
|
130
|
+
const name = fm.name;
|
|
131
|
+
if (!name) {
|
|
132
|
+
addCheck("name_present", false, "Missing 'name' field in frontmatter");
|
|
133
|
+
} else {
|
|
134
|
+
addCheck("name_present", true, `name: ${name}`);
|
|
135
|
+
const isNameKebab = KEBAB_RE.test(String(name));
|
|
136
|
+
addCheck("name_kebab_case", isNameKebab, `name '${name}' ${isNameKebab ? "is" : "is NOT"} kebab-case`);
|
|
137
|
+
|
|
138
|
+
const nameLower = String(name).toLowerCase();
|
|
139
|
+
const hasReserved = nameLower.includes("claude") || nameLower.includes("anthropic");
|
|
140
|
+
addCheck(
|
|
141
|
+
"name_not_reserved",
|
|
142
|
+
!hasReserved,
|
|
143
|
+
hasReserved ? "Name contains 'claude' or 'anthropic' (reserved)" : "Name does not use reserved terms",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const namesMatch = String(name) === folderName;
|
|
147
|
+
addCheck(
|
|
148
|
+
"name_matches_folder",
|
|
149
|
+
namesMatch,
|
|
150
|
+
namesMatch
|
|
151
|
+
? `name '${name}' matches folder '${folderName}'`
|
|
152
|
+
: `name '${name}' does NOT match folder '${folderName}'`,
|
|
153
|
+
"warning",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Check 7: description field ---
|
|
158
|
+
const desc = fm.description;
|
|
159
|
+
if (!desc) {
|
|
160
|
+
addCheck("description_present", false, "Missing 'description' field in frontmatter");
|
|
161
|
+
} else {
|
|
162
|
+
const descStr = String(desc).trim();
|
|
163
|
+
addCheck("description_present", true, `description present (${descStr.length} chars)`);
|
|
164
|
+
|
|
165
|
+
addCheck("description_length", descStr.length <= 1024, `Description length: ${descStr.length}/1024 chars`);
|
|
166
|
+
|
|
167
|
+
const hasXml = descStr.includes("<") || descStr.includes(">");
|
|
168
|
+
addCheck(
|
|
169
|
+
"description_no_xml",
|
|
170
|
+
!hasXml,
|
|
171
|
+
hasXml ? "XML angle brackets found in description (forbidden)" : "No XML brackets in description",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const triggerKeywords = ["use when", "use for", "use this", "trigger", "ask for", "asks to", "says", "mentions"];
|
|
175
|
+
const descLower = descStr.toLowerCase();
|
|
176
|
+
const hasTriggers = triggerKeywords.some((kw) => descLower.includes(kw));
|
|
177
|
+
addCheck(
|
|
178
|
+
"description_has_triggers",
|
|
179
|
+
hasTriggers,
|
|
180
|
+
hasTriggers
|
|
181
|
+
? "Description includes trigger guidance"
|
|
182
|
+
: "Missing trigger phrases — add 'Use when...' guidance (mandatory per CONTRIBUTING.md)",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const negativeKeywords = ["do not use", "don't use", "not for", "not intended for"];
|
|
186
|
+
const hasNegativeScope = negativeKeywords.some((kw) => descLower.includes(kw));
|
|
187
|
+
addCheck(
|
|
188
|
+
"description_has_negative_scope",
|
|
189
|
+
hasNegativeScope,
|
|
190
|
+
hasNegativeScope
|
|
191
|
+
? "Description includes negative scope"
|
|
192
|
+
: "Missing negative scope — add 'Do NOT use for...' guidance (mandatory per CONTRIBUTING.md)",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Check 7b: metadata field ---
|
|
197
|
+
const metadata = fm.metadata;
|
|
198
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
199
|
+
addCheck(
|
|
200
|
+
"metadata_present",
|
|
201
|
+
false,
|
|
202
|
+
"Missing 'metadata' field in frontmatter (expected metadata.version and metadata.author)",
|
|
203
|
+
"warning",
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
addCheck("metadata_present", true, "metadata field present");
|
|
207
|
+
const meta = metadata as Record<string, unknown>;
|
|
208
|
+
|
|
209
|
+
const metaVersion = meta.version;
|
|
210
|
+
addCheck(
|
|
211
|
+
"metadata_version",
|
|
212
|
+
Boolean(metaVersion),
|
|
213
|
+
metaVersion ? `metadata.version: ${metaVersion}` : "Missing metadata.version",
|
|
214
|
+
"warning",
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const metaAuthor = meta.author;
|
|
218
|
+
addCheck(
|
|
219
|
+
"metadata_author",
|
|
220
|
+
Boolean(metaAuthor),
|
|
221
|
+
metaAuthor ? `metadata.author: ${metaAuthor}` : "Missing metadata.author",
|
|
222
|
+
"warning",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- Check 8: Body content ---
|
|
227
|
+
const body = content.slice(fmMatch[0].length);
|
|
228
|
+
const lineCount = body.trim().split("\n").length;
|
|
229
|
+
addCheck(
|
|
230
|
+
"body_line_count",
|
|
231
|
+
lineCount <= 500,
|
|
232
|
+
`SKILL.md body: ${lineCount} lines ${lineCount <= 500 ? "(good)" : "(consider moving content to references/)"}`,
|
|
233
|
+
lineCount > 500 ? "warning" : "error",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const hasExamples = /(example|user says|result:)/i.test(body);
|
|
237
|
+
addCheck(
|
|
238
|
+
"body_has_examples",
|
|
239
|
+
hasExamples,
|
|
240
|
+
hasExamples ? "Instructions include examples" : "Consider adding usage examples",
|
|
241
|
+
"warning",
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const hasErrorHandling = /(error|fail|troubleshoot|issue|problem|if.*fails)/i.test(body);
|
|
245
|
+
addCheck(
|
|
246
|
+
"body_has_error_handling",
|
|
247
|
+
hasErrorHandling,
|
|
248
|
+
hasErrorHandling ? "Instructions include error handling" : "Consider adding error handling guidance",
|
|
249
|
+
"warning",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// --- Check 9: Optional files ---
|
|
253
|
+
const refsDir = path.join(skillPath, "references");
|
|
254
|
+
if (entries.includes("references") && existsSync(refsDir) && statSync(refsDir).isDirectory()) {
|
|
255
|
+
for (const ref of readdirSync(refsDir)) {
|
|
256
|
+
const refMentioned = body.includes(ref) || body.includes(`references/${ref}`);
|
|
257
|
+
addCheck(
|
|
258
|
+
`ref_linked_${ref}`,
|
|
259
|
+
refMentioned,
|
|
260
|
+
refMentioned
|
|
261
|
+
? `references/${ref} is referenced in SKILL.md`
|
|
262
|
+
: `references/${ref} exists but is not referenced in SKILL.md`,
|
|
263
|
+
"warning",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- Summary ---
|
|
269
|
+
if (results.failed === 0) {
|
|
270
|
+
results.summary =
|
|
271
|
+
`PASS — ${results.passed} checks passed` + (results.warnings > 0 ? `, ${results.warnings} warnings` : "");
|
|
272
|
+
} else {
|
|
273
|
+
results.summary = `FAIL — ${results.failed} errors, ${results.warnings} warnings`;
|
|
274
|
+
results.next_steps = results.checks
|
|
275
|
+
.filter((c) => !c.passed && c.severity === "error")
|
|
276
|
+
.map((c) => `Fix check '${c.name}': ${c.message}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function printReport(results: Results, verbose: boolean): void {
|
|
283
|
+
const bar = "=".repeat(60);
|
|
284
|
+
const line = "─".repeat(60);
|
|
285
|
+
console.log(`\n${bar}`);
|
|
286
|
+
console.log(" Skill Validation Report");
|
|
287
|
+
console.log(` Path: ${results.path}`);
|
|
288
|
+
console.log(` Parser: ${results.parser_mode}`);
|
|
289
|
+
console.log(`${bar}\n`);
|
|
290
|
+
|
|
291
|
+
for (const check of results.checks) {
|
|
292
|
+
if (check.passed && !verbose) continue;
|
|
293
|
+
const icon = check.passed ? "✅" : check.severity === "warning" ? "⚠️" : "❌";
|
|
294
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log(`\n${line}`);
|
|
298
|
+
console.log(` ${results.summary}`);
|
|
299
|
+
console.log(` Passed: ${results.passed} | Failed: ${results.failed} | Warnings: ${results.warnings}`);
|
|
300
|
+
console.log(`${line}\n`);
|
|
301
|
+
|
|
302
|
+
if (results.next_steps.length > 0) {
|
|
303
|
+
console.log(" Next steps:");
|
|
304
|
+
results.next_steps.forEach((step, i) => console.log(` ${i + 1}. ${step}`));
|
|
305
|
+
console.log("");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (import.meta.main) {
|
|
310
|
+
const args = process.argv.slice(2);
|
|
311
|
+
let skillPath: string | undefined;
|
|
312
|
+
let format: "human" | "json" | "both" = "human";
|
|
313
|
+
let verbose = false;
|
|
314
|
+
let prettyJson = false;
|
|
315
|
+
let jsonOut: string | undefined;
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < args.length; i++) {
|
|
318
|
+
const a = args[i]!;
|
|
319
|
+
if (a === "--format") {
|
|
320
|
+
const v = args[++i];
|
|
321
|
+
if (v !== "human" && v !== "json" && v !== "both") {
|
|
322
|
+
console.error(`Invalid --format: ${v} (choose human|json|both)`);
|
|
323
|
+
process.exit(2);
|
|
324
|
+
}
|
|
325
|
+
format = v;
|
|
326
|
+
} else if (a === "--verbose") verbose = true;
|
|
327
|
+
else if (a === "--pretty-json") prettyJson = true;
|
|
328
|
+
else if (a === "--json-out") jsonOut = args[++i];
|
|
329
|
+
else if (a === "-h" || a === "--help") {
|
|
330
|
+
console.log(
|
|
331
|
+
"Usage: bun scripts/validate_skill.ts <path> [--format human|json|both] [--verbose] [--pretty-json] [--json-out FILE]\n" +
|
|
332
|
+
"Tip: use --json-out FILE to save full results and avoid re-running for later feedback.",
|
|
333
|
+
);
|
|
334
|
+
process.exit(0);
|
|
335
|
+
} else if (!a.startsWith("-") && skillPath === undefined) skillPath = a;
|
|
336
|
+
else {
|
|
337
|
+
console.error(`Unknown argument: ${a}`);
|
|
338
|
+
process.exit(2);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!skillPath) {
|
|
343
|
+
console.error("Missing required argument: path to the skill folder");
|
|
344
|
+
process.exit(2);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const results = validateSkill(skillPath);
|
|
348
|
+
const reportJson = JSON.stringify(results, null, prettyJson ? 2 : undefined);
|
|
349
|
+
|
|
350
|
+
if (format === "human" || format === "both") {
|
|
351
|
+
printReport(results, verbose);
|
|
352
|
+
if (!jsonOut) console.log(" Tip: add --json-out FILE to reuse this report without re-running.\n");
|
|
353
|
+
}
|
|
354
|
+
if (format === "json" || format === "both") {
|
|
355
|
+
if (format === "both") console.log("--- JSON Report ---");
|
|
356
|
+
console.log(reportJson);
|
|
357
|
+
}
|
|
358
|
+
if (jsonOut) {
|
|
359
|
+
writeFileSync(jsonOut, reportJson);
|
|
360
|
+
if (format === "human" || format === "both") console.log(` JSON report saved to: ${jsonOut}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
process.exit(results.failed === 0 ? 0 : 1);
|
|
364
|
+
}
|