@rune-kit/rune 2.4.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -17
- package/compiler/__tests__/executive-dashboards.test.js +285 -0
- package/compiler/__tests__/inject.test.js +128 -0
- package/compiler/__tests__/orchestrators.test.js +151 -0
- package/compiler/__tests__/org-templates.test.js +447 -0
- package/compiler/__tests__/pack-split.test.js +141 -1
- package/compiler/__tests__/parser.test.js +147 -1
- package/compiler/__tests__/scripts-bundling.test.js +10 -11
- package/compiler/__tests__/skill-index.test.js +218 -0
- package/compiler/__tests__/status.test.js +336 -0
- package/compiler/__tests__/templates.test.js +245 -0
- package/compiler/__tests__/visualizer.test.js +325 -0
- package/compiler/adapters/antigravity.js +18 -4
- package/compiler/bin/rune.js +90 -1
- package/compiler/doctor.js +283 -2
- package/compiler/emitter.js +490 -17
- package/compiler/parser.js +255 -4
- package/compiler/status.js +342 -0
- package/compiler/visualizer.js +622 -0
- package/hooks/hooks.json +12 -0
- package/hooks/intent-router/index.cjs +108 -0
- package/hooks/pre-tool-guard/index.cjs +177 -68
- package/package.json +63 -63
- package/skills/autopsy/SKILL.md +48 -1
- package/skills/brainstorm/SKILL.md +2 -0
- package/skills/completion-gate/SKILL.md +26 -1
- package/skills/context-engine/SKILL.md +93 -2
- package/skills/cook/SKILL.md +794 -648
- package/skills/debug/SKILL.md +409 -392
- package/skills/deploy/SKILL.md +2 -0
- package/skills/docs/SKILL.md +28 -3
- package/skills/fix/SKILL.md +284 -281
- package/skills/mcp-builder/SKILL.md +53 -1
- package/skills/onboard/SKILL.md +58 -1
- package/skills/perf/SKILL.md +34 -1
- package/skills/plan/SKILL.md +372 -342
- package/skills/preflight/SKILL.md +396 -360
- package/skills/retro/SKILL.md +95 -1
- package/skills/review/SKILL.md +535 -489
- package/skills/scope-guard/SKILL.md +1 -0
- package/skills/scout/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +353 -299
- package/skills/sentinel/references/policy-driven-constraints.md +424 -0
- package/skills/session-bridge/SKILL.md +58 -2
- package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
- package/skills/team/SKILL.md +16 -1
- package/skills/test/SKILL.md +587 -585
- package/skills/verification/SKILL.md +1 -0
- package/skills/watchdog/SKILL.md +2 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
3
5
|
import path from 'node:path';
|
|
4
|
-
import { test } from 'node:test';
|
|
6
|
+
import { describe, test } from 'node:test';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { getAdapter } from '../adapters/index.js';
|
|
9
|
+
import { buildAll } from '../emitter.js';
|
|
6
10
|
import { parsePack } from '../parser.js';
|
|
7
11
|
|
|
8
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -143,3 +147,139 @@ test('parsePack: real backend PACK.md parses as split (post-split)', () => {
|
|
|
143
147
|
assert.strictEqual(parsed.layer, 'L4');
|
|
144
148
|
assert.ok(parsed.sections.size > 0);
|
|
145
149
|
});
|
|
150
|
+
|
|
151
|
+
// --- Integration: buildAll auto-discovers split pack skill files ---
|
|
152
|
+
|
|
153
|
+
describe('buildAll split pack auto-discovery', () => {
|
|
154
|
+
async function createTempWithSplitPack() {
|
|
155
|
+
const tmp = path.join(tmpdir(), `rune-split-test-${Date.now()}`);
|
|
156
|
+
const skillsDir = path.join(tmp, 'skills', 'test-skill');
|
|
157
|
+
const extDir = path.join(tmp, 'extensions', 'test-pack');
|
|
158
|
+
const extSkillsDir = path.join(extDir, 'skills');
|
|
159
|
+
|
|
160
|
+
await mkdir(skillsDir, { recursive: true });
|
|
161
|
+
await mkdir(extSkillsDir, { recursive: true });
|
|
162
|
+
|
|
163
|
+
// Minimal core skill (required for build)
|
|
164
|
+
await writeFile(
|
|
165
|
+
path.join(skillsDir, 'SKILL.md'),
|
|
166
|
+
[
|
|
167
|
+
'---',
|
|
168
|
+
'name: test-skill',
|
|
169
|
+
'description: "A test skill"',
|
|
170
|
+
'layer: L3',
|
|
171
|
+
'group: utility',
|
|
172
|
+
'connections: []',
|
|
173
|
+
'tags: [test]',
|
|
174
|
+
'---',
|
|
175
|
+
'',
|
|
176
|
+
'# test-skill',
|
|
177
|
+
'',
|
|
178
|
+
'Hello.',
|
|
179
|
+
].join('\n'),
|
|
180
|
+
'utf-8',
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Split pack WITHOUT skills: array in frontmatter
|
|
184
|
+
await writeFile(
|
|
185
|
+
path.join(extDir, 'PACK.md'),
|
|
186
|
+
[
|
|
187
|
+
'---',
|
|
188
|
+
'name: "@rune/test-pack"',
|
|
189
|
+
'description: "Test pack"',
|
|
190
|
+
'metadata:',
|
|
191
|
+
' version: "0.1.0"',
|
|
192
|
+
' format: split',
|
|
193
|
+
'---',
|
|
194
|
+
'',
|
|
195
|
+
'# @rune/test-pack',
|
|
196
|
+
'',
|
|
197
|
+
'Pack index body.',
|
|
198
|
+
].join('\n'),
|
|
199
|
+
'utf-8',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Two skill files in skills/ subdir
|
|
203
|
+
await writeFile(
|
|
204
|
+
path.join(extSkillsDir, 'alpha.md'),
|
|
205
|
+
[
|
|
206
|
+
'---',
|
|
207
|
+
'name: "alpha"',
|
|
208
|
+
'description: "Alpha skill"',
|
|
209
|
+
'model: sonnet',
|
|
210
|
+
'---',
|
|
211
|
+
'',
|
|
212
|
+
'# alpha',
|
|
213
|
+
'',
|
|
214
|
+
'Alpha body content here.',
|
|
215
|
+
].join('\n'),
|
|
216
|
+
'utf-8',
|
|
217
|
+
);
|
|
218
|
+
await writeFile(
|
|
219
|
+
path.join(extSkillsDir, 'beta.md'),
|
|
220
|
+
[
|
|
221
|
+
'---',
|
|
222
|
+
'name: "beta"',
|
|
223
|
+
'description: "Beta skill"',
|
|
224
|
+
'model: sonnet',
|
|
225
|
+
'---',
|
|
226
|
+
'',
|
|
227
|
+
'# beta',
|
|
228
|
+
'',
|
|
229
|
+
'Beta body content here.',
|
|
230
|
+
].join('\n'),
|
|
231
|
+
'utf-8',
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return tmp;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
test('auto-discovers skill files from skills/ subdir when manifest is empty', async () => {
|
|
238
|
+
const tmp = await createTempWithSplitPack();
|
|
239
|
+
try {
|
|
240
|
+
const outputRoot = path.join(tmp, 'out');
|
|
241
|
+
const adapter = getAdapter('cursor');
|
|
242
|
+
const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
243
|
+
|
|
244
|
+
assert.strictEqual(stats.packCount, 1, 'should build 1 pack');
|
|
245
|
+
|
|
246
|
+
// Read the compiled pack output
|
|
247
|
+
const packOutput = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-ext-test-pack.mdc'), 'utf-8');
|
|
248
|
+
|
|
249
|
+
// Should contain the pack index body
|
|
250
|
+
assert.ok(packOutput.includes('Pack index body'), 'missing pack index body');
|
|
251
|
+
|
|
252
|
+
// Should contain BOTH auto-discovered skill bodies
|
|
253
|
+
assert.ok(packOutput.includes('Alpha body content here'), 'missing alpha skill body');
|
|
254
|
+
assert.ok(packOutput.includes('Beta body content here'), 'missing beta skill body');
|
|
255
|
+
} finally {
|
|
256
|
+
await rm(tmp, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('real ai-ml pack includes skill content after build', async () => {
|
|
261
|
+
const aimlPath = path.join(EXTENSIONS_DIR, 'ai-ml', 'PACK.md');
|
|
262
|
+
if (!existsSync(aimlPath)) {
|
|
263
|
+
console.log(' skip: ai-ml PACK.md not found');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tmp = path.join(tmpdir(), `rune-aiml-test-${Date.now()}`);
|
|
268
|
+
const runeRoot = path.resolve(__dirname, '../..');
|
|
269
|
+
const adapter = getAdapter('cursor');
|
|
270
|
+
await buildAll({ runeRoot, outputRoot: tmp, adapter });
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const packOutput = await readFile(path.join(tmp, adapter.outputDir, 'rune-ext-ai-ml.mdc'), 'utf-8');
|
|
274
|
+
|
|
275
|
+
// Should be significantly longer than just the index body (~99 lines without skills)
|
|
276
|
+
const lineCount = packOutput.split('\n').length;
|
|
277
|
+
assert.ok(lineCount > 200, `ai-ml pack output too short (${lineCount} lines) — skills likely not included`);
|
|
278
|
+
|
|
279
|
+
// Should contain content from individual skill files
|
|
280
|
+
assert.ok(packOutput.includes('ai-agents') || packOutput.includes('AI agent'), 'missing ai-agents skill content');
|
|
281
|
+
} finally {
|
|
282
|
+
await rm(tmp, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { test } from 'node:test';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseSkill } from '../parser.js';
|
|
7
7
|
|
|
@@ -53,3 +53,149 @@ test('parse verification SKILL.md', () => {
|
|
|
53
53
|
assert.strictEqual(parsed.name, 'verification');
|
|
54
54
|
assert.strictEqual(parsed.layer, 'L3');
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
describe('signals parsing', () => {
|
|
58
|
+
test('parses emit and listen from metadata', () => {
|
|
59
|
+
const content = [
|
|
60
|
+
'---',
|
|
61
|
+
'name: alpha',
|
|
62
|
+
'description: "Test skill"',
|
|
63
|
+
'metadata:',
|
|
64
|
+
' layer: L2',
|
|
65
|
+
' emit: code.changed, tests.passed',
|
|
66
|
+
' listen: plan.ready',
|
|
67
|
+
'---',
|
|
68
|
+
'',
|
|
69
|
+
'# alpha',
|
|
70
|
+
].join('\n');
|
|
71
|
+
|
|
72
|
+
const parsed = parseSkill(content);
|
|
73
|
+
assert.ok(parsed.signals, 'signals should not be null');
|
|
74
|
+
assert.deepStrictEqual(parsed.signals.emit, ['code.changed', 'tests.passed']);
|
|
75
|
+
assert.deepStrictEqual(parsed.signals.listen, ['plan.ready']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('emit only — no listen', () => {
|
|
79
|
+
const content = [
|
|
80
|
+
'---',
|
|
81
|
+
'name: beta',
|
|
82
|
+
'description: "Emitter only"',
|
|
83
|
+
'metadata:',
|
|
84
|
+
' layer: L3',
|
|
85
|
+
' emit: deploy.complete',
|
|
86
|
+
'---',
|
|
87
|
+
'',
|
|
88
|
+
'# beta',
|
|
89
|
+
].join('\n');
|
|
90
|
+
|
|
91
|
+
const parsed = parseSkill(content);
|
|
92
|
+
assert.ok(parsed.signals);
|
|
93
|
+
assert.deepStrictEqual(parsed.signals.emit, ['deploy.complete']);
|
|
94
|
+
assert.deepStrictEqual(parsed.signals.listen, []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('listen only — no emit', () => {
|
|
98
|
+
const content = [
|
|
99
|
+
'---',
|
|
100
|
+
'name: gamma',
|
|
101
|
+
'description: "Listener only"',
|
|
102
|
+
'metadata:',
|
|
103
|
+
' layer: L3',
|
|
104
|
+
' listen: code.changed',
|
|
105
|
+
'---',
|
|
106
|
+
'',
|
|
107
|
+
'# gamma',
|
|
108
|
+
].join('\n');
|
|
109
|
+
|
|
110
|
+
const parsed = parseSkill(content);
|
|
111
|
+
assert.ok(parsed.signals);
|
|
112
|
+
assert.deepStrictEqual(parsed.signals.emit, []);
|
|
113
|
+
assert.deepStrictEqual(parsed.signals.listen, ['code.changed']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('no signals — returns null', () => {
|
|
117
|
+
const content = [
|
|
118
|
+
'---',
|
|
119
|
+
'name: delta',
|
|
120
|
+
'description: "No signals"',
|
|
121
|
+
'metadata:',
|
|
122
|
+
' layer: L3',
|
|
123
|
+
'---',
|
|
124
|
+
'',
|
|
125
|
+
'# delta',
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
const parsed = parseSkill(content);
|
|
129
|
+
assert.strictEqual(parsed.signals, null);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('real cook skill has signals', () => {
|
|
133
|
+
const content = readFileSync(path.join(SKILLS_DIR, 'cook/SKILL.md'), 'utf-8');
|
|
134
|
+
const parsed = parseSkill(content, 'cook/SKILL.md');
|
|
135
|
+
assert.ok(parsed.signals, 'cook should have signals');
|
|
136
|
+
assert.ok(parsed.signals.emit.includes('phase.complete'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('real test skill has emit and listen', () => {
|
|
140
|
+
const content = readFileSync(path.join(SKILLS_DIR, 'test/SKILL.md'), 'utf-8');
|
|
141
|
+
const parsed = parseSkill(content, 'test/SKILL.md');
|
|
142
|
+
assert.ok(parsed.signals, 'test should have signals');
|
|
143
|
+
assert.ok(parsed.signals.emit.includes('tests.passed'));
|
|
144
|
+
assert.ok(parsed.signals.emit.includes('tests.failed'));
|
|
145
|
+
assert.ok(parsed.signals.listen.includes('code.changed'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('top-level emit/listen (Pro/Business pack format)', () => {
|
|
149
|
+
const content = [
|
|
150
|
+
'---',
|
|
151
|
+
'name: "feature-spec"',
|
|
152
|
+
'pack: "@rune-pro/product"',
|
|
153
|
+
'version: "1.2.0"',
|
|
154
|
+
'model: opus',
|
|
155
|
+
'tools: [Read, Write]',
|
|
156
|
+
'emit: product.spec.drafted',
|
|
157
|
+
'listen: sales.account.researched, product.research.complete',
|
|
158
|
+
'---',
|
|
159
|
+
'',
|
|
160
|
+
'# feature-spec',
|
|
161
|
+
].join('\n');
|
|
162
|
+
|
|
163
|
+
const parsed = parseSkill(content);
|
|
164
|
+
assert.ok(parsed.signals, 'top-level signals should be parsed');
|
|
165
|
+
assert.deepStrictEqual(parsed.signals.emit, ['product.spec.drafted']);
|
|
166
|
+
assert.deepStrictEqual(parsed.signals.listen, ['sales.account.researched', 'product.research.complete']);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('top-level emit only (no listen)', () => {
|
|
170
|
+
const content = [
|
|
171
|
+
'---',
|
|
172
|
+
'name: "account-research"',
|
|
173
|
+
'emit: sales.account.researched',
|
|
174
|
+
'---',
|
|
175
|
+
'',
|
|
176
|
+
'# account-research',
|
|
177
|
+
].join('\n');
|
|
178
|
+
|
|
179
|
+
const parsed = parseSkill(content);
|
|
180
|
+
assert.ok(parsed.signals);
|
|
181
|
+
assert.deepStrictEqual(parsed.signals.emit, ['sales.account.researched']);
|
|
182
|
+
assert.deepStrictEqual(parsed.signals.listen, []);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('metadata emit takes precedence over top-level', () => {
|
|
186
|
+
const content = [
|
|
187
|
+
'---',
|
|
188
|
+
'name: "hybrid"',
|
|
189
|
+
'emit: top.level.signal',
|
|
190
|
+
'metadata:',
|
|
191
|
+
' emit: nested.signal',
|
|
192
|
+
'---',
|
|
193
|
+
'',
|
|
194
|
+
'# hybrid',
|
|
195
|
+
].join('\n');
|
|
196
|
+
|
|
197
|
+
const parsed = parseSkill(content);
|
|
198
|
+
assert.ok(parsed.signals);
|
|
199
|
+
assert.deepStrictEqual(parsed.signals.emit, ['nested.signal']);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -138,11 +138,7 @@ describe('buildAll with scripts', () => {
|
|
|
138
138
|
|
|
139
139
|
// Minimal .claude-plugin for openclaw
|
|
140
140
|
await mkdir(path.join(tmp, '.claude-plugin'), { recursive: true });
|
|
141
|
-
await writeFile(
|
|
142
|
-
path.join(tmp, '.claude-plugin', 'plugin.json'),
|
|
143
|
-
JSON.stringify({ version: '0.0.1' }),
|
|
144
|
-
'utf-8',
|
|
145
|
-
);
|
|
141
|
+
await writeFile(path.join(tmp, '.claude-plugin', 'plugin.json'), JSON.stringify({ version: '0.0.1' }), 'utf-8');
|
|
146
142
|
|
|
147
143
|
return tmp;
|
|
148
144
|
}
|
|
@@ -168,7 +164,10 @@ describe('buildAll with scripts', () => {
|
|
|
168
164
|
assert.ok(mdc.includes('.cursor/rules/rune-test-slide-scripts/build-deck.js'), 'resolved path missing');
|
|
169
165
|
|
|
170
166
|
// Plain skill: no scripts dir created
|
|
171
|
-
assert.ok(
|
|
167
|
+
assert.ok(
|
|
168
|
+
!existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain-scripts')),
|
|
169
|
+
'plain skill should not have scripts dir',
|
|
170
|
+
);
|
|
172
171
|
} finally {
|
|
173
172
|
await rm(tmp, { recursive: true, force: true });
|
|
174
173
|
}
|
|
@@ -189,10 +188,7 @@ describe('buildAll with scripts', () => {
|
|
|
189
188
|
assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing');
|
|
190
189
|
|
|
191
190
|
// Placeholder resolved
|
|
192
|
-
const md = await readFile(
|
|
193
|
-
path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'),
|
|
194
|
-
'utf-8',
|
|
195
|
-
);
|
|
191
|
+
const md = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'), 'utf-8');
|
|
196
192
|
assert.ok(!md.includes('{scripts_dir}'), 'placeholder should be resolved');
|
|
197
193
|
assert.ok(md.includes('.codex/skills/rune-test-slide/scripts/build-deck.js'), 'resolved path missing');
|
|
198
194
|
} finally {
|
|
@@ -254,7 +250,10 @@ describe('buildAll with scripts', () => {
|
|
|
254
250
|
await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
255
251
|
|
|
256
252
|
const original = await readFile(path.join(tmp, 'skills', 'test-slide', 'scripts', 'build-deck.js'), 'utf-8');
|
|
257
|
-
const copied = await readFile(
|
|
253
|
+
const copied = await readFile(
|
|
254
|
+
path.join(outputRoot, adapter.outputDir, 'rune-test-slide-scripts', 'build-deck.js'),
|
|
255
|
+
'utf-8',
|
|
256
|
+
);
|
|
258
257
|
assert.strictEqual(copied, original, 'script content should be identical');
|
|
259
258
|
} finally {
|
|
260
259
|
await rm(tmp, { recursive: true, force: true });
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Index Generation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests that buildAll generates a valid skill-index.json with:
|
|
5
|
+
* - Intent patterns mapped to skills
|
|
6
|
+
* - Mesh-aware chain prediction from connections
|
|
7
|
+
* - Complete skill graph
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from 'node:assert';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { describe, test } from 'node:test';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { getAdapter } from '../adapters/index.js';
|
|
18
|
+
import { buildAll } from '../emitter.js';
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const RUNE_ROOT = path.resolve(__dirname, '../..');
|
|
22
|
+
|
|
23
|
+
describe('skill-index.json generation', () => {
|
|
24
|
+
test('buildAll emits skill-index.json with correct structure', async () => {
|
|
25
|
+
const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
|
|
26
|
+
try {
|
|
27
|
+
const adapter = getAdapter('cursor');
|
|
28
|
+
await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
|
|
29
|
+
|
|
30
|
+
const indexPath = path.join(tmp, adapter.outputDir, 'skill-index.json');
|
|
31
|
+
assert.ok(existsSync(indexPath), 'skill-index.json not found in output');
|
|
32
|
+
|
|
33
|
+
const index = JSON.parse(await readFile(indexPath, 'utf-8'));
|
|
34
|
+
|
|
35
|
+
// Structure checks
|
|
36
|
+
assert.strictEqual(index.version, 2);
|
|
37
|
+
assert.ok(index.generated, 'missing generated timestamp');
|
|
38
|
+
assert.ok(index.skillCount >= 50, `too few skills: ${index.skillCount}`);
|
|
39
|
+
assert.ok(typeof index.skills === 'object', 'missing skills object');
|
|
40
|
+
assert.ok(typeof index.graph === 'object', 'missing graph object');
|
|
41
|
+
assert.ok(typeof index.signals === 'object', 'missing signals object');
|
|
42
|
+
assert.ok(typeof index.intents === 'object', 'missing intents object');
|
|
43
|
+
} finally {
|
|
44
|
+
await rm(tmp, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('skill-index contains intent patterns with chains', async () => {
|
|
49
|
+
const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
|
|
50
|
+
try {
|
|
51
|
+
const adapter = getAdapter('cursor');
|
|
52
|
+
await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
|
|
53
|
+
|
|
54
|
+
const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
|
|
55
|
+
|
|
56
|
+
// cook intent should exist with keywords and chain
|
|
57
|
+
assert.ok(index.intents.cook, 'missing cook intent');
|
|
58
|
+
assert.ok(Array.isArray(index.intents.cook.keywords), 'cook keywords not array');
|
|
59
|
+
assert.ok(index.intents.cook.keywords.includes('implement'), 'cook missing "implement" keyword');
|
|
60
|
+
assert.ok(Array.isArray(index.intents.cook.chain), 'cook chain not array');
|
|
61
|
+
assert.strictEqual(index.intents.cook.chain[0], 'cook', 'cook chain should start with cook');
|
|
62
|
+
assert.ok(index.intents.cook.chain.length > 1, 'cook chain should have connected skills');
|
|
63
|
+
|
|
64
|
+
// debug intent
|
|
65
|
+
assert.ok(index.intents.debug, 'missing debug intent');
|
|
66
|
+
assert.ok(index.intents.debug.keywords.includes('bug'), 'debug missing "bug" keyword');
|
|
67
|
+
|
|
68
|
+
// sentinel intent
|
|
69
|
+
assert.ok(index.intents.sentinel, 'missing sentinel intent');
|
|
70
|
+
assert.ok(index.intents.sentinel.keywords.includes('security'), 'sentinel missing "security" keyword');
|
|
71
|
+
} finally {
|
|
72
|
+
await rm(tmp, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('skill-index graph has connections from cross-refs', async () => {
|
|
77
|
+
const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
|
|
78
|
+
try {
|
|
79
|
+
const adapter = getAdapter('cursor');
|
|
80
|
+
await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
|
|
81
|
+
|
|
82
|
+
const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
|
|
83
|
+
|
|
84
|
+
// cook should have outbound connections
|
|
85
|
+
assert.ok(index.graph.cook, 'cook not in graph');
|
|
86
|
+
assert.ok(index.graph.cook.length > 3, `cook should have many connections, got ${index.graph.cook.length}`);
|
|
87
|
+
|
|
88
|
+
// Skills entry should have layer and description
|
|
89
|
+
assert.ok(index.skills.cook, 'cook not in skills');
|
|
90
|
+
assert.strictEqual(index.skills.cook.layer, 'L1');
|
|
91
|
+
assert.ok(index.skills.cook.description.length > 20, 'cook description too short');
|
|
92
|
+
} finally {
|
|
93
|
+
await rm(tmp, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('skill-index contains signal graph', async () => {
|
|
98
|
+
const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
|
|
99
|
+
try {
|
|
100
|
+
const adapter = getAdapter('cursor');
|
|
101
|
+
await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
|
|
102
|
+
|
|
103
|
+
const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
|
|
104
|
+
|
|
105
|
+
// Signal graph should exist
|
|
106
|
+
assert.ok(typeof index.signals === 'object', 'missing signals object');
|
|
107
|
+
assert.ok(Object.keys(index.signals).length >= 10, `too few signals: ${Object.keys(index.signals).length}`);
|
|
108
|
+
|
|
109
|
+
// code.changed should be a well-connected signal
|
|
110
|
+
assert.ok(index.signals['code.changed'], 'missing code.changed signal');
|
|
111
|
+
assert.ok(index.signals['code.changed'].emitters.includes('fix'), 'fix should emit code.changed');
|
|
112
|
+
assert.ok(index.signals['code.changed'].listeners.length >= 3, 'code.changed should have 3+ listeners');
|
|
113
|
+
|
|
114
|
+
// Per-skill signals should be included
|
|
115
|
+
assert.ok(index.skills.test.signals, 'test skill should have signals');
|
|
116
|
+
assert.ok(index.skills.test.signals.emit.includes('tests.passed'), 'test should emit tests.passed');
|
|
117
|
+
assert.ok(index.skills.test.signals.listen.includes('code.changed'), 'test should listen to code.changed');
|
|
118
|
+
|
|
119
|
+
// Skills without signals should not have the field
|
|
120
|
+
const noSignalSkill = Object.values(index.skills).find((s) => !s.signals);
|
|
121
|
+
assert.ok(noSignalSkill, 'at least one skill should have no signals');
|
|
122
|
+
} finally {
|
|
123
|
+
await rm(tmp, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('skill-index signal graph with synthetic skills', async () => {
|
|
128
|
+
const tmp = path.join(tmpdir(), `rune-idx-sig-${Date.now()}`);
|
|
129
|
+
const skillsDir = path.join(tmp, 'skills');
|
|
130
|
+
await mkdir(path.join(skillsDir, 'emitter'), { recursive: true });
|
|
131
|
+
await mkdir(path.join(skillsDir, 'listener'), { recursive: true });
|
|
132
|
+
await mkdir(path.join(tmp, 'extensions'), { recursive: true });
|
|
133
|
+
|
|
134
|
+
await writeFile(
|
|
135
|
+
path.join(skillsDir, 'emitter', 'SKILL.md'),
|
|
136
|
+
[
|
|
137
|
+
'---',
|
|
138
|
+
'name: emitter',
|
|
139
|
+
'description: "Emits signals"',
|
|
140
|
+
'metadata:',
|
|
141
|
+
' layer: L2',
|
|
142
|
+
' emit: data.ready',
|
|
143
|
+
'---',
|
|
144
|
+
'',
|
|
145
|
+
'# emitter',
|
|
146
|
+
].join('\n'),
|
|
147
|
+
'utf-8',
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await writeFile(
|
|
151
|
+
path.join(skillsDir, 'listener', 'SKILL.md'),
|
|
152
|
+
[
|
|
153
|
+
'---',
|
|
154
|
+
'name: listener',
|
|
155
|
+
'description: "Listens to signals"',
|
|
156
|
+
'metadata:',
|
|
157
|
+
' layer: L3',
|
|
158
|
+
' listen: data.ready',
|
|
159
|
+
'---',
|
|
160
|
+
'',
|
|
161
|
+
'# listener',
|
|
162
|
+
].join('\n'),
|
|
163
|
+
'utf-8',
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const adapter = getAdapter('generic');
|
|
168
|
+
await buildAll({ runeRoot: tmp, outputRoot: tmp, adapter });
|
|
169
|
+
|
|
170
|
+
const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
|
|
171
|
+
|
|
172
|
+
assert.ok(index.signals['data.ready'], 'data.ready signal should exist');
|
|
173
|
+
assert.deepStrictEqual(index.signals['data.ready'].emitters, ['emitter']);
|
|
174
|
+
assert.deepStrictEqual(index.signals['data.ready'].listeners, ['listener']);
|
|
175
|
+
assert.ok(index.skills.emitter.signals);
|
|
176
|
+
assert.deepStrictEqual(index.skills.emitter.signals.emit, ['data.ready']);
|
|
177
|
+
} finally {
|
|
178
|
+
await rm(tmp, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('skill-index works with minimal skill tree', async () => {
|
|
183
|
+
const tmp = path.join(tmpdir(), `rune-idx-min-${Date.now()}`);
|
|
184
|
+
const skillsDir = path.join(tmp, 'skills', 'alpha');
|
|
185
|
+
await mkdir(skillsDir, { recursive: true });
|
|
186
|
+
await mkdir(path.join(tmp, 'extensions'), { recursive: true });
|
|
187
|
+
|
|
188
|
+
await writeFile(
|
|
189
|
+
path.join(skillsDir, 'SKILL.md'),
|
|
190
|
+
[
|
|
191
|
+
'---',
|
|
192
|
+
'name: alpha',
|
|
193
|
+
'description: "Test skill"',
|
|
194
|
+
'metadata:',
|
|
195
|
+
' layer: L3',
|
|
196
|
+
' group: utility',
|
|
197
|
+
'---',
|
|
198
|
+
'',
|
|
199
|
+
'# alpha',
|
|
200
|
+
'',
|
|
201
|
+
'Body.',
|
|
202
|
+
].join('\n'),
|
|
203
|
+
'utf-8',
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const adapter = getAdapter('generic');
|
|
208
|
+
await buildAll({ runeRoot: tmp, outputRoot: tmp, adapter });
|
|
209
|
+
|
|
210
|
+
const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
|
|
211
|
+
assert.strictEqual(index.skillCount, 1);
|
|
212
|
+
assert.ok(index.skills.alpha, 'alpha not in skills');
|
|
213
|
+
assert.deepStrictEqual(index.graph.alpha, [], 'alpha should have no connections');
|
|
214
|
+
} finally {
|
|
215
|
+
await rm(tmp, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|