@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
package/compiler/emitter.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Emitter
|
|
3
3
|
*
|
|
4
4
|
* Writes transformed skill files to the platform's output directory.
|
|
5
|
-
* Handles file naming, directory creation,
|
|
5
|
+
* Handles file naming, directory creation, index generation, and AGENTS.md creation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { extractCrossRefs, extractToolRefs, parsePack, parseSkill } from './parser.js';
|
|
11
|
+
import { extractCrossRefs, extractToolRefs, parseOrgConfig, parsePack, parseSkill, parseTemplate } from './parser.js';
|
|
12
12
|
import { transformSkill } from './transformer.js';
|
|
13
13
|
import { resolveScriptsPath } from './transforms/scripts-path.js';
|
|
14
14
|
|
|
@@ -60,6 +60,33 @@ async function discoverPacks(extensionsDir, enabledPacks = null) {
|
|
|
60
60
|
return paths.sort();
|
|
61
61
|
}
|
|
62
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
|
+
|
|
63
90
|
/**
|
|
64
91
|
* Copy scripts directory from skill source to output.
|
|
65
92
|
*
|
|
@@ -151,6 +178,217 @@ export async function discoverTieredPacks(freeExtDir, tierSources = {}, enabledP
|
|
|
151
178
|
return [...packMap.values()].sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
152
179
|
}
|
|
153
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Discover workflow templates in a pack's templates/ directory
|
|
183
|
+
*
|
|
184
|
+
* @param {string} packDir - path to the pack directory (e.g. extensions/pro-product/)
|
|
185
|
+
* @returns {Promise<Array<{name: string, file: string, parsed: object}>>} array of parsed templates
|
|
186
|
+
*/
|
|
187
|
+
async function discoverTemplates(packDir) {
|
|
188
|
+
const templatesDir = path.join(packDir, 'templates');
|
|
189
|
+
if (!existsSync(templatesDir)) return [];
|
|
190
|
+
|
|
191
|
+
const entries = await readdir(templatesDir, { withFileTypes: true });
|
|
192
|
+
const templates = [];
|
|
193
|
+
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
196
|
+
const filePath = path.join(templatesDir, entry.name);
|
|
197
|
+
const content = await readFile(filePath, 'utf-8');
|
|
198
|
+
const parsed = parseTemplate(content, filePath);
|
|
199
|
+
templates.push({
|
|
200
|
+
name: parsed.name || entry.name.replace(/\.md$/, ''),
|
|
201
|
+
file: `templates/${entry.name}`,
|
|
202
|
+
parsed,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return templates.sort((a, b) => a.name.localeCompare(b.name));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Load reference injection rules from all tier extension directories.
|
|
211
|
+
* Each pack can have an inject.json file defining which references to inject
|
|
212
|
+
* into Free core skills when that pack is installed.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} freeExtDir - path to free extensions/ directory
|
|
215
|
+
* @param {Object<string, string>} tierSources - { pro: "/path", business: "/path" }
|
|
216
|
+
* @returns {Promise<Map<string, Array<{ref: string, context: string, packDir: string}>>>}
|
|
217
|
+
* Map of skillName → array of injections
|
|
218
|
+
*/
|
|
219
|
+
async function loadInjectionRules(freeExtDir, tierSources) {
|
|
220
|
+
const injections = new Map(); // skillName → [{ref, context, packDir}]
|
|
221
|
+
|
|
222
|
+
const extDirs = [freeExtDir, tierSources.pro, tierSources.business].filter(Boolean);
|
|
223
|
+
|
|
224
|
+
for (const extDir of extDirs) {
|
|
225
|
+
if (!existsSync(extDir)) continue;
|
|
226
|
+
const entries = await readdir(extDir, { withFileTypes: true });
|
|
227
|
+
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
if (!entry.isDirectory()) continue;
|
|
230
|
+
const injectFile = path.join(extDir, entry.name, 'inject.json');
|
|
231
|
+
if (!existsSync(injectFile)) continue;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const raw = await readFile(injectFile, 'utf-8');
|
|
235
|
+
const config = JSON.parse(raw);
|
|
236
|
+
const packDir = path.join(extDir, entry.name);
|
|
237
|
+
|
|
238
|
+
for (const rule of config.injections || []) {
|
|
239
|
+
if (!rule.skill || !rule.ref) continue;
|
|
240
|
+
if (!injections.has(rule.skill)) {
|
|
241
|
+
injections.set(rule.skill, []);
|
|
242
|
+
}
|
|
243
|
+
injections.get(rule.skill).push({
|
|
244
|
+
ref: rule.ref,
|
|
245
|
+
context: rule.context || '',
|
|
246
|
+
packDir,
|
|
247
|
+
pack: entry.name,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
// Invalid JSON — skip gracefully
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return injections;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Apply reference injections to a skill's body.
|
|
261
|
+
* Reads the referenced files and appends them as context blocks.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} body - original skill body
|
|
264
|
+
* @param {Array<{ref: string, context: string, packDir: string}>} rules - injection rules
|
|
265
|
+
* @returns {Promise<{body: string, count: number}>} injected body + count of injections applied
|
|
266
|
+
*/
|
|
267
|
+
async function applyInjections(body, rules) {
|
|
268
|
+
const blocks = [];
|
|
269
|
+
|
|
270
|
+
for (const rule of rules) {
|
|
271
|
+
const refPath = path.join(rule.packDir, rule.ref);
|
|
272
|
+
if (!existsSync(refPath)) continue;
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const content = await readFile(refPath, 'utf-8');
|
|
276
|
+
// Strip frontmatter if present
|
|
277
|
+
const cleaned = content
|
|
278
|
+
.replace(/\r\n/g, '\n')
|
|
279
|
+
.replace(/^---\n[\s\S]*?\n---\n?/, '')
|
|
280
|
+
.trim();
|
|
281
|
+
blocks.push(
|
|
282
|
+
`\n\n<!-- Pro Reference: ${rule.pack}/${rule.ref} -->\n` +
|
|
283
|
+
`<CONTEXT-INJECTION source="${rule.pack}" context="${rule.context}">\n` +
|
|
284
|
+
`${cleaned}\n` +
|
|
285
|
+
`</CONTEXT-INJECTION>`,
|
|
286
|
+
);
|
|
287
|
+
} catch {
|
|
288
|
+
// Unreadable reference — skip
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { body: body + blocks.join(''), count: blocks.length };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Load org config from .rune/org/org.md if it exists.
|
|
297
|
+
* Returns parsed org config or null if not present.
|
|
298
|
+
*/
|
|
299
|
+
async function loadOrgConfig(runeRoot, tierSources) {
|
|
300
|
+
// Org templates are a Business feature — only load if business tier is available
|
|
301
|
+
if (!tierSources || !tierSources.business) return null;
|
|
302
|
+
|
|
303
|
+
const orgPath = path.join(runeRoot, '.rune', 'org', 'org.md');
|
|
304
|
+
if (!existsSync(orgPath)) return null;
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const content = await readFile(orgPath, 'utf-8');
|
|
308
|
+
return parseOrgConfig(content, orgPath);
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Generate <ORG-POLICY> block from org config for injection into sentinel/preflight.
|
|
316
|
+
*/
|
|
317
|
+
function buildOrgPolicyBlock(orgConfig, targetSkill) {
|
|
318
|
+
if (!orgConfig) return null;
|
|
319
|
+
|
|
320
|
+
const lines = [];
|
|
321
|
+
lines.push(`\n\n<!-- Business: Organization Policy (${orgConfig.name}) -->`);
|
|
322
|
+
lines.push(`<ORG-POLICY template="${orgConfig.name}" governance="${orgConfig.governanceLevel.level}">`);
|
|
323
|
+
|
|
324
|
+
if (targetSkill === 'sentinel') {
|
|
325
|
+
// Inject security + code review + deployment policies
|
|
326
|
+
const security = orgConfig.policies.security || [];
|
|
327
|
+
const codeReview = orgConfig.policies.code_review || [];
|
|
328
|
+
const deployment = orgConfig.policies.deployment || [];
|
|
329
|
+
|
|
330
|
+
if (security.length > 0) {
|
|
331
|
+
lines.push('\n### Security Policies');
|
|
332
|
+
for (const rule of security) {
|
|
333
|
+
lines.push(`- **${rule.key}**: ${rule.value}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (codeReview.length > 0) {
|
|
337
|
+
lines.push('\n### Code Review Policies');
|
|
338
|
+
for (const rule of codeReview) {
|
|
339
|
+
lines.push(`- **${rule.key}**: ${rule.value}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (deployment.length > 0) {
|
|
343
|
+
lines.push('\n### Deployment Policies');
|
|
344
|
+
for (const rule of deployment) {
|
|
345
|
+
lines.push(`- **${rule.key}**: ${rule.value}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else if (targetSkill === 'preflight') {
|
|
349
|
+
// Inject code review + deployment + approval flows
|
|
350
|
+
const codeReview = orgConfig.policies.code_review || [];
|
|
351
|
+
const deployment = orgConfig.policies.deployment || [];
|
|
352
|
+
|
|
353
|
+
if (codeReview.length > 0) {
|
|
354
|
+
lines.push('\n### Code Review Requirements');
|
|
355
|
+
for (const rule of codeReview) {
|
|
356
|
+
lines.push(`- **${rule.key}**: ${rule.value}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (deployment.length > 0) {
|
|
360
|
+
lines.push('\n### Deployment Requirements');
|
|
361
|
+
for (const rule of deployment) {
|
|
362
|
+
lines.push(`- **${rule.key}**: ${rule.value}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Include approval flows
|
|
367
|
+
const flows = orgConfig.approvalFlows;
|
|
368
|
+
if (Object.keys(flows).length > 0) {
|
|
369
|
+
lines.push('\n### Approval Flows');
|
|
370
|
+
for (const [name, flow] of Object.entries(flows)) {
|
|
371
|
+
lines.push(`\n**${name}**:`);
|
|
372
|
+
lines.push('```');
|
|
373
|
+
lines.push(flow);
|
|
374
|
+
lines.push('```');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Governance settings
|
|
380
|
+
const gov = orgConfig.governanceLevel;
|
|
381
|
+
if (gov.settings.length > 0) {
|
|
382
|
+
lines.push('\n### Governance Settings');
|
|
383
|
+
for (const setting of gov.settings) {
|
|
384
|
+
lines.push(`- ${setting}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
lines.push('</ORG-POLICY>');
|
|
389
|
+
return lines.join('\n');
|
|
390
|
+
}
|
|
391
|
+
|
|
154
392
|
/**
|
|
155
393
|
* Generate output filename for a skill
|
|
156
394
|
*/
|
|
@@ -215,13 +453,23 @@ export async function buildAll({
|
|
|
215
453
|
crossRefsResolved: 0,
|
|
216
454
|
toolRefsResolved: 0,
|
|
217
455
|
scriptsCopied: 0,
|
|
456
|
+
templateCount: 0,
|
|
457
|
+
injectionsApplied: 0,
|
|
218
458
|
files: [],
|
|
219
459
|
skipped: [],
|
|
220
460
|
errors: [],
|
|
221
461
|
tierOverrides: [],
|
|
222
462
|
};
|
|
223
463
|
|
|
224
|
-
//
|
|
464
|
+
// Load reference injection rules from Pro/Business packs
|
|
465
|
+
const injectionRules = hasTiers ? await loadInjectionRules(extensionsDir, tierSources) : new Map();
|
|
466
|
+
|
|
467
|
+
// Load org config from .rune/org/org.md (Business feature)
|
|
468
|
+
const orgConfig = await loadOrgConfig(runeRoot, tierSources);
|
|
469
|
+
|
|
470
|
+
// Build skills — collect parsed data for skill-index + openclaw reuse
|
|
471
|
+
const parsedSkills = [];
|
|
472
|
+
|
|
225
473
|
for (const skillPath of skillPaths) {
|
|
226
474
|
try {
|
|
227
475
|
const content = await readFile(skillPath, 'utf-8');
|
|
@@ -252,15 +500,33 @@ export async function buildAll({
|
|
|
252
500
|
});
|
|
253
501
|
}
|
|
254
502
|
|
|
255
|
-
|
|
503
|
+
// Apply reference injections from Pro/Business packs
|
|
504
|
+
let finalBody = body;
|
|
505
|
+
if (injectionRules.has(parsed.name)) {
|
|
506
|
+
const { body: injectedBody, count } = await applyInjections(body, injectionRules.get(parsed.name));
|
|
507
|
+
finalBody = injectedBody;
|
|
508
|
+
stats.injectionsApplied += count;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Inject org policies into sentinel/preflight (Business feature)
|
|
512
|
+
if (orgConfig && (parsed.name === 'sentinel' || parsed.name === 'preflight')) {
|
|
513
|
+
const orgBlock = buildOrgPolicyBlock(orgConfig, parsed.name);
|
|
514
|
+
if (orgBlock) {
|
|
515
|
+
finalBody += orgBlock;
|
|
516
|
+
stats.orgPoliciesInjected = (stats.orgPoliciesInjected || 0) + 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const output = [header, finalBody, footer].filter(Boolean).join('\n');
|
|
256
521
|
|
|
257
522
|
let outputPath;
|
|
258
523
|
let displayName;
|
|
524
|
+
let skillDir = null;
|
|
259
525
|
|
|
260
526
|
if (adapter.useSkillDirectories) {
|
|
261
527
|
// Directory-per-skill: .codex/skills/rune-{name}/SKILL.md
|
|
262
528
|
const dirName = `${adapter.skillPrefix}${parsed.name}`;
|
|
263
|
-
|
|
529
|
+
skillDir = path.join(outputDir, dirName);
|
|
264
530
|
await mkdir(skillDir, { recursive: true });
|
|
265
531
|
outputPath = path.join(skillDir, adapter.skillFileName || 'SKILL.md');
|
|
266
532
|
displayName = `${dirName}/${adapter.skillFileName || 'SKILL.md'}`;
|
|
@@ -272,6 +538,11 @@ export async function buildAll({
|
|
|
272
538
|
|
|
273
539
|
await writeFile(outputPath, output, 'utf-8');
|
|
274
540
|
|
|
541
|
+
// Copy extra directories (references/, etc.) from skill source
|
|
542
|
+
if (adapter.useSkillDirectories && skillDir) {
|
|
543
|
+
await copySkillExtraDirs(skillSourceDir, skillDir);
|
|
544
|
+
}
|
|
545
|
+
|
|
275
546
|
// Copy scripts/ directory if present
|
|
276
547
|
if (hasScripts) {
|
|
277
548
|
const scriptsOutput = path.join(outputDir, adapter.scriptsDir(parsed.name));
|
|
@@ -279,6 +550,7 @@ export async function buildAll({
|
|
|
279
550
|
stats.scriptsCopied += copied.length;
|
|
280
551
|
}
|
|
281
552
|
|
|
553
|
+
parsedSkills.push(parsed);
|
|
282
554
|
stats.skillCount++;
|
|
283
555
|
stats.crossRefsResolved += parsed.crossRefs.length;
|
|
284
556
|
stats.toolRefsResolved += parsed.toolRefs.length;
|
|
@@ -327,6 +599,17 @@ export async function buildAll({
|
|
|
327
599
|
}
|
|
328
600
|
}
|
|
329
601
|
|
|
602
|
+
// For split packs: auto-discover skill files from skills/ subdir when manifest is empty
|
|
603
|
+
if (parsed.isSplit && parsed.skillManifest.length === 0) {
|
|
604
|
+
const skillsSubdir = path.join(packDir, 'skills');
|
|
605
|
+
if (existsSync(skillsSubdir)) {
|
|
606
|
+
const skillFiles = (await readdir(skillsSubdir)).filter((f) => f.endsWith('.md')).sort();
|
|
607
|
+
for (const sf of skillFiles) {
|
|
608
|
+
parsed.skillManifest.push({ name: sf.replace(/\.md$/, ''), file: `skills/${sf}` });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
330
613
|
// For split packs, load individual skill files and concatenate into body
|
|
331
614
|
if (parsed.isSplit && parsed.skillManifest.length > 0) {
|
|
332
615
|
const skillBodies = [];
|
|
@@ -351,6 +634,31 @@ export async function buildAll({
|
|
|
351
634
|
parsed.toolRefs = extractToolRefs(parsed.body);
|
|
352
635
|
}
|
|
353
636
|
|
|
637
|
+
// Discover and append workflow templates (Pro/Business packs)
|
|
638
|
+
const templates = await discoverTemplates(packDir);
|
|
639
|
+
if (templates.length > 0) {
|
|
640
|
+
const templateSection = [
|
|
641
|
+
'',
|
|
642
|
+
'## Workflow Templates',
|
|
643
|
+
'',
|
|
644
|
+
'Pre-built multi-phase workflows for `cook --template <name>`:',
|
|
645
|
+
'',
|
|
646
|
+
...templates.map(
|
|
647
|
+
(t) => `| \`${t.name}\` | ${t.parsed.description.slice(0, 100)} | chain: ${t.parsed.chain} |`,
|
|
648
|
+
),
|
|
649
|
+
'',
|
|
650
|
+
...templates.map((t) => {
|
|
651
|
+
const header = `### Template: ${t.name}`;
|
|
652
|
+
return `${header}\n\n${t.parsed.body}`;
|
|
653
|
+
}),
|
|
654
|
+
];
|
|
655
|
+
parsed.body = `${parsed.body}\n\n${templateSection.join('\n')}`;
|
|
656
|
+
// Re-extract refs after template injection
|
|
657
|
+
parsed.crossRefs = extractCrossRefs(parsed.body);
|
|
658
|
+
parsed.toolRefs = extractToolRefs(parsed.body);
|
|
659
|
+
stats.templateCount = (stats.templateCount || 0) + templates.length;
|
|
660
|
+
}
|
|
661
|
+
|
|
354
662
|
// Normalize pack name for headers (ext-trading instead of @rune/trading)
|
|
355
663
|
parsed.name = `ext-${packName}`;
|
|
356
664
|
|
|
@@ -387,6 +695,18 @@ export async function buildAll({
|
|
|
387
695
|
await writeFile(path.join(outputDir, indexFileName), indexContent, 'utf-8');
|
|
388
696
|
stats.files.push(indexFileName);
|
|
389
697
|
|
|
698
|
+
// Generate skill-index.json — compiled intent mesh for auto-trigger hooks
|
|
699
|
+
const skillIndex = generateSkillIndex(parsedSkills);
|
|
700
|
+
await writeFile(path.join(outputDir, 'skill-index.json'), `${JSON.stringify(skillIndex, null, 2)}\n`, 'utf-8');
|
|
701
|
+
stats.files.push('skill-index.json');
|
|
702
|
+
|
|
703
|
+
// Generate AGENTS.md for Codex (OpenAI convention — not used by other platforms)
|
|
704
|
+
if (adapter.name === 'codex') {
|
|
705
|
+
const agentsMdContent = generateAgentsMd(stats, adapter);
|
|
706
|
+
await writeFile(path.join(outputRoot, 'AGENTS.md'), agentsMdContent, 'utf-8');
|
|
707
|
+
stats.files.push('AGENTS.md');
|
|
708
|
+
}
|
|
709
|
+
|
|
390
710
|
// OpenClaw adapter: generate manifest + TypeScript entry point
|
|
391
711
|
if (adapter.name === 'openclaw' && adapter.generateManifest && adapter.generateEntryPoint) {
|
|
392
712
|
const pluginJsonPath = path.join(runeRoot, '.claude-plugin', 'plugin.json');
|
|
@@ -395,17 +715,6 @@ export async function buildAll({
|
|
|
395
715
|
pluginJson = JSON.parse(await readFile(pluginJsonPath, 'utf-8'));
|
|
396
716
|
}
|
|
397
717
|
|
|
398
|
-
// Collect parsed skills for manifest/entry generation
|
|
399
|
-
const parsedSkills = [];
|
|
400
|
-
for (const sp of skillPaths) {
|
|
401
|
-
try {
|
|
402
|
-
const c = await readFile(sp, 'utf-8');
|
|
403
|
-
parsedSkills.push(parseSkill(c, sp));
|
|
404
|
-
} catch {
|
|
405
|
-
/* skip on error */
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
718
|
// Read skill-router content for system prompt injection
|
|
410
719
|
const routerPath = path.join(runeRoot, 'skills', 'skill-router', 'SKILL.md');
|
|
411
720
|
let routerContent = '';
|
|
@@ -447,7 +756,7 @@ function generateIndex(stats, adapter) {
|
|
|
447
756
|
const lines = [
|
|
448
757
|
'# Rune Skill Index',
|
|
449
758
|
'',
|
|
450
|
-
`> Platform: ${adapter.name} | Skills: ${stats.skillCount} | Extensions: ${stats.packCount}`,
|
|
759
|
+
`> Platform: ${adapter.name} | Skills: ${stats.skillCount} | Extensions: ${stats.packCount}${stats.templateCount > 0 ? ` | Templates: ${stats.templateCount}` : ''}${stats.injectionsApplied > 0 ? ` | Injections: ${stats.injectionsApplied}` : ''}`,
|
|
451
760
|
'',
|
|
452
761
|
'## Core Skills',
|
|
453
762
|
'',
|
|
@@ -464,3 +773,167 @@ function generateIndex(stats, adapter) {
|
|
|
464
773
|
|
|
465
774
|
return lines.join('\n');
|
|
466
775
|
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Generate AGENTS.md for Codex (OpenAI convention)
|
|
779
|
+
* Uses dynamic counts from build stats — no hardcoded skill lists
|
|
780
|
+
*/
|
|
781
|
+
function generateAgentsMd(stats, adapter) {
|
|
782
|
+
const lines = [
|
|
783
|
+
'# Rune — Project Configuration',
|
|
784
|
+
'',
|
|
785
|
+
'## Overview',
|
|
786
|
+
'',
|
|
787
|
+
'Rune is an interconnected skill ecosystem for AI coding assistants.',
|
|
788
|
+
`${stats.skillCount} core skills | 5-layer mesh architecture | ${stats.crossRefsResolved} connections | Multi-platform.`,
|
|
789
|
+
'Philosophy: "Less skills. Deeper connections."',
|
|
790
|
+
'',
|
|
791
|
+
`Platform: ${adapter.name}`,
|
|
792
|
+
'',
|
|
793
|
+
'## Skills',
|
|
794
|
+
'',
|
|
795
|
+
`**${stats.skillCount} core skills** + **${stats.packCount} extension packs**`,
|
|
796
|
+
'',
|
|
797
|
+
'## Usage',
|
|
798
|
+
'',
|
|
799
|
+
'Reference skills using the `Skill` tool or delegate to subagents using the `Agent` tool.',
|
|
800
|
+
'',
|
|
801
|
+
'## Skills Directory',
|
|
802
|
+
'',
|
|
803
|
+
`Skills are located in: ${adapter.outputDir}/`,
|
|
804
|
+
'',
|
|
805
|
+
'---',
|
|
806
|
+
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
807
|
+
'',
|
|
808
|
+
];
|
|
809
|
+
|
|
810
|
+
return lines.join('\n');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Intent keyword patterns for each skill — extracted from description + Triggers section
|
|
815
|
+
* Maps common user intent words to the skill that handles them
|
|
816
|
+
*/
|
|
817
|
+
const INTENT_KEYWORDS = {
|
|
818
|
+
cook: ['implement', 'build', 'create', 'add', 'feature', 'fix', 'code', 'write', 'make', 'develop'],
|
|
819
|
+
team: ['parallel', 'split', 'multiple', 'large', 'many files', 'multi-module'],
|
|
820
|
+
launch: ['deploy', 'launch', 'release', 'ship', 'publish', 'production'],
|
|
821
|
+
rescue: ['legacy', 'refactor', 'modernize', 'rescue', 'clean up', 'old code', 'messy'],
|
|
822
|
+
scaffold: ['new project', 'bootstrap', 'scaffold', 'init', 'greenfield', 'starter'],
|
|
823
|
+
plan: ['plan', 'architect', 'design system', 'roadmap', 'strategy'],
|
|
824
|
+
brainstorm: ['brainstorm', 'explore', 'ideas', 'alternatives', 'approaches'],
|
|
825
|
+
debug: ['debug', 'error', 'bug', 'broken', 'trace', 'diagnose', 'crash', 'fail'],
|
|
826
|
+
fix: ['fix', 'patch', 'hotfix', 'resolve', 'repair'],
|
|
827
|
+
test: ['test', 'tdd', 'coverage', 'unit test', 'e2e', 'spec'],
|
|
828
|
+
review: ['review', 'code review', 'check quality', 'audit code'],
|
|
829
|
+
sentinel: ['security', 'vulnerability', 'owasp', 'secret', 'audit security'],
|
|
830
|
+
preflight: ['pre-commit', 'quality gate', 'check before'],
|
|
831
|
+
deploy: ['deploy', 'ci/cd', 'pipeline', 'kubernetes', 'docker'],
|
|
832
|
+
design: ['ui', 'ux', 'design', 'layout', 'component design', 'wireframe'],
|
|
833
|
+
perf: ['performance', 'slow', 'optimize', 'n+1', 'memory leak', 'bundle size'],
|
|
834
|
+
db: ['database', 'migration', 'schema', 'sql', 'query', 'index'],
|
|
835
|
+
audit: ['audit', 'health check', 'project assessment', 'codebase review'],
|
|
836
|
+
onboard: ['onboard', 'setup', 'configure project', 'get started'],
|
|
837
|
+
docs: ['document', 'readme', 'api docs', 'changelog'],
|
|
838
|
+
ba: ['requirements', 'business analysis', 'user stories', 'stakeholder'],
|
|
839
|
+
adversary: ['red team', 'challenge', 'stress test', 'edge case'],
|
|
840
|
+
incident: ['incident', 'outage', 'downtime', 'postmortem'],
|
|
841
|
+
surgeon: ['refactor', 'extract', 'strangler', 'decompose'],
|
|
842
|
+
'mcp-builder': ['mcp', 'mcp server', 'tool server', 'model context'],
|
|
843
|
+
'skill-forge': ['new skill', 'create skill', 'edit skill'],
|
|
844
|
+
'review-intake': ['pr feedback', 'review comments', 'received review'],
|
|
845
|
+
'logic-guardian': ['business logic', 'protect logic', 'critical path'],
|
|
846
|
+
marketing: ['marketing', 'landing page', 'seo', 'social media', 'copy'],
|
|
847
|
+
retro: ['retrospective', 'sprint review', 'velocity', 'team health'],
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Generate skill-index.json — compiled intent mesh for runtime auto-trigger
|
|
852
|
+
*
|
|
853
|
+
* Extracts from parsed skills: name, description, layer, model, group,
|
|
854
|
+
* cross-references (connections), and maps intent keywords to skill chains.
|
|
855
|
+
*
|
|
856
|
+
* @param {Array} parsedSkills - array of parsed skill objects
|
|
857
|
+
* @returns {object} skill index with graph + intents
|
|
858
|
+
*/
|
|
859
|
+
function generateSkillIndex(parsedSkills) {
|
|
860
|
+
// Build adjacency graph from cross-references
|
|
861
|
+
const graph = {};
|
|
862
|
+
const skills = {};
|
|
863
|
+
|
|
864
|
+
for (const skill of parsedSkills) {
|
|
865
|
+
const outbound = [...new Set(skill.crossRefs.map((r) => r.skillName))];
|
|
866
|
+
graph[skill.name] = outbound;
|
|
867
|
+
skills[skill.name] = {
|
|
868
|
+
layer: skill.layer,
|
|
869
|
+
model: skill.model,
|
|
870
|
+
group: skill.group,
|
|
871
|
+
description: skill.description.slice(0, 200),
|
|
872
|
+
connections: outbound,
|
|
873
|
+
...(skill.signals ? { signals: skill.signals } : {}),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Build signal graph — maps each signal to its emitters and listeners
|
|
878
|
+
const signalGraph = buildSignalGraph(parsedSkills);
|
|
879
|
+
|
|
880
|
+
// Build intent patterns from INTENT_KEYWORDS + skill descriptions
|
|
881
|
+
const intents = {};
|
|
882
|
+
for (const [skillName, keywords] of Object.entries(INTENT_KEYWORDS)) {
|
|
883
|
+
if (!skills[skillName]) continue;
|
|
884
|
+
const skill = skills[skillName];
|
|
885
|
+
|
|
886
|
+
// Build chain: primary skill + its direct connections (1-hop)
|
|
887
|
+
const chain = [skillName, ...graph[skillName].filter((c) => skills[c]).slice(0, 5)];
|
|
888
|
+
|
|
889
|
+
intents[skillName] = {
|
|
890
|
+
keywords,
|
|
891
|
+
layer: skill.layer,
|
|
892
|
+
model: skill.model,
|
|
893
|
+
chain,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
version: 2,
|
|
899
|
+
generated: new Date().toISOString(),
|
|
900
|
+
skillCount: parsedSkills.length,
|
|
901
|
+
skills,
|
|
902
|
+
graph,
|
|
903
|
+
signals: signalGraph,
|
|
904
|
+
intents,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Build signal graph from parsed skills' emit/listen declarations.
|
|
910
|
+
* Maps each signal name to its emitters and listeners.
|
|
911
|
+
*
|
|
912
|
+
* @param {Array} parsedSkills
|
|
913
|
+
* @returns {object} { "code.changed": { emitters: ["fix"], listeners: ["test", "review"] } }
|
|
914
|
+
*/
|
|
915
|
+
function buildSignalGraph(parsedSkills) {
|
|
916
|
+
const signals = {};
|
|
917
|
+
|
|
918
|
+
for (const skill of parsedSkills) {
|
|
919
|
+
if (!skill.signals) continue;
|
|
920
|
+
|
|
921
|
+
for (const signal of skill.signals.emit) {
|
|
922
|
+
if (!signals[signal]) signals[signal] = { emitters: [], listeners: [] };
|
|
923
|
+
signals[signal].emitters.push(skill.name);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
for (const signal of skill.signals.listen) {
|
|
927
|
+
if (!signals[signal]) signals[signal] = { emitters: [], listeners: [] };
|
|
928
|
+
signals[signal].listeners.push(skill.name);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Sort for deterministic output
|
|
933
|
+
for (const entry of Object.values(signals)) {
|
|
934
|
+
entry.emitters.sort();
|
|
935
|
+
entry.listeners.sort();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return signals;
|
|
939
|
+
}
|