@rune-kit/rune 2.2.2 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compiler/__tests__/adapters.test.js +109 -0
- package/compiler/__tests__/openclaw-adapter.test.js +149 -140
- package/compiler/__tests__/pack-split.test.js +145 -145
- package/compiler/__tests__/parser.test.js +3 -3
- package/compiler/__tests__/pipeline.test.js +110 -0
- package/compiler/__tests__/skill-validation.test.js +148 -0
- package/compiler/__tests__/transformer.test.js +70 -0
- package/compiler/__tests__/transforms.test.js +92 -0
- package/compiler/adapters/antigravity.js +5 -6
- package/compiler/adapters/claude.js +1 -1
- package/compiler/adapters/codex.js +69 -77
- package/compiler/adapters/cursor.js +5 -6
- package/compiler/adapters/generic.js +5 -6
- package/compiler/adapters/index.js +3 -3
- package/compiler/adapters/openclaw.js +146 -150
- package/compiler/adapters/opencode.js +78 -86
- package/compiler/adapters/windsurf.js +5 -6
- package/compiler/bin/rune.js +32 -23
- package/compiler/doctor.js +19 -7
- package/compiler/emitter.js +11 -18
- package/compiler/parser.js +5 -5
- package/compiler/transformer.js +3 -5
- package/compiler/transforms/branding.js +5 -4
- package/compiler/transforms/compliance.js +40 -40
- package/compiler/transforms/frontmatter.js +1 -1
- package/compiler/transforms/hooks.js +12 -14
- package/compiler/transforms/subagents.js +1 -3
- package/compiler/transforms/tool-names.js +1 -1
- package/docs/guides/index.html +48 -7
- package/docs/index.html +67 -2
- package/docs/style.css +18 -1
- package/extensions/security/PACK.md +4 -3
- package/extensions/security/skills/defense-in-depth.md +103 -0
- package/package.json +8 -1
- package/skills/completion-gate/SKILL.md +31 -1
- package/skills/cook/SKILL.md +20 -2
- package/skills/debug/SKILL.md +16 -1
- package/skills/hallucination-guard/SKILL.md +21 -6
- package/skills/plan/SKILL.md +90 -1
- package/skills/review/SKILL.md +1 -0
- package/skills/sentinel-env/SKILL.md +31 -1
- package/skills/skill-forge/SKILL.md +57 -1
- package/skills/team/SKILL.md +61 -15
- package/skills/test/SKILL.md +101 -10
- package/skills/verification/SKILL.md +29 -1
|
@@ -1,86 +1,78 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenCode Adapter
|
|
3
|
-
*
|
|
4
|
-
* Emits SKILL.md files into .opencode/skills/{name}/ directories.
|
|
5
|
-
* OpenCode uses the same SKILL.md frontmatter format (name, description)
|
|
6
|
-
* with markdown body — identical to Codex pattern.
|
|
7
|
-
*
|
|
8
|
-
* OpenCode project context: AGENTS.md (+ CLAUDE.md fallback)
|
|
9
|
-
* OpenCode skills dir: .opencode/skills/
|
|
10
|
-
* OpenCode skill format: .opencode/skills/{name}/SKILL.md
|
|
11
|
-
* OpenCode agents dir: .opencode/agents/
|
|
12
|
-
*
|
|
13
|
-
* OpenCode also searches:
|
|
14
|
-
* .claude/skills/{name}/SKILL.md (Claude-compatible)
|
|
15
|
-
* .agents/skills/{name}/SKILL.md (agent-compatible)
|
|
16
|
-
*
|
|
17
|
-
* @see https://opencode.ai/docs/skills/
|
|
18
|
-
* @see https://opencode.ai/docs/agents/
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const TOOL_MAP = {
|
|
22
|
-
Read: 'read the file',
|
|
23
|
-
Write: 'write/create the file',
|
|
24
|
-
Edit: 'edit the file',
|
|
25
|
-
Glob: 'find files by pattern',
|
|
26
|
-
Grep: 'search file contents',
|
|
27
|
-
Bash: 'run a shell command',
|
|
28
|
-
TodoWrite: 'track task progress',
|
|
29
|
-
Skill: 'invoke the named skill',
|
|
30
|
-
Agent: 'delegate to a subagent',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export default {
|
|
34
|
-
name: 'opencode',
|
|
35
|
-
outputDir: '.opencode/skills',
|
|
36
|
-
fileExtension: '.md',
|
|
37
|
-
skillPrefix: 'rune-',
|
|
38
|
-
skillSuffix: '',
|
|
39
|
-
|
|
40
|
-
// OpenCode uses directory-per-skill: .opencode/skills/{name}/SKILL.md
|
|
41
|
-
useSkillDirectories: true,
|
|
42
|
-
skillFileName: 'SKILL.md',
|
|
43
|
-
|
|
44
|
-
transformReference(skillName, raw) {
|
|
45
|
-
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
46
|
-
const ref = `the rune-${skillName} skill`;
|
|
47
|
-
return isBackticked ? `\`${ref}\`` : ref;
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
transformToolName(toolName) {
|
|
51
|
-
return TOOL_MAP[toolName] || toolName;
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
generateHeader(skill) {
|
|
55
|
-
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
56
|
-
return [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'',
|
|
62
|
-
'',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
postProcess(content) {
|
|
82
|
-
return content
|
|
83
|
-
.replace(/^context: fork\n/gm, '')
|
|
84
|
-
.replace(/^agent: general-purpose\n/gm, '');
|
|
85
|
-
},
|
|
86
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Adapter
|
|
3
|
+
*
|
|
4
|
+
* Emits SKILL.md files into .opencode/skills/{name}/ directories.
|
|
5
|
+
* OpenCode uses the same SKILL.md frontmatter format (name, description)
|
|
6
|
+
* with markdown body — identical to Codex pattern.
|
|
7
|
+
*
|
|
8
|
+
* OpenCode project context: AGENTS.md (+ CLAUDE.md fallback)
|
|
9
|
+
* OpenCode skills dir: .opencode/skills/
|
|
10
|
+
* OpenCode skill format: .opencode/skills/{name}/SKILL.md
|
|
11
|
+
* OpenCode agents dir: .opencode/agents/
|
|
12
|
+
*
|
|
13
|
+
* OpenCode also searches:
|
|
14
|
+
* .claude/skills/{name}/SKILL.md (Claude-compatible)
|
|
15
|
+
* .agents/skills/{name}/SKILL.md (agent-compatible)
|
|
16
|
+
*
|
|
17
|
+
* @see https://opencode.ai/docs/skills/
|
|
18
|
+
* @see https://opencode.ai/docs/agents/
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const TOOL_MAP = {
|
|
22
|
+
Read: 'read the file',
|
|
23
|
+
Write: 'write/create the file',
|
|
24
|
+
Edit: 'edit the file',
|
|
25
|
+
Glob: 'find files by pattern',
|
|
26
|
+
Grep: 'search file contents',
|
|
27
|
+
Bash: 'run a shell command',
|
|
28
|
+
TodoWrite: 'track task progress',
|
|
29
|
+
Skill: 'invoke the named skill',
|
|
30
|
+
Agent: 'delegate to a subagent',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
name: 'opencode',
|
|
35
|
+
outputDir: '.opencode/skills',
|
|
36
|
+
fileExtension: '.md',
|
|
37
|
+
skillPrefix: 'rune-',
|
|
38
|
+
skillSuffix: '',
|
|
39
|
+
|
|
40
|
+
// OpenCode uses directory-per-skill: .opencode/skills/{name}/SKILL.md
|
|
41
|
+
useSkillDirectories: true,
|
|
42
|
+
skillFileName: 'SKILL.md',
|
|
43
|
+
|
|
44
|
+
transformReference(skillName, raw) {
|
|
45
|
+
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
46
|
+
const ref = `the rune-${skillName} skill`;
|
|
47
|
+
return isBackticked ? `\`${ref}\`` : ref;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
transformToolName(toolName) {
|
|
51
|
+
return TOOL_MAP[toolName] || toolName;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
generateHeader(skill) {
|
|
55
|
+
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
56
|
+
return ['---', `name: rune-${skill.name}`, `description: "${desc}"`, '---', '', ''].join('\n');
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
generateFooter() {
|
|
60
|
+
return [
|
|
61
|
+
'',
|
|
62
|
+
'---',
|
|
63
|
+
'> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
|
|
64
|
+
'> Source: https://github.com/rune-kit/rune (MIT)',
|
|
65
|
+
'> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
|
|
66
|
+
'> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
|
|
67
|
+
].join('\n');
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
transformSubagentInstruction(text) {
|
|
71
|
+
// OpenCode has native subagent support — preserve parallel agent instructions
|
|
72
|
+
return text;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
postProcess(content) {
|
|
76
|
+
return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -42,9 +42,10 @@ export default {
|
|
|
42
42
|
return [
|
|
43
43
|
'',
|
|
44
44
|
'---',
|
|
45
|
-
'> **Rune Skill Mesh** — 58 skills, 200+ connections',
|
|
46
|
-
'> Source: https://github.com/rune-kit/rune',
|
|
47
|
-
'>
|
|
45
|
+
'> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
|
|
46
|
+
'> Source: https://github.com/rune-kit/rune (MIT)',
|
|
47
|
+
'> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
|
|
48
|
+
'> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
|
|
48
49
|
].join('\n');
|
|
49
50
|
},
|
|
50
51
|
|
|
@@ -53,8 +54,6 @@ export default {
|
|
|
53
54
|
},
|
|
54
55
|
|
|
55
56
|
postProcess(content) {
|
|
56
|
-
return content
|
|
57
|
-
.replace(/^context: fork\n/gm, '')
|
|
58
|
-
.replace(/^agent: general-purpose\n/gm, '');
|
|
57
|
+
return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
|
|
59
58
|
},
|
|
60
59
|
};
|
package/compiler/bin/rune.js
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* rune doctor — Validate compiled output
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
13
12
|
import { existsSync } from 'node:fs';
|
|
13
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { fileURLToPath } from 'node:url';
|
|
16
15
|
import { createInterface } from 'node:readline';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { getAdapter, listPlatforms } from '../adapters/index.js';
|
|
18
|
+
import { formatDoctorResults, runDoctor } from '../doctor.js';
|
|
18
19
|
import { buildAll } from '../emitter.js';
|
|
19
|
-
import { runDoctor, formatDoctorResults } from '../doctor.js';
|
|
20
20
|
|
|
21
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
22
|
const __dirname = path.dirname(__filename);
|
|
@@ -26,8 +26,12 @@ const CONFIG_FILE = 'rune.config.json';
|
|
|
26
26
|
|
|
27
27
|
// ─── Helpers ───
|
|
28
28
|
|
|
29
|
-
function log(msg) {
|
|
30
|
-
|
|
29
|
+
function log(msg) {
|
|
30
|
+
console.log(msg);
|
|
31
|
+
}
|
|
32
|
+
function logStep(icon, msg) {
|
|
33
|
+
console.log(` ${icon} ${msg}`);
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
async function readConfig(projectRoot) {
|
|
33
37
|
const configPath = path.join(projectRoot, CONFIG_FILE);
|
|
@@ -37,7 +41,7 @@ async function readConfig(projectRoot) {
|
|
|
37
41
|
|
|
38
42
|
async function writeConfig(projectRoot, config) {
|
|
39
43
|
const configPath = path.join(projectRoot, CONFIG_FILE);
|
|
40
|
-
await writeFile(configPath, JSON.stringify(config, null, 2)
|
|
44
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function detectPlatform(projectRoot) {
|
|
@@ -51,11 +55,10 @@ function detectPlatform(projectRoot) {
|
|
|
51
55
|
return null;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
55
58
|
async function prompt(question) {
|
|
56
59
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
57
|
-
return new Promise(resolve => {
|
|
58
|
-
rl.question(question, answer => {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
rl.question(question, (answer) => {
|
|
59
62
|
rl.close();
|
|
60
63
|
resolve(answer.trim());
|
|
61
64
|
});
|
|
@@ -77,7 +80,7 @@ async function cmdInit(projectRoot, args) {
|
|
|
77
80
|
if (platform) {
|
|
78
81
|
logStep('→', `Detected: ${platform}`);
|
|
79
82
|
} else {
|
|
80
|
-
log(
|
|
83
|
+
log(` Available platforms: ${listPlatforms().join(', ')}`);
|
|
81
84
|
const answer = await prompt(' ? Select platform: ');
|
|
82
85
|
platform = answer.toLowerCase();
|
|
83
86
|
if (!listPlatforms().includes(platform)) {
|
|
@@ -93,9 +96,7 @@ async function cmdInit(projectRoot, args) {
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
// Extension pack selection
|
|
96
|
-
const extensions = args.extensions
|
|
97
|
-
? args.extensions.split(',')
|
|
98
|
-
: null; // null = all
|
|
99
|
+
const extensions = args.extensions ? args.extensions.split(',') : null; // null = all
|
|
99
100
|
|
|
100
101
|
// Build config
|
|
101
102
|
const config = {
|
|
@@ -158,9 +159,7 @@ async function cmdBuild(projectRoot, args) {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
const adapter = getAdapter(platform);
|
|
161
|
-
const runeRoot =
|
|
162
|
-
? RUNE_ROOT
|
|
163
|
-
: (config?.source || RUNE_ROOT);
|
|
162
|
+
const runeRoot = config?.source === '@rune-kit/rune' ? RUNE_ROOT : config?.source || RUNE_ROOT;
|
|
164
163
|
const outputRoot = typeof args.output === 'string' ? args.output : projectRoot;
|
|
165
164
|
const disabledSkills = config?.skills?.disabled || [];
|
|
166
165
|
const enabledPacks = config?.extensions?.enabled || null;
|
|
@@ -200,15 +199,23 @@ async function cmdDoctor(projectRoot, args) {
|
|
|
200
199
|
const config = await readConfig(projectRoot);
|
|
201
200
|
|
|
202
201
|
if (!config) {
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
// No config = CI or fresh clone. Run source-only checks (split packs).
|
|
203
|
+
log('');
|
|
204
|
+
log(' ℹ No rune.config.json found — running source-only checks.');
|
|
205
|
+
const results = await runDoctor({
|
|
206
|
+
outputRoot: projectRoot,
|
|
207
|
+
adapter: getAdapter('claude'),
|
|
208
|
+
config: {},
|
|
209
|
+
runeRoot: RUNE_ROOT,
|
|
210
|
+
});
|
|
211
|
+
log(formatDoctorResults(results));
|
|
212
|
+
if (!results.healthy) process.exit(1);
|
|
213
|
+
return;
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
const platform = args.platform || config.platform;
|
|
208
217
|
const adapter = getAdapter(platform);
|
|
209
|
-
const runeRoot =
|
|
210
|
-
? RUNE_ROOT
|
|
211
|
-
: (config.source || RUNE_ROOT);
|
|
218
|
+
const runeRoot = config.source === '@rune-kit/rune' ? RUNE_ROOT : config.source || RUNE_ROOT;
|
|
212
219
|
|
|
213
220
|
const results = await runDoctor({
|
|
214
221
|
outputRoot: projectRoot,
|
|
@@ -295,7 +302,9 @@ async function main() {
|
|
|
295
302
|
log(' doctor Validate compiled output');
|
|
296
303
|
log('');
|
|
297
304
|
log(' Options:');
|
|
298
|
-
log(
|
|
305
|
+
log(
|
|
306
|
+
' --platform <name> Override platform (cursor, windsurf, antigravity, codex, openclaw, opencode, generic)',
|
|
307
|
+
);
|
|
299
308
|
log(' --output <dir> Override output directory');
|
|
300
309
|
log(' --disable <skills> Comma-separated skills to disable');
|
|
301
310
|
log(' --version, -v Show version');
|
|
@@ -307,7 +316,7 @@ async function main() {
|
|
|
307
316
|
}
|
|
308
317
|
}
|
|
309
318
|
|
|
310
|
-
main().catch(err => {
|
|
319
|
+
main().catch((err) => {
|
|
311
320
|
console.error(' ✗ Fatal:', err.message);
|
|
312
321
|
process.exit(1);
|
|
313
322
|
});
|
package/compiler/doctor.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Checks: files exist, cross-references resolve, layer discipline, source freshness.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
8
7
|
import { existsSync } from 'node:fs';
|
|
8
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { parsePack } from './parser.js';
|
|
11
11
|
|
|
@@ -28,14 +28,18 @@ export async function runDoctor({ outputRoot, adapter, config, runeRoot }) {
|
|
|
28
28
|
healthy: true,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
// Check 1: Config exists
|
|
31
|
+
// Check 1: Config exists (skip in CI / source-only mode)
|
|
32
32
|
const configPath = path.join(outputRoot, 'rune.config.json');
|
|
33
33
|
if (existsSync(configPath)) {
|
|
34
34
|
results.checks.push({ name: 'Config file', status: 'pass' });
|
|
35
|
-
} else {
|
|
35
|
+
} else if (config && Object.keys(config).length > 0) {
|
|
36
|
+
// Config was passed but file doesn't exist on disk — real problem
|
|
36
37
|
results.checks.push({ name: 'Config file', status: 'fail', detail: 'rune.config.json not found' });
|
|
37
38
|
results.errors.push('rune.config.json not found. Run `rune init` first.');
|
|
38
39
|
results.healthy = false;
|
|
40
|
+
} else {
|
|
41
|
+
// No config at all (CI / fresh clone) — skip gracefully
|
|
42
|
+
results.checks.push({ name: 'Config file', status: 'skip', detail: 'No config — source-only mode' });
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
// Check 2: Output directory exists
|
|
@@ -56,13 +60,17 @@ export async function runDoctor({ outputRoot, adapter, config, runeRoot }) {
|
|
|
56
60
|
|
|
57
61
|
// Check 3: Count skill files
|
|
58
62
|
const files = await readdir(outputDir);
|
|
59
|
-
const skillFiles = files.filter(f => f.startsWith('rune-') && f !== `rune-index${adapter.fileExtension}`);
|
|
63
|
+
const skillFiles = files.filter((f) => f.startsWith('rune-') && f !== `rune-index${adapter.fileExtension}`);
|
|
60
64
|
const expectedSkillCount = 55 - (config.skills?.disabled?.length || 0);
|
|
61
65
|
|
|
62
66
|
if (skillFiles.length >= expectedSkillCount) {
|
|
63
67
|
results.checks.push({ name: 'Skill files', status: 'pass', detail: `${skillFiles.length}/${expectedSkillCount}` });
|
|
64
68
|
} else {
|
|
65
|
-
results.checks.push({
|
|
69
|
+
results.checks.push({
|
|
70
|
+
name: 'Skill files',
|
|
71
|
+
status: 'warn',
|
|
72
|
+
detail: `${skillFiles.length}/${expectedSkillCount} present`,
|
|
73
|
+
});
|
|
66
74
|
results.warnings.push(`Expected ${expectedSkillCount} skill files, found ${skillFiles.length}`);
|
|
67
75
|
}
|
|
68
76
|
|
|
@@ -97,7 +105,11 @@ export async function runDoctor({ outputRoot, adapter, config, runeRoot }) {
|
|
|
97
105
|
if (splitPackErrors.length === 0) {
|
|
98
106
|
results.checks.push({ name: 'Split packs', status: 'pass' });
|
|
99
107
|
} else {
|
|
100
|
-
results.checks.push({
|
|
108
|
+
results.checks.push({
|
|
109
|
+
name: 'Split packs',
|
|
110
|
+
status: 'fail',
|
|
111
|
+
detail: `${splitPackErrors.length} missing skill files`,
|
|
112
|
+
});
|
|
101
113
|
results.errors.push(...splitPackErrors);
|
|
102
114
|
}
|
|
103
115
|
}
|
|
@@ -169,7 +181,7 @@ export function formatDoctorResults(results) {
|
|
|
169
181
|
lines.push(`\n Platform: ${results.platform}`);
|
|
170
182
|
|
|
171
183
|
for (const check of results.checks) {
|
|
172
|
-
const icon = check.status === 'pass' ? '✓' : check.status === 'warn' ? '!' : '✗';
|
|
184
|
+
const icon = check.status === 'pass' ? '✓' : check.status === 'warn' ? '!' : check.status === 'skip' ? '–' : '✗';
|
|
173
185
|
const detail = check.detail ? ` (${check.detail})` : '';
|
|
174
186
|
lines.push(` [${icon}] ${check.name}${detail}`);
|
|
175
187
|
}
|
package/compiler/emitter.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Handles file naming, directory creation, and index generation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readdir, readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
9
8
|
import { existsSync } from 'node:fs';
|
|
9
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { extractCrossRefs, extractToolRefs, parsePack, parseSkill } from './parser.js';
|
|
12
12
|
import { transformSkill } from './transformer.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -176,7 +176,7 @@ export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills =
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
// Concatenate: index body + all skill bodies
|
|
179
|
-
parsed.body = parsed.body
|
|
179
|
+
parsed.body = `${parsed.body}\n\n${skillBodies.join('\n\n---\n\n')}`;
|
|
180
180
|
// Re-extract refs from the full concatenated body
|
|
181
181
|
parsed.crossRefs = extractCrossRefs(parsed.body);
|
|
182
182
|
parsed.toolRefs = extractToolRefs(parsed.body);
|
|
@@ -232,7 +232,9 @@ export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills =
|
|
|
232
232
|
try {
|
|
233
233
|
const c = await readFile(sp, 'utf-8');
|
|
234
234
|
parsedSkills.push(parseSkill(c, sp));
|
|
235
|
-
} catch {
|
|
235
|
+
} catch {
|
|
236
|
+
/* skip on error */
|
|
237
|
+
}
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
// Read skill-router content for system prompt injection
|
|
@@ -245,11 +247,7 @@ export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills =
|
|
|
245
247
|
// Write openclaw.plugin.json to parent of skills dir (.openclaw/rune/)
|
|
246
248
|
const openclawRoot = path.resolve(outputDir, '..');
|
|
247
249
|
const manifest = adapter.generateManifest(parsedSkills, pluginJson);
|
|
248
|
-
await writeFile(
|
|
249
|
-
path.join(openclawRoot, 'openclaw.plugin.json'),
|
|
250
|
-
JSON.stringify(manifest, null, 2) + '\n',
|
|
251
|
-
'utf-8',
|
|
252
|
-
);
|
|
250
|
+
await writeFile(path.join(openclawRoot, 'openclaw.plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
253
251
|
stats.files.push('openclaw.plugin.json');
|
|
254
252
|
|
|
255
253
|
// Write src/index.ts entry point
|
|
@@ -274,21 +272,16 @@ function generateIndex(stats, adapter) {
|
|
|
274
272
|
'',
|
|
275
273
|
'## Core Skills',
|
|
276
274
|
'',
|
|
277
|
-
...stats.files
|
|
278
|
-
.filter(f => !f.match(/[-/]ext-/) && !f.includes('index'))
|
|
279
|
-
.map(f => `- ${f}`),
|
|
275
|
+
...stats.files.filter((f) => !f.match(/[-/]ext-/) && !f.includes('index')).map((f) => `- ${f}`),
|
|
280
276
|
'',
|
|
281
277
|
];
|
|
282
278
|
|
|
283
|
-
const extFiles = stats.files.filter(f => f.match(/[-/]ext-/));
|
|
279
|
+
const extFiles = stats.files.filter((f) => f.match(/[-/]ext-/));
|
|
284
280
|
if (extFiles.length > 0) {
|
|
285
|
-
lines.push('## Extension Packs', '', ...extFiles.map(f => `- ${f}`), '');
|
|
281
|
+
lines.push('## Extension Packs', '', ...extFiles.map((f) => `- ${f}`), '');
|
|
286
282
|
}
|
|
287
283
|
|
|
288
|
-
lines.push(
|
|
289
|
-
'---',
|
|
290
|
-
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
291
|
-
);
|
|
284
|
+
lines.push('---', '> Rune Skill Mesh — https://github.com/rune-kit/rune');
|
|
292
285
|
|
|
293
286
|
return lines.join('\n');
|
|
294
287
|
}
|
package/compiler/parser.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const CROSS_REF_PATTERN = /`?rune:([a-z][\w-]*)`?/g;
|
|
9
9
|
const TOOL_REF_PATTERN = /`(Read|Write|Edit|Glob|Grep|Bash|TodoWrite|Skill|Agent)`/g;
|
|
10
10
|
const HARD_GATE_PATTERN = /<HARD-GATE>([\s\S]*?)<\/HARD-GATE>/g;
|
|
11
|
-
const
|
|
11
|
+
const _SECTION_PATTERN = /^## (.+)$/gm;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Parse YAML-like frontmatter from SKILL.md
|
|
@@ -26,7 +26,7 @@ function parseFrontmatter(content) {
|
|
|
26
26
|
|
|
27
27
|
let currentIndent = null;
|
|
28
28
|
let nestedKey = null;
|
|
29
|
-
const
|
|
29
|
+
const _nestedObj = {};
|
|
30
30
|
|
|
31
31
|
for (const line of raw.split('\n')) {
|
|
32
32
|
const trimmed = line.trim();
|
|
@@ -43,7 +43,7 @@ function parseFrontmatter(content) {
|
|
|
43
43
|
if (currentIndent === 'nested' && line.startsWith(' ')) {
|
|
44
44
|
const kvMatch = trimmed.match(/^(\w+):\s*(.+)$/);
|
|
45
45
|
if (kvMatch) {
|
|
46
|
-
|
|
46
|
+
const value = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
47
47
|
frontmatter[nestedKey][kvMatch[1]] = value;
|
|
48
48
|
}
|
|
49
49
|
continue;
|
|
@@ -54,7 +54,7 @@ function parseFrontmatter(content) {
|
|
|
54
54
|
nestedKey = null;
|
|
55
55
|
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
56
56
|
if (kvMatch) {
|
|
57
|
-
|
|
57
|
+
const value = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
58
58
|
frontmatter[kvMatch[1]] = value;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -226,7 +226,7 @@ export function parsePack(content, filePath = '') {
|
|
|
226
226
|
function parseSkillManifest(skills) {
|
|
227
227
|
if (!Array.isArray(skills)) return [];
|
|
228
228
|
|
|
229
|
-
return skills.map(skill => {
|
|
229
|
+
return skills.map((skill) => {
|
|
230
230
|
// Handle both string format ("skill-name") and object format ({name, file, model, description})
|
|
231
231
|
if (typeof skill === 'string') {
|
|
232
232
|
return {
|
package/compiler/transformer.js
CHANGED
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
* Pipeline: frontmatter → cross-refs → tool-names → subagents → compliance → hooks → branding
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { transformCompliance } from './transforms/compliance.js';
|
|
8
9
|
import { transformCrossReferences } from './transforms/cross-references.js';
|
|
9
|
-
import { transformToolNames } from './transforms/tool-names.js';
|
|
10
|
-
import { transformFrontmatter } from './transforms/frontmatter.js';
|
|
11
|
-
import { transformSubagents } from './transforms/subagents.js';
|
|
12
10
|
import { generateHookConstraints } from './transforms/hooks.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { transformSubagents } from './transforms/subagents.js';
|
|
12
|
+
import { transformToolNames } from './transforms/tool-names.js';
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* Run the full transform pipeline on a parsed skill
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
const DEFAULT_FOOTER = [
|
|
8
8
|
'',
|
|
9
9
|
'---',
|
|
10
|
-
'> **Rune Skill Mesh** — 58 skills, 200+ connections',
|
|
11
|
-
'> Source: https://github.com/rune-kit/rune',
|
|
12
|
-
'>
|
|
10
|
+
'> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
|
|
11
|
+
'> Source: https://github.com/rune-kit/rune (MIT)',
|
|
12
|
+
'> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
|
|
13
|
+
'> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
|
|
13
14
|
].join('\n');
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -23,5 +24,5 @@ export function addBranding(body, adapter) {
|
|
|
23
24
|
if (adapter.name === 'claude') return body;
|
|
24
25
|
|
|
25
26
|
const footer = adapter.generateFooter ? adapter.generateFooter() : DEFAULT_FOOTER;
|
|
26
|
-
return body
|
|
27
|
+
return `${body}\n${footer}`;
|
|
27
28
|
}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compliance Transform
|
|
3
|
-
*
|
|
4
|
-
* Injects a compliance preamble into every compiled skill file.
|
|
5
|
-
* This distributes enforcement to ALL platforms (Cursor, Windsurf, etc.)
|
|
6
|
-
* so the AI is reminded of routing discipline even if skill-router
|
|
7
|
-
* itself is not loaded in context.
|
|
8
|
-
*
|
|
9
|
-
* Only applies to non-Claude platforms (Claude has native plugin enforcement).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const COMPLIANCE_PREAMBLE = [
|
|
13
|
-
'',
|
|
14
|
-
'> **RUNE COMPLIANCE**: Before ANY code response, you MUST:',
|
|
15
|
-
'> 1. Classify this request (CODE_CHANGE | QUESTION | DEBUG | REVIEW | EXPLORE)',
|
|
16
|
-
'> 2. Route through the correct Rune skill (see skill-router routing table)',
|
|
17
|
-
"> 3. Follow the skill's workflow — do NOT freelance or skip steps",
|
|
18
|
-
'> Violation: writing code without skill routing = incorrect behavior.',
|
|
19
|
-
'',
|
|
20
|
-
].join('\n');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Inject compliance preamble after the first heading in a skill body.
|
|
24
|
-
*
|
|
25
|
-
* @param {string} body - skill body text
|
|
26
|
-
* @param {object} adapter - platform adapter
|
|
27
|
-
* @returns {string} body with compliance preamble injected
|
|
28
|
-
*/
|
|
29
|
-
export function transformCompliance(body, adapter) {
|
|
30
|
-
if (adapter.name === 'claude') return body;
|
|
31
|
-
|
|
32
|
-
// Insert after first heading (# skill-name), before first ## section
|
|
33
|
-
const firstH2 = body.indexOf('\n## ');
|
|
34
|
-
if (firstH2 !== -1) {
|
|
35
|
-
return body.slice(0, firstH2) + COMPLIANCE_PREAMBLE + body.slice(firstH2);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Fallback: append at end if no ## sections found
|
|
39
|
-
return body + COMPLIANCE_PREAMBLE;
|
|
40
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Compliance Transform
|
|
3
|
+
*
|
|
4
|
+
* Injects a compliance preamble into every compiled skill file.
|
|
5
|
+
* This distributes enforcement to ALL platforms (Cursor, Windsurf, etc.)
|
|
6
|
+
* so the AI is reminded of routing discipline even if skill-router
|
|
7
|
+
* itself is not loaded in context.
|
|
8
|
+
*
|
|
9
|
+
* Only applies to non-Claude platforms (Claude has native plugin enforcement).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const COMPLIANCE_PREAMBLE = [
|
|
13
|
+
'',
|
|
14
|
+
'> **RUNE COMPLIANCE**: Before ANY code response, you MUST:',
|
|
15
|
+
'> 1. Classify this request (CODE_CHANGE | QUESTION | DEBUG | REVIEW | EXPLORE)',
|
|
16
|
+
'> 2. Route through the correct Rune skill (see skill-router routing table)',
|
|
17
|
+
"> 3. Follow the skill's workflow — do NOT freelance or skip steps",
|
|
18
|
+
'> Violation: writing code without skill routing = incorrect behavior.',
|
|
19
|
+
'',
|
|
20
|
+
].join('\n');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Inject compliance preamble after the first heading in a skill body.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} body - skill body text
|
|
26
|
+
* @param {object} adapter - platform adapter
|
|
27
|
+
* @returns {string} body with compliance preamble injected
|
|
28
|
+
*/
|
|
29
|
+
export function transformCompliance(body, adapter) {
|
|
30
|
+
if (adapter.name === 'claude') return body;
|
|
31
|
+
|
|
32
|
+
// Insert after first heading (# skill-name), before first ## section
|
|
33
|
+
const firstH2 = body.indexOf('\n## ');
|
|
34
|
+
if (firstH2 !== -1) {
|
|
35
|
+
return body.slice(0, firstH2) + COMPLIANCE_PREAMBLE + body.slice(firstH2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback: append at end if no ## sections found
|
|
39
|
+
return body + COMPLIANCE_PREAMBLE;
|
|
40
|
+
}
|
|
@@ -26,7 +26,7 @@ export function transformFrontmatter(content, adapter) {
|
|
|
26
26
|
// Remove Claude Code-specific directives
|
|
27
27
|
const cleaned = frontmatterBlock
|
|
28
28
|
.split('\n')
|
|
29
|
-
.filter(line => {
|
|
29
|
+
.filter((line) => {
|
|
30
30
|
const trimmed = line.trim();
|
|
31
31
|
if (trimmed.startsWith('context:')) return false;
|
|
32
32
|
if (trimmed.startsWith('agent:')) return false;
|