@rune-kit/rune 2.22.2 → 2.23.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 +5 -1
- package/compiler/__tests__/adapters.test.js +75 -30
- package/compiler/__tests__/pack-split.test.js +5 -2
- package/compiler/__tests__/pipeline.test.js +6 -6
- package/compiler/__tests__/scripts-bundling.test.js +29 -19
- package/compiler/__tests__/transformer.test.js +4 -4
- package/compiler/adapters/antigravity.js +1 -5
- package/compiler/adapters/codex.js +10 -6
- package/compiler/adapters/copilot.js +26 -29
- package/compiler/adapters/cursor.js +23 -15
- package/compiler/adapters/gemini.js +25 -48
- package/compiler/adapters/qoder.js +14 -14
- package/compiler/adapters/qwen.js +16 -22
- package/compiler/adapters/windsurf.js +22 -7
- package/compiler/emitter.js +5 -4
- package/compiler/parser.js +11 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,7 +83,11 @@ _Methodology: Claude Code CLI headless mode (`claude -p --output-format json`),
|
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
|
-
## What's New (v2.
|
|
86
|
+
## What's New (v2.23.0 — Native Skills)
|
|
87
|
+
|
|
88
|
+
> **v2.23.0 (2026-07-04):** Seven platform adapters move to the **Agent Skills open standard** (dir-per-skill `SKILL.md`, discovered and lazy-loaded by each platform's native loader). The headline fix: **Codex** dropped `.codex/skills/` from its scan list, so compiled skills were only findable via the AGENTS.md pointer — agents kept "re-finding" the path mid-session. Codex now emits to **`.agents/skills/`** (scanned CWD → repo root). Same treatment across the fleet: **cursor** `.cursor/rules/*.mdc` → `.cursor/skills/` (Cursor 2.4+ Skills, on-demand instead of always-on), **windsurf** → `.windsurf/skills/` (Cascade Skills), **copilot** → `.github/skills/`, **qoder** → `.qoder/skills/`, and **gemini/qwen** drop their all-skills-always-on context bombs (GEMINI.md bundle, QWEN.md `@import` wall) for native `.gemini/skills/` / `.qwen/skills/` + slim pointer files — a big context-window win on those platforms. Runtime hooks intentionally stay on `.cursor/rules` / `.windsurf/rules` (always-on is correct for hook context). Also fixes a YAML double-escaping bug that corrupted compiled frontmatter for skills with quoted descriptions on 6 platforms, and a duplicate `scripts/` copy in dir-per-skill builds. If you previously built for Codex/Cursor/Windsurf/Copilot/Gemini/Qwen/Qoder: re-run `npx @rune-kit/rune build` and delete the old output dirs. CI 1571/1571.
|
|
89
|
+
|
|
90
|
+
### Previous (v2.22.2 — Convergence, Dogfooded)
|
|
87
91
|
|
|
88
92
|
> **v2.22.2 (2026-07-04):** Patch: `rune setup` now installs tier skills into the Claude Code PLUGIN CACHE (newest version dir) instead of the executing package's own root — npx runs were copying Pro skills into npx's ephemeral cache (invisible to the plugin runtime, 'Unknown skill: rune:autopilot' returned), and source-checkout runs polluted the git tree. **v2.22.1:** tier-hook loader accepts PreCompact + SessionStart lifecycle events — required by Pro hooks v1.2.0 (context-reset). **v2.22.0:** The v2.21.0 gates went through a live-fire dogfood: fresh executor agents (zero author context) ran `converge` and `verification` Level 3.5 against a fixture with a dead Submit button, a handler-less Export button, a navigation-anchor decoy, and a declared placeholder. **Both gates caught the dead button with file:line evidence and zero false positives** — and the 16 ambiguities the executors reported became spec fixes: converge v0.2.0 adds a `deferred-debt` class (declared design debt can no longer force a false escalation), a `Plan Claims vs Reality` section (tasks marked `[x]` whose code doesn't exist — surfaced first-class), and derived story verdicts; verification v0.7.0 gets FAIL-dominates precedence, per-route reverse checks, and server/static entry-point exemptions. The dogfood fixture itself shipped as the seed of **`npm run eval`** — a behavioral eval harness that runs a fresh headless agent against fixture repos and asserts outcomes, because structural validation can't prove a skill makes an agent behave. Pro `autopilot` v1.6.0 now explicitly runs Phase 6.5 CONVERGE in autonomous mode.
|
|
89
93
|
|
|
@@ -29,7 +29,7 @@ test('listPlatforms returns all 13 platform adapters', () => {
|
|
|
29
29
|
test('getAdapter returns adapter by name', () => {
|
|
30
30
|
const cursor = getAdapter('cursor');
|
|
31
31
|
assert.strictEqual(cursor.name, 'cursor');
|
|
32
|
-
assert.strictEqual(cursor.fileExtension, '.
|
|
32
|
+
assert.strictEqual(cursor.fileExtension, '.md');
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
test('getAdapter throws for unknown adapter', () => {
|
|
@@ -112,18 +112,45 @@ for (const adapterName of ADAPTER_NAMES) {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// --- Frontmatter escaping (regression: descriptions containing quoted phrases) ---
|
|
116
|
+
|
|
117
|
+
test('generateHeader escapes quoted descriptions exactly once (valid YAML)', () => {
|
|
118
|
+
// Parser hands adapters CLEAN text (interior \" already unescaped) — the
|
|
119
|
+
// adapter re-escapes exactly once. Double-escaping produced `\\"` which
|
|
120
|
+
// prematurely terminates the YAML scalar.
|
|
121
|
+
const skill = { name: 'docs', layer: 'L2', group: 'workflow', description: 'The "docs are never outdated" skill' };
|
|
122
|
+
for (const name of ['cursor', 'windsurf', 'copilot', 'codex', 'antigravity', 'opencode', 'gemini', 'qoder', 'qwen']) {
|
|
123
|
+
const header = getAdapter(name).generateHeader(skill);
|
|
124
|
+
assert.ok(header.includes('\\"docs are never outdated\\"'), `${name} should escape quotes once`);
|
|
125
|
+
assert.ok(!header.includes('\\\\"'), `${name} must not double-escape quotes`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
115
129
|
// --- Cursor-specific ---
|
|
116
130
|
|
|
117
|
-
test('cursor adapter
|
|
131
|
+
test('cursor adapter emits Agent Skills (dir-per-skill SKILL.md)', () => {
|
|
118
132
|
const cursor = getAdapter('cursor');
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
assert.strictEqual(cursor.outputDir, '.cursor/skills');
|
|
134
|
+
assert.strictEqual(cursor.useSkillDirectories, true);
|
|
135
|
+
assert.strictEqual(cursor.skillFileName, 'SKILL.md');
|
|
121
136
|
|
|
122
|
-
const
|
|
123
|
-
assert.ok(
|
|
137
|
+
const header = cursor.generateHeader({ name: 'fix', layer: 'L2', group: 'workflow', description: 'Fix code' });
|
|
138
|
+
assert.ok(header.includes('name: rune-fix'));
|
|
139
|
+
assert.ok(header.includes('description: "Fix code"'));
|
|
140
|
+
assert.ok(!header.includes('alwaysApply'), 'skills format has no alwaysApply');
|
|
141
|
+
});
|
|
124
142
|
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
// --- Windsurf-specific ---
|
|
144
|
+
|
|
145
|
+
test('windsurf adapter emits Cascade Skills (dir-per-skill SKILL.md)', () => {
|
|
146
|
+
const windsurf = getAdapter('windsurf');
|
|
147
|
+
assert.strictEqual(windsurf.outputDir, '.windsurf/skills');
|
|
148
|
+
assert.strictEqual(windsurf.useSkillDirectories, true);
|
|
149
|
+
assert.strictEqual(windsurf.skillFileName, 'SKILL.md');
|
|
150
|
+
|
|
151
|
+
const header = windsurf.generateHeader({ name: 'fix', layer: 'L2', group: 'workflow', description: 'Fix code' });
|
|
152
|
+
assert.ok(header.includes('name: rune-fix'));
|
|
153
|
+
assert.ok(header.includes('description: "Fix code"'));
|
|
127
154
|
});
|
|
128
155
|
|
|
129
156
|
// --- Codex-specific ---
|
|
@@ -136,26 +163,35 @@ test('codex adapter uses skill directories', () => {
|
|
|
136
163
|
|
|
137
164
|
// --- New v2.18 adapters: shape + generateExtraFiles contract ---
|
|
138
165
|
|
|
139
|
-
test('qoder adapter targets .qoder/
|
|
166
|
+
test('qoder adapter targets .qoder/skills (dir-per-skill) and emits AGENTS.md', async () => {
|
|
140
167
|
const qoder = getAdapter('qoder');
|
|
141
|
-
assert.strictEqual(qoder.outputDir, '.qoder/
|
|
142
|
-
assert.strictEqual(qoder.useSkillDirectories,
|
|
168
|
+
assert.strictEqual(qoder.outputDir, '.qoder/skills');
|
|
169
|
+
assert.strictEqual(qoder.useSkillDirectories, true);
|
|
170
|
+
assert.strictEqual(qoder.skillFileName, 'SKILL.md');
|
|
143
171
|
assert.strictEqual(typeof qoder.generateExtraFiles, 'function');
|
|
144
172
|
const extras = await qoder.generateExtraFiles({ stats: { skillCount: 5, packCount: 1, files: [] } });
|
|
145
|
-
assert.ok(extras.some((e) => e.path === 'AGENTS.md' && e.content.includes('
|
|
173
|
+
assert.ok(extras.some((e) => e.path === 'AGENTS.md' && e.content.includes('.qoder/skills')));
|
|
146
174
|
});
|
|
147
175
|
|
|
148
|
-
test('copilot adapter targets .github/
|
|
176
|
+
test('copilot adapter targets .github/skills with SKILL.md format', async () => {
|
|
149
177
|
const copilot = getAdapter('copilot');
|
|
150
|
-
assert.strictEqual(copilot.outputDir, '.github/
|
|
151
|
-
assert.strictEqual(copilot.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
178
|
+
assert.strictEqual(copilot.outputDir, '.github/skills');
|
|
179
|
+
assert.strictEqual(copilot.useSkillDirectories, true);
|
|
180
|
+
assert.strictEqual(copilot.skillFileName, 'SKILL.md');
|
|
181
|
+
const header = copilot.generateHeader({
|
|
182
|
+
name: 'cook',
|
|
183
|
+
layer: 'L1',
|
|
184
|
+
group: 'orchestrator',
|
|
185
|
+
description: 'Test',
|
|
186
|
+
model: 'opus',
|
|
187
|
+
});
|
|
188
|
+
// Agent Skills spec: name + description frontmatter; tier hint stays a body comment.
|
|
189
|
+
assert.ok(header.includes('name: rune-cook'));
|
|
190
|
+
assert.ok(header.includes('description: "Test"'));
|
|
191
|
+
assert.ok(header.includes('<!-- tier-hint: tier:heavy -->'));
|
|
192
|
+
assert.ok(!header.includes('applyTo'), 'skills format has no applyTo');
|
|
157
193
|
const extras = await copilot.generateExtraFiles({ stats: { skillCount: 5, packCount: 1, files: [] } });
|
|
158
|
-
assert.ok(extras.some((e) => e.path === '.github/copilot-instructions.md'));
|
|
194
|
+
assert.ok(extras.some((e) => e.path === '.github/copilot-instructions.md' && e.content.includes('.github/skills')));
|
|
159
195
|
assert.ok(extras.some((e) => e.path === 'AGENTS.md'));
|
|
160
196
|
});
|
|
161
197
|
|
|
@@ -201,21 +237,30 @@ test('aider adapter emits .aider.conf.yml with read array', async () => {
|
|
|
201
237
|
assert.ok(!conf.content.includes('aider/rules/index.md'));
|
|
202
238
|
});
|
|
203
239
|
|
|
204
|
-
test('qwen adapter emits QWEN.md
|
|
240
|
+
test('qwen adapter targets .qwen/skills and emits slim QWEN.md pointer', async () => {
|
|
205
241
|
const qwen = getAdapter('qwen');
|
|
206
|
-
assert.strictEqual(qwen.outputDir, 'qwen/skills');
|
|
242
|
+
assert.strictEqual(qwen.outputDir, '.qwen/skills');
|
|
243
|
+
assert.strictEqual(qwen.useSkillDirectories, true);
|
|
244
|
+
assert.strictEqual(qwen.skillFileName, 'SKILL.md');
|
|
207
245
|
const extras = await qwen.generateExtraFiles({
|
|
208
|
-
stats: { skillCount: 2, packCount: 0, files: ['rune-cook.md', 'rune-fix.md'] },
|
|
246
|
+
stats: { skillCount: 2, packCount: 0, files: ['rune-cook/SKILL.md', 'rune-fix/SKILL.md'] },
|
|
209
247
|
});
|
|
210
248
|
const qwenMd = extras.find((e) => e.path === 'QWEN.md');
|
|
211
249
|
assert.ok(qwenMd);
|
|
212
|
-
assert.ok(qwenMd.content.includes('
|
|
213
|
-
assert.ok(qwenMd.content.includes('@qwen/skills/
|
|
250
|
+
assert.ok(qwenMd.content.includes('.qwen/skills/rune-<name>/SKILL.md'));
|
|
251
|
+
assert.ok(!qwenMd.content.includes('@qwen/skills/'), 'no @import lines — skills are lazy-loaded natively');
|
|
214
252
|
});
|
|
215
253
|
|
|
216
|
-
test('gemini adapter
|
|
254
|
+
test('gemini adapter targets .gemini/skills and emits slim GEMINI.md pointer', async () => {
|
|
217
255
|
const gemini = getAdapter('gemini');
|
|
218
|
-
assert.strictEqual(gemini.outputDir, 'gemini/skills');
|
|
219
|
-
assert.strictEqual(
|
|
220
|
-
|
|
256
|
+
assert.strictEqual(gemini.outputDir, '.gemini/skills');
|
|
257
|
+
assert.strictEqual(gemini.useSkillDirectories, true);
|
|
258
|
+
assert.strictEqual(gemini.skillFileName, 'SKILL.md');
|
|
259
|
+
const extras = await gemini.generateExtraFiles({
|
|
260
|
+
stats: { skillCount: 2, packCount: 0, files: ['rune-cook/SKILL.md', 'rune-fix/SKILL.md'] },
|
|
261
|
+
});
|
|
262
|
+
const geminiMd = extras.find((e) => e.path === 'GEMINI.md');
|
|
263
|
+
assert.ok(geminiMd);
|
|
264
|
+
assert.ok(geminiMd.content.includes('.gemini/skills/rune-<name>/SKILL.md'));
|
|
265
|
+
assert.ok(!geminiMd.content.includes('## rune-cook'), 'no bundled H2 sections — skills are lazy-loaded natively');
|
|
221
266
|
});
|
|
@@ -244,7 +244,10 @@ describe('buildAll split pack auto-discovery', () => {
|
|
|
244
244
|
assert.strictEqual(stats.packCount, 1, 'should build 1 pack');
|
|
245
245
|
|
|
246
246
|
// Read the compiled pack output
|
|
247
|
-
const packOutput = await readFile(
|
|
247
|
+
const packOutput = await readFile(
|
|
248
|
+
path.join(outputRoot, adapter.outputDir, 'rune-ext-test-pack', 'SKILL.md'),
|
|
249
|
+
'utf-8',
|
|
250
|
+
);
|
|
248
251
|
|
|
249
252
|
// Should contain the pack index body
|
|
250
253
|
assert.ok(packOutput.includes('Pack index body'), 'missing pack index body');
|
|
@@ -270,7 +273,7 @@ describe('buildAll split pack auto-discovery', () => {
|
|
|
270
273
|
await buildAll({ runeRoot, outputRoot: tmp, adapter });
|
|
271
274
|
|
|
272
275
|
try {
|
|
273
|
-
const packOutput = await readFile(path.join(tmp, adapter.outputDir, 'rune-ext-ai-ml.
|
|
276
|
+
const packOutput = await readFile(path.join(tmp, adapter.outputDir, 'rune-ext-ai-ml', 'SKILL.md'), 'utf-8');
|
|
274
277
|
|
|
275
278
|
// Should be significantly longer than just the index body (~99 lines without skills)
|
|
276
279
|
const lineCount = packOutput.split('\n').length;
|
|
@@ -58,12 +58,12 @@ describe('full pipeline: cook skill', () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
describe('full pipeline: fix skill', () => {
|
|
61
|
-
test('cursor: cross-refs transformed to
|
|
61
|
+
test('cursor: cross-refs transformed to skill references', () => {
|
|
62
62
|
const adapter = getAdapter('cursor');
|
|
63
63
|
const result = transformSkill(fixParsed, adapter);
|
|
64
64
|
|
|
65
65
|
// Fix references cook, debug, test, etc. — at least some should be transformed
|
|
66
|
-
assert.ok(result.body.includes('
|
|
66
|
+
assert.ok(result.body.includes('the rune-'), 'cursor output should contain skill references');
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
test('generic: cross-refs transformed to descriptive text', () => {
|
|
@@ -82,15 +82,15 @@ describe('full pipeline: fix skill', () => {
|
|
|
82
82
|
// --- Output assembly (header + body + footer) ---
|
|
83
83
|
|
|
84
84
|
describe('output assembly', () => {
|
|
85
|
-
test('cursor output assembles into valid .
|
|
85
|
+
test('cursor output assembles into valid SKILL.md file', () => {
|
|
86
86
|
const adapter = getAdapter('cursor');
|
|
87
87
|
const result = transformSkill(cookParsed, adapter);
|
|
88
88
|
const assembled = result.header + result.body + result.footer;
|
|
89
89
|
|
|
90
90
|
// Should start with YAML frontmatter
|
|
91
|
-
assert.ok(assembled.startsWith('---\n'), 'cursor .
|
|
92
|
-
assert.ok(assembled.includes('
|
|
93
|
-
assert.ok(assembled.includes('
|
|
91
|
+
assert.ok(assembled.startsWith('---\n'), 'cursor SKILL.md should start with YAML frontmatter');
|
|
92
|
+
assert.ok(assembled.includes('name: rune-'), 'cursor SKILL.md should have name');
|
|
93
|
+
assert.ok(assembled.includes('description:'), 'cursor SKILL.md should have description');
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test('codex output has YAML frontmatter with name', () => {
|
|
@@ -54,7 +54,7 @@ describe('resolveScriptsPath', () => {
|
|
|
54
54
|
|
|
55
55
|
describe('adapter scriptsDir', () => {
|
|
56
56
|
test('flat-file adapters return sibling dir pattern', () => {
|
|
57
|
-
for (const name of ['
|
|
57
|
+
for (const name of ['generic', 'openclaw']) {
|
|
58
58
|
const adapter = getAdapter(name);
|
|
59
59
|
assert.ok(adapter.scriptsDir, `${name} should have scriptsDir`);
|
|
60
60
|
assert.strictEqual(adapter.scriptsDir('slides'), 'rune-slides-scripts', `${name} scriptsDir mismatch`);
|
|
@@ -62,7 +62,17 @@ describe('adapter scriptsDir', () => {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
test('directory-per-skill adapters return nested dir pattern', () => {
|
|
65
|
-
for (const name of [
|
|
65
|
+
for (const name of [
|
|
66
|
+
'codex',
|
|
67
|
+
'opencode',
|
|
68
|
+
'cursor',
|
|
69
|
+
'windsurf',
|
|
70
|
+
'copilot',
|
|
71
|
+
'qoder',
|
|
72
|
+
'gemini',
|
|
73
|
+
'qwen',
|
|
74
|
+
'antigravity',
|
|
75
|
+
]) {
|
|
66
76
|
const adapter = getAdapter(name);
|
|
67
77
|
assert.ok(adapter.scriptsDir, `${name} should have scriptsDir`);
|
|
68
78
|
assert.strictEqual(adapter.scriptsDir('slides'), 'rune-slides/scripts', `${name} scriptsDir mismatch`);
|
|
@@ -143,7 +153,7 @@ describe('buildAll with scripts', () => {
|
|
|
143
153
|
return tmp;
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
test('cursor: scripts
|
|
156
|
+
test('cursor: scripts inside skill directory, placeholder resolved', async () => {
|
|
147
157
|
const tmp = await createTempSkillTree();
|
|
148
158
|
try {
|
|
149
159
|
const outputRoot = path.join(tmp, 'out');
|
|
@@ -153,19 +163,19 @@ describe('buildAll with scripts', () => {
|
|
|
153
163
|
// Scripts copied
|
|
154
164
|
assert.ok(stats.scriptsCopied >= 2, `expected 2+ scripts copied, got ${stats.scriptsCopied}`);
|
|
155
165
|
|
|
156
|
-
// Scripts
|
|
157
|
-
const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide
|
|
166
|
+
// Scripts inside skill dir: .cursor/skills/rune-test-slide/scripts/
|
|
167
|
+
const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'scripts');
|
|
158
168
|
assert.ok(existsSync(path.join(scriptsOut, 'build-deck.js')), 'build-deck.js missing in output');
|
|
159
169
|
assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing in output');
|
|
160
170
|
|
|
161
|
-
// Placeholder resolved in .
|
|
162
|
-
const
|
|
163
|
-
assert.ok(!
|
|
164
|
-
assert.ok(
|
|
171
|
+
// Placeholder resolved in SKILL.md output
|
|
172
|
+
const md = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'), 'utf-8');
|
|
173
|
+
assert.ok(!md.includes('{scripts_dir}'), 'placeholder should be resolved');
|
|
174
|
+
assert.ok(md.includes('.cursor/skills/rune-test-slide/scripts/build-deck.js'), 'resolved path missing');
|
|
165
175
|
|
|
166
176
|
// Plain skill: no scripts dir created
|
|
167
177
|
assert.ok(
|
|
168
|
-
!existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain
|
|
178
|
+
!existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain', 'scripts')),
|
|
169
179
|
'plain skill should not have scripts dir',
|
|
170
180
|
);
|
|
171
181
|
} finally {
|
|
@@ -182,7 +192,7 @@ describe('buildAll with scripts', () => {
|
|
|
182
192
|
|
|
183
193
|
assert.ok(stats.scriptsCopied >= 2);
|
|
184
194
|
|
|
185
|
-
// Scripts inside skill dir: .
|
|
195
|
+
// Scripts inside skill dir: .agents/skills/rune-test-slide/scripts/
|
|
186
196
|
const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'scripts');
|
|
187
197
|
assert.ok(existsSync(path.join(scriptsOut, 'build-deck.js')), 'build-deck.js missing');
|
|
188
198
|
assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing');
|
|
@@ -190,7 +200,7 @@ describe('buildAll with scripts', () => {
|
|
|
190
200
|
// Placeholder resolved
|
|
191
201
|
const md = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'), 'utf-8');
|
|
192
202
|
assert.ok(!md.includes('{scripts_dir}'), 'placeholder should be resolved');
|
|
193
|
-
assert.ok(md.includes('.
|
|
203
|
+
assert.ok(md.includes('.agents/skills/rune-test-slide/scripts/build-deck.js'), 'resolved path missing');
|
|
194
204
|
} finally {
|
|
195
205
|
await rm(tmp, { recursive: true, force: true });
|
|
196
206
|
}
|
|
@@ -251,7 +261,7 @@ describe('buildAll with scripts', () => {
|
|
|
251
261
|
|
|
252
262
|
const original = await readFile(path.join(tmp, 'skills', 'test-slide', 'scripts', 'build-deck.js'), 'utf-8');
|
|
253
263
|
const copied = await readFile(
|
|
254
|
-
path.join(outputRoot, adapter.outputDir, 'rune-test-slide
|
|
264
|
+
path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'scripts', 'build-deck.js'),
|
|
255
265
|
'utf-8',
|
|
256
266
|
);
|
|
257
267
|
assert.strictEqual(copied, original, 'script content should be identical');
|
|
@@ -263,19 +273,19 @@ describe('buildAll with scripts', () => {
|
|
|
263
273
|
test('multiple adapters resolve different paths', async () => {
|
|
264
274
|
const tmp = await createTempSkillTree();
|
|
265
275
|
try {
|
|
266
|
-
// Cursor (
|
|
276
|
+
// Cursor (directory)
|
|
267
277
|
const cursorOut = path.join(tmp, 'out-cursor');
|
|
268
278
|
const cursorAdapter = getAdapter('cursor');
|
|
269
279
|
await buildAll({ runeRoot: tmp, outputRoot: cursorOut, adapter: cursorAdapter });
|
|
270
|
-
const
|
|
271
|
-
assert.ok(
|
|
280
|
+
const cursorMd = await readFile(path.join(cursorOut, '.cursor/skills/rune-test-slide/SKILL.md'), 'utf-8');
|
|
281
|
+
assert.ok(cursorMd.includes('.cursor/skills/rune-test-slide/scripts/'), 'cursor path');
|
|
272
282
|
|
|
273
|
-
// Codex (directory)
|
|
283
|
+
// Codex (directory, shared .agents/skills standard)
|
|
274
284
|
const codexOut = path.join(tmp, 'out-codex');
|
|
275
285
|
const codexAdapter = getAdapter('codex');
|
|
276
286
|
await buildAll({ runeRoot: tmp, outputRoot: codexOut, adapter: codexAdapter });
|
|
277
|
-
const codexMd = await readFile(path.join(codexOut, '.
|
|
278
|
-
assert.ok(codexMd.includes('.
|
|
287
|
+
const codexMd = await readFile(path.join(codexOut, '.agents/skills/rune-test-slide/SKILL.md'), 'utf-8');
|
|
288
|
+
assert.ok(codexMd.includes('.agents/skills/rune-test-slide/scripts/'), 'codex path');
|
|
279
289
|
} finally {
|
|
280
290
|
await rm(tmp, { recursive: true, force: true });
|
|
281
291
|
}
|
|
@@ -27,10 +27,10 @@ describe('transformSkill', () => {
|
|
|
27
27
|
assert.strictEqual(result.footer, '');
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test('Cursor adapter transforms cross-refs to
|
|
30
|
+
test('Cursor adapter transforms cross-refs to skill references', () => {
|
|
31
31
|
const cursor = getAdapter('cursor');
|
|
32
32
|
const result = transformSkill(mockSkill, cursor);
|
|
33
|
-
assert.ok(result.body.includes('
|
|
33
|
+
assert.ok(result.body.includes('the rune-cook skill'));
|
|
34
34
|
assert.ok(!result.body.includes('rune:cook'));
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -44,11 +44,11 @@ describe('transformSkill', () => {
|
|
|
44
44
|
assert.ok(result.footer.length > 0, 'footer should not be empty for non-Claude');
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
test('Cursor header contains frontmatter with description', () => {
|
|
47
|
+
test('Cursor header contains SKILL.md frontmatter with name and description', () => {
|
|
48
48
|
const cursor = getAdapter('cursor');
|
|
49
49
|
const result = transformSkill(mockSkill, cursor);
|
|
50
|
+
assert.ok(result.header.includes('name: rune-'));
|
|
50
51
|
assert.ok(result.header.includes('description:'));
|
|
51
|
-
assert.ok(result.header.includes('alwaysApply:'));
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test('Generic adapter transforms refs to descriptive text', () => {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Codex Adapter
|
|
3
3
|
*
|
|
4
|
-
* Emits SKILL.md files into .
|
|
4
|
+
* Emits SKILL.md files into .agents/skills/{name}/ directories.
|
|
5
5
|
* Codex uses the same SKILL.md frontmatter format (name, description)
|
|
6
6
|
* with markdown body — very close to Rune's native format.
|
|
7
7
|
*
|
|
8
8
|
* Codex project context: AGENTS.md (equivalent to CLAUDE.md)
|
|
9
|
-
* Codex skills dir: .
|
|
10
|
-
* Codex skill format: .
|
|
9
|
+
* Codex skills dir: .agents/skills/ (scanned from CWD up to repo root)
|
|
10
|
+
* Codex skill format: .agents/skills/{name}/SKILL.md
|
|
11
|
+
*
|
|
12
|
+
* NOTE: Codex dropped .codex/skills/ from its scan list — .codex/ is config
|
|
13
|
+
* only (config.toml). Repo skills MUST live in .agents/skills/ to be
|
|
14
|
+
* auto-discovered. @see https://developers.openai.com/codex/skills
|
|
11
15
|
*
|
|
12
16
|
* MODEL TIER MAPPING (v2.15+):
|
|
13
17
|
* Skill frontmatter `model: opus|sonnet|haiku` (Anthropic naming) is
|
|
@@ -37,12 +41,12 @@ const TOOL_MAP = {
|
|
|
37
41
|
|
|
38
42
|
export default {
|
|
39
43
|
name: 'codex',
|
|
40
|
-
outputDir: '.
|
|
44
|
+
outputDir: '.agents/skills',
|
|
41
45
|
fileExtension: '.md',
|
|
42
46
|
skillPrefix: 'rune-',
|
|
43
47
|
skillSuffix: '',
|
|
44
48
|
|
|
45
|
-
// Codex uses directory-per-skill: .
|
|
49
|
+
// Codex uses directory-per-skill: .agents/skills/{name}/SKILL.md
|
|
46
50
|
useSkillDirectories: true,
|
|
47
51
|
skillFileName: 'SKILL.md',
|
|
48
52
|
|
|
@@ -105,7 +109,7 @@ export default {
|
|
|
105
109
|
'',
|
|
106
110
|
'## Skills Directory',
|
|
107
111
|
'',
|
|
108
|
-
'Skills are located in: .
|
|
112
|
+
'Skills are located in: .agents/skills/ (auto-discovered by Codex)',
|
|
109
113
|
'',
|
|
110
114
|
'---',
|
|
111
115
|
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GitHub Copilot
|
|
2
|
+
* GitHub Copilot Adapter
|
|
3
3
|
*
|
|
4
|
-
* Emits
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Emits SKILL.md files into .github/skills/{name}/ directories — GitHub
|
|
5
|
+
* Copilot's native Agent Skills format (supported since Dec 2025 across
|
|
6
|
+
* Copilot CLI, VS Code agent mode, and github.com). Skills are loaded
|
|
7
|
+
* on-demand based on the description, keeping context lean.
|
|
7
8
|
*
|
|
8
|
-
* Copilot
|
|
9
|
-
* Copilot
|
|
9
|
+
* Copilot skills dir: .github/skills/ (also reads .claude/skills and .agents/skills)
|
|
10
|
+
* Copilot skill format: .github/skills/{name}/SKILL.md
|
|
10
11
|
* Copilot project context: AGENTS.md (Copilot Spaces / Coding Agent convention)
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @see https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-custom-instructions
|
|
16
|
-
* @see https://docs.github.com/en/copilot/concepts/response-customization
|
|
13
|
+
* @see https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
|
|
14
|
+
* @see https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-skills
|
|
17
15
|
*
|
|
18
16
|
* MODEL TIER MAPPING (v2.18+):
|
|
19
17
|
* Copilot multi-model support exposes Anthropic/OpenAI/Google providers. Tier
|
|
@@ -37,23 +35,24 @@ const TOOL_MAP = {
|
|
|
37
35
|
Grep: 'search file contents',
|
|
38
36
|
Bash: 'run a shell command',
|
|
39
37
|
TodoWrite: 'track task progress',
|
|
40
|
-
Skill: '
|
|
38
|
+
Skill: 'invoke the named skill',
|
|
41
39
|
Agent: 'execute the workflow',
|
|
42
40
|
};
|
|
43
41
|
|
|
44
42
|
export default {
|
|
45
43
|
name: 'copilot',
|
|
46
|
-
outputDir: '.github/
|
|
47
|
-
fileExtension: '.
|
|
44
|
+
outputDir: '.github/skills',
|
|
45
|
+
fileExtension: '.md',
|
|
48
46
|
skillPrefix: 'rune-',
|
|
49
47
|
skillSuffix: '',
|
|
50
48
|
|
|
51
|
-
// Copilot
|
|
52
|
-
useSkillDirectories:
|
|
49
|
+
// Copilot uses directory-per-skill: .github/skills/{name}/SKILL.md
|
|
50
|
+
useSkillDirectories: true,
|
|
51
|
+
skillFileName: 'SKILL.md',
|
|
53
52
|
|
|
54
53
|
transformReference(skillName, raw) {
|
|
55
54
|
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
56
|
-
const ref = `the rune-${skillName}
|
|
55
|
+
const ref = `the rune-${skillName} skill`;
|
|
57
56
|
return isBackticked ? `\`${ref}\`` : ref;
|
|
58
57
|
},
|
|
59
58
|
|
|
@@ -62,15 +61,13 @@ export default {
|
|
|
62
61
|
},
|
|
63
62
|
|
|
64
63
|
generateHeader(skill) {
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
// survives parsing across CLI/IDE/extensions consistently.
|
|
69
|
-
const desc = (skill.description || '').replace(/\n/g, ' ');
|
|
64
|
+
// Agent Skills spec: name + description frontmatter, markdown body.
|
|
65
|
+
// Tier hint stays a body comment — Copilot has no model frontmatter key.
|
|
66
|
+
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
70
67
|
const tierHint = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
|
|
71
|
-
const lines = ['---',
|
|
72
|
-
if (tierHint) lines.push(
|
|
73
|
-
lines.push(''
|
|
68
|
+
const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`, '---', ''];
|
|
69
|
+
if (tierHint) lines.push(`<!-- tier-hint: ${tierHint} -->`, '');
|
|
70
|
+
lines.push('');
|
|
74
71
|
return lines.join('\n');
|
|
75
72
|
},
|
|
76
73
|
|
|
@@ -83,7 +80,7 @@ export default {
|
|
|
83
80
|
},
|
|
84
81
|
|
|
85
82
|
scriptsDir(skillName) {
|
|
86
|
-
return `rune-${skillName}
|
|
83
|
+
return `rune-${skillName}/scripts`;
|
|
87
84
|
},
|
|
88
85
|
|
|
89
86
|
postProcess(content) {
|
|
@@ -96,9 +93,9 @@ export default {
|
|
|
96
93
|
const copilotIndex = [
|
|
97
94
|
'# Rune — Copilot Custom Instructions',
|
|
98
95
|
'',
|
|
99
|
-
`Per-skill
|
|
96
|
+
`Per-skill Agent Skills live under \`.github/skills/rune-<name>/SKILL.md\` (${stats.skillCount} skills + ${stats.packCount} packs). Copilot discovers and loads them on demand based on each skill's description.`,
|
|
100
97
|
'',
|
|
101
|
-
'When a user request matches a skill\'s domain (e.g. "fix the failing test", "review this PR"), prefer
|
|
98
|
+
'When a user request matches a skill\'s domain (e.g. "fix the failing test", "review this PR"), prefer invoking the corresponding rune-<name> skill over freestyling.',
|
|
102
99
|
'',
|
|
103
100
|
'---',
|
|
104
101
|
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
@@ -111,7 +108,7 @@ export default {
|
|
|
111
108
|
'Rune is an interconnected skill ecosystem for AI coding assistants.',
|
|
112
109
|
`${stats.skillCount} core skills + ${stats.packCount} extension packs.`,
|
|
113
110
|
'',
|
|
114
|
-
'Per-skill
|
|
111
|
+
'Per-skill Agent Skills: `.github/skills/rune-<name>/SKILL.md`',
|
|
115
112
|
'',
|
|
116
113
|
'---',
|
|
117
114
|
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cursor Adapter
|
|
3
3
|
*
|
|
4
|
-
* Emits .
|
|
5
|
-
*
|
|
4
|
+
* Emits SKILL.md files into .cursor/skills/{name}/ directories — Cursor's
|
|
5
|
+
* native Agent Skills format (Cursor 2.4+, Jan 2026). Skills are loaded
|
|
6
|
+
* dynamically when the agent decides they're relevant, unlike .cursor/rules
|
|
7
|
+
* which are always-on context.
|
|
8
|
+
*
|
|
9
|
+
* Cursor skills dir: .cursor/skills/
|
|
10
|
+
* Cursor skill format: .cursor/skills/{name}/SKILL.md
|
|
11
|
+
* @see https://cursor.com/docs/skills
|
|
12
|
+
*
|
|
13
|
+
* NOTE: .cursor/rules/ is still used by the runtime-hooks installer
|
|
14
|
+
* (adapters/hooks/cursor.js) — rules are the correct vehicle for always-on
|
|
15
|
+
* hook context. Only skill emission moved to .cursor/skills/.
|
|
6
16
|
*
|
|
7
17
|
* MODEL TIER MAPPING (v2.15+):
|
|
8
18
|
* No-op. Cursor's Anthropic API integration understands `model: opus|sonnet|haiku`
|
|
@@ -19,20 +29,24 @@ const TOOL_MAP = {
|
|
|
19
29
|
Grep: 'search file contents',
|
|
20
30
|
Bash: 'run a terminal command',
|
|
21
31
|
TodoWrite: 'track progress',
|
|
22
|
-
Skill: '
|
|
32
|
+
Skill: 'invoke the named skill',
|
|
23
33
|
Agent: 'execute the workflow',
|
|
24
34
|
};
|
|
25
35
|
|
|
26
36
|
export default {
|
|
27
37
|
name: 'cursor',
|
|
28
|
-
outputDir: '.cursor/
|
|
29
|
-
fileExtension: '.
|
|
38
|
+
outputDir: '.cursor/skills',
|
|
39
|
+
fileExtension: '.md',
|
|
30
40
|
skillPrefix: 'rune-',
|
|
31
41
|
skillSuffix: '',
|
|
32
42
|
|
|
43
|
+
// Cursor uses directory-per-skill: .cursor/skills/{name}/SKILL.md
|
|
44
|
+
useSkillDirectories: true,
|
|
45
|
+
skillFileName: 'SKILL.md',
|
|
46
|
+
|
|
33
47
|
transformReference(skillName, raw) {
|
|
34
48
|
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
35
|
-
const ref =
|
|
49
|
+
const ref = `the rune-${skillName} skill`;
|
|
36
50
|
return isBackticked ? `\`${ref}\`` : ref;
|
|
37
51
|
},
|
|
38
52
|
|
|
@@ -41,14 +55,8 @@ export default {
|
|
|
41
55
|
},
|
|
42
56
|
|
|
43
57
|
generateHeader(skill) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`description: "${skill.description}"`,
|
|
47
|
-
'globs: []',
|
|
48
|
-
`alwaysApply: ${skill.layer === 'L0'}`,
|
|
49
|
-
'---',
|
|
50
|
-
'',
|
|
51
|
-
].join('\n');
|
|
58
|
+
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
59
|
+
return ['---', `name: rune-${skill.name}`, `description: "${desc}"`, '---', '', ''].join('\n');
|
|
52
60
|
},
|
|
53
61
|
|
|
54
62
|
generateFooter() {
|
|
@@ -60,7 +68,7 @@ export default {
|
|
|
60
68
|
},
|
|
61
69
|
|
|
62
70
|
scriptsDir(skillName) {
|
|
63
|
-
return `rune-${skillName}
|
|
71
|
+
return `rune-${skillName}/scripts`;
|
|
64
72
|
},
|
|
65
73
|
|
|
66
74
|
postProcess(content) {
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gemini CLI Adapter
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Emits SKILL.md files into .gemini/skills/{name}/ directories — Gemini CLI's
|
|
5
|
+
* native Agent Skills format. Gemini CLI discovers skills automatically from
|
|
6
|
+
* .gemini/skills/ (and the .agents/skills/ interop alias) and lazy-loads the
|
|
7
|
+
* full SKILL.md only when a skill is invoked — no more bundling every skill
|
|
8
|
+
* into GEMINI.md (which loaded all 65 skills as always-on context).
|
|
9
9
|
*
|
|
10
|
-
* Gemini
|
|
11
|
-
* Gemini
|
|
10
|
+
* Gemini skills dir: .gemini/skills/
|
|
11
|
+
* Gemini skill format: .gemini/skills/{name}/SKILL.md
|
|
12
|
+
* Gemini project context: GEMINI.md (slim pointer, project root)
|
|
12
13
|
*
|
|
13
|
-
* @see https://
|
|
14
|
-
* @see https://
|
|
14
|
+
* @see https://geminicli.com/docs/cli/skills/
|
|
15
|
+
* @see https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/skills.md
|
|
15
16
|
*
|
|
16
17
|
* MODEL TIER MAPPING (v2.18+):
|
|
17
|
-
* Gemini CLI exposes
|
|
18
|
-
* translate to Gemini families
|
|
19
|
-
*
|
|
20
|
-
* config, not from rule body.
|
|
18
|
+
* Gemini CLI exposes Pro / Flash / Flash-Lite families. Anthropic tier names
|
|
19
|
+
* translate to Gemini families as a hint comment — Gemini CLI reads model
|
|
20
|
+
* from --model flag / config, not from the skill body.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { readFile } from 'node:fs/promises';
|
|
24
|
-
import nodePath from 'node:path';
|
|
25
23
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
|
26
24
|
|
|
27
25
|
const MODEL_MAP = {
|
|
@@ -38,23 +36,24 @@ const TOOL_MAP = {
|
|
|
38
36
|
Grep: 'search file contents',
|
|
39
37
|
Bash: 'run a shell command',
|
|
40
38
|
TodoWrite: 'track task progress',
|
|
41
|
-
Skill: '
|
|
39
|
+
Skill: 'invoke the rune-{name} skill',
|
|
42
40
|
Agent: 'execute the workflow',
|
|
43
41
|
};
|
|
44
42
|
|
|
45
43
|
export default {
|
|
46
44
|
name: 'gemini',
|
|
47
|
-
outputDir: 'gemini/skills',
|
|
45
|
+
outputDir: '.gemini/skills',
|
|
48
46
|
fileExtension: '.md',
|
|
49
47
|
skillPrefix: 'rune-',
|
|
50
48
|
skillSuffix: '',
|
|
51
49
|
|
|
52
|
-
//
|
|
53
|
-
useSkillDirectories:
|
|
50
|
+
// Gemini CLI uses directory-per-skill: .gemini/skills/{name}/SKILL.md
|
|
51
|
+
useSkillDirectories: true,
|
|
52
|
+
skillFileName: 'SKILL.md',
|
|
54
53
|
|
|
55
54
|
transformReference(skillName, raw) {
|
|
56
55
|
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
57
|
-
const ref = `the rune-${skillName}
|
|
56
|
+
const ref = `the rune-${skillName} skill`;
|
|
58
57
|
return isBackticked ? `\`${ref}\`` : ref;
|
|
59
58
|
},
|
|
60
59
|
|
|
@@ -80,46 +79,24 @@ export default {
|
|
|
80
79
|
},
|
|
81
80
|
|
|
82
81
|
scriptsDir(skillName) {
|
|
83
|
-
return `rune-${skillName}
|
|
82
|
+
return `rune-${skillName}/scripts`;
|
|
84
83
|
},
|
|
85
84
|
|
|
86
85
|
postProcess(content) {
|
|
87
86
|
return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
|
|
88
87
|
},
|
|
89
88
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
// requires relative paths (resolved against outputRoot by the emitter).
|
|
93
|
-
async generateExtraFiles({ stats, outputDir }) {
|
|
94
|
-
const skillFiles = [...stats.files]
|
|
95
|
-
.filter((f) => f.startsWith('rune-') && f.endsWith('.md') && !f.includes('/') && !f.includes('\\'))
|
|
96
|
-
.sort();
|
|
97
|
-
|
|
98
|
-
const sections = [];
|
|
99
|
-
for (const fname of skillFiles) {
|
|
100
|
-
const sourcePath = nodePath.join(outputDir, fname);
|
|
101
|
-
try {
|
|
102
|
-
const raw = await readFile(sourcePath, 'utf-8');
|
|
103
|
-
// Strip frontmatter — Gemini reads plain markdown.
|
|
104
|
-
const stripped = raw.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
105
|
-
const skillName = fname.replace(/^rune-/, '').replace(/\.md$/, '');
|
|
106
|
-
sections.push(`\n\n## rune-${skillName}\n\n${stripped}`);
|
|
107
|
-
} catch {
|
|
108
|
-
// Skip unreadable per-skill file — sections array still produces valid bundle.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
89
|
+
// Slim GEMINI.md pointer — skills themselves are lazy-loaded natively.
|
|
90
|
+
generateExtraFiles({ stats }) {
|
|
112
91
|
const geminiMd = [
|
|
113
92
|
'# Rune — Project Configuration',
|
|
114
93
|
'',
|
|
115
94
|
'Rune is an interconnected skill ecosystem for AI coding assistants.',
|
|
116
|
-
`${stats.skillCount} core skills + ${stats.packCount} extension packs
|
|
117
|
-
'',
|
|
118
|
-
'When a user request matches a skill\'s domain (e.g. "implement", "review", "debug"), follow the matching `## rune-<name>` section.',
|
|
95
|
+
`${stats.skillCount} core skills + ${stats.packCount} extension packs.`,
|
|
119
96
|
'',
|
|
120
|
-
'
|
|
97
|
+
'Per-skill Agent Skills live under `.gemini/skills/rune-<name>/SKILL.md`. Gemini CLI discovers and lazy-loads them automatically.',
|
|
121
98
|
'',
|
|
122
|
-
|
|
99
|
+
'When a user request matches a skill\'s domain (e.g. "implement", "review", "debug"), invoke the matching rune-<name> skill.',
|
|
123
100
|
'',
|
|
124
101
|
'---',
|
|
125
102
|
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Qoder Adapter
|
|
3
3
|
*
|
|
4
|
-
* Emits
|
|
5
|
-
*
|
|
4
|
+
* Emits SKILL.md files into .qoder/skills/{name}/ directories — Qoder's
|
|
5
|
+
* documented project-level Agent Skills location (works identically in
|
|
6
|
+
* Qoder IDE and CLI). Qoder also reads AGENTS.md as its project-context file.
|
|
6
7
|
*
|
|
7
|
-
* Qoder
|
|
8
|
-
* Qoder
|
|
8
|
+
* Qoder skills dir: .qoder/skills/
|
|
9
|
+
* Qoder skill format: .qoder/skills/{name}/SKILL.md
|
|
9
10
|
* Qoder project context: AGENTS.md (open AGENTS.md standard)
|
|
10
11
|
*
|
|
11
|
-
* @see https://docs.qoder.com/
|
|
12
|
+
* @see https://docs.qoder.com/extensions/skills
|
|
12
13
|
* @see https://agents.md/
|
|
13
14
|
*
|
|
14
15
|
* MODEL TIER MAPPING (v2.18+):
|
|
@@ -32,23 +33,24 @@ const TOOL_MAP = {
|
|
|
32
33
|
Grep: 'search file contents',
|
|
33
34
|
Bash: 'run a shell command',
|
|
34
35
|
TodoWrite: 'track task progress',
|
|
35
|
-
Skill: '
|
|
36
|
+
Skill: 'invoke the rune-{name} skill',
|
|
36
37
|
Agent: 'execute the workflow',
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export default {
|
|
40
41
|
name: 'qoder',
|
|
41
|
-
outputDir: '.qoder/
|
|
42
|
+
outputDir: '.qoder/skills',
|
|
42
43
|
fileExtension: '.md',
|
|
43
44
|
skillPrefix: 'rune-',
|
|
44
45
|
skillSuffix: '',
|
|
45
46
|
|
|
46
|
-
// Qoder
|
|
47
|
-
useSkillDirectories:
|
|
47
|
+
// Qoder uses directory-per-skill: .qoder/skills/{name}/SKILL.md
|
|
48
|
+
useSkillDirectories: true,
|
|
49
|
+
skillFileName: 'SKILL.md',
|
|
48
50
|
|
|
49
51
|
transformReference(skillName, raw) {
|
|
50
52
|
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
51
|
-
const ref = `the rune-${skillName}
|
|
53
|
+
const ref = `the rune-${skillName} skill`;
|
|
52
54
|
return isBackticked ? `\`${ref}\`` : ref;
|
|
53
55
|
},
|
|
54
56
|
|
|
@@ -74,7 +76,7 @@ export default {
|
|
|
74
76
|
},
|
|
75
77
|
|
|
76
78
|
scriptsDir(skillName) {
|
|
77
|
-
return `rune-${skillName}
|
|
79
|
+
return `rune-${skillName}/scripts`;
|
|
78
80
|
},
|
|
79
81
|
|
|
80
82
|
postProcess(content) {
|
|
@@ -89,9 +91,7 @@ export default {
|
|
|
89
91
|
'Rune is an interconnected skill ecosystem for AI coding assistants.',
|
|
90
92
|
`${stats.skillCount} core skills + ${stats.packCount} extension packs.`,
|
|
91
93
|
'',
|
|
92
|
-
'Per-skill
|
|
93
|
-
'',
|
|
94
|
-
'Reference a skill by name (e.g. "follow the rune-cook rule") inside any chat — the rule file is auto-injected.',
|
|
94
|
+
'Per-skill Agent Skills live under `.qoder/skills/rune-<name>/SKILL.md`. Qoder discovers and loads them on demand.',
|
|
95
95
|
'',
|
|
96
96
|
'---',
|
|
97
97
|
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Qwen Coder Adapter
|
|
3
3
|
*
|
|
4
|
-
* Emits
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Emits SKILL.md files into .qwen/skills/{name}/ directories — Qwen Code's
|
|
5
|
+
* native Agent Skills format. Qwen Code discovers project skills from
|
|
6
|
+
* .qwen/skills/ and lazy-loads them on demand (browse via /skills) — no more
|
|
7
|
+
* QWEN.md @import lines that loaded every skill as always-on context.
|
|
7
8
|
*
|
|
8
|
-
* Qwen
|
|
9
|
-
* Qwen
|
|
10
|
-
* Qwen project context: QWEN.md
|
|
9
|
+
* Qwen skills dir: .qwen/skills/
|
|
10
|
+
* Qwen skill format: .qwen/skills/{name}/SKILL.md
|
|
11
|
+
* Qwen project context: QWEN.md (slim pointer, loaded hierarchically)
|
|
11
12
|
*
|
|
12
|
-
* @see https://qwenlm.github.io/qwen-code-docs/en/users/
|
|
13
|
+
* @see https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/
|
|
13
14
|
* @see https://github.com/QwenLM/qwen-code
|
|
14
15
|
*
|
|
15
16
|
* MODEL TIER MAPPING (v2.18+):
|
|
@@ -34,19 +35,20 @@ const TOOL_MAP = {
|
|
|
34
35
|
Grep: 'search file contents',
|
|
35
36
|
Bash: 'run a shell command',
|
|
36
37
|
TodoWrite: 'track task progress',
|
|
37
|
-
Skill: '
|
|
38
|
+
Skill: 'invoke the rune-{name} skill',
|
|
38
39
|
Agent: 'execute the workflow',
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
export default {
|
|
42
43
|
name: 'qwen',
|
|
43
|
-
outputDir: 'qwen/skills',
|
|
44
|
+
outputDir: '.qwen/skills',
|
|
44
45
|
fileExtension: '.md',
|
|
45
46
|
skillPrefix: 'rune-',
|
|
46
47
|
skillSuffix: '',
|
|
47
48
|
|
|
48
|
-
// Qwen
|
|
49
|
-
useSkillDirectories:
|
|
49
|
+
// Qwen Code uses directory-per-skill: .qwen/skills/{name}/SKILL.md
|
|
50
|
+
useSkillDirectories: true,
|
|
51
|
+
skillFileName: 'SKILL.md',
|
|
50
52
|
|
|
51
53
|
transformReference(skillName, raw) {
|
|
52
54
|
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
@@ -76,30 +78,22 @@ export default {
|
|
|
76
78
|
},
|
|
77
79
|
|
|
78
80
|
scriptsDir(skillName) {
|
|
79
|
-
return `rune-${skillName}
|
|
81
|
+
return `rune-${skillName}/scripts`;
|
|
80
82
|
},
|
|
81
83
|
|
|
82
84
|
postProcess(content) {
|
|
83
85
|
return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
|
|
84
86
|
},
|
|
85
87
|
|
|
86
|
-
//
|
|
88
|
+
// Slim QWEN.md pointer — skills themselves are lazy-loaded natively.
|
|
87
89
|
generateExtraFiles({ stats }) {
|
|
88
|
-
const skillFiles = stats.files
|
|
89
|
-
.filter((f) => f.startsWith('rune-') && f.endsWith('.md') && !f.includes('/') && !f.includes('\\'))
|
|
90
|
-
.sort();
|
|
91
|
-
|
|
92
90
|
const qwenMd = [
|
|
93
91
|
'# Rune — Project Configuration',
|
|
94
92
|
'',
|
|
95
93
|
'Rune is an interconnected skill ecosystem for AI coding assistants.',
|
|
96
94
|
`${stats.skillCount} core skills + ${stats.packCount} extension packs.`,
|
|
97
95
|
'',
|
|
98
|
-
'
|
|
99
|
-
'',
|
|
100
|
-
'Qwen Code loads each referenced file as part of this project context.',
|
|
101
|
-
'',
|
|
102
|
-
...skillFiles.map((f) => `@qwen/skills/${f}`),
|
|
96
|
+
'Per-skill Agent Skills live under `.qwen/skills/rune-<name>/SKILL.md`. Qwen Code discovers and lazy-loads them on demand (browse via /skills).',
|
|
103
97
|
'',
|
|
104
98
|
'---',
|
|
105
99
|
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Windsurf Adapter
|
|
3
3
|
*
|
|
4
|
-
* Emits .md
|
|
5
|
-
*
|
|
4
|
+
* Emits SKILL.md files into .windsurf/skills/{name}/ directories — Windsurf's
|
|
5
|
+
* native Cascade Skills format. Cascade uses progressive disclosure: only the
|
|
6
|
+
* skill's name and description are shown to the model by default; the full
|
|
7
|
+
* SKILL.md is loaded when Cascade invokes the skill (or via @skill-name).
|
|
8
|
+
*
|
|
9
|
+
* Windsurf skills dir: .windsurf/skills/
|
|
10
|
+
* Windsurf skill format: .windsurf/skills/{name}/SKILL.md
|
|
11
|
+
* @see https://docs.windsurf.com/windsurf/cascade/skills
|
|
12
|
+
*
|
|
13
|
+
* NOTE: .windsurf/rules/ is still used by the runtime-hooks installer
|
|
14
|
+
* (adapters/hooks/windsurf.js) — rules are the correct vehicle for always-on
|
|
15
|
+
* hook context. Only skill emission moved to .windsurf/skills/.
|
|
6
16
|
*
|
|
7
17
|
* MODEL TIER MAPPING (v2.15+):
|
|
8
18
|
* No-op. Windsurf's Anthropic API integration understands `model: opus|sonnet|haiku`
|
|
@@ -19,20 +29,24 @@ const TOOL_MAP = {
|
|
|
19
29
|
Grep: 'search for text in files',
|
|
20
30
|
Bash: 'run a shell command',
|
|
21
31
|
TodoWrite: 'track task progress',
|
|
22
|
-
Skill: '
|
|
32
|
+
Skill: 'invoke the named skill',
|
|
23
33
|
Agent: 'execute the workflow',
|
|
24
34
|
};
|
|
25
35
|
|
|
26
36
|
export default {
|
|
27
37
|
name: 'windsurf',
|
|
28
|
-
outputDir: '.windsurf/
|
|
38
|
+
outputDir: '.windsurf/skills',
|
|
29
39
|
fileExtension: '.md',
|
|
30
40
|
skillPrefix: 'rune-',
|
|
31
41
|
skillSuffix: '',
|
|
32
42
|
|
|
43
|
+
// Windsurf uses directory-per-skill: .windsurf/skills/{name}/SKILL.md
|
|
44
|
+
useSkillDirectories: true,
|
|
45
|
+
skillFileName: 'SKILL.md',
|
|
46
|
+
|
|
33
47
|
transformReference(skillName, raw) {
|
|
34
48
|
const isBackticked = raw.startsWith('`') && raw.endsWith('`');
|
|
35
|
-
const ref = `the rune-${skillName}
|
|
49
|
+
const ref = `the rune-${skillName} skill`;
|
|
36
50
|
return isBackticked ? `\`${ref}\`` : ref;
|
|
37
51
|
},
|
|
38
52
|
|
|
@@ -41,7 +55,8 @@ export default {
|
|
|
41
55
|
},
|
|
42
56
|
|
|
43
57
|
generateHeader(skill) {
|
|
44
|
-
|
|
58
|
+
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
59
|
+
return ['---', `name: rune-${skill.name}`, `description: "${desc}"`, '---', '', ''].join('\n');
|
|
45
60
|
},
|
|
46
61
|
|
|
47
62
|
generateFooter() {
|
|
@@ -53,7 +68,7 @@ export default {
|
|
|
53
68
|
},
|
|
54
69
|
|
|
55
70
|
scriptsDir(skillName) {
|
|
56
|
-
return `rune-${skillName}
|
|
71
|
+
return `rune-${skillName}/scripts`;
|
|
57
72
|
},
|
|
58
73
|
|
|
59
74
|
postProcess(content) {
|
package/compiler/emitter.js
CHANGED
|
@@ -65,7 +65,7 @@ async function discoverPacks(extensionsDir, enabledPacks = null) {
|
|
|
65
65
|
* Copies directories except SKILL.md (already processed) and denylisted dirs
|
|
66
66
|
*
|
|
67
67
|
* @param {string} sourceSkillDir - e.g. skills/cook/
|
|
68
|
-
* @param {string} outputSkillDir - e.g. .
|
|
68
|
+
* @param {string} outputSkillDir - e.g. .agents/skills/rune-cook/
|
|
69
69
|
* @returns {Promise<string[]>} list of copied directory names
|
|
70
70
|
*/
|
|
71
71
|
const COPY_DENYLIST = new Set(['.git', 'node_modules', '__pycache__', '.DS_Store', '.venv', '.env']);
|
|
@@ -74,7 +74,8 @@ async function copySkillExtraDirs(sourceSkillDir, outputSkillDir) {
|
|
|
74
74
|
if (!existsSync(sourceSkillDir)) return [];
|
|
75
75
|
|
|
76
76
|
const entries = await readdir(sourceSkillDir, { withFileTypes: true });
|
|
77
|
-
|
|
77
|
+
// scripts/ is excluded: copyScriptsDir handles it explicitly (with stats counting)
|
|
78
|
+
const dirs = entries.filter((e) => e.isDirectory() && !COPY_DENYLIST.has(e.name) && e.name !== 'scripts');
|
|
78
79
|
|
|
79
80
|
const copied = [];
|
|
80
81
|
for (const dir of dirs) {
|
|
@@ -91,7 +92,7 @@ async function copySkillExtraDirs(sourceSkillDir, outputSkillDir) {
|
|
|
91
92
|
* Copy scripts directory from skill source to output.
|
|
92
93
|
*
|
|
93
94
|
* @param {string} sourceScriptsDir - e.g. skills/slides/scripts/
|
|
94
|
-
* @param {string} outputScriptsDir - e.g. .cursor/
|
|
95
|
+
* @param {string} outputScriptsDir - e.g. .cursor/skills/rune-slides/scripts/
|
|
95
96
|
* @returns {Promise<string[]>} list of copied file paths
|
|
96
97
|
*/
|
|
97
98
|
async function copyScriptsDir(sourceScriptsDir, outputScriptsDir) {
|
|
@@ -524,7 +525,7 @@ export async function buildAll({
|
|
|
524
525
|
let skillDir = null;
|
|
525
526
|
|
|
526
527
|
if (adapter.useSkillDirectories) {
|
|
527
|
-
// Directory-per-skill: .
|
|
528
|
+
// Directory-per-skill: .agents/skills/rune-{name}/SKILL.md
|
|
528
529
|
const dirName = `${adapter.skillPrefix}${parsed.name}`;
|
|
529
530
|
skillDir = path.join(outputDir, dirName);
|
|
530
531
|
await mkdir(skillDir, { recursive: true });
|
package/compiler/parser.js
CHANGED
|
@@ -34,6 +34,15 @@ function parseFrontmatter(content) {
|
|
|
34
34
|
let nestedKey = null;
|
|
35
35
|
const _nestedObj = {};
|
|
36
36
|
|
|
37
|
+
// Strip outer quotes; for double-quoted scalars also unescape interior \"
|
|
38
|
+
// so consumers (adapter generateHeader) hold clean text and can re-escape
|
|
39
|
+
// exactly once. Without this, `\"` survives into skill.description and a
|
|
40
|
+
// later .replace(/"/g, '\\"') turns it into `\\"` — invalid YAML.
|
|
41
|
+
const parseScalar = (rawValue) => {
|
|
42
|
+
const stripped = rawValue.replace(/^["']|["']$/g, '');
|
|
43
|
+
return rawValue.startsWith('"') ? stripped.replace(/\\"/g, '"') : stripped;
|
|
44
|
+
};
|
|
45
|
+
|
|
37
46
|
for (const line of raw.split('\n')) {
|
|
38
47
|
const trimmed = line.trim();
|
|
39
48
|
if (!trimmed) continue;
|
|
@@ -60,7 +69,7 @@ function parseFrontmatter(content) {
|
|
|
60
69
|
|
|
61
70
|
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
62
71
|
if (kvMatch) {
|
|
63
|
-
const rawValue = kvMatch[2]
|
|
72
|
+
const rawValue = parseScalar(kvMatch[2]);
|
|
64
73
|
// Comma-separated list fields → parse as array
|
|
65
74
|
if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
|
|
66
75
|
frontmatter[nestedKey][kvMatch[1]] = rawValue
|
|
@@ -79,7 +88,7 @@ function parseFrontmatter(content) {
|
|
|
79
88
|
nestedKey = null;
|
|
80
89
|
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
81
90
|
if (kvMatch) {
|
|
82
|
-
const value = kvMatch[2]
|
|
91
|
+
const value = parseScalar(kvMatch[2]);
|
|
83
92
|
// Comma-separated list fields at top level too (Pro/Business packs)
|
|
84
93
|
if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
|
|
85
94
|
frontmatter[kvMatch[1]] = value
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
4
4
|
"description": "65-skill mesh for AI coding assistants — runtime auto-discipline via native hooks (Claude/Cursor/Windsurf/Antigravity), 5-layer architecture, 204 connections + 43 signals, multi-platform compiler. converge (L3) scans spec vs code so dead-button/frontend-only implementations can't ship.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|