@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/parser.js
CHANGED
|
@@ -1,247 +1,267 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SKILL.md Parser
|
|
3
|
-
*
|
|
4
|
-
* Parses SKILL.md files into a structured intermediate representation (IR).
|
|
5
|
-
* Extracts frontmatter, cross-references, tool references, HARD-GATE blocks, and sections.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SKILL.md Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses SKILL.md files into a structured intermediate representation (IR).
|
|
5
|
+
* Extracts frontmatter, cross-references, tool references, HARD-GATE blocks, and sections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Metadata fields that should be parsed as comma-separated arrays
|
|
10
|
+
* e.g. emit: code.changed, tests.passed → ["code.changed", "tests.passed"]
|
|
11
|
+
*/
|
|
12
|
+
const COMMA_LIST_FIELDS = new Set(['emit', 'listen']);
|
|
13
|
+
|
|
14
|
+
const CROSS_REF_PATTERN = /`?rune:([a-z][\w-]*)`?/g;
|
|
15
|
+
const TOOL_REF_PATTERN = /`(Read|Write|Edit|Glob|Grep|Bash|TodoWrite|Skill|Agent)`/g;
|
|
16
|
+
const HARD_GATE_PATTERN = /<HARD-GATE>([\s\S]*?)<\/HARD-GATE>/g;
|
|
17
|
+
const _SECTION_PATTERN = /^## (.+)$/gm;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse YAML-like frontmatter from SKILL.md
|
|
21
|
+
* Handles nested metadata block
|
|
22
|
+
*/
|
|
23
|
+
function parseFrontmatter(content) {
|
|
24
|
+
// Normalize line endings to \n for cross-platform compatibility
|
|
25
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
26
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---/);
|
|
27
|
+
if (!match) return { frontmatter: {}, body: normalized };
|
|
28
|
+
|
|
29
|
+
const raw = match[1];
|
|
30
|
+
const body = normalized.slice(match[0].length).trim();
|
|
31
|
+
const frontmatter = {};
|
|
32
|
+
|
|
33
|
+
let currentIndent = null;
|
|
34
|
+
let nestedKey = null;
|
|
35
|
+
const _nestedObj = {};
|
|
36
|
+
|
|
37
|
+
for (const line of raw.split('\n')) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed) continue;
|
|
40
|
+
|
|
41
|
+
// Detect nested block (e.g., metadata:)
|
|
42
|
+
if (/^\w+:\s*$/.test(trimmed)) {
|
|
43
|
+
nestedKey = trimmed.replace(':', '').trim();
|
|
44
|
+
frontmatter[nestedKey] = {};
|
|
45
|
+
currentIndent = 'nested';
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (currentIndent === 'nested' && line.startsWith(' ')) {
|
|
50
|
+
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
51
|
+
if (kvMatch) {
|
|
52
|
+
const rawValue = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
53
|
+
// Comma-separated list fields → parse as array
|
|
54
|
+
if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
|
|
55
|
+
frontmatter[nestedKey][kvMatch[1]] = rawValue
|
|
56
|
+
.split(',')
|
|
57
|
+
.map((s) => s.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
} else {
|
|
60
|
+
frontmatter[nestedKey][kvMatch[1]] = rawValue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Top-level key-value
|
|
67
|
+
currentIndent = null;
|
|
68
|
+
nestedKey = null;
|
|
69
|
+
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
70
|
+
if (kvMatch) {
|
|
71
|
+
const value = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
72
|
+
frontmatter[kvMatch[1]] = value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { frontmatter, body };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract all rune:<name> cross-references from body
|
|
81
|
+
*/
|
|
82
|
+
export function extractCrossRefs(body) {
|
|
83
|
+
const refs = [];
|
|
84
|
+
const lines = body.split('\n');
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
const line = lines[i];
|
|
88
|
+
let match;
|
|
89
|
+
const regex = new RegExp(CROSS_REF_PATTERN.source, 'g');
|
|
90
|
+
|
|
91
|
+
while ((match = regex.exec(line)) !== null) {
|
|
92
|
+
refs.push({
|
|
93
|
+
raw: match[0],
|
|
94
|
+
skillName: match[1],
|
|
95
|
+
line: i + 1,
|
|
96
|
+
context: line.trim(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return refs;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract all Claude Code tool references from body
|
|
106
|
+
*/
|
|
107
|
+
export function extractToolRefs(body) {
|
|
108
|
+
const refs = [];
|
|
109
|
+
const lines = body.split('\n');
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
const line = lines[i];
|
|
113
|
+
let match;
|
|
114
|
+
const regex = new RegExp(TOOL_REF_PATTERN.source, 'g');
|
|
115
|
+
|
|
116
|
+
while ((match = regex.exec(line)) !== null) {
|
|
117
|
+
refs.push({
|
|
118
|
+
raw: match[0],
|
|
119
|
+
toolName: match[1],
|
|
120
|
+
line: i + 1,
|
|
121
|
+
context: line.trim(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return refs;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract HARD-GATE blocks
|
|
131
|
+
*/
|
|
132
|
+
function extractHardGates(body) {
|
|
133
|
+
const gates = [];
|
|
134
|
+
let match;
|
|
135
|
+
const regex = new RegExp(HARD_GATE_PATTERN.source, 'gs');
|
|
136
|
+
|
|
137
|
+
while ((match = regex.exec(body)) !== null) {
|
|
138
|
+
gates.push(match[1].trim());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return gates;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Extract ## sections as a Map<sectionName, content>
|
|
146
|
+
*/
|
|
147
|
+
function extractSections(body) {
|
|
148
|
+
const sections = new Map();
|
|
149
|
+
const lines = body.split('\n');
|
|
150
|
+
let currentSection = null;
|
|
151
|
+
let currentContent = [];
|
|
152
|
+
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const sectionMatch = line.match(/^## (.+)$/);
|
|
155
|
+
if (sectionMatch) {
|
|
156
|
+
if (currentSection) {
|
|
157
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
158
|
+
}
|
|
159
|
+
currentSection = sectionMatch[1];
|
|
160
|
+
currentContent = [];
|
|
161
|
+
} else if (currentSection) {
|
|
162
|
+
currentContent.push(line);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (currentSection) {
|
|
167
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return sections;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse a SKILL.md file content into structured IR
|
|
175
|
+
*
|
|
176
|
+
* @param {string} content - raw SKILL.md content
|
|
177
|
+
* @param {string} [filePath] - optional file path for error reporting
|
|
178
|
+
* @returns {ParsedSkill}
|
|
179
|
+
*/
|
|
180
|
+
export function parseSkill(content, filePath = '') {
|
|
181
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
182
|
+
|
|
183
|
+
const metadata = frontmatter.metadata || {};
|
|
184
|
+
|
|
185
|
+
// Extract signals — emit/listen arrays from metadata
|
|
186
|
+
const emit = Array.isArray(metadata.emit) ? metadata.emit : [];
|
|
187
|
+
const listen = Array.isArray(metadata.listen) ? metadata.listen : [];
|
|
188
|
+
const signals = emit.length > 0 || listen.length > 0 ? { emit, listen } : null;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
name: frontmatter.name || '',
|
|
192
|
+
description: frontmatter.description || '',
|
|
193
|
+
layer: metadata.layer || 'L2',
|
|
194
|
+
model: metadata.model || 'sonnet',
|
|
195
|
+
group: metadata.group || 'general',
|
|
196
|
+
signals,
|
|
197
|
+
contextFork: frontmatter.context === 'fork',
|
|
198
|
+
agentType: frontmatter.agent || null,
|
|
199
|
+
body,
|
|
200
|
+
crossRefs: extractCrossRefs(body),
|
|
201
|
+
toolRefs: extractToolRefs(body),
|
|
202
|
+
hardGates: extractHardGates(body),
|
|
203
|
+
sections: extractSections(body),
|
|
204
|
+
filePath,
|
|
205
|
+
frontmatter,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Parse a PACK.md extension file (same format, slightly different metadata)
|
|
211
|
+
* Supports both monolith format (all skills inline) and split format (skills in separate files)
|
|
212
|
+
*
|
|
213
|
+
* Split format detection: metadata.format === "split" OR metadata.skills array present
|
|
214
|
+
*/
|
|
215
|
+
export function parsePack(content, filePath = '') {
|
|
216
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
217
|
+
const metadata = frontmatter.metadata || {};
|
|
218
|
+
|
|
219
|
+
// Detect split format
|
|
220
|
+
const isSplit = metadata.format === 'split' || Array.isArray(metadata.skills);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
name: frontmatter.name || '',
|
|
224
|
+
description: frontmatter.description || '',
|
|
225
|
+
version: metadata.version || frontmatter.version || '1.0.0',
|
|
226
|
+
layer: 'L4',
|
|
227
|
+
group: 'extension',
|
|
228
|
+
isSplit,
|
|
229
|
+
skillManifest: isSplit ? parseSkillManifest(metadata.skills || []) : [],
|
|
230
|
+
body,
|
|
231
|
+
crossRefs: extractCrossRefs(body),
|
|
232
|
+
toolRefs: extractToolRefs(body),
|
|
233
|
+
hardGates: extractHardGates(body),
|
|
234
|
+
sections: extractSections(body),
|
|
235
|
+
filePath,
|
|
236
|
+
frontmatter,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Parse the skills manifest from split PACK.md frontmatter
|
|
242
|
+
*
|
|
243
|
+
* @param {Array} skills - array of skill entries from frontmatter metadata.skills
|
|
244
|
+
* @returns {Array<{name: string, file: string, model: string, description: string}>}
|
|
245
|
+
*/
|
|
246
|
+
function parseSkillManifest(skills) {
|
|
247
|
+
if (!Array.isArray(skills)) return [];
|
|
248
|
+
|
|
249
|
+
return skills.map((skill) => {
|
|
250
|
+
// Handle both string format ("skill-name") and object format ({name, file, model, description})
|
|
251
|
+
if (typeof skill === 'string') {
|
|
252
|
+
return {
|
|
253
|
+
name: skill,
|
|
254
|
+
file: `skills/${skill}.md`,
|
|
255
|
+
model: 'sonnet',
|
|
256
|
+
description: '',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
name: skill.name || '',
|
|
262
|
+
file: skill.file || `skills/${skill.name}.md`,
|
|
263
|
+
model: skill.model || 'sonnet',
|
|
264
|
+
description: skill.description || '',
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
]
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
|
+
"UserPromptSubmit": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": ".*",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" intent-router",
|
|
22
|
+
"async": true
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
15
27
|
"PreToolUse": [
|
|
16
28
|
{
|
|
17
29
|
"matcher": "Read|Write|Edit",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Rune Intent Router Hook — Compiled Intent Mesh (CIM)
|
|
2
|
+
// Auto-suggests skill routing based on user prompt analysis
|
|
3
|
+
// Runs as UserPromptSubmit hook — scores prompt against compiled skill-index.json
|
|
4
|
+
//
|
|
5
|
+
// Key difference from runtime scoring approaches:
|
|
6
|
+
// - Compile-time generated index (zero runtime deps)
|
|
7
|
+
// - Mesh-aware chain prediction (uses actual connection graph)
|
|
8
|
+
// - Works on all platforms (index ships with every build)
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// Read user prompt from Claude Code hook stdin
|
|
14
|
+
let input = '';
|
|
15
|
+
process.stdin.setEncoding('utf-8');
|
|
16
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
17
|
+
process.stdin.on('end', () => {
|
|
18
|
+
let userPrompt = '';
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(input);
|
|
21
|
+
userPrompt = parsed.user_prompt || parsed.prompt || parsed.content || '';
|
|
22
|
+
} catch {
|
|
23
|
+
// Raw text input
|
|
24
|
+
userPrompt = input.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!userPrompt || userPrompt.length < 3) {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find skill-index.json — check multiple locations
|
|
32
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '../..');
|
|
33
|
+
const candidates = [
|
|
34
|
+
path.join(pluginRoot, '.cursor', 'rules', 'skill-index.json'),
|
|
35
|
+
path.join(pluginRoot, '.windsurf', 'rules', 'skill-index.json'),
|
|
36
|
+
path.join(pluginRoot, 'dist', 'cursor', 'skill-index.json'),
|
|
37
|
+
path.join(pluginRoot, 'dist', 'generic', 'skill-index.json'),
|
|
38
|
+
// Also check .rune/ for locally cached index
|
|
39
|
+
path.join(process.cwd(), '.rune', 'skill-index.json'),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
let index = null;
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
try {
|
|
45
|
+
if (fs.existsSync(candidate)) {
|
|
46
|
+
index = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Try next candidate
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!index || !index.intents) {
|
|
55
|
+
// No index available — exit silently (not an error, just not built yet)
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Score prompt against intent patterns
|
|
60
|
+
const promptLower = userPrompt.toLowerCase();
|
|
61
|
+
const scores = [];
|
|
62
|
+
|
|
63
|
+
for (const [skillName, intent] of Object.entries(index.intents)) {
|
|
64
|
+
let score = 0;
|
|
65
|
+
|
|
66
|
+
for (const keyword of intent.keywords) {
|
|
67
|
+
if (promptLower.includes(keyword)) {
|
|
68
|
+
// Exact word boundary match scores higher
|
|
69
|
+
const wordBoundary = new RegExp(`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
70
|
+
score += wordBoundary.test(userPrompt) ? 3 : 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Layer priority boost: L1 orchestrators get a small bonus (they're entry points)
|
|
75
|
+
if (intent.layer === 'L1') score += 1;
|
|
76
|
+
|
|
77
|
+
if (score > 0) {
|
|
78
|
+
scores.push({ skill: skillName, score, chain: intent.chain, model: intent.model, layer: intent.layer });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (scores.length === 0) {
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sort by score descending, take top match
|
|
87
|
+
scores.sort((a, b) => b.score - a.score);
|
|
88
|
+
const top = scores[0];
|
|
89
|
+
|
|
90
|
+
// Only suggest if confidence is reasonable (score >= 3 = at least one strong keyword match)
|
|
91
|
+
if (top.score < 3) {
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Format chain for display
|
|
96
|
+
const chainDisplay = top.chain.slice(0, 4).join(' → ');
|
|
97
|
+
const alternates = scores.slice(1, 3).map((s) => s.skill).join(', ');
|
|
98
|
+
|
|
99
|
+
// Output routing suggestion
|
|
100
|
+
console.log(`\n🧭 [Rune intent-router] Suggested: rune:${top.skill} (${top.layer}, ${top.model})`);
|
|
101
|
+
console.log(` Chain: ${chainDisplay}`);
|
|
102
|
+
if (alternates) {
|
|
103
|
+
console.log(` Also consider: ${alternates}`);
|
|
104
|
+
}
|
|
105
|
+
console.log('');
|
|
106
|
+
|
|
107
|
+
process.exit(0);
|
|
108
|
+
});
|