@rune-kit/rune 2.2.2 → 2.2.4
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/compiler/__tests__/adapters.test.js +109 -0
- package/compiler/__tests__/openclaw-adapter.test.js +149 -140
- package/compiler/__tests__/pack-split.test.js +145 -145
- package/compiler/__tests__/parser.test.js +3 -3
- package/compiler/__tests__/pipeline.test.js +110 -0
- package/compiler/__tests__/skill-validation.test.js +148 -0
- package/compiler/__tests__/transformer.test.js +70 -0
- package/compiler/__tests__/transforms.test.js +92 -0
- package/compiler/adapters/antigravity.js +5 -6
- package/compiler/adapters/claude.js +1 -1
- package/compiler/adapters/codex.js +69 -77
- package/compiler/adapters/cursor.js +5 -6
- package/compiler/adapters/generic.js +5 -6
- package/compiler/adapters/index.js +3 -3
- package/compiler/adapters/openclaw.js +146 -150
- package/compiler/adapters/opencode.js +78 -86
- package/compiler/adapters/windsurf.js +5 -6
- package/compiler/bin/rune.js +32 -23
- package/compiler/doctor.js +19 -7
- package/compiler/emitter.js +11 -18
- package/compiler/parser.js +5 -5
- package/compiler/transformer.js +3 -5
- package/compiler/transforms/branding.js +5 -4
- package/compiler/transforms/compliance.js +40 -40
- package/compiler/transforms/frontmatter.js +1 -1
- package/compiler/transforms/hooks.js +12 -14
- package/compiler/transforms/subagents.js +1 -3
- package/compiler/transforms/tool-names.js +1 -1
- package/docs/guides/index.html +48 -7
- package/docs/index.html +67 -2
- package/docs/style.css +18 -1
- package/extensions/security/PACK.md +4 -3
- package/extensions/security/skills/defense-in-depth.md +103 -0
- package/package.json +8 -1
- package/skills/completion-gate/SKILL.md +31 -1
- package/skills/cook/SKILL.md +20 -2
- package/skills/debug/SKILL.md +16 -1
- package/skills/hallucination-guard/SKILL.md +21 -6
- package/skills/plan/SKILL.md +90 -1
- package/skills/review/SKILL.md +1 -0
- package/skills/sentinel-env/SKILL.md +31 -1
- package/skills/skill-forge/SKILL.md +57 -1
- package/skills/team/SKILL.md +61 -15
- package/skills/test/SKILL.md +101 -10
- package/skills/verification/SKILL.md +29 -1
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const EXTENSIONS_DIR = path.resolve(__dirname, '../../extensions');
|
|
10
|
-
|
|
11
|
-
// --- Monolith format (regression) ---
|
|
12
|
-
|
|
13
|
-
test('parsePack: monolith format has isSplit=false', () => {
|
|
14
|
-
const content = `---
|
|
15
|
-
name: "@rune/trading"
|
|
16
|
-
description: "Trading patterns"
|
|
17
|
-
metadata:
|
|
18
|
-
version: "0.2.0"
|
|
19
|
-
layer: L4
|
|
20
|
-
price: free
|
|
21
|
-
target: Traders
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
# @rune/trading
|
|
25
|
-
|
|
26
|
-
## Purpose
|
|
27
|
-
Trading pack.
|
|
28
|
-
|
|
29
|
-
## Skills Included
|
|
30
|
-
|
|
31
|
-
### risk-management
|
|
32
|
-
Risk management workflow.
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
const parsed = parsePack(content, 'extensions/trading/PACK.md');
|
|
36
|
-
|
|
37
|
-
assert.strictEqual(parsed.isSplit, false);
|
|
38
|
-
assert.deepStrictEqual(parsed.skillManifest, []);
|
|
39
|
-
assert.strictEqual(parsed.name, '@rune/trading');
|
|
40
|
-
assert.strictEqual(parsed.version, '0.2.0');
|
|
41
|
-
assert.strictEqual(parsed.layer, 'L4');
|
|
42
|
-
assert.ok(parsed.body.includes('risk-management'));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('parsePack: monolith without metadata block defaults correctly', () => {
|
|
46
|
-
const content = `---
|
|
47
|
-
name: "@rune/test"
|
|
48
|
-
description: "Test pack"
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
# @rune/test
|
|
52
|
-
Body here.
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
|
-
const parsed = parsePack(content, 'test/PACK.md');
|
|
56
|
-
|
|
57
|
-
assert.strictEqual(parsed.isSplit, false);
|
|
58
|
-
assert.strictEqual(parsed.version, '1.0.0');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// --- Split format detection ---
|
|
62
|
-
|
|
63
|
-
test('parsePack: split format detected via metadata.format', () => {
|
|
64
|
-
const content = `---
|
|
65
|
-
name: "@rune/backend"
|
|
66
|
-
description: "Backend patterns"
|
|
67
|
-
metadata:
|
|
68
|
-
version: "0.3.0"
|
|
69
|
-
layer: L4
|
|
70
|
-
price: free
|
|
71
|
-
target: Backend developers
|
|
72
|
-
format: split
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
# @rune/backend
|
|
76
|
-
|
|
77
|
-
## Purpose
|
|
78
|
-
Backend pack index.
|
|
79
|
-
|
|
80
|
-
## Skills Included
|
|
81
|
-
| Skill | Model | Description |
|
|
82
|
-
|-------|-------|-------------|
|
|
83
|
-
| api-design | sonnet | API patterns |
|
|
84
|
-
| auth | sonnet | Auth patterns |
|
|
85
|
-
`;
|
|
86
|
-
|
|
87
|
-
const parsed = parsePack(content, 'extensions/backend/PACK.md');
|
|
88
|
-
|
|
89
|
-
assert.strictEqual(parsed.isSplit, true);
|
|
90
|
-
assert.strictEqual(parsed.version, '0.3.0');
|
|
91
|
-
assert.strictEqual(parsed.name, '@rune/backend');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('parsePack: skill manifest parsed from string array', () => {
|
|
95
|
-
// Note: our YAML parser is simple — it handles nested objects but not arrays.
|
|
96
|
-
// This test validates the parseSkillManifest function directly with object skills.
|
|
97
|
-
const content = `---
|
|
98
|
-
name: "@rune/test"
|
|
99
|
-
description: "Test"
|
|
100
|
-
metadata:
|
|
101
|
-
format: split
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
Body.
|
|
105
|
-
`;
|
|
106
|
-
|
|
107
|
-
const parsed = parsePack(content, 'test/PACK.md');
|
|
108
|
-
assert.strictEqual(parsed.isSplit, true);
|
|
109
|
-
// With simple YAML parser, skills array won't parse — manifest will be empty
|
|
110
|
-
// Real usage will use the skills/ directory detection as fallback
|
|
111
|
-
assert.ok(Array.isArray(parsed.skillManifest));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// --- Real pack regression: existing monolith packs parse correctly ---
|
|
115
|
-
|
|
116
|
-
test('parsePack: real trading PACK.md parses as split', () => {
|
|
117
|
-
const tradingPath = path.join(EXTENSIONS_DIR, 'trading', 'PACK.md');
|
|
118
|
-
if (!existsSync(tradingPath)) {
|
|
119
|
-
console.log(' skip: trading PACK.md not found');
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const content = readFileSync(tradingPath, 'utf-8');
|
|
124
|
-
const parsed = parsePack(content, tradingPath);
|
|
125
|
-
|
|
126
|
-
assert.strictEqual(parsed.isSplit, true);
|
|
127
|
-
assert.strictEqual(parsed.layer, 'L4');
|
|
128
|
-
assert.strictEqual(parsed.group, 'extension');
|
|
129
|
-
assert.ok(parsed.body.length > 50);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('parsePack: real backend PACK.md parses as split (post-split)', () => {
|
|
133
|
-
const backendPath = path.join(EXTENSIONS_DIR, 'backend', 'PACK.md');
|
|
134
|
-
if (!existsSync(backendPath)) {
|
|
135
|
-
console.log(' skip: backend PACK.md not found');
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const content = readFileSync(backendPath, 'utf-8');
|
|
140
|
-
const parsed = parsePack(content, backendPath);
|
|
141
|
-
|
|
142
|
-
assert.strictEqual(parsed.isSplit, true);
|
|
143
|
-
assert.strictEqual(parsed.layer, 'L4');
|
|
144
|
-
assert.ok(parsed.sections.size > 0);
|
|
145
|
-
});
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parsePack } from '../parser.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const EXTENSIONS_DIR = path.resolve(__dirname, '../../extensions');
|
|
10
|
+
|
|
11
|
+
// --- Monolith format (regression) ---
|
|
12
|
+
|
|
13
|
+
test('parsePack: monolith format has isSplit=false', () => {
|
|
14
|
+
const content = `---
|
|
15
|
+
name: "@rune/trading"
|
|
16
|
+
description: "Trading patterns"
|
|
17
|
+
metadata:
|
|
18
|
+
version: "0.2.0"
|
|
19
|
+
layer: L4
|
|
20
|
+
price: free
|
|
21
|
+
target: Traders
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# @rune/trading
|
|
25
|
+
|
|
26
|
+
## Purpose
|
|
27
|
+
Trading pack.
|
|
28
|
+
|
|
29
|
+
## Skills Included
|
|
30
|
+
|
|
31
|
+
### risk-management
|
|
32
|
+
Risk management workflow.
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const parsed = parsePack(content, 'extensions/trading/PACK.md');
|
|
36
|
+
|
|
37
|
+
assert.strictEqual(parsed.isSplit, false);
|
|
38
|
+
assert.deepStrictEqual(parsed.skillManifest, []);
|
|
39
|
+
assert.strictEqual(parsed.name, '@rune/trading');
|
|
40
|
+
assert.strictEqual(parsed.version, '0.2.0');
|
|
41
|
+
assert.strictEqual(parsed.layer, 'L4');
|
|
42
|
+
assert.ok(parsed.body.includes('risk-management'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('parsePack: monolith without metadata block defaults correctly', () => {
|
|
46
|
+
const content = `---
|
|
47
|
+
name: "@rune/test"
|
|
48
|
+
description: "Test pack"
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# @rune/test
|
|
52
|
+
Body here.
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const parsed = parsePack(content, 'test/PACK.md');
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(parsed.isSplit, false);
|
|
58
|
+
assert.strictEqual(parsed.version, '1.0.0');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// --- Split format detection ---
|
|
62
|
+
|
|
63
|
+
test('parsePack: split format detected via metadata.format', () => {
|
|
64
|
+
const content = `---
|
|
65
|
+
name: "@rune/backend"
|
|
66
|
+
description: "Backend patterns"
|
|
67
|
+
metadata:
|
|
68
|
+
version: "0.3.0"
|
|
69
|
+
layer: L4
|
|
70
|
+
price: free
|
|
71
|
+
target: Backend developers
|
|
72
|
+
format: split
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
# @rune/backend
|
|
76
|
+
|
|
77
|
+
## Purpose
|
|
78
|
+
Backend pack index.
|
|
79
|
+
|
|
80
|
+
## Skills Included
|
|
81
|
+
| Skill | Model | Description |
|
|
82
|
+
|-------|-------|-------------|
|
|
83
|
+
| api-design | sonnet | API patterns |
|
|
84
|
+
| auth | sonnet | Auth patterns |
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const parsed = parsePack(content, 'extensions/backend/PACK.md');
|
|
88
|
+
|
|
89
|
+
assert.strictEqual(parsed.isSplit, true);
|
|
90
|
+
assert.strictEqual(parsed.version, '0.3.0');
|
|
91
|
+
assert.strictEqual(parsed.name, '@rune/backend');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('parsePack: skill manifest parsed from string array', () => {
|
|
95
|
+
// Note: our YAML parser is simple — it handles nested objects but not arrays.
|
|
96
|
+
// This test validates the parseSkillManifest function directly with object skills.
|
|
97
|
+
const content = `---
|
|
98
|
+
name: "@rune/test"
|
|
99
|
+
description: "Test"
|
|
100
|
+
metadata:
|
|
101
|
+
format: split
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Body.
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const parsed = parsePack(content, 'test/PACK.md');
|
|
108
|
+
assert.strictEqual(parsed.isSplit, true);
|
|
109
|
+
// With simple YAML parser, skills array won't parse — manifest will be empty
|
|
110
|
+
// Real usage will use the skills/ directory detection as fallback
|
|
111
|
+
assert.ok(Array.isArray(parsed.skillManifest));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// --- Real pack regression: existing monolith packs parse correctly ---
|
|
115
|
+
|
|
116
|
+
test('parsePack: real trading PACK.md parses as split', () => {
|
|
117
|
+
const tradingPath = path.join(EXTENSIONS_DIR, 'trading', 'PACK.md');
|
|
118
|
+
if (!existsSync(tradingPath)) {
|
|
119
|
+
console.log(' skip: trading PACK.md not found');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const content = readFileSync(tradingPath, 'utf-8');
|
|
124
|
+
const parsed = parsePack(content, tradingPath);
|
|
125
|
+
|
|
126
|
+
assert.strictEqual(parsed.isSplit, true);
|
|
127
|
+
assert.strictEqual(parsed.layer, 'L4');
|
|
128
|
+
assert.strictEqual(parsed.group, 'extension');
|
|
129
|
+
assert.ok(parsed.body.length > 50);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('parsePack: real backend PACK.md parses as split (post-split)', () => {
|
|
133
|
+
const backendPath = path.join(EXTENSIONS_DIR, 'backend', 'PACK.md');
|
|
134
|
+
if (!existsSync(backendPath)) {
|
|
135
|
+
console.log(' skip: backend PACK.md not found');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = readFileSync(backendPath, 'utf-8');
|
|
140
|
+
const parsed = parsePack(content, backendPath);
|
|
141
|
+
|
|
142
|
+
assert.strictEqual(parsed.isSplit, true);
|
|
143
|
+
assert.strictEqual(parsed.layer, 'L4');
|
|
144
|
+
assert.ok(parsed.sections.size > 0);
|
|
145
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { parseSkill } from '../parser.js';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { test } from 'node:test';
|
|
4
1
|
import assert from 'node:assert';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
5
3
|
import path from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
6
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseSkill } from '../parser.js';
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const SKILLS_DIR = path.resolve(__dirname, '../../skills');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E Pipeline Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the full compile pipeline: parse → transform → assemble output.
|
|
5
|
+
* Verifies that real skills produce valid output for each platform adapter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { describe, test } from 'node:test';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { getAdapter, listPlatforms } from '../adapters/index.js';
|
|
14
|
+
import { parseSkill } from '../parser.js';
|
|
15
|
+
import { transformSkill } from '../transformer.js';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const SKILLS_DIR = path.resolve(__dirname, '../../skills');
|
|
19
|
+
|
|
20
|
+
// Use a real skill (cook) for e2e tests
|
|
21
|
+
const cookContent = readFileSync(path.join(SKILLS_DIR, 'cook/SKILL.md'), 'utf-8');
|
|
22
|
+
const cookParsed = parseSkill(cookContent, 'cook/SKILL.md');
|
|
23
|
+
|
|
24
|
+
const fixContent = readFileSync(path.join(SKILLS_DIR, 'fix/SKILL.md'), 'utf-8');
|
|
25
|
+
const fixParsed = parseSkill(fixContent, 'fix/SKILL.md');
|
|
26
|
+
|
|
27
|
+
// --- Full pipeline per platform ---
|
|
28
|
+
|
|
29
|
+
describe('full pipeline: cook skill', () => {
|
|
30
|
+
for (const platform of listPlatforms()) {
|
|
31
|
+
test(`${platform}: parse → transform → output`, () => {
|
|
32
|
+
const adapter = getAdapter(platform);
|
|
33
|
+
const result = transformSkill(cookParsed, adapter);
|
|
34
|
+
|
|
35
|
+
// Result structure
|
|
36
|
+
assert.ok(typeof result.header === 'string', `${platform}: header not string`);
|
|
37
|
+
assert.ok(typeof result.body === 'string', `${platform}: body not string`);
|
|
38
|
+
assert.ok(typeof result.footer === 'string', `${platform}: footer not string`);
|
|
39
|
+
|
|
40
|
+
// Body should contain meaningful content
|
|
41
|
+
assert.ok(result.body.length > 500, `${platform}: body too short (${result.body.length})`);
|
|
42
|
+
|
|
43
|
+
if (platform === 'claude') {
|
|
44
|
+
// Claude: passthrough, no header/footer
|
|
45
|
+
assert.strictEqual(result.header, '');
|
|
46
|
+
assert.strictEqual(result.footer, '');
|
|
47
|
+
// Should still have original cross-refs
|
|
48
|
+
assert.ok(result.body.includes('rune:'), `claude: should preserve rune: cross-refs`);
|
|
49
|
+
} else {
|
|
50
|
+
// Non-Claude: should have transformed cross-refs
|
|
51
|
+
assert.ok(!result.body.includes('`rune:cook`'), `${platform}: should not have backticked rune:cook cross-ref`);
|
|
52
|
+
|
|
53
|
+
// Should have footer with branding
|
|
54
|
+
assert.ok(result.footer.includes('Rune'), `${platform}: footer missing branding`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('full pipeline: fix skill', () => {
|
|
61
|
+
test('cursor: cross-refs transformed to .mdc', () => {
|
|
62
|
+
const adapter = getAdapter('cursor');
|
|
63
|
+
const result = transformSkill(fixParsed, adapter);
|
|
64
|
+
|
|
65
|
+
// Fix references cook, debug, test, etc. — at least some should be transformed
|
|
66
|
+
assert.ok(result.body.includes('.mdc'), 'cursor output should contain .mdc references');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('generic: cross-refs transformed to descriptive text', () => {
|
|
70
|
+
const adapter = getAdapter('generic');
|
|
71
|
+
const result = transformSkill(fixParsed, adapter);
|
|
72
|
+
assert.ok(result.body.includes('rule file'), 'generic output should contain "rule file" references');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('openclaw: cross-refs transformed to .md file refs', () => {
|
|
76
|
+
const adapter = getAdapter('openclaw');
|
|
77
|
+
const result = transformSkill(fixParsed, adapter);
|
|
78
|
+
assert.ok(result.body.includes('.md'), 'openclaw output should contain .md references');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// --- Output assembly (header + body + footer) ---
|
|
83
|
+
|
|
84
|
+
describe('output assembly', () => {
|
|
85
|
+
test('cursor output assembles into valid .mdc file', () => {
|
|
86
|
+
const adapter = getAdapter('cursor');
|
|
87
|
+
const result = transformSkill(cookParsed, adapter);
|
|
88
|
+
const assembled = result.header + result.body + result.footer;
|
|
89
|
+
|
|
90
|
+
// Should start with YAML frontmatter
|
|
91
|
+
assert.ok(assembled.startsWith('---\n'), 'cursor .mdc should start with YAML frontmatter');
|
|
92
|
+
assert.ok(assembled.includes('description:'), 'cursor .mdc should have description');
|
|
93
|
+
assert.ok(assembled.includes('alwaysApply:'), 'cursor .mdc should have alwaysApply');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('codex output has YAML frontmatter with name', () => {
|
|
97
|
+
const adapter = getAdapter('codex');
|
|
98
|
+
const result = transformSkill(cookParsed, adapter);
|
|
99
|
+
const assembled = result.header + result.body + result.footer;
|
|
100
|
+
|
|
101
|
+
assert.ok(assembled.includes('name: rune-cook'), 'codex should have name: rune-cook');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('generic output has markdown heading', () => {
|
|
105
|
+
const adapter = getAdapter('generic');
|
|
106
|
+
const result = transformSkill(cookParsed, adapter);
|
|
107
|
+
|
|
108
|
+
assert.ok(result.header.includes('# rune-cook'), 'generic header should have # rune-cook');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Validation Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies ALL 58 SKILL.md files parse correctly and meet structural requirements.
|
|
5
|
+
* This is the "mesh integrity" test — if any skill breaks parsing, mesh connections fail.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { describe, test } from 'node:test';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { parseSkill } from '../parser.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const SKILLS_DIR = path.resolve(__dirname, '../../skills');
|
|
17
|
+
|
|
18
|
+
// Discover all skill directories
|
|
19
|
+
const skillDirs = readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
20
|
+
.filter((d) => d.isDirectory())
|
|
21
|
+
.map((d) => d.name)
|
|
22
|
+
.sort();
|
|
23
|
+
|
|
24
|
+
const VALID_LAYERS = ['L0', 'L1', 'L2', 'L3'];
|
|
25
|
+
const VALID_MODELS = ['haiku', 'sonnet', 'opus'];
|
|
26
|
+
|
|
27
|
+
// --- Discovery ---
|
|
28
|
+
|
|
29
|
+
test(`discovers all 58 core skills`, () => {
|
|
30
|
+
assert.ok(skillDirs.length >= 58, `expected >=58 skills, found ${skillDirs.length}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// --- Per-skill validation ---
|
|
34
|
+
|
|
35
|
+
describe('skill parsing', () => {
|
|
36
|
+
for (const skillName of skillDirs) {
|
|
37
|
+
const skillFile = path.join(SKILLS_DIR, skillName, 'SKILL.md');
|
|
38
|
+
|
|
39
|
+
test(`${skillName}/SKILL.md exists`, () => {
|
|
40
|
+
assert.ok(existsSync(skillFile), `missing SKILL.md for ${skillName}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test(`${skillName} parses without errors`, () => {
|
|
44
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
45
|
+
const parsed = parseSkill(content, `${skillName}/SKILL.md`);
|
|
46
|
+
|
|
47
|
+
// Must have a name
|
|
48
|
+
assert.ok(parsed.name, `${skillName}: missing name`);
|
|
49
|
+
assert.strictEqual(parsed.name, skillName, `${skillName}: parsed name mismatch`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test(`${skillName} has valid layer`, () => {
|
|
53
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
54
|
+
const parsed = parseSkill(content, `${skillName}/SKILL.md`);
|
|
55
|
+
assert.ok(VALID_LAYERS.includes(parsed.layer), `${skillName}: invalid layer "${parsed.layer}"`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test(`${skillName} has valid model`, () => {
|
|
59
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
60
|
+
const parsed = parseSkill(content, `${skillName}/SKILL.md`);
|
|
61
|
+
if (parsed.model) {
|
|
62
|
+
assert.ok(VALID_MODELS.includes(parsed.model), `${skillName}: invalid model "${parsed.model}"`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test(`${skillName} has description`, () => {
|
|
67
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
68
|
+
const parsed = parseSkill(content, `${skillName}/SKILL.md`);
|
|
69
|
+
assert.ok(parsed.description && parsed.description.length > 10, `${skillName}: missing or too short description`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test(`${skillName} has non-empty body`, () => {
|
|
73
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
74
|
+
const parsed = parseSkill(content, `${skillName}/SKILL.md`);
|
|
75
|
+
assert.ok(
|
|
76
|
+
parsed.body && parsed.body.length > 100,
|
|
77
|
+
`${skillName}: body too short (${parsed.body?.length || 0} chars)`,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// --- Mesh integrity ---
|
|
84
|
+
|
|
85
|
+
describe('mesh integrity', () => {
|
|
86
|
+
const allParsed = skillDirs.map((name) => {
|
|
87
|
+
const content = readFileSync(path.join(SKILLS_DIR, name, 'SKILL.md'), 'utf-8');
|
|
88
|
+
return parseSkill(content, `${name}/SKILL.md`);
|
|
89
|
+
});
|
|
90
|
+
const allNames = new Set(allParsed.map((s) => s.name));
|
|
91
|
+
|
|
92
|
+
test('all string cross-references point to existing skills', () => {
|
|
93
|
+
const broken = [];
|
|
94
|
+
for (const skill of allParsed) {
|
|
95
|
+
for (const ref of skill.crossRefs) {
|
|
96
|
+
// Skip non-string refs (parser quirk for some metadata formats)
|
|
97
|
+
if (typeof ref !== 'string') continue;
|
|
98
|
+
if (!allNames.has(ref)) {
|
|
99
|
+
broken.push(`${skill.name} → ${ref}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
assert.strictEqual(broken.length, 0, `broken cross-refs:\n ${broken.join('\n ')}`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('L0 layer has exactly 1 skill (skill-router)', () => {
|
|
107
|
+
const l0 = allParsed.filter((s) => s.layer === 'L0');
|
|
108
|
+
assert.strictEqual(l0.length, 1);
|
|
109
|
+
assert.strictEqual(l0[0].name, 'skill-router');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('L1 orchestrators exist', () => {
|
|
113
|
+
const l1 = allParsed.filter((s) => s.layer === 'L1');
|
|
114
|
+
const l1Names = l1.map((s) => s.name);
|
|
115
|
+
for (const expected of ['cook', 'team', 'launch', 'rescue', 'scaffold']) {
|
|
116
|
+
assert.ok(l1Names.includes(expected), `missing L1 orchestrator: ${expected}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('mesh has 200+ total cross-references', () => {
|
|
121
|
+
const totalRefs = allParsed.reduce((sum, s) => sum + s.crossRefs.length, 0);
|
|
122
|
+
assert.ok(totalRefs >= 200, `expected >=200 cross-refs, found ${totalRefs}`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('no true orphan skills (every skill is referenced by at least one other)', () => {
|
|
126
|
+
// Collect all names referenced via crossRefs OR mentioned in body text as rune:name
|
|
127
|
+
const referenced = new Set();
|
|
128
|
+
const refPattern = /rune:([a-z][\w-]*)/g;
|
|
129
|
+
for (const skill of allParsed) {
|
|
130
|
+
// From parsed crossRefs
|
|
131
|
+
for (const ref of skill.crossRefs) {
|
|
132
|
+
if (typeof ref === 'string') referenced.add(ref);
|
|
133
|
+
}
|
|
134
|
+
// From body text mentions
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = refPattern.exec(skill.body)) !== null) {
|
|
137
|
+
referenced.add(match[1]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Skills that are dispatched dynamically by skill-router (not via explicit rune:X refs)
|
|
141
|
+
const dynamicDispatch = new Set(['constraint-check', 'context-engine', 'sast', 'scope-guard', 'worktree']);
|
|
142
|
+
// A skill is orphan only if it's never referenced and not dynamically dispatched
|
|
143
|
+
const orphans = allParsed.filter(
|
|
144
|
+
(s) => !referenced.has(s.name) && !dynamicDispatch.has(s.name) && s.layer !== 'L0',
|
|
145
|
+
);
|
|
146
|
+
assert.strictEqual(orphans.length, 0, `orphan skills (never referenced): ${orphans.map((s) => s.name).join(', ')}`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
import { getAdapter } from '../adapters/index.js';
|
|
4
|
+
import { transformSkill } from '../transformer.js';
|
|
5
|
+
|
|
6
|
+
// --- Transformer pipeline ---
|
|
7
|
+
|
|
8
|
+
describe('transformSkill', () => {
|
|
9
|
+
const mockSkill = {
|
|
10
|
+
name: 'fix',
|
|
11
|
+
layer: 'L2',
|
|
12
|
+
group: 'workflow',
|
|
13
|
+
description: 'Apply code fixes',
|
|
14
|
+
body: '# fix\n\nUse `rune:cook` for orchestration.\n\nUse `Read` to check files.\n\n## Steps\n\n1. Analyze\n2. Fix\n3. Verify',
|
|
15
|
+
crossRefs: ['cook'],
|
|
16
|
+
toolRefs: ['Read'],
|
|
17
|
+
hardGates: [],
|
|
18
|
+
frontmatter: { layer: 'L2', model: 'sonnet' },
|
|
19
|
+
sections: new Map(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('Claude adapter returns body unchanged', () => {
|
|
23
|
+
const claude = getAdapter('claude');
|
|
24
|
+
const result = transformSkill(mockSkill, claude);
|
|
25
|
+
assert.strictEqual(result.body, mockSkill.body);
|
|
26
|
+
assert.strictEqual(result.header, '');
|
|
27
|
+
assert.strictEqual(result.footer, '');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('Cursor adapter transforms cross-refs to .mdc format', () => {
|
|
31
|
+
const cursor = getAdapter('cursor');
|
|
32
|
+
const result = transformSkill(mockSkill, cursor);
|
|
33
|
+
assert.ok(result.body.includes('@rune-cook.mdc'));
|
|
34
|
+
assert.ok(!result.body.includes('rune:cook'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('result has header, body, and footer', () => {
|
|
38
|
+
const cursor = getAdapter('cursor');
|
|
39
|
+
const result = transformSkill(mockSkill, cursor);
|
|
40
|
+
assert.ok(typeof result.header === 'string');
|
|
41
|
+
assert.ok(typeof result.body === 'string');
|
|
42
|
+
assert.ok(typeof result.footer === 'string');
|
|
43
|
+
assert.ok(result.header.length > 0, 'header should not be empty for non-Claude');
|
|
44
|
+
assert.ok(result.footer.length > 0, 'footer should not be empty for non-Claude');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('Cursor header contains frontmatter with description', () => {
|
|
48
|
+
const cursor = getAdapter('cursor');
|
|
49
|
+
const result = transformSkill(mockSkill, cursor);
|
|
50
|
+
assert.ok(result.header.includes('description:'));
|
|
51
|
+
assert.ok(result.header.includes('alwaysApply:'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('Generic adapter transforms refs to descriptive text', () => {
|
|
55
|
+
const generic = getAdapter('generic');
|
|
56
|
+
const result = transformSkill(mockSkill, generic);
|
|
57
|
+
assert.ok(result.body.includes('rune-cook rule file'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('postProcess strips context: fork and agent: general-purpose', () => {
|
|
61
|
+
const skillWithDirectives = {
|
|
62
|
+
...mockSkill,
|
|
63
|
+
body: 'context: fork\nagent: general-purpose\n# fix\n\nContent here.',
|
|
64
|
+
};
|
|
65
|
+
const cursor = getAdapter('cursor');
|
|
66
|
+
const result = transformSkill(skillWithDirectives, cursor);
|
|
67
|
+
assert.ok(!result.body.includes('context: fork'));
|
|
68
|
+
assert.ok(!result.body.includes('agent: general-purpose'));
|
|
69
|
+
});
|
|
70
|
+
});
|