@rune-kit/rune 2.4.0 → 2.6.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 +36 -17
- package/compiler/__tests__/pack-split.test.js +141 -1
- package/compiler/__tests__/parser.test.js +147 -55
- package/compiler/__tests__/scripts-bundling.test.js +10 -11
- package/compiler/__tests__/skill-index.test.js +218 -0
- package/compiler/adapters/antigravity.js +71 -57
- package/compiler/bin/rune.js +355 -355
- package/compiler/doctor.js +11 -1
- package/compiler/emitter.js +678 -466
- package/compiler/parser.js +267 -247
- 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/brainstorm/SKILL.md +2 -0
- package/skills/cook/SKILL.md +661 -648
- package/skills/debug/SKILL.md +394 -392
- package/skills/deploy/SKILL.md +2 -0
- package/skills/fix/SKILL.md +283 -281
- package/skills/onboard/SKILL.md +7 -0
- package/skills/plan/SKILL.md +344 -342
- package/skills/preflight/SKILL.md +362 -360
- package/skills/review/SKILL.md +491 -489
- package/skills/scout/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +319 -299
- package/skills/session-bridge/SKILL.md +1 -0
- package/skills/team/SKILL.md +1 -0
- package/skills/test/SKILL.md +587 -585
- package/skills/verification/SKILL.md +1 -0
- package/skills/watchdog/SKILL.md +2 -0
package/compiler/emitter.js
CHANGED
|
@@ -1,466 +1,678 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Emitter
|
|
3
|
-
*
|
|
4
|
-
* Writes transformed skill files to the platform's output directory.
|
|
5
|
-
* Handles file naming, directory creation,
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
9
|
-
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import { extractCrossRefs, extractToolRefs, parsePack, parseSkill } from './parser.js';
|
|
12
|
-
import { transformSkill } from './transformer.js';
|
|
13
|
-
import { resolveScriptsPath } from './transforms/scripts-path.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Discover all SKILL.md files in the skills directory
|
|
17
|
-
*
|
|
18
|
-
* @param {string} skillsDir - path to skills/ directory
|
|
19
|
-
* @returns {Promise<string[]>} array of SKILL.md file paths
|
|
20
|
-
*/
|
|
21
|
-
async function discoverSkills(skillsDir) {
|
|
22
|
-
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
23
|
-
const paths = [];
|
|
24
|
-
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
if (!entry.isDirectory()) continue;
|
|
27
|
-
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
28
|
-
if (existsSync(skillFile)) {
|
|
29
|
-
paths.push(skillFile);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return paths.sort();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Discover all PACK.md files in the extensions directory
|
|
38
|
-
*
|
|
39
|
-
* @param {string} extensionsDir - path to extensions/ directory
|
|
40
|
-
* @param {string[]} [enabledPacks] - list of enabled pack names (null = all)
|
|
41
|
-
* @returns {Promise<string[]>} array of PACK.md file paths
|
|
42
|
-
*/
|
|
43
|
-
async function discoverPacks(extensionsDir, enabledPacks = null) {
|
|
44
|
-
if (!existsSync(extensionsDir)) return [];
|
|
45
|
-
|
|
46
|
-
const entries = await readdir(extensionsDir, { withFileTypes: true });
|
|
47
|
-
const paths = [];
|
|
48
|
-
|
|
49
|
-
for (const entry of entries) {
|
|
50
|
-
if (!entry.isDirectory()) continue;
|
|
51
|
-
if (enabledPacks && !enabledPacks.includes(entry.name) && !enabledPacks.includes(`@rune/${entry.name}`)) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
const packFile = path.join(extensionsDir, entry.name, 'PACK.md');
|
|
55
|
-
if (existsSync(packFile)) {
|
|
56
|
-
paths.push(packFile);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return paths.sort();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Copy
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* @param {string}
|
|
68
|
-
* @
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* e.g.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
'',
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Emitter
|
|
3
|
+
*
|
|
4
|
+
* Writes transformed skill files to the platform's output directory.
|
|
5
|
+
* Handles file naming, directory creation, index generation, and AGENTS.md creation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { extractCrossRefs, extractToolRefs, parsePack, parseSkill } from './parser.js';
|
|
12
|
+
import { transformSkill } from './transformer.js';
|
|
13
|
+
import { resolveScriptsPath } from './transforms/scripts-path.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Discover all SKILL.md files in the skills directory
|
|
17
|
+
*
|
|
18
|
+
* @param {string} skillsDir - path to skills/ directory
|
|
19
|
+
* @returns {Promise<string[]>} array of SKILL.md file paths
|
|
20
|
+
*/
|
|
21
|
+
async function discoverSkills(skillsDir) {
|
|
22
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
23
|
+
const paths = [];
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (!entry.isDirectory()) continue;
|
|
27
|
+
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
28
|
+
if (existsSync(skillFile)) {
|
|
29
|
+
paths.push(skillFile);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return paths.sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discover all PACK.md files in the extensions directory
|
|
38
|
+
*
|
|
39
|
+
* @param {string} extensionsDir - path to extensions/ directory
|
|
40
|
+
* @param {string[]} [enabledPacks] - list of enabled pack names (null = all)
|
|
41
|
+
* @returns {Promise<string[]>} array of PACK.md file paths
|
|
42
|
+
*/
|
|
43
|
+
async function discoverPacks(extensionsDir, enabledPacks = null) {
|
|
44
|
+
if (!existsSync(extensionsDir)) return [];
|
|
45
|
+
|
|
46
|
+
const entries = await readdir(extensionsDir, { withFileTypes: true });
|
|
47
|
+
const paths = [];
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.isDirectory()) continue;
|
|
51
|
+
if (enabledPacks && !enabledPacks.includes(entry.name) && !enabledPacks.includes(`@rune/${entry.name}`)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const packFile = path.join(extensionsDir, entry.name, 'PACK.md');
|
|
55
|
+
if (existsSync(packFile)) {
|
|
56
|
+
paths.push(packFile);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return paths.sort();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Copy extra directories from skill source to output
|
|
65
|
+
* Copies directories except SKILL.md (already processed) and denylisted dirs
|
|
66
|
+
*
|
|
67
|
+
* @param {string} sourceSkillDir - e.g. skills/cook/
|
|
68
|
+
* @param {string} outputSkillDir - e.g. .codex/skills/rune-cook/
|
|
69
|
+
* @returns {Promise<string[]>} list of copied directory names
|
|
70
|
+
*/
|
|
71
|
+
const COPY_DENYLIST = new Set(['.git', 'node_modules', '__pycache__', '.DS_Store', '.venv', '.env']);
|
|
72
|
+
|
|
73
|
+
async function copySkillExtraDirs(sourceSkillDir, outputSkillDir) {
|
|
74
|
+
if (!existsSync(sourceSkillDir)) return [];
|
|
75
|
+
|
|
76
|
+
const entries = await readdir(sourceSkillDir, { withFileTypes: true });
|
|
77
|
+
const dirs = entries.filter((e) => e.isDirectory() && !COPY_DENYLIST.has(e.name));
|
|
78
|
+
|
|
79
|
+
const copied = [];
|
|
80
|
+
for (const dir of dirs) {
|
|
81
|
+
const sourcePath = path.join(sourceSkillDir, dir.name);
|
|
82
|
+
const outputPath = path.join(outputSkillDir, dir.name);
|
|
83
|
+
await cp(sourcePath, outputPath, { recursive: true });
|
|
84
|
+
copied.push(dir.name);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return copied;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Copy scripts directory from skill source to output.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} sourceScriptsDir - e.g. skills/slides/scripts/
|
|
94
|
+
* @param {string} outputScriptsDir - e.g. .cursor/rules/rune-slides-scripts/
|
|
95
|
+
* @returns {Promise<string[]>} list of copied file paths
|
|
96
|
+
*/
|
|
97
|
+
async function copyScriptsDir(sourceScriptsDir, outputScriptsDir) {
|
|
98
|
+
if (!existsSync(sourceScriptsDir)) return [];
|
|
99
|
+
|
|
100
|
+
const entries = await readdir(sourceScriptsDir, { recursive: true, withFileTypes: true });
|
|
101
|
+
const files = entries.filter((e) => e.isFile());
|
|
102
|
+
if (entries.length === 0) return [];
|
|
103
|
+
|
|
104
|
+
await cp(sourceScriptsDir, outputScriptsDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
// Return relative paths within the scripts dir (same structure as source after recursive cp)
|
|
107
|
+
return files.map((e) => {
|
|
108
|
+
const parent = e.parentPath || e.path;
|
|
109
|
+
return path.relative(sourceScriptsDir, path.join(parent, e.name));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Tier priority: higher number = higher priority (wins override)
|
|
115
|
+
*/
|
|
116
|
+
const TIER_PRIORITY = { free: 0, pro: 1, business: 2 };
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalize pack name for tier comparison.
|
|
120
|
+
* Strips tier prefixes (pro-, business-) so packs can be compared across tiers.
|
|
121
|
+
* e.g. "pro-product" → "product", "saas" → "saas"
|
|
122
|
+
*/
|
|
123
|
+
function normalizePackName(dirName) {
|
|
124
|
+
return dirName.replace(/^(pro|business)-/, '');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Discover packs across multiple tier sources and resolve overrides.
|
|
129
|
+
* Business > Pro > Free: if the same normalized pack name exists in multiple tiers,
|
|
130
|
+
* the highest-priority tier wins.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} freeExtDir - path to free extensions/ directory
|
|
133
|
+
* @param {Object<string, string>} [tierSources] - { pro: "/path/to/pro/extensions", business: "/path/to/business/extensions" }
|
|
134
|
+
* @param {string[]} [enabledPacks] - list of enabled pack names (null = all)
|
|
135
|
+
* @returns {Promise<Array<{path: string, tier: string, dirName: string}>>} resolved pack entries
|
|
136
|
+
*/
|
|
137
|
+
export async function discoverTieredPacks(freeExtDir, tierSources = {}, enabledPacks = null) {
|
|
138
|
+
// Collect all packs with their tier info: Map<normalizedName, {path, tier, priority, dirName}>
|
|
139
|
+
const packMap = new Map();
|
|
140
|
+
|
|
141
|
+
// Helper: scan one extensions directory for packs
|
|
142
|
+
async function scanDir(extDir, tier) {
|
|
143
|
+
if (!existsSync(extDir)) return;
|
|
144
|
+
const entries = await readdir(extDir, { withFileTypes: true });
|
|
145
|
+
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (!entry.isDirectory()) continue;
|
|
148
|
+
if (enabledPacks && !enabledPacks.includes(entry.name) && !enabledPacks.includes(`@rune/${entry.name}`)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const packFile = path.join(extDir, entry.name, 'PACK.md');
|
|
152
|
+
if (!existsSync(packFile)) continue;
|
|
153
|
+
|
|
154
|
+
const normalized = normalizePackName(entry.name);
|
|
155
|
+
const priority = TIER_PRIORITY[tier] ?? 0;
|
|
156
|
+
const existing = packMap.get(normalized);
|
|
157
|
+
|
|
158
|
+
// Higher priority tier wins — track overridden lower-tier entries for skill-level merging
|
|
159
|
+
if (!existing || priority > existing.priority) {
|
|
160
|
+
const overrides = existing
|
|
161
|
+
? [...(existing.overrides || []), { path: existing.path, tier: existing.tier, dirName: existing.dirName }]
|
|
162
|
+
: [];
|
|
163
|
+
packMap.set(normalized, { path: packFile, tier, priority, dirName: entry.name, overrides });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Scan free first (lowest priority), then pro, then business
|
|
169
|
+
await scanDir(freeExtDir, 'free');
|
|
170
|
+
if (tierSources.pro) {
|
|
171
|
+
await scanDir(tierSources.pro, 'pro');
|
|
172
|
+
}
|
|
173
|
+
if (tierSources.business) {
|
|
174
|
+
await scanDir(tierSources.business, 'business');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Return sorted by dirName for deterministic output
|
|
178
|
+
return [...packMap.values()].sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Generate output filename for a skill
|
|
183
|
+
*/
|
|
184
|
+
function outputFileName(skillName, adapter) {
|
|
185
|
+
return `${adapter.skillPrefix}${skillName}${adapter.skillSuffix}${adapter.fileExtension}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build all skills for a target platform
|
|
190
|
+
*
|
|
191
|
+
* @param {object} options
|
|
192
|
+
* @param {string} options.runeRoot - root of the Rune repo
|
|
193
|
+
* @param {string} options.outputRoot - where to write output (project root or dist/)
|
|
194
|
+
* @param {object} options.adapter - platform adapter
|
|
195
|
+
* @param {string[]} [options.disabledSkills] - skills to skip
|
|
196
|
+
* @param {string[]} [options.enabledPacks] - extension packs to include (null = all)
|
|
197
|
+
* @param {Object<string, string>} [options.tierSources] - tier extension dirs { pro: "path", business: "path" }
|
|
198
|
+
* @returns {Promise<object>} build result stats
|
|
199
|
+
*/
|
|
200
|
+
export async function buildAll({
|
|
201
|
+
runeRoot,
|
|
202
|
+
outputRoot,
|
|
203
|
+
adapter,
|
|
204
|
+
disabledSkills = [],
|
|
205
|
+
enabledPacks = null,
|
|
206
|
+
tierSources = {},
|
|
207
|
+
}) {
|
|
208
|
+
// Claude Code = passthrough, no build needed
|
|
209
|
+
if (adapter.name === 'claude') {
|
|
210
|
+
return {
|
|
211
|
+
platform: 'claude',
|
|
212
|
+
message: 'Claude Code uses source SKILL.md files directly. No compilation needed.',
|
|
213
|
+
skillCount: 0,
|
|
214
|
+
packCount: 0,
|
|
215
|
+
files: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const skillsDir = path.join(runeRoot, 'skills');
|
|
220
|
+
const extensionsDir = path.join(runeRoot, 'extensions');
|
|
221
|
+
const outputDir = path.join(outputRoot, adapter.outputDir);
|
|
222
|
+
|
|
223
|
+
// Ensure output directory exists
|
|
224
|
+
await mkdir(outputDir, { recursive: true });
|
|
225
|
+
|
|
226
|
+
const skillPaths = await discoverSkills(skillsDir);
|
|
227
|
+
|
|
228
|
+
// Tier-aware pack discovery: if tierSources provided, resolve overrides
|
|
229
|
+
const hasTiers = tierSources && (tierSources.pro || tierSources.business);
|
|
230
|
+
const packEntries = hasTiers
|
|
231
|
+
? await discoverTieredPacks(extensionsDir, tierSources, enabledPacks)
|
|
232
|
+
: (await discoverPacks(extensionsDir, enabledPacks)).map((p) => ({
|
|
233
|
+
path: p,
|
|
234
|
+
tier: 'free',
|
|
235
|
+
dirName: path.basename(path.dirname(p)),
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
const stats = {
|
|
239
|
+
platform: adapter.name,
|
|
240
|
+
skillCount: 0,
|
|
241
|
+
packCount: 0,
|
|
242
|
+
crossRefsResolved: 0,
|
|
243
|
+
toolRefsResolved: 0,
|
|
244
|
+
scriptsCopied: 0,
|
|
245
|
+
files: [],
|
|
246
|
+
skipped: [],
|
|
247
|
+
errors: [],
|
|
248
|
+
tierOverrides: [],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Build skills — collect parsed data for skill-index + openclaw reuse
|
|
252
|
+
const parsedSkills = [];
|
|
253
|
+
|
|
254
|
+
for (const skillPath of skillPaths) {
|
|
255
|
+
try {
|
|
256
|
+
const content = await readFile(skillPath, 'utf-8');
|
|
257
|
+
const parsed = parseSkill(content, skillPath);
|
|
258
|
+
|
|
259
|
+
// Check disabled
|
|
260
|
+
if (disabledSkills.includes(parsed.name)) {
|
|
261
|
+
stats.skipped.push(parsed.name);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const { header, body: rawBody, footer } = transformSkill(parsed, adapter);
|
|
266
|
+
|
|
267
|
+
// Resolve {scripts_dir} placeholder if adapter supports scripts
|
|
268
|
+
const skillSourceDir = path.dirname(skillPath);
|
|
269
|
+
const scriptsSource = path.join(skillSourceDir, 'scripts');
|
|
270
|
+
const hasScripts = existsSync(scriptsSource) && adapter.scriptsDir;
|
|
271
|
+
const scriptsRelPath = hasScripts
|
|
272
|
+
? path.join(adapter.outputDir, adapter.scriptsDir(parsed.name)).replaceAll('\\', '/')
|
|
273
|
+
: null;
|
|
274
|
+
const body = hasScripts ? resolveScriptsPath(rawBody, scriptsRelPath) : rawBody;
|
|
275
|
+
|
|
276
|
+
// Warn if {scripts_dir} placeholder exists but no scripts/ folder to resolve it
|
|
277
|
+
if (!hasScripts && rawBody.includes('{scripts_dir}')) {
|
|
278
|
+
stats.errors.push({
|
|
279
|
+
file: skillPath,
|
|
280
|
+
error: `{scripts_dir} placeholder found but no scripts/ directory exists for skill "${parsed.name}"`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const output = [header, body, footer].filter(Boolean).join('\n');
|
|
285
|
+
|
|
286
|
+
let outputPath;
|
|
287
|
+
let displayName;
|
|
288
|
+
let skillDir = null;
|
|
289
|
+
|
|
290
|
+
if (adapter.useSkillDirectories) {
|
|
291
|
+
// Directory-per-skill: .codex/skills/rune-{name}/SKILL.md
|
|
292
|
+
const dirName = `${adapter.skillPrefix}${parsed.name}`;
|
|
293
|
+
skillDir = path.join(outputDir, dirName);
|
|
294
|
+
await mkdir(skillDir, { recursive: true });
|
|
295
|
+
outputPath = path.join(skillDir, adapter.skillFileName || 'SKILL.md');
|
|
296
|
+
displayName = `${dirName}/${adapter.skillFileName || 'SKILL.md'}`;
|
|
297
|
+
} else {
|
|
298
|
+
const fileName = outputFileName(parsed.name, adapter);
|
|
299
|
+
outputPath = path.join(outputDir, fileName);
|
|
300
|
+
displayName = fileName;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await writeFile(outputPath, output, 'utf-8');
|
|
304
|
+
|
|
305
|
+
// Copy extra directories (references/, etc.) from skill source
|
|
306
|
+
if (adapter.useSkillDirectories && skillDir) {
|
|
307
|
+
await copySkillExtraDirs(skillSourceDir, skillDir);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Copy scripts/ directory if present
|
|
311
|
+
if (hasScripts) {
|
|
312
|
+
const scriptsOutput = path.join(outputDir, adapter.scriptsDir(parsed.name));
|
|
313
|
+
const copied = await copyScriptsDir(scriptsSource, scriptsOutput);
|
|
314
|
+
stats.scriptsCopied += copied.length;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
parsedSkills.push(parsed);
|
|
318
|
+
stats.skillCount++;
|
|
319
|
+
stats.crossRefsResolved += parsed.crossRefs.length;
|
|
320
|
+
stats.toolRefsResolved += parsed.toolRefs.length;
|
|
321
|
+
stats.files.push(displayName);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
stats.errors.push({ file: skillPath, error: err.message });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Build extension packs (tier-aware)
|
|
328
|
+
for (const packEntry of packEntries) {
|
|
329
|
+
try {
|
|
330
|
+
const packPath = packEntry.path;
|
|
331
|
+
const content = await readFile(packPath, 'utf-8');
|
|
332
|
+
const parsed = parsePack(content, packPath);
|
|
333
|
+
const packName = packEntry.dirName;
|
|
334
|
+
const packDir = path.dirname(packPath);
|
|
335
|
+
|
|
336
|
+
// Track tier overrides for reporting
|
|
337
|
+
if (packEntry.tier !== 'free') {
|
|
338
|
+
stats.tierOverrides.push({ pack: packName, tier: packEntry.tier });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Tier Override: merge skill manifests from lower tiers
|
|
342
|
+
// If a Pro/Business pack overrides a Free pack, inherit skills the higher tier doesn't provide
|
|
343
|
+
if (packEntry.overrides?.length > 0 && parsed.isSplit && parsed.skillManifest.length > 0) {
|
|
344
|
+
const winnerSkillNames = new Set(parsed.skillManifest.map((s) => s.name));
|
|
345
|
+
for (const lower of packEntry.overrides) {
|
|
346
|
+
try {
|
|
347
|
+
const lowerContent = await readFile(lower.path, 'utf-8');
|
|
348
|
+
const lowerParsed = parsePack(lowerContent, lower.path);
|
|
349
|
+
if (lowerParsed.isSplit) {
|
|
350
|
+
const lowerPackDir = path.dirname(lower.path);
|
|
351
|
+
for (const lowerSkill of lowerParsed.skillManifest) {
|
|
352
|
+
if (!winnerSkillNames.has(lowerSkill.name)) {
|
|
353
|
+
// Inherit skill from lower tier — track source directory for file resolution
|
|
354
|
+
parsed.skillManifest.push({ ...lowerSkill, _sourceDir: lowerPackDir });
|
|
355
|
+
winnerSkillNames.add(lowerSkill.name);
|
|
356
|
+
stats.tierOverrides.push({ pack: packName, skill: lowerSkill.name, inherited: lower.tier });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// Lower-tier pack unreadable — skip gracefully
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// For split packs: auto-discover skill files from skills/ subdir when manifest is empty
|
|
367
|
+
if (parsed.isSplit && parsed.skillManifest.length === 0) {
|
|
368
|
+
const skillsSubdir = path.join(packDir, 'skills');
|
|
369
|
+
if (existsSync(skillsSubdir)) {
|
|
370
|
+
const skillFiles = (await readdir(skillsSubdir)).filter((f) => f.endsWith('.md')).sort();
|
|
371
|
+
for (const sf of skillFiles) {
|
|
372
|
+
parsed.skillManifest.push({ name: sf.replace(/\.md$/, ''), file: `skills/${sf}` });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// For split packs, load individual skill files and concatenate into body
|
|
378
|
+
if (parsed.isSplit && parsed.skillManifest.length > 0) {
|
|
379
|
+
const skillBodies = [];
|
|
380
|
+
for (const skill of parsed.skillManifest) {
|
|
381
|
+
// Resolve skill file path — use _sourceDir for inherited lower-tier skills
|
|
382
|
+
const sourceDir = skill._sourceDir || packDir;
|
|
383
|
+
const skillPath = path.join(sourceDir, skill.file);
|
|
384
|
+
if (existsSync(skillPath)) {
|
|
385
|
+
const skillContent = await readFile(skillPath, 'utf-8');
|
|
386
|
+
// Strip frontmatter from skill file — we only need the body
|
|
387
|
+
const skillBodyMatch = skillContent.replace(/\r\n/g, '\n').match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
|
|
388
|
+
const skillBody = skillBodyMatch ? skillBodyMatch[1].trim() : skillContent.trim();
|
|
389
|
+
skillBodies.push(skillBody);
|
|
390
|
+
} else {
|
|
391
|
+
stats.errors.push({ file: skillPath, error: `Skill file not found (listed in ${packPath} manifest)` });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Concatenate: index body + all skill bodies
|
|
395
|
+
parsed.body = `${parsed.body}\n\n${skillBodies.join('\n\n---\n\n')}`;
|
|
396
|
+
// Re-extract refs from the full concatenated body
|
|
397
|
+
parsed.crossRefs = extractCrossRefs(parsed.body);
|
|
398
|
+
parsed.toolRefs = extractToolRefs(parsed.body);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Normalize pack name for headers (ext-trading instead of @rune/trading)
|
|
402
|
+
parsed.name = `ext-${packName}`;
|
|
403
|
+
|
|
404
|
+
const { header, body, footer } = transformSkill(parsed, adapter);
|
|
405
|
+
const output = [header, body, footer].filter(Boolean).join('\n');
|
|
406
|
+
|
|
407
|
+
let outputPath;
|
|
408
|
+
let displayName;
|
|
409
|
+
|
|
410
|
+
if (adapter.useSkillDirectories) {
|
|
411
|
+
const dirName = `${adapter.skillPrefix}ext-${packName}`;
|
|
412
|
+
const outPackDir = path.join(outputDir, dirName);
|
|
413
|
+
await mkdir(outPackDir, { recursive: true });
|
|
414
|
+
outputPath = path.join(outPackDir, adapter.skillFileName || 'SKILL.md');
|
|
415
|
+
displayName = `${dirName}/${adapter.skillFileName || 'SKILL.md'}`;
|
|
416
|
+
} else {
|
|
417
|
+
const fileName = outputFileName(`ext-${packName}`, adapter);
|
|
418
|
+
outputPath = path.join(outputDir, fileName);
|
|
419
|
+
displayName = fileName;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
await writeFile(outputPath, output, 'utf-8');
|
|
423
|
+
|
|
424
|
+
stats.packCount++;
|
|
425
|
+
stats.files.push(displayName);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
stats.errors.push({ file: packPath, error: err.message });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Generate index file
|
|
432
|
+
const indexContent = generateIndex(stats, adapter);
|
|
433
|
+
const indexFileName = outputFileName('index', adapter);
|
|
434
|
+
await writeFile(path.join(outputDir, indexFileName), indexContent, 'utf-8');
|
|
435
|
+
stats.files.push(indexFileName);
|
|
436
|
+
|
|
437
|
+
// Generate skill-index.json — compiled intent mesh for auto-trigger hooks
|
|
438
|
+
const skillIndex = generateSkillIndex(parsedSkills);
|
|
439
|
+
await writeFile(path.join(outputDir, 'skill-index.json'), `${JSON.stringify(skillIndex, null, 2)}\n`, 'utf-8');
|
|
440
|
+
stats.files.push('skill-index.json');
|
|
441
|
+
|
|
442
|
+
// Generate AGENTS.md for Codex (OpenAI convention — not used by other platforms)
|
|
443
|
+
if (adapter.name === 'codex') {
|
|
444
|
+
const agentsMdContent = generateAgentsMd(stats, adapter);
|
|
445
|
+
await writeFile(path.join(outputRoot, 'AGENTS.md'), agentsMdContent, 'utf-8');
|
|
446
|
+
stats.files.push('AGENTS.md');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// OpenClaw adapter: generate manifest + TypeScript entry point
|
|
450
|
+
if (adapter.name === 'openclaw' && adapter.generateManifest && adapter.generateEntryPoint) {
|
|
451
|
+
const pluginJsonPath = path.join(runeRoot, '.claude-plugin', 'plugin.json');
|
|
452
|
+
let pluginJson = { version: '0.0.0' };
|
|
453
|
+
if (existsSync(pluginJsonPath)) {
|
|
454
|
+
pluginJson = JSON.parse(await readFile(pluginJsonPath, 'utf-8'));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Read skill-router content for system prompt injection
|
|
458
|
+
const routerPath = path.join(runeRoot, 'skills', 'skill-router', 'SKILL.md');
|
|
459
|
+
let routerContent = '';
|
|
460
|
+
if (existsSync(routerPath)) {
|
|
461
|
+
routerContent = await readFile(routerPath, 'utf-8');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Write openclaw.plugin.json to parent of skills dir (.openclaw/rune/)
|
|
465
|
+
const openclawRoot = path.resolve(outputDir, '..');
|
|
466
|
+
const manifest = adapter.generateManifest(parsedSkills, pluginJson);
|
|
467
|
+
await writeFile(path.join(openclawRoot, 'openclaw.plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
468
|
+
stats.files.push('openclaw.plugin.json');
|
|
469
|
+
|
|
470
|
+
// Write src/index.ts entry point
|
|
471
|
+
const srcDir = path.join(openclawRoot, 'src');
|
|
472
|
+
await mkdir(srcDir, { recursive: true });
|
|
473
|
+
const entryPoint = adapter.generateEntryPoint(parsedSkills, routerContent);
|
|
474
|
+
await writeFile(path.join(srcDir, 'index.ts'), entryPoint, 'utf-8');
|
|
475
|
+
stats.files.push('src/index.ts');
|
|
476
|
+
|
|
477
|
+
// Write README.md + SKILL.md for ClawHub listing page
|
|
478
|
+
if (adapter.generateReadme) {
|
|
479
|
+
const readme = adapter.generateReadme(parsedSkills, pluginJson);
|
|
480
|
+
await writeFile(path.join(openclawRoot, 'README.md'), readme, 'utf-8');
|
|
481
|
+
stats.files.push('README.md');
|
|
482
|
+
// SKILL.md required by clawhub publish
|
|
483
|
+
await writeFile(path.join(openclawRoot, 'SKILL.md'), readme, 'utf-8');
|
|
484
|
+
stats.files.push('SKILL.md');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return stats;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Generate an index file listing all compiled skills
|
|
493
|
+
*/
|
|
494
|
+
function generateIndex(stats, adapter) {
|
|
495
|
+
const lines = [
|
|
496
|
+
'# Rune Skill Index',
|
|
497
|
+
'',
|
|
498
|
+
`> Platform: ${adapter.name} | Skills: ${stats.skillCount} | Extensions: ${stats.packCount}`,
|
|
499
|
+
'',
|
|
500
|
+
'## Core Skills',
|
|
501
|
+
'',
|
|
502
|
+
...stats.files.filter((f) => !f.match(/[-/]ext-/) && !f.includes('index')).map((f) => `- ${f}`),
|
|
503
|
+
'',
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
const extFiles = stats.files.filter((f) => f.match(/[-/]ext-/));
|
|
507
|
+
if (extFiles.length > 0) {
|
|
508
|
+
lines.push('## Extension Packs', '', ...extFiles.map((f) => `- ${f}`), '');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
lines.push('---', '> Rune Skill Mesh — https://github.com/rune-kit/rune');
|
|
512
|
+
|
|
513
|
+
return lines.join('\n');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Generate AGENTS.md for Codex (OpenAI convention)
|
|
518
|
+
* Uses dynamic counts from build stats — no hardcoded skill lists
|
|
519
|
+
*/
|
|
520
|
+
function generateAgentsMd(stats, adapter) {
|
|
521
|
+
const lines = [
|
|
522
|
+
'# Rune — Project Configuration',
|
|
523
|
+
'',
|
|
524
|
+
'## Overview',
|
|
525
|
+
'',
|
|
526
|
+
'Rune is an interconnected skill ecosystem for AI coding assistants.',
|
|
527
|
+
`${stats.skillCount} core skills | 5-layer mesh architecture | ${stats.crossRefsResolved} connections | Multi-platform.`,
|
|
528
|
+
'Philosophy: "Less skills. Deeper connections."',
|
|
529
|
+
'',
|
|
530
|
+
`Platform: ${adapter.name}`,
|
|
531
|
+
'',
|
|
532
|
+
'## Skills',
|
|
533
|
+
'',
|
|
534
|
+
`**${stats.skillCount} core skills** + **${stats.packCount} extension packs**`,
|
|
535
|
+
'',
|
|
536
|
+
'## Usage',
|
|
537
|
+
'',
|
|
538
|
+
'Reference skills using the `Skill` tool or delegate to subagents using the `Agent` tool.',
|
|
539
|
+
'',
|
|
540
|
+
'## Skills Directory',
|
|
541
|
+
'',
|
|
542
|
+
`Skills are located in: ${adapter.outputDir}/`,
|
|
543
|
+
'',
|
|
544
|
+
'---',
|
|
545
|
+
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
546
|
+
'',
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
return lines.join('\n');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Intent keyword patterns for each skill — extracted from description + Triggers section
|
|
554
|
+
* Maps common user intent words to the skill that handles them
|
|
555
|
+
*/
|
|
556
|
+
const INTENT_KEYWORDS = {
|
|
557
|
+
cook: ['implement', 'build', 'create', 'add', 'feature', 'fix', 'code', 'write', 'make', 'develop'],
|
|
558
|
+
team: ['parallel', 'split', 'multiple', 'large', 'many files', 'multi-module'],
|
|
559
|
+
launch: ['deploy', 'launch', 'release', 'ship', 'publish', 'production'],
|
|
560
|
+
rescue: ['legacy', 'refactor', 'modernize', 'rescue', 'clean up', 'old code', 'messy'],
|
|
561
|
+
scaffold: ['new project', 'bootstrap', 'scaffold', 'init', 'greenfield', 'starter'],
|
|
562
|
+
plan: ['plan', 'architect', 'design system', 'roadmap', 'strategy'],
|
|
563
|
+
brainstorm: ['brainstorm', 'explore', 'ideas', 'alternatives', 'approaches'],
|
|
564
|
+
debug: ['debug', 'error', 'bug', 'broken', 'trace', 'diagnose', 'crash', 'fail'],
|
|
565
|
+
fix: ['fix', 'patch', 'hotfix', 'resolve', 'repair'],
|
|
566
|
+
test: ['test', 'tdd', 'coverage', 'unit test', 'e2e', 'spec'],
|
|
567
|
+
review: ['review', 'code review', 'check quality', 'audit code'],
|
|
568
|
+
sentinel: ['security', 'vulnerability', 'owasp', 'secret', 'audit security'],
|
|
569
|
+
preflight: ['pre-commit', 'quality gate', 'check before'],
|
|
570
|
+
deploy: ['deploy', 'ci/cd', 'pipeline', 'kubernetes', 'docker'],
|
|
571
|
+
design: ['ui', 'ux', 'design', 'layout', 'component design', 'wireframe'],
|
|
572
|
+
perf: ['performance', 'slow', 'optimize', 'n+1', 'memory leak', 'bundle size'],
|
|
573
|
+
db: ['database', 'migration', 'schema', 'sql', 'query', 'index'],
|
|
574
|
+
audit: ['audit', 'health check', 'project assessment', 'codebase review'],
|
|
575
|
+
onboard: ['onboard', 'setup', 'configure project', 'get started'],
|
|
576
|
+
docs: ['document', 'readme', 'api docs', 'changelog'],
|
|
577
|
+
ba: ['requirements', 'business analysis', 'user stories', 'stakeholder'],
|
|
578
|
+
adversary: ['red team', 'challenge', 'stress test', 'edge case'],
|
|
579
|
+
incident: ['incident', 'outage', 'downtime', 'postmortem'],
|
|
580
|
+
surgeon: ['refactor', 'extract', 'strangler', 'decompose'],
|
|
581
|
+
'mcp-builder': ['mcp', 'mcp server', 'tool server', 'model context'],
|
|
582
|
+
'skill-forge': ['new skill', 'create skill', 'edit skill'],
|
|
583
|
+
'review-intake': ['pr feedback', 'review comments', 'received review'],
|
|
584
|
+
'logic-guardian': ['business logic', 'protect logic', 'critical path'],
|
|
585
|
+
marketing: ['marketing', 'landing page', 'seo', 'social media', 'copy'],
|
|
586
|
+
retro: ['retrospective', 'sprint review', 'velocity', 'team health'],
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Generate skill-index.json — compiled intent mesh for runtime auto-trigger
|
|
591
|
+
*
|
|
592
|
+
* Extracts from parsed skills: name, description, layer, model, group,
|
|
593
|
+
* cross-references (connections), and maps intent keywords to skill chains.
|
|
594
|
+
*
|
|
595
|
+
* @param {Array} parsedSkills - array of parsed skill objects
|
|
596
|
+
* @returns {object} skill index with graph + intents
|
|
597
|
+
*/
|
|
598
|
+
function generateSkillIndex(parsedSkills) {
|
|
599
|
+
// Build adjacency graph from cross-references
|
|
600
|
+
const graph = {};
|
|
601
|
+
const skills = {};
|
|
602
|
+
|
|
603
|
+
for (const skill of parsedSkills) {
|
|
604
|
+
const outbound = [...new Set(skill.crossRefs.map((r) => r.skillName))];
|
|
605
|
+
graph[skill.name] = outbound;
|
|
606
|
+
skills[skill.name] = {
|
|
607
|
+
layer: skill.layer,
|
|
608
|
+
model: skill.model,
|
|
609
|
+
group: skill.group,
|
|
610
|
+
description: skill.description.slice(0, 200),
|
|
611
|
+
connections: outbound,
|
|
612
|
+
...(skill.signals ? { signals: skill.signals } : {}),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Build signal graph — maps each signal to its emitters and listeners
|
|
617
|
+
const signalGraph = buildSignalGraph(parsedSkills);
|
|
618
|
+
|
|
619
|
+
// Build intent patterns from INTENT_KEYWORDS + skill descriptions
|
|
620
|
+
const intents = {};
|
|
621
|
+
for (const [skillName, keywords] of Object.entries(INTENT_KEYWORDS)) {
|
|
622
|
+
if (!skills[skillName]) continue;
|
|
623
|
+
const skill = skills[skillName];
|
|
624
|
+
|
|
625
|
+
// Build chain: primary skill + its direct connections (1-hop)
|
|
626
|
+
const chain = [skillName, ...graph[skillName].filter((c) => skills[c]).slice(0, 5)];
|
|
627
|
+
|
|
628
|
+
intents[skillName] = {
|
|
629
|
+
keywords,
|
|
630
|
+
layer: skill.layer,
|
|
631
|
+
model: skill.model,
|
|
632
|
+
chain,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
version: 2,
|
|
638
|
+
generated: new Date().toISOString(),
|
|
639
|
+
skillCount: parsedSkills.length,
|
|
640
|
+
skills,
|
|
641
|
+
graph,
|
|
642
|
+
signals: signalGraph,
|
|
643
|
+
intents,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Build signal graph from parsed skills' emit/listen declarations.
|
|
649
|
+
* Maps each signal name to its emitters and listeners.
|
|
650
|
+
*
|
|
651
|
+
* @param {Array} parsedSkills
|
|
652
|
+
* @returns {object} { "code.changed": { emitters: ["fix"], listeners: ["test", "review"] } }
|
|
653
|
+
*/
|
|
654
|
+
function buildSignalGraph(parsedSkills) {
|
|
655
|
+
const signals = {};
|
|
656
|
+
|
|
657
|
+
for (const skill of parsedSkills) {
|
|
658
|
+
if (!skill.signals) continue;
|
|
659
|
+
|
|
660
|
+
for (const signal of skill.signals.emit) {
|
|
661
|
+
if (!signals[signal]) signals[signal] = { emitters: [], listeners: [] };
|
|
662
|
+
signals[signal].emitters.push(skill.name);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
for (const signal of skill.signals.listen) {
|
|
666
|
+
if (!signals[signal]) signals[signal] = { emitters: [], listeners: [] };
|
|
667
|
+
signals[signal].listeners.push(skill.name);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Sort for deterministic output
|
|
672
|
+
for (const entry of Object.values(signals)) {
|
|
673
|
+
entry.emitters.sort();
|
|
674
|
+
entry.listeners.sort();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return signals;
|
|
678
|
+
}
|