cc-dev-template 0.1.5 → 0.1.7
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/bin/install.js +23 -16
- package/package.json +1 -1
- package/src/skills/create-agent-skills/SKILL.md +51 -0
- package/src/skills/create-agent-skills/references/audit.md +429 -0
- package/src/skills/create-agent-skills/references/create.md +129 -0
- package/src/skills/create-agent-skills/references/modify.md +104 -0
- package/src/skills/create-agent-skills/references/principles.md +614 -0
- package/src/skills/create-agent-skills/scripts/find-skills.js +206 -0
- package/src/skills/create-agent-skills/scripts/validate-skill.js +386 -0
- package/src/skills/orchestration/SKILL.md +1 -1
- package/src/skills/orchestration/scripts/plan-status.js +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* find-skills.js
|
|
5
|
+
*
|
|
6
|
+
* Discovers all Claude Code skills at user and project levels.
|
|
7
|
+
* Returns JSON with skill locations and basic metadata.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node find-skills.js [--json] [--verbose]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
// Parse arguments
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const jsonOutput = args.includes('--json');
|
|
19
|
+
const verbose = args.includes('--verbose');
|
|
20
|
+
|
|
21
|
+
// Skill locations to scan
|
|
22
|
+
const locations = [
|
|
23
|
+
{
|
|
24
|
+
name: 'user',
|
|
25
|
+
path: path.join(os.homedir(), '.claude', 'skills'),
|
|
26
|
+
description: 'User-level skills (~/.claude/skills/)'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'project',
|
|
30
|
+
path: path.join(process.cwd(), '.claude', 'skills'),
|
|
31
|
+
description: 'Project-level skills (.claude/skills/)'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'source',
|
|
35
|
+
path: path.join(process.cwd(), 'src', 'skills'),
|
|
36
|
+
description: 'Source skills (src/skills/)'
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse YAML frontmatter from SKILL.md content
|
|
42
|
+
*/
|
|
43
|
+
function parseFrontmatter(content) {
|
|
44
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
45
|
+
if (!match) return null;
|
|
46
|
+
|
|
47
|
+
const frontmatter = {};
|
|
48
|
+
const lines = match[1].split('\n');
|
|
49
|
+
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const colonIndex = line.indexOf(':');
|
|
52
|
+
if (colonIndex > 0) {
|
|
53
|
+
const key = line.slice(0, colonIndex).trim();
|
|
54
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
55
|
+
|
|
56
|
+
// Remove quotes if present
|
|
57
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
58
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
59
|
+
value = value.slice(1, -1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
frontmatter[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return frontmatter;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get basic stats about a skill
|
|
71
|
+
*/
|
|
72
|
+
function getSkillStats(skillPath) {
|
|
73
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
80
|
+
const frontmatter = parseFrontmatter(content);
|
|
81
|
+
|
|
82
|
+
// Count words in body (after frontmatter)
|
|
83
|
+
const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
84
|
+
const body = bodyMatch ? bodyMatch[1] : content;
|
|
85
|
+
const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
|
|
86
|
+
|
|
87
|
+
// Check for subdirectories
|
|
88
|
+
const hasReferences = fs.existsSync(path.join(skillPath, 'references'));
|
|
89
|
+
const hasScripts = fs.existsSync(path.join(skillPath, 'scripts'));
|
|
90
|
+
const hasAssets = fs.existsSync(path.join(skillPath, 'assets'));
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: frontmatter?.name || path.basename(skillPath),
|
|
94
|
+
description: frontmatter?.description || null,
|
|
95
|
+
wordCount,
|
|
96
|
+
hasReferences,
|
|
97
|
+
hasScripts,
|
|
98
|
+
hasAssets,
|
|
99
|
+
frontmatterValid: !!(frontmatter?.name && frontmatter?.description)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scan a location for skills
|
|
105
|
+
*/
|
|
106
|
+
function scanLocation(location) {
|
|
107
|
+
const skills = [];
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(location.path)) {
|
|
110
|
+
return skills;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const entries = fs.readdirSync(location.path, { withFileTypes: true });
|
|
114
|
+
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (!entry.isDirectory()) continue;
|
|
117
|
+
|
|
118
|
+
const skillPath = path.join(location.path, entry.name);
|
|
119
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(skillMdPath)) {
|
|
122
|
+
const stats = getSkillStats(skillPath);
|
|
123
|
+
|
|
124
|
+
skills.push({
|
|
125
|
+
directory: entry.name,
|
|
126
|
+
path: skillPath,
|
|
127
|
+
location: location.name,
|
|
128
|
+
...stats
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return skills;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Main execution
|
|
137
|
+
const results = {
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
cwd: process.cwd(),
|
|
140
|
+
locations: {},
|
|
141
|
+
skills: [],
|
|
142
|
+
summary: {
|
|
143
|
+
total: 0,
|
|
144
|
+
byLocation: {}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
for (const location of locations) {
|
|
149
|
+
const skills = scanLocation(location);
|
|
150
|
+
|
|
151
|
+
results.locations[location.name] = {
|
|
152
|
+
path: location.path,
|
|
153
|
+
exists: fs.existsSync(location.path),
|
|
154
|
+
count: skills.length
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
results.skills.push(...skills);
|
|
158
|
+
results.summary.byLocation[location.name] = skills.length;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
results.summary.total = results.skills.length;
|
|
162
|
+
|
|
163
|
+
// Output
|
|
164
|
+
if (jsonOutput) {
|
|
165
|
+
console.log(JSON.stringify(results, null, 2));
|
|
166
|
+
} else {
|
|
167
|
+
console.log('\n=== Claude Code Skills Discovery ===\n');
|
|
168
|
+
|
|
169
|
+
for (const location of locations) {
|
|
170
|
+
const info = results.locations[location.name];
|
|
171
|
+
console.log(`${location.description}`);
|
|
172
|
+
console.log(` Path: ${info.path}`);
|
|
173
|
+
console.log(` Exists: ${info.exists ? 'Yes' : 'No'}`);
|
|
174
|
+
console.log(` Skills found: ${info.count}`);
|
|
175
|
+
console.log();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (results.skills.length === 0) {
|
|
179
|
+
console.log('No skills found.\n');
|
|
180
|
+
} else {
|
|
181
|
+
console.log('=== Skills ===\n');
|
|
182
|
+
|
|
183
|
+
for (const skill of results.skills) {
|
|
184
|
+
console.log(`[${skill.location}] ${skill.name}`);
|
|
185
|
+
console.log(` Path: ${skill.path}`);
|
|
186
|
+
|
|
187
|
+
if (verbose) {
|
|
188
|
+
console.log(` Words: ${skill.wordCount}`);
|
|
189
|
+
console.log(` Valid frontmatter: ${skill.frontmatterValid ? 'Yes' : 'No'}`);
|
|
190
|
+
console.log(` Has references/: ${skill.hasReferences ? 'Yes' : 'No'}`);
|
|
191
|
+
console.log(` Has scripts/: ${skill.hasScripts ? 'Yes' : 'No'}`);
|
|
192
|
+
|
|
193
|
+
if (skill.description) {
|
|
194
|
+
const truncated = skill.description.length > 80
|
|
195
|
+
? skill.description.slice(0, 80) + '...'
|
|
196
|
+
: skill.description;
|
|
197
|
+
console.log(` Description: ${truncated}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(`Total: ${results.summary.total} skill(s)\n`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate-skill.js
|
|
5
|
+
*
|
|
6
|
+
* Validates a Claude Code skill against best practices.
|
|
7
|
+
* Returns detailed findings with severity levels.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node validate-skill.js <skill-path> [--json] [--fix]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Parse arguments
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const skillPath = args.find(a => !a.startsWith('--'));
|
|
18
|
+
const jsonOutput = args.includes('--json');
|
|
19
|
+
const autoFix = args.includes('--fix');
|
|
20
|
+
|
|
21
|
+
if (!skillPath) {
|
|
22
|
+
console.error('Usage: node validate-skill.js <skill-path> [--json] [--fix]');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Resolve skill path
|
|
27
|
+
const resolvedPath = path.resolve(skillPath);
|
|
28
|
+
const skillMdPath = path.join(resolvedPath, 'SKILL.md');
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
31
|
+
console.error(`Error: SKILL.md not found at ${skillMdPath}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Findings storage
|
|
36
|
+
const findings = [];
|
|
37
|
+
|
|
38
|
+
function addFinding(category, severity, message, line = null, suggestion = null) {
|
|
39
|
+
findings.push({ category, severity, message, line, suggestion });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse YAML frontmatter
|
|
44
|
+
*/
|
|
45
|
+
function parseFrontmatter(content) {
|
|
46
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
47
|
+
if (!match) return { valid: false, data: null, raw: null };
|
|
48
|
+
|
|
49
|
+
const raw = match[1];
|
|
50
|
+
const data = {};
|
|
51
|
+
const lines = raw.split('\n');
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const colonIndex = line.indexOf(':');
|
|
55
|
+
if (colonIndex > 0) {
|
|
56
|
+
const key = line.slice(0, colonIndex).trim();
|
|
57
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
58
|
+
|
|
59
|
+
// Handle multi-line values (basic support)
|
|
60
|
+
if (value === '' || value === '|' || value === '>') {
|
|
61
|
+
// Skip complex multi-line for now
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Remove quotes if present
|
|
66
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
67
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
68
|
+
value = value.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { valid: true, data, raw };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get body content (after frontmatter)
|
|
80
|
+
*/
|
|
81
|
+
function getBody(content) {
|
|
82
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
83
|
+
return match ? match[1] : content;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Read skill content
|
|
87
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
88
|
+
const frontmatter = parseFrontmatter(content);
|
|
89
|
+
const body = getBody(content);
|
|
90
|
+
const lines = content.split('\n');
|
|
91
|
+
|
|
92
|
+
// === VALIDATION CHECKS ===
|
|
93
|
+
|
|
94
|
+
// Check 1: Frontmatter exists
|
|
95
|
+
if (!frontmatter.valid) {
|
|
96
|
+
addFinding('STRUCTURE', 'error', 'Missing or invalid YAML frontmatter');
|
|
97
|
+
} else {
|
|
98
|
+
// Check 2: Required fields
|
|
99
|
+
if (!frontmatter.data.name) {
|
|
100
|
+
addFinding('STRUCTURE', 'error', 'Missing required field: name');
|
|
101
|
+
} else {
|
|
102
|
+
// Check name format (hyphen-case)
|
|
103
|
+
const namePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
104
|
+
if (!namePattern.test(frontmatter.data.name)) {
|
|
105
|
+
addFinding('STRUCTURE', 'error',
|
|
106
|
+
`Invalid name format: "${frontmatter.data.name}" - must be hyphen-case (lowercase letters, numbers, hyphens)`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check name matches directory
|
|
110
|
+
const dirName = path.basename(resolvedPath);
|
|
111
|
+
if (frontmatter.data.name !== dirName) {
|
|
112
|
+
addFinding('STRUCTURE', 'warning',
|
|
113
|
+
`Name "${frontmatter.data.name}" doesn't match directory "${dirName}"`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check name length
|
|
117
|
+
if (frontmatter.data.name.length > 64) {
|
|
118
|
+
addFinding('STRUCTURE', 'error',
|
|
119
|
+
`Name exceeds 64 characters (${frontmatter.data.name.length})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!frontmatter.data.description) {
|
|
124
|
+
addFinding('STRUCTURE', 'error', 'Missing required field: description');
|
|
125
|
+
} else {
|
|
126
|
+
// Check description length
|
|
127
|
+
if (frontmatter.data.description.length > 1024) {
|
|
128
|
+
addFinding('DESCRIPTION', 'error',
|
|
129
|
+
`Description exceeds 1024 characters (${frontmatter.data.description.length})`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for third person
|
|
133
|
+
const desc = frontmatter.data.description.toLowerCase();
|
|
134
|
+
if (desc.startsWith('use this') || desc.startsWith('use when')) {
|
|
135
|
+
addFinding('DESCRIPTION', 'warning',
|
|
136
|
+
'Description should use third person ("This skill should be used when...") not second person',
|
|
137
|
+
null,
|
|
138
|
+
'Start with "This skill should be used when..." or similar third-person phrasing');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check for trigger phrases (quoted text)
|
|
142
|
+
const hasQuotedPhrases = /"[^"]+"|'[^']+'/.test(frontmatter.data.description);
|
|
143
|
+
if (!hasQuotedPhrases) {
|
|
144
|
+
addFinding('DESCRIPTION', 'warning',
|
|
145
|
+
'Description lacks quoted trigger phrases',
|
|
146
|
+
null,
|
|
147
|
+
'Add specific phrases users might say, e.g., "create a skill", "build a new skill"');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if description is too detailed (might prevent activation)
|
|
151
|
+
if (frontmatter.data.description.length > 500 && !hasQuotedPhrases) {
|
|
152
|
+
addFinding('DESCRIPTION', 'info',
|
|
153
|
+
'Long description without trigger phrases may reduce activation reliability');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check 3: Second person violations
|
|
159
|
+
const secondPersonPatterns = [
|
|
160
|
+
/\byou should\b/gi,
|
|
161
|
+
/\byou can\b/gi,
|
|
162
|
+
/\byou need\b/gi,
|
|
163
|
+
/\byou might\b/gi,
|
|
164
|
+
/\byou will\b/gi,
|
|
165
|
+
/\byou must\b/gi,
|
|
166
|
+
/\byour\b/gi
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
let secondPersonCount = 0;
|
|
170
|
+
const secondPersonExamples = [];
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < lines.length; i++) {
|
|
173
|
+
const line = lines[i];
|
|
174
|
+
// Skip frontmatter
|
|
175
|
+
if (i < 3) continue;
|
|
176
|
+
|
|
177
|
+
for (const pattern of secondPersonPatterns) {
|
|
178
|
+
const matches = line.match(pattern);
|
|
179
|
+
if (matches) {
|
|
180
|
+
secondPersonCount += matches.length;
|
|
181
|
+
if (secondPersonExamples.length < 3) {
|
|
182
|
+
secondPersonExamples.push({ line: i + 1, text: line.trim().slice(0, 60) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (secondPersonCount > 0) {
|
|
189
|
+
addFinding('STYLE', 'error',
|
|
190
|
+
`Found ${secondPersonCount} instance(s) of second person language`,
|
|
191
|
+
secondPersonExamples[0]?.line,
|
|
192
|
+
'Convert to imperative form: "You should parse" → "Parse"');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check 4: Negative framing
|
|
196
|
+
const negativePatterns = [
|
|
197
|
+
/\bdon't\b/gi,
|
|
198
|
+
/\bdo not\b/gi,
|
|
199
|
+
/\bnever\b/gi,
|
|
200
|
+
/\bavoid\b/gi,
|
|
201
|
+
/\bshould not\b/gi,
|
|
202
|
+
/\bshouldn't\b/gi,
|
|
203
|
+
/\bcan't\b/gi,
|
|
204
|
+
/\bcannot\b/gi
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
let negativeCount = 0;
|
|
208
|
+
const negativeExamples = [];
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < lines.length; i++) {
|
|
211
|
+
const line = lines[i];
|
|
212
|
+
if (i < 3) continue;
|
|
213
|
+
|
|
214
|
+
for (const pattern of negativePatterns) {
|
|
215
|
+
const matches = line.match(pattern);
|
|
216
|
+
if (matches) {
|
|
217
|
+
negativeCount += matches.length;
|
|
218
|
+
if (negativeExamples.length < 3) {
|
|
219
|
+
negativeExamples.push({ line: i + 1, text: line.trim().slice(0, 60) });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (negativeCount > 3) {
|
|
226
|
+
addFinding('STYLE', 'warning',
|
|
227
|
+
`Found ${negativeCount} instance(s) of negative framing`,
|
|
228
|
+
negativeExamples[0]?.line,
|
|
229
|
+
'Use positive framing: "Don\'t do X" → "Do Y instead"');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check 5: Body size
|
|
233
|
+
const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
|
|
234
|
+
const lineCount = body.split('\n').length;
|
|
235
|
+
|
|
236
|
+
if (wordCount > 5000) {
|
|
237
|
+
addFinding('SIZE', 'error',
|
|
238
|
+
`SKILL.md body is ${wordCount} words - strongly exceeds 2,000 word recommendation`,
|
|
239
|
+
null,
|
|
240
|
+
'Move detailed content to references/ directory');
|
|
241
|
+
} else if (wordCount > 3500) {
|
|
242
|
+
addFinding('SIZE', 'warning',
|
|
243
|
+
`SKILL.md body is ${wordCount} words - exceeds 2,000 word recommendation`,
|
|
244
|
+
null,
|
|
245
|
+
'Consider moving detailed content to references/ directory');
|
|
246
|
+
} else if (wordCount > 2000) {
|
|
247
|
+
addFinding('SIZE', 'info',
|
|
248
|
+
`SKILL.md body is ${wordCount} words - at upper limit of recommendation`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check 6: Progressive disclosure
|
|
252
|
+
const hasReferences = fs.existsSync(path.join(resolvedPath, 'references'));
|
|
253
|
+
const hasScripts = fs.existsSync(path.join(resolvedPath, 'scripts'));
|
|
254
|
+
|
|
255
|
+
if (wordCount > 2000 && !hasReferences) {
|
|
256
|
+
addFinding('DISCLOSURE', 'warning',
|
|
257
|
+
'Large SKILL.md without references/ directory - not using progressive disclosure',
|
|
258
|
+
null,
|
|
259
|
+
'Create references/ and move detailed content there');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check 7: Reference integrity
|
|
263
|
+
const referencePaths = body.match(/`references\/[^`]+`|references\/[\w.-]+/g) || [];
|
|
264
|
+
const scriptPaths = body.match(/`scripts\/[^`]+`|scripts\/[\w.-]+/g) || [];
|
|
265
|
+
|
|
266
|
+
for (const ref of referencePaths) {
|
|
267
|
+
const cleanPath = ref.replace(/`/g, '');
|
|
268
|
+
const fullPath = path.join(resolvedPath, cleanPath);
|
|
269
|
+
|
|
270
|
+
if (!fs.existsSync(fullPath)) {
|
|
271
|
+
addFinding('BROKEN', 'error',
|
|
272
|
+
`Referenced file doesn't exist: ${cleanPath}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const ref of scriptPaths) {
|
|
277
|
+
const cleanPath = ref.replace(/`/g, '');
|
|
278
|
+
const fullPath = path.join(resolvedPath, cleanPath);
|
|
279
|
+
|
|
280
|
+
if (!fs.existsSync(fullPath)) {
|
|
281
|
+
addFinding('BROKEN', 'error',
|
|
282
|
+
`Referenced script doesn't exist: ${cleanPath}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check 8: Unreferenced resources
|
|
287
|
+
if (hasReferences) {
|
|
288
|
+
const refDir = path.join(resolvedPath, 'references');
|
|
289
|
+
const refFiles = fs.readdirSync(refDir);
|
|
290
|
+
|
|
291
|
+
for (const file of refFiles) {
|
|
292
|
+
const isReferenced = body.includes(`references/${file}`) ||
|
|
293
|
+
body.includes(`references/${path.basename(file, '.md')}`);
|
|
294
|
+
if (!isReferenced && file.endsWith('.md')) {
|
|
295
|
+
addFinding('DISCLOSURE', 'info',
|
|
296
|
+
`File references/${file} exists but may not be referenced in SKILL.md`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (hasScripts) {
|
|
302
|
+
const scriptDir = path.join(resolvedPath, 'scripts');
|
|
303
|
+
const scriptFiles = fs.readdirSync(scriptDir);
|
|
304
|
+
|
|
305
|
+
for (const file of scriptFiles) {
|
|
306
|
+
const isReferenced = body.includes(`scripts/${file}`);
|
|
307
|
+
if (!isReferenced) {
|
|
308
|
+
addFinding('DISCLOSURE', 'info',
|
|
309
|
+
`File scripts/${file} exists but may not be referenced in SKILL.md`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// === OUTPUT ===
|
|
315
|
+
|
|
316
|
+
const result = {
|
|
317
|
+
skill: {
|
|
318
|
+
path: resolvedPath,
|
|
319
|
+
name: frontmatter.data?.name || path.basename(resolvedPath),
|
|
320
|
+
wordCount,
|
|
321
|
+
lineCount,
|
|
322
|
+
hasReferences,
|
|
323
|
+
hasScripts
|
|
324
|
+
},
|
|
325
|
+
findings,
|
|
326
|
+
summary: {
|
|
327
|
+
errors: findings.filter(f => f.severity === 'error').length,
|
|
328
|
+
warnings: findings.filter(f => f.severity === 'warning').length,
|
|
329
|
+
info: findings.filter(f => f.severity === 'info').length,
|
|
330
|
+
passed: findings.filter(f => f.severity === 'error').length === 0
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (jsonOutput) {
|
|
335
|
+
console.log(JSON.stringify(result, null, 2));
|
|
336
|
+
} else {
|
|
337
|
+
console.log(`\n=== Skill Validation: ${result.skill.name} ===\n`);
|
|
338
|
+
console.log(`Path: ${result.skill.path}`);
|
|
339
|
+
console.log(`Words: ${result.skill.wordCount}`);
|
|
340
|
+
console.log(`Has references/: ${result.skill.hasReferences ? 'Yes' : 'No'}`);
|
|
341
|
+
console.log(`Has scripts/: ${result.skill.hasScripts ? 'Yes' : 'No'}`);
|
|
342
|
+
console.log();
|
|
343
|
+
|
|
344
|
+
if (findings.length === 0) {
|
|
345
|
+
console.log('✓ All checks passed!\n');
|
|
346
|
+
} else {
|
|
347
|
+
// Group by severity
|
|
348
|
+
const errors = findings.filter(f => f.severity === 'error');
|
|
349
|
+
const warnings = findings.filter(f => f.severity === 'warning');
|
|
350
|
+
const infos = findings.filter(f => f.severity === 'info');
|
|
351
|
+
|
|
352
|
+
if (errors.length > 0) {
|
|
353
|
+
console.log('ERRORS:');
|
|
354
|
+
for (const f of errors) {
|
|
355
|
+
console.log(` ✗ [${f.category}] ${f.message}`);
|
|
356
|
+
if (f.line) console.log(` Line ${f.line}`);
|
|
357
|
+
if (f.suggestion) console.log(` Suggestion: ${f.suggestion}`);
|
|
358
|
+
}
|
|
359
|
+
console.log();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (warnings.length > 0) {
|
|
363
|
+
console.log('WARNINGS:');
|
|
364
|
+
for (const f of warnings) {
|
|
365
|
+
console.log(` ! [${f.category}] ${f.message}`);
|
|
366
|
+
if (f.line) console.log(` Line ${f.line}`);
|
|
367
|
+
if (f.suggestion) console.log(` Suggestion: ${f.suggestion}`);
|
|
368
|
+
}
|
|
369
|
+
console.log();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (infos.length > 0) {
|
|
373
|
+
console.log('INFO:');
|
|
374
|
+
for (const f of infos) {
|
|
375
|
+
console.log(` i [${f.category}] ${f.message}`);
|
|
376
|
+
}
|
|
377
|
+
console.log();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(`Summary: ${result.summary.errors} error(s), ${result.summary.warnings} warning(s), ${result.summary.info} info(s)`);
|
|
381
|
+
console.log(`Status: ${result.summary.passed ? 'PASSED' : 'FAILED'}\n`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Exit with error code if validation failed
|
|
386
|
+
process.exit(result.summary.passed ? 0 : 1);
|
|
@@ -23,7 +23,7 @@ Complete each step in order before proceeding to the next.
|
|
|
23
23
|
Run the plan status script to find existing plans:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
node ~/.claude/scripts/plan-status.js
|
|
26
|
+
node ~/.claude/skills/orchestration/scripts/plan-status.js
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
This returns a list of plans with their current status (draft, approved, in_progress, completed).
|