@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 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.22.2Convergence, Dogfooded)
86
+ ## What's New (v2.23.0Native 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, '.mdc');
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 generates .mdc frontmatter with alwaysApply', () => {
131
+ test('cursor adapter emits Agent Skills (dir-per-skill SKILL.md)', () => {
118
132
  const cursor = getAdapter('cursor');
119
- const l0Skill = { name: 'router', layer: 'L0', group: 'meta', description: 'Routes tasks' };
120
- const l2Skill = { name: 'fix', layer: 'L2', group: 'workflow', description: 'Fix code' };
133
+ assert.strictEqual(cursor.outputDir, '.cursor/skills');
134
+ assert.strictEqual(cursor.useSkillDirectories, true);
135
+ assert.strictEqual(cursor.skillFileName, 'SKILL.md');
121
136
 
122
- const l0Header = cursor.generateHeader(l0Skill);
123
- assert.ok(l0Header.includes('alwaysApply: true'));
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
- const l2Header = cursor.generateHeader(l2Skill);
126
- assert.ok(l2Header.includes('alwaysApply: false'));
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/rules and emits AGENTS.md', async () => {
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/rules');
142
- assert.strictEqual(qoder.useSkillDirectories, false);
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('Rune')));
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/instructions with .instructions.md ext', async () => {
176
+ test('copilot adapter targets .github/skills with SKILL.md format', async () => {
149
177
  const copilot = getAdapter('copilot');
150
- assert.strictEqual(copilot.outputDir, '.github/instructions');
151
- assert.strictEqual(copilot.fileExtension, '.instructions.md');
152
- const header = copilot.generateHeader({ name: 'cook', layer: 'L1', group: 'orchestrator', description: 'Test' });
153
- // Per docs.github.com Copilot CLI custom-instructions spec, only `applyTo` is a
154
- // documented frontmatter field. Description and tier-hint must live in the body.
155
- assert.ok(header.includes('applyTo: "**"'));
156
- assert.ok(!/^description:/m.test(header.split('---')[1] || ''), 'description must NOT appear in frontmatter');
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 with @import lines', async () => {
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('@qwen/skills/rune-cook.md'));
213
- assert.ok(qwenMd.content.includes('@qwen/skills/rune-fix.md'));
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 declares generateExtraFiles for bundled GEMINI.md', () => {
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(typeof gemini.generateExtraFiles, 'function');
220
- // Full bundle test happens in pipeline test (needs filesystem) — contract checked here.
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(path.join(outputRoot, adapter.outputDir, 'rune-ext-test-pack.mdc'), 'utf-8');
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.mdc'), 'utf-8');
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 .mdc', () => {
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('.mdc'), 'cursor output should contain .mdc references');
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 .mdc file', () => {
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 .mdc should start with YAML frontmatter');
92
- assert.ok(assembled.includes('description:'), 'cursor .mdc should have description');
93
- assert.ok(assembled.includes('alwaysApply:'), 'cursor .mdc should have alwaysApply');
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 ['cursor', 'windsurf', 'antigravity', 'generic', 'openclaw']) {
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 ['codex', 'opencode']) {
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 copied to sibling dir, placeholder resolved', async () => {
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 dir exists with files
157
- const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide-scripts');
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 .mdc output
162
- const mdc = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide.mdc'), 'utf-8');
163
- assert.ok(!mdc.includes('{scripts_dir}'), 'placeholder should be resolved');
164
- assert.ok(mdc.includes('.cursor/rules/rune-test-slide-scripts/build-deck.js'), 'resolved path missing');
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-scripts')),
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: .codex/skills/rune-test-slide/scripts/
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('.codex/skills/rune-test-slide/scripts/build-deck.js'), 'resolved path missing');
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-scripts', 'build-deck.js'),
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 (flat)
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 cursorMdc = await readFile(path.join(cursorOut, '.cursor/rules/rune-test-slide.mdc'), 'utf-8');
271
- assert.ok(cursorMdc.includes('.cursor/rules/rune-test-slide-scripts/'), 'cursor path');
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, '.codex/skills/rune-test-slide/SKILL.md'), 'utf-8');
278
- assert.ok(codexMd.includes('.codex/skills/rune-test-slide/scripts/'), 'codex path');
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 .mdc format', () => {
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('@rune-cook.mdc'));
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', () => {
@@ -74,11 +74,7 @@ export default {
74
74
  },
75
75
 
76
76
  scriptsDir(skillName) {
77
- return `rune-${skillName}-scripts`;
78
- },
79
-
80
- referencesDir(skillName) {
81
- return `rune-${skillName}-references`;
77
+ return `rune-${skillName}/scripts`;
82
78
  },
83
79
 
84
80
  postProcess(content) {
@@ -1,13 +1,17 @@
1
1
  /**
2
2
  * OpenAI Codex Adapter
3
3
  *
4
- * Emits SKILL.md files into .codex/skills/{name}/ directories.
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: .codex/skills/
10
- * Codex skill format: .codex/skills/{name}/SKILL.md
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: '.codex/skills',
44
+ outputDir: '.agents/skills',
41
45
  fileExtension: '.md',
42
46
  skillPrefix: 'rune-',
43
47
  skillSuffix: '',
44
48
 
45
- // Codex uses directory-per-skill: .codex/skills/{name}/SKILL.md
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: .codex/skills/',
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 CLI Adapter
2
+ * GitHub Copilot Adapter
3
3
  *
4
- * Emits per-skill instruction files into .github/instructions/ — the documented
5
- * convention for GitHub Copilot custom instructions (read by both the CLI and
6
- * the GitHub Copilot IDE plugin family).
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 instructions dir: .github/instructions/
9
- * Copilot instruction file: .github/instructions/rune-{name}.instructions.md
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
- * Each .instructions.md uses YAML frontmatter with `applyTo` for path scoping.
13
- * Default applyTo: "**" means "apply to all files."
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: 'follow the referenced instructions',
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/instructions',
47
- fileExtension: '.instructions.md',
44
+ outputDir: '.github/skills',
45
+ fileExtension: '.md',
48
46
  skillPrefix: 'rune-',
49
47
  skillSuffix: '',
50
48
 
51
- // Copilot instructions are flat per-skill files, not directories.
52
- useSkillDirectories: false,
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} instructions`;
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
- // Per docs.github.com Copilot CLI custom-instructions spec, the only
66
- // documented frontmatter key for `.instructions.md` is `applyTo`. Other
67
- // metadata (description, tier hint) belongs in the markdown body so it
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 = ['---', 'applyTo: "**"', '---', '', `# rune-${skill.name}`, '', `> ${desc}`];
72
- if (tierHint) lines.push('', `<!-- tier-hint: ${tierHint} -->`);
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}-scripts`;
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 instructions live under \`.github/instructions/rune-<name>.instructions.md\` (${stats.skillCount} skills + ${stats.packCount} packs). Copilot loads them based on each file's \`applyTo\` glob.`,
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 following the corresponding rune-<name>.instructions.md over freestyling.',
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 custom instructions: `.github/instructions/rune-<name>.instructions.md`',
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 .mdc rule files for .cursor/rules/ directory.
5
- * Uses @file references for cross-skill mesh.
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: 'follow the referenced skill rules',
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/rules',
29
- fileExtension: '.mdc',
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 = `@rune-${skillName}.mdc`;
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
- return [
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}-scripts`;
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
- * Gemini CLI loads a single GEMINI.md at project root for context. It does NOT
5
- * support per-skill imports as of writing so the canonical output is a
6
- * bundled GEMINI.md with every skill concatenated under `## rune-<name>` H2
7
- * sections. Per-skill files are still emitted under gemini/skills/ for human
8
- * inspection and forward-compatibility if Gemini adds @import support later.
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 context file: GEMINI.md (project root)
11
- * Gemini per-skill files: gemini/skills/rune-{name}.md (forward-compat staging)
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://github.com/google-gemini/gemini-cli
14
- * @see https://geminicli.com/docs/reference/configuration/
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 1.5 Pro / 1.5 Flash / 2.0 Flash etc. Anthropic tier names
18
- * translate to Gemini families: opus→1.5-pro, sonnet→1.5-flash, haiku→
19
- * 2.0-flash-lite. Hint only Gemini CLI reads model from --model flag /
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: 'follow the referenced rune-{name} section in GEMINI.md',
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
- // Per-skill files staged for forward compat; canonical entry is bundled GEMINI.md.
53
- useSkillDirectories: false,
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} section in GEMINI.md`;
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}-scripts`;
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
- // Bundle every per-skill file into a single GEMINI.md with H2 section per skill.
91
- // Gemini CLI loads GEMINI.md from project root as its context file. Hook contract
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, bundled below.`,
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
- '> Per-skill files are staged in `gemini/skills/` for forward-compat if Gemini CLI adds @import support.',
97
+ 'Per-skill Agent Skills live under `.gemini/skills/rune-<name>/SKILL.md`. Gemini CLI discovers and lazy-loads them automatically.',
121
98
  '',
122
- ...sections,
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 per-skill rule files into .qoder/rules/ — Qoder's documented project-level
5
- * rule directory. Qoder also reads AGENTS.md as its project-context file.
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 rules dir: .qoder/rules/
8
- * Qoder rule file: .qoder/rules/rune-{name}.md (one file per skill)
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/user-guide/rules
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: 'follow the referenced rune-{name} rule',
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/rules',
42
+ outputDir: '.qoder/skills',
42
43
  fileExtension: '.md',
43
44
  skillPrefix: 'rune-',
44
45
  skillSuffix: '',
45
46
 
46
- // Qoder rules are flat .md files, not directory-per-skill
47
- useSkillDirectories: false,
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} rule`;
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}-scripts`;
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 rules live under `.qoder/rules/rune-<name>.md`. Qoder loads them automatically.',
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 per-skill rule files into qwen/skills/ and a top-level QWEN.md that
5
- * uses Qwen Code's @import syntax to load each rule file. Qwen Code loads
6
- * QWEN.md hierarchically (cwd parent ~/.qwen/QWEN.md).
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 rules dir: qwen/skills/
9
- * Qwen rule file: qwen/skills/rune-{name}.md
10
- * Qwen project context: QWEN.md at project root (with @path imports)
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/configuration/settings/
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: 'follow the imported rune-{name} 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 skills are flat per-skill markdown files imported from QWEN.md.
49
- useSkillDirectories: false,
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}-scripts`;
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
- // Emit QWEN.md at project root with @import lines for every per-skill file.
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
- '## Loaded Skills',
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 rule files for .windsurf/rules/ directory.
5
- * Uses prose references for cross-skill mesh (no @file support).
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: 'follow the referenced skill workflow',
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/rules',
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} rule file`;
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
- return `# rune-${skill.name}\n\n> Layer: ${skill.layer} | Group: ${skill.group}\n\n`;
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}-scripts`;
71
+ return `rune-${skillName}/scripts`;
57
72
  },
58
73
 
59
74
  postProcess(content) {
@@ -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. .codex/skills/rune-cook/
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
- const dirs = entries.filter((e) => e.isDirectory() && !COPY_DENYLIST.has(e.name));
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/rules/rune-slides-scripts/
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: .codex/skills/rune-{name}/SKILL.md
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 });
@@ -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].replace(/^["']|["']$/g, '');
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].replace(/^["']|["']$/g, '');
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.22.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": {