ai-developer-skill-os 3.1.0 → 3.1.2
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/.github/workflows/ci.yml +56 -0
- package/CHANGELOG.md +53 -50
- package/README.md +97 -84
- package/bin/install.js +22 -6
- package/package.json +44 -34
- package/rename.js +3 -3
- package/skills/qk-access-policy/SKILL.md +8 -1
- package/skills/qk-ai-builder/SKILL.md +8 -1
- package/skills/qk-api-lifecycle/SKILL.md +9 -1
- package/skills/qk-bug-resolution/SKILL.md +8 -1
- package/skills/qk-context-loader/SKILL.md +9 -1
- package/skills/qk-data-lifecycle/SKILL.md +9 -1
- package/skills/qk-design-to-code/SKILL.md +8 -1
- package/skills/qk-docs/SKILL.md +9 -1
- package/skills/qk-documentation-system/SKILL.md +8 -1
- package/skills/qk-engineering-standard/SKILL.md +7 -1
- package/skills/qk-feature-delivery/SKILL.md +9 -1
- package/skills/qk-help/SKILL.md +8 -1
- package/skills/qk-orchestrator/SKILL.md +9 -1
- package/skills/qk-policy-engine/SKILL.md +9 -1
- package/skills/qk-production-release/SKILL.md +6 -1
- package/skills/qk-project-bootstrap/SKILL.md +8 -1
- package/skills/qk-project-health/SKILL.md +7 -1
- package/skills/qk-project-memory/SKILL.md +8 -1
- package/skills/qk-system-evolution/SKILL.md +7 -1
- package/skills/qk-ui-audit/SKILL.md +7 -1
- package/skills/qk-ui-system-builder/SKILL.md +8 -1
- package/skills/qk-validation-gate/SKILL.md +7 -1
- package/skills.json +405 -524
- package/tests/install-script.test.js +38 -0
- package/tests/registry.test.js +111 -0
- package/tests/spec-compliance.test.js +193 -0
- package/vitest.config.js +11 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
8
|
+
|
|
9
|
+
describe('Install Script Logic', () => {
|
|
10
|
+
it('systemPrompt should contain required keywords', () => {
|
|
11
|
+
const scriptPath = path.join(rootDir, 'bin', 'install.js');
|
|
12
|
+
const content = fs.readFileSync(scriptPath, 'utf8');
|
|
13
|
+
expect(content).toContain('[Role]');
|
|
14
|
+
expect(content).toContain('[Trigger Mechanism]');
|
|
15
|
+
expect(content).toContain('[Command Arguments]');
|
|
16
|
+
expect(content).toContain('qk-[tên-skill]');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle multi-IDE mode', () => {
|
|
20
|
+
const scriptPath = path.join(rootDir, 'bin', 'install.js');
|
|
21
|
+
const content = fs.readFileSync(scriptPath, 'utf8');
|
|
22
|
+
expect(content).toContain("case '7'");
|
|
23
|
+
expect(content).toContain('CLAUDE.md');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should support Kilo config generation', () => {
|
|
27
|
+
const scriptPath = path.join(rootDir, 'bin', 'install.js');
|
|
28
|
+
const content = fs.readFileSync(scriptPath, 'utf8');
|
|
29
|
+
expect(content).toContain('ensureKiloConfig');
|
|
30
|
+
expect(content).toContain('kiloJsonPath');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should clean old skills before installation', () => {
|
|
34
|
+
const scriptPath = path.join(rootDir, 'bin', 'install.js');
|
|
35
|
+
const content = fs.readFileSync(scriptPath, 'utf8');
|
|
36
|
+
expect(content).toContain('cleanOldSkills');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import YAML from 'js-yaml';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
9
|
+
const skillsDir = path.join(rootDir, 'skills');
|
|
10
|
+
const registryPath = path.join(rootDir, 'skills.json');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse YAML frontmatter from a SKILL.md file.
|
|
14
|
+
* Returns the frontmatter object or null if invalid/missing.
|
|
15
|
+
*/
|
|
16
|
+
function parseFrontmatter(skillPath) {
|
|
17
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
18
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
try {
|
|
21
|
+
return YAML.load(match[1]);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read all active skill directories (excluding archives).
|
|
29
|
+
*/
|
|
30
|
+
function getActiveSkillDirs() {
|
|
31
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
32
|
+
return fs.readdirSync(skillsDir)
|
|
33
|
+
.filter(name => !name.startsWith('_') && fs.statSync(path.join(skillsDir, name)).isDirectory())
|
|
34
|
+
.map(name => path.join(skillsDir, name));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('Skill Registry Integrity', () => {
|
|
38
|
+
it('should have skills.json present', () => {
|
|
39
|
+
expect(fs.existsSync(registryPath)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should be valid JSON', () => {
|
|
43
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
44
|
+
expect(() => JSON.parse(raw)).not.toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('registry skills count should match active skill directories', () => {
|
|
48
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
49
|
+
const activeDirs = getActiveSkillDirs();
|
|
50
|
+
const registryNames = registry.skills.map(s => s.name);
|
|
51
|
+
const dirNames = activeDirs.map(d => path.basename(d));
|
|
52
|
+
|
|
53
|
+
expect(registryNames.length).toBe(dirNames.length);
|
|
54
|
+
dirNames.forEach(name => {
|
|
55
|
+
expect(registryNames).toContain(name);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('every registry skill should have a valid SKILL.md file', () => {
|
|
60
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
61
|
+
registry.skills.forEach(skill => {
|
|
62
|
+
const skillPath = path.join(rootDir, skill.path);
|
|
63
|
+
expect(fs.existsSync(skillPath)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should have correct platforms (no invalid IDE names)', () => {
|
|
68
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
69
|
+
const validPlatforms = ['claude-code', 'cursor', 'windsurf', 'gemini-cli'];
|
|
70
|
+
registry.skills.forEach(skill => {
|
|
71
|
+
skill.platforms.forEach(p => {
|
|
72
|
+
expect(validPlatforms).toContain(p);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should have no duplicate skill names', () => {
|
|
78
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
79
|
+
const names = registry.skills.map(s => s.name);
|
|
80
|
+
const unique = new Set(names);
|
|
81
|
+
expect(unique.size).toBe(names.length);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('categories should map to existing skills', () => {
|
|
85
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
86
|
+
const allSkills = new Set(registry.skills.map(s => s.name));
|
|
87
|
+
Object.entries(registry.categories).forEach(([cat, skills]) => {
|
|
88
|
+
skills.forEach(s => {
|
|
89
|
+
expect(allSkills.has(s)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('knowledge references should exist', () => {
|
|
95
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
96
|
+
if (registry.knowledge) {
|
|
97
|
+
registry.knowledge.forEach(k => {
|
|
98
|
+
expect(fs.existsSync(path.join(rootDir, k))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('template references should exist', () => {
|
|
104
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
105
|
+
if (registry.templates) {
|
|
106
|
+
registry.templates.forEach(t => {
|
|
107
|
+
expect(fs.existsSync(path.join(rootDir, t))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import YAML from 'js-yaml';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
9
|
+
const skillsDir = path.join(rootDir, 'skills');
|
|
10
|
+
|
|
11
|
+
function parseFrontmatter(skillPath) {
|
|
12
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
13
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
try {
|
|
16
|
+
return YAML.load(match[1]);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getActiveSkillDirs() {
|
|
23
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
24
|
+
return fs.readdirSync(skillsDir)
|
|
25
|
+
.filter(name => !name.startsWith('_') && fs.statSync(path.join(skillsDir, name)).isDirectory())
|
|
26
|
+
.map(name => path.join(skillsDir, name));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('SKILL.md Spec Compliance', () => {
|
|
30
|
+
const REQUIRED_FIELDS = [
|
|
31
|
+
'name', 'version', 'updated', 'description', 'behavior', 'intent',
|
|
32
|
+
'priority', 'tags', 'platforms', 'trigger', 'inputs', 'outputs',
|
|
33
|
+
'allowed_tools', 'pipeline'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const VALID_BEHAVIORS = ['static-analysis', 'development', 'validation', 'maintenance'];
|
|
37
|
+
const VALID_INTENTS = ['review-code', 'fix-bug', 'implement-feature', 'validate', 'maintain'];
|
|
38
|
+
const VALID_PRIORITIES = ['low', 'medium', 'high', 'critical'];
|
|
39
|
+
const VALID_PIPELINE_STEPS = new Set([
|
|
40
|
+
'analyze', 'plan', 'design', 'implement', 'review', 'validate', 'complete', 'delegate', 'evaluate', 'engineering-standard'
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
it('every active SKILL.md should have frontmatter', () => {
|
|
44
|
+
const dirs = getActiveSkillDirs();
|
|
45
|
+
dirs.forEach(dir => {
|
|
46
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
47
|
+
expect(fs.existsSync(skillPath)).toBe(true);
|
|
48
|
+
const fm = parseFrontmatter(skillPath);
|
|
49
|
+
expect(fm).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('every SKILL.md frontmatter should contain all required fields', () => {
|
|
54
|
+
const dirs = getActiveSkillDirs();
|
|
55
|
+
dirs.forEach(dir => {
|
|
56
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
57
|
+
const fm = parseFrontmatter(skillPath);
|
|
58
|
+
REQUIRED_FIELDS.forEach(field => {
|
|
59
|
+
expect(fm[field], `Missing ${field} in ${path.basename(dir)}/SKILL.md`).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('behavior field should be one of the allowed values', () => {
|
|
65
|
+
const dirs = getActiveSkillDirs();
|
|
66
|
+
dirs.forEach(dir => {
|
|
67
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
68
|
+
const fm = parseFrontmatter(skillPath);
|
|
69
|
+
expect(VALID_BEHAVIORS).toContain(fm.behavior);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('intent field should be one of the allowed values', () => {
|
|
74
|
+
const dirs = getActiveSkillDirs();
|
|
75
|
+
dirs.forEach(dir => {
|
|
76
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
77
|
+
const fm = parseFrontmatter(skillPath);
|
|
78
|
+
expect(VALID_INTENTS).toContain(fm.intent);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('priority field should be one of the allowed values', () => {
|
|
83
|
+
const dirs = getActiveSkillDirs();
|
|
84
|
+
dirs.forEach(dir => {
|
|
85
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
86
|
+
const fm = parseFrontmatter(skillPath);
|
|
87
|
+
expect(VALID_PRIORITIES).toContain(fm.priority);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('pipeline steps should use only allowed verbs', () => {
|
|
92
|
+
const dirs = getActiveSkillDirs();
|
|
93
|
+
dirs.forEach(dir => {
|
|
94
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
95
|
+
const fm = parseFrontmatter(skillPath);
|
|
96
|
+
const steps = Array.isArray(fm.pipeline) ? fm.pipeline : [fm.pipeline];
|
|
97
|
+
steps.forEach(step => {
|
|
98
|
+
expect(VALID_PIPELINE_STEPS.has(step), `Invalid pipeline step "${step}" in ${path.basename(dir)}`).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('name field should match directory name and use qk- prefix', () => {
|
|
104
|
+
const dirs = getActiveSkillDirs();
|
|
105
|
+
dirs.forEach(dir => {
|
|
106
|
+
const skillName = path.basename(dir);
|
|
107
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
108
|
+
const fm = parseFrontmatter(skillPath);
|
|
109
|
+
expect(fm.name).toBe(skillName);
|
|
110
|
+
expect(fm.name.startsWith('qk-')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('version should be semver string', () => {
|
|
115
|
+
const dirs = getActiveSkillDirs();
|
|
116
|
+
dirs.forEach(dir => {
|
|
117
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
118
|
+
const fm = parseFrontmatter(skillPath);
|
|
119
|
+
expect(fm.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('updated should be YYYY-MM-DD', () => {
|
|
124
|
+
const dirs = getActiveSkillDirs();
|
|
125
|
+
dirs.forEach(dir => {
|
|
126
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
127
|
+
const fm = parseFrontmatter(skillPath);
|
|
128
|
+
const val = typeof fm.updated === 'string' ? fm.updated : new Date(fm.updated).toISOString().split('T')[0];
|
|
129
|
+
expect(val).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('platforms should be an array of valid IDE strings', () => {
|
|
134
|
+
const valid = ['claude-code', 'cursor', 'windsurf', 'gemini-cli'];
|
|
135
|
+
const dirs = getActiveSkillDirs();
|
|
136
|
+
dirs.forEach(dir => {
|
|
137
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
138
|
+
const fm = parseFrontmatter(skillPath);
|
|
139
|
+
expect(Array.isArray(fm.platforms)).toBe(true);
|
|
140
|
+
fm.platforms.forEach(p => expect(valid).toContain(p));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('tags should be an array of strings', () => {
|
|
145
|
+
const dirs = getActiveSkillDirs();
|
|
146
|
+
dirs.forEach(dir => {
|
|
147
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
148
|
+
const fm = parseFrontmatter(skillPath);
|
|
149
|
+
expect(Array.isArray(fm.tags)).toBe(true);
|
|
150
|
+
fm.tags.forEach(t => expect(typeof t).toBe('string'));
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('trigger should be a non-empty string', () => {
|
|
155
|
+
const dirs = getActiveSkillDirs();
|
|
156
|
+
dirs.forEach(dir => {
|
|
157
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
158
|
+
const fm = parseFrontmatter(skillPath);
|
|
159
|
+
expect(typeof fm.trigger).toBe('string');
|
|
160
|
+
expect(fm.trigger.length).toBeGreaterThan(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('allowed_tools should be an array of strings', () => {
|
|
165
|
+
const dirs = getActiveSkillDirs();
|
|
166
|
+
dirs.forEach(dir => {
|
|
167
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
168
|
+
const fm = parseFrontmatter(skillPath);
|
|
169
|
+
expect(Array.isArray(fm.allowed_tools)).toBe(true);
|
|
170
|
+
fm.allowed_tools.forEach(t => expect(typeof t).toBe('string'));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('body should contain required sections', () => {
|
|
175
|
+
const dirs = getActiveSkillDirs();
|
|
176
|
+
dirs.forEach(dir => {
|
|
177
|
+
const skillPath = path.join(dir, 'SKILL.md');
|
|
178
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
179
|
+
expect(content).toContain('Goal');
|
|
180
|
+
expect(content).toContain('Chain of Thought');
|
|
181
|
+
expect(content).toContain('Constraints');
|
|
182
|
+
expect(content).toContain('Output Format');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('no skill should reference archived version of itself', () => {
|
|
187
|
+
const registryPath = path.join(rootDir, 'skills.json');
|
|
188
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
189
|
+
registry.skills.forEach(skill => {
|
|
190
|
+
expect(skill.path).not.toContain('_archive_old_skills');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|