cc-dev-template 0.1.4 → 0.1.6
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 +95 -0
- package/src/skills/create-agent-skills/references/audit.md +334 -0
- package/src/skills/create-agent-skills/references/create.md +370 -0
- package/src/skills/create-agent-skills/references/modify.md +377 -0
- package/src/skills/create-agent-skills/references/principles.md +537 -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/references/planning/draft.md +24 -1
- package/src/skills/orchestration/references/planning/explore.md +5 -2
- package/src/skills/orchestration/references/planning/finalize.md +6 -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).
|
|
@@ -94,14 +94,37 @@ relevant_adrs:
|
|
|
94
94
|
- id: ADR-[XXX]
|
|
95
95
|
title: [title]
|
|
96
96
|
constraint: [What this ADR requires us to do or avoid]
|
|
97
|
+
|
|
98
|
+
# Submodules affected by this work (for repos with git submodules)
|
|
99
|
+
affected_submodules:
|
|
100
|
+
- path: [relative/path/to/submodule]
|
|
101
|
+
reason: [Why this submodule is affected by the plan]
|
|
97
102
|
```
|
|
98
103
|
|
|
99
104
|
**Field guidance:**
|
|
100
105
|
|
|
101
106
|
- **Required**: id, title, type, status, created, problem_statement, goals, success_criteria
|
|
102
|
-
- **Include if relevant**: integration_points, data_models, api_contracts, relevant_adrs
|
|
107
|
+
- **Include if relevant**: integration_points, data_models, api_contracts, relevant_adrs, affected_submodules
|
|
103
108
|
- **Only include fields that were discussed**: The plan reflects the conversation from Phases 1 and 2
|
|
104
109
|
|
|
110
|
+
**affected_submodules field:**
|
|
111
|
+
|
|
112
|
+
Include this field when working in repositories with git submodules and the plan's scope touches specific submodules. This enables scope-aware ADR discovery—the adr-agent can filter to only show ADRs that are either global or scoped to the affected submodules.
|
|
113
|
+
|
|
114
|
+
- `path` (required): Relative path to the submodule, matching what appears in `.gitmodules`
|
|
115
|
+
- `reason` (optional): Brief explanation of why this submodule is affected by the plan
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
```yaml
|
|
119
|
+
affected_submodules:
|
|
120
|
+
- path: packages/auth
|
|
121
|
+
reason: Adding new authentication method
|
|
122
|
+
- path: packages/shared
|
|
123
|
+
reason: Shared types need updating for new auth flow
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
When a plan declares affected_submodules, ADRs scoped to those submodules become relevant, while ADRs scoped to unaffected submodules can be filtered out. ADRs with no scope field are always treated as global and remain relevant regardless of which submodules are affected.
|
|
127
|
+
|
|
105
128
|
**Why no approach or risks?** The plan defines WHAT we're building, not HOW. The approach (phases, task breakdown) gets figured out during decomposition in execution. Risks emerge naturally during exploration or decomposition.
|
|
106
129
|
</step>
|
|
107
130
|
|
|
@@ -56,10 +56,13 @@ While codebase exploration is happening, also spawn the adr-agent to find archit
|
|
|
56
56
|
```
|
|
57
57
|
Spawn adr-agent: "Find ADRs that would be relevant to [summary of requirements].
|
|
58
58
|
For each relevant ADR, explain why it matters and what
|
|
59
|
-
constraints it imposes on this work.
|
|
59
|
+
constraints it imposes on this work.
|
|
60
|
+
|
|
61
|
+
If this work affects specific submodules, mention which ones
|
|
62
|
+
so the agent can filter ADRs by scope."
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
ADRs discovered now will inform the plan you write. Better to know the rules before you design than to discover violations later.
|
|
65
|
+
ADRs discovered now will inform the plan you write. Better to know the rules before you design than to discover violations later. When specific submodules are involved, scope-aware discovery surfaces the most relevant ADRs while filtering out ADRs that only apply elsewhere.
|
|
63
66
|
</step>
|
|
64
67
|
|
|
65
68
|
<step name="Synthesize Findings">
|
|
@@ -18,6 +18,7 @@ Complete each step in order before proceeding to the next.
|
|
|
18
18
|
Spawn the adr-agent to validate the plan. The agent will:
|
|
19
19
|
- Read the plan.yaml
|
|
20
20
|
- Find ALL ADRs that might be relevant (not just the ones declared in the plan)
|
|
21
|
+
- Use the plan's `affected_submodules` field for scope-aware filtering
|
|
21
22
|
- Check compliance for each: COMPLIANT, TENSION, or CONFLICT
|
|
22
23
|
- Report what needs attention
|
|
23
24
|
|
|
@@ -28,12 +29,16 @@ Find all ADRs that could be relevant to this work—check beyond what's
|
|
|
28
29
|
declared in the plan's relevant_adrs field. Better to surface extra
|
|
29
30
|
ADRs than miss important ones.
|
|
30
31
|
|
|
32
|
+
Use the plan's affected_submodules field to focus on ADRs that apply
|
|
33
|
+
to the relevant submodules. Global ADRs always apply; submodule-scoped
|
|
34
|
+
ADRs only apply when their scope matches the affected submodules.
|
|
35
|
+
|
|
31
36
|
For each relevant ADR, report:
|
|
32
37
|
- COMPLIANT: Plan follows this ADR
|
|
33
38
|
- TENSION: Potential friction, worth noting
|
|
34
39
|
- CONFLICT: Plan violates this ADR, must resolve
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
Note in the report when ADRs were filtered by scope."
|
|
37
42
|
```
|
|
38
43
|
|
|
39
44
|
**Show the validation report to the user** before proceeding.
|