@rune-kit/rune 2.17.1 → 2.18.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
@@ -5,7 +5,7 @@
5
5
  <p align="center">
6
6
  <strong>Less skills. Deeper connections.</strong><br>
7
7
  A lean, interconnected skill ecosystem for AI coding assistants.<br>
8
- 64 skills · 203 connections · 40 signals · 8 platforms · MIT
8
+ 64 skills · 203 connections · 40 signals · 13 platforms · MIT
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -16,7 +16,7 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <strong>Claude Code</strong> (native plugin) · <strong>Cursor</strong> · <strong>Windsurf</strong> · <strong>Google Antigravity</strong> · <strong>OpenAI Codex</strong> · <strong>OpenCode</strong> · any AI IDE
19
+ <strong>Claude Code</strong> (native plugin) · <strong>Cursor</strong> · <strong>Windsurf</strong> · <strong>Google Antigravity</strong> · <strong>OpenAI Codex</strong> · <strong>OpenCode</strong> · <strong>Aider</strong> · <strong>GitHub Copilot CLI</strong> · <strong>Gemini CLI</strong> · <strong>Qoder</strong> · <strong>Qwen Coder</strong> · any AI IDE
20
20
  </p>
21
21
 
22
22
  ## Why Rune?
@@ -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.17.1One-Command Setup Wizard)
86
+ ## What's New (v2.18.0Cross-Platform Reach + Discipline Tightening)
87
+
88
+ > **v2.18.0 (2026-05-15):** Compiler grew from 8 → 13 platforms with five new adapters: **Aider** (per-skill `aider/rules/` + auto-generated `.aider.conf.yml` `read:` array), **GitHub Copilot CLI** (`.github/instructions/*.instructions.md` w/ documented `applyTo` YAML), **Gemini CLI** (bundled `GEMINI.md` for single-file context), **Qoder** (`.qoder/rules/` + AGENTS.md), **Qwen Coder** (`qwen/skills/` + `QWEN.md` with `@import`). New `adapter.generateExtraFiles()` hook with path-traversal guard + frozen stats snapshot — replaces ad-hoc adapter special-cases (codex AGENTS.md migrated). Discipline tightening: `design` v0.6.0 adds Step 2.9 Rules 4/5/6 (measurable constraints, no #000/#fff/lorem ipsum, CJK-first font stack); `skill-forge` v1.9.0 adds soft `examples/` convention for output-format skills; `sentinel-env` v0.4.0 expands Tier 8 binary detection (Bun, Cargo, Deno, Volta, asdf, proto). New `CONTRIBUTING.md` "What we don't accept" non-goals section. Source: graft from `nexu-io/html-anything` (Apache-2.0). CI 1435/1435.
89
+
90
+ ### Previous (v2.17.1 — One-Command Setup Wizard)
87
91
 
88
92
  > **v2.17.1 (2026-05-06):** New `rune setup` interactive wizard collapses the multi-step `cd <project> && export RUNE_PRO_ROOT && rune hooks install --preset gentle --tier pro` workflow into one command — auto-detects Pro/Business tiers across env var / sibling / well-known paths, asks for scope (current project / global) + preset, installs hooks. New `--global` flag on `rune hooks install` writes to `~/.claude/settings.json` (every Claude Code session, regardless of project). Non-interactive mode via `--here` / `--global` / `--tier` / `--preset` / `--dry` flags. Anti-paywall — wizard ships in Free, NOT Pro/Business (tier-agnostic infrastructure UX). Doc sweep: README "One-Command Setup", HOOKS.md restructure, agent skill-routing row for "set up rune". CI 1376/1376.
89
93
 
@@ -243,7 +247,7 @@ The wizard auto-detects what you have:
243
247
  ```
244
248
  Rune Setup Wizard
245
249
  ──────────────────
246
- Free version: 2.17.1 (cached)
250
+ Free version: 2.18.0 (cached)
247
251
  Pro detected: sibling (../Pro) (v1.1.0)
248
252
  Business: not detected
249
253
 
@@ -131,6 +131,86 @@ describe('Anthropic-native adapters are no-op for model field', () => {
131
131
  });
132
132
  });
133
133
 
134
+ describe('qoder adapter model mapping (provider-agnostic)', () => {
135
+ const qoder = getAdapter('qoder');
136
+
137
+ test('opus translates to tier:heavy', () => {
138
+ const header = qoder.generateHeader({ ...baseSkill, model: 'opus' });
139
+ assert.match(header, /model: tier:heavy/);
140
+ });
141
+
142
+ test('omitted model produces no model line', () => {
143
+ const header = qoder.generateHeader({ ...baseSkill });
144
+ assert.doesNotMatch(header, /model:/);
145
+ });
146
+ });
147
+
148
+ describe('copilot adapter model mapping (tier-hint comment)', () => {
149
+ const copilot = getAdapter('copilot');
150
+
151
+ test('opus emits tier-hint:tier:heavy as comment (Copilot ignores model field)', () => {
152
+ const header = copilot.generateHeader({ ...baseSkill, model: 'opus' });
153
+ assert.match(header, /tier-hint: tier:heavy/);
154
+ });
155
+
156
+ test('omitted model produces no tier-hint line', () => {
157
+ const header = copilot.generateHeader({ ...baseSkill });
158
+ assert.doesNotMatch(header, /tier-hint/);
159
+ });
160
+ });
161
+
162
+ describe('aider adapter model mapping (inline header)', () => {
163
+ const aider = getAdapter('aider');
164
+
165
+ test('opus appears as tier:heavy inline', () => {
166
+ const header = aider.generateHeader({ ...baseSkill, model: 'opus' });
167
+ assert.match(header, /tier:heavy/);
168
+ });
169
+
170
+ test('omitted model produces no tier line', () => {
171
+ const header = aider.generateHeader({ ...baseSkill });
172
+ assert.doesNotMatch(header, /tier:/);
173
+ });
174
+ });
175
+
176
+ describe('qwen adapter model mapping (Qwen family)', () => {
177
+ const qwen = getAdapter('qwen');
178
+
179
+ test('opus translates to qwen3-coder-plus', () => {
180
+ const header = qwen.generateHeader({ ...baseSkill, model: 'opus' });
181
+ assert.match(header, /qwen3-coder-plus/);
182
+ });
183
+
184
+ test('sonnet translates to qwen3-coder', () => {
185
+ const header = qwen.generateHeader({ ...baseSkill, model: 'sonnet' });
186
+ assert.match(header, /qwen3-coder(?!-)/);
187
+ });
188
+
189
+ test('haiku translates to qwen3-coder-flash', () => {
190
+ const header = qwen.generateHeader({ ...baseSkill, model: 'haiku' });
191
+ assert.match(header, /qwen3-coder-flash/);
192
+ });
193
+ });
194
+
195
+ describe('gemini adapter model mapping (Gemini family)', () => {
196
+ const gemini = getAdapter('gemini');
197
+
198
+ test('opus translates to gemini-2.5-pro', () => {
199
+ const header = gemini.generateHeader({ ...baseSkill, model: 'opus' });
200
+ assert.match(header, /gemini-2\.5-pro/);
201
+ });
202
+
203
+ test('sonnet translates to gemini-2.5-flash', () => {
204
+ const header = gemini.generateHeader({ ...baseSkill, model: 'sonnet' });
205
+ assert.match(header, /gemini-2\.5-flash/);
206
+ });
207
+
208
+ test('haiku translates to gemini-2.0-flash-lite', () => {
209
+ const header = gemini.generateHeader({ ...baseSkill, model: 'haiku' });
210
+ assert.match(header, /gemini-2\.0-flash-lite/);
211
+ });
212
+ });
213
+
134
214
  describe('cross-adapter consistency', () => {
135
215
  test('all 5 non-Anthropic adapters emit a model line for opus skills', () => {
136
216
  const adapterNames = ['codex', 'antigravity', 'opencode', 'openclaw', 'generic'];
@@ -4,9 +4,23 @@ import { getAdapter, listPlatforms } from '../adapters/index.js';
4
4
 
5
5
  // --- Adapter registry ---
6
6
 
7
- test('listPlatforms returns all 8 platform adapters', () => {
7
+ test('listPlatforms returns all 13 platform adapters', () => {
8
8
  const adapters = listPlatforms();
9
- const expected = ['claude', 'cursor', 'windsurf', 'antigravity', 'codex', 'opencode', 'openclaw', 'generic'];
9
+ const expected = [
10
+ 'claude',
11
+ 'cursor',
12
+ 'windsurf',
13
+ 'antigravity',
14
+ 'codex',
15
+ 'opencode',
16
+ 'openclaw',
17
+ 'generic',
18
+ 'aider',
19
+ 'copilot',
20
+ 'gemini',
21
+ 'qoder',
22
+ 'qwen',
23
+ ];
10
24
  for (const name of expected) {
11
25
  assert.ok(adapters.includes(name), `missing adapter: ${name}`);
12
26
  }
@@ -35,7 +49,19 @@ const REQUIRED_METHODS = [
35
49
 
36
50
  const REQUIRED_PROPS = ['name', 'outputDir', 'fileExtension', 'skillPrefix', 'skillSuffix'];
37
51
 
38
- const ADAPTER_NAMES = ['cursor', 'windsurf', 'antigravity', 'codex', 'opencode', 'generic'];
52
+ const ADAPTER_NAMES = [
53
+ 'cursor',
54
+ 'windsurf',
55
+ 'antigravity',
56
+ 'codex',
57
+ 'opencode',
58
+ 'generic',
59
+ 'aider',
60
+ 'copilot',
61
+ 'gemini',
62
+ 'qoder',
63
+ 'qwen',
64
+ ];
39
65
 
40
66
  for (const adapterName of ADAPTER_NAMES) {
41
67
  describe(`${adapterName} adapter contract`, () => {
@@ -107,3 +133,89 @@ test('codex adapter uses skill directories', () => {
107
133
  assert.strictEqual(codex.useSkillDirectories, true);
108
134
  assert.strictEqual(codex.skillFileName, 'SKILL.md');
109
135
  });
136
+
137
+ // --- New v2.18 adapters: shape + generateExtraFiles contract ---
138
+
139
+ test('qoder adapter targets .qoder/rules and emits AGENTS.md', async () => {
140
+ const qoder = getAdapter('qoder');
141
+ assert.strictEqual(qoder.outputDir, '.qoder/rules');
142
+ assert.strictEqual(qoder.useSkillDirectories, false);
143
+ assert.strictEqual(typeof qoder.generateExtraFiles, 'function');
144
+ 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')));
146
+ });
147
+
148
+ test('copilot adapter targets .github/instructions with .instructions.md ext', async () => {
149
+ 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');
157
+ const extras = await copilot.generateExtraFiles({ stats: { skillCount: 5, packCount: 1, files: [] } });
158
+ assert.ok(extras.some((e) => e.path === '.github/copilot-instructions.md'));
159
+ assert.ok(extras.some((e) => e.path === 'AGENTS.md'));
160
+ });
161
+
162
+ test('codex adapter migrated to generateExtraFiles for AGENTS.md (no longer special-cased in emitter)', async () => {
163
+ const codex = getAdapter('codex');
164
+ assert.strictEqual(typeof codex.generateExtraFiles, 'function');
165
+ const extras = await codex.generateExtraFiles({
166
+ stats: { skillCount: 64, packCount: 14, crossRefsResolved: 287, files: [] },
167
+ });
168
+ const agentsMd = extras.find((e) => e.path === 'AGENTS.md');
169
+ assert.ok(agentsMd, 'codex must emit AGENTS.md via generateExtraFiles');
170
+ assert.ok(agentsMd.content.includes('Rune'));
171
+ assert.ok(agentsMd.content.includes('64 core skills'));
172
+ });
173
+
174
+ test('emitter rejects absolute paths from generateExtraFiles (path-traversal guard)', async () => {
175
+ // Smoke: verify both qoder and gemini return relative paths only (no absolute).
176
+ for (const name of ['qoder', 'copilot', 'aider', 'qwen', 'gemini', 'codex']) {
177
+ const adapter = getAdapter(name);
178
+ const extras = await adapter.generateExtraFiles({
179
+ stats: { skillCount: 1, packCount: 0, crossRefsResolved: 0, files: ['rune-cook.md'] },
180
+ outputDir: '/tmp/test',
181
+ });
182
+ for (const extra of extras || []) {
183
+ assert.ok(!extra.path.startsWith('/'), `${name} returned absolute path: ${extra.path}`);
184
+ assert.ok(!/^[A-Z]:[\\/]/.test(extra.path), `${name} returned Windows absolute path: ${extra.path}`);
185
+ }
186
+ }
187
+ });
188
+
189
+ test('aider adapter emits .aider.conf.yml with read array', async () => {
190
+ const aider = getAdapter('aider');
191
+ assert.strictEqual(aider.outputDir, 'aider/rules');
192
+ const extras = await aider.generateExtraFiles({
193
+ stats: { skillCount: 2, packCount: 0, files: ['rune-cook.md', 'rune-fix.md', 'index.md'] },
194
+ });
195
+ const conf = extras.find((e) => e.path === '.aider.conf.yml');
196
+ assert.ok(conf);
197
+ assert.ok(conf.content.includes('read:'));
198
+ assert.ok(conf.content.includes('aider/rules/rune-cook.md'));
199
+ assert.ok(conf.content.includes('aider/rules/rune-fix.md'));
200
+ // index.md must NOT be in the read array
201
+ assert.ok(!conf.content.includes('aider/rules/index.md'));
202
+ });
203
+
204
+ test('qwen adapter emits QWEN.md with @import lines', async () => {
205
+ const qwen = getAdapter('qwen');
206
+ assert.strictEqual(qwen.outputDir, 'qwen/skills');
207
+ const extras = await qwen.generateExtraFiles({
208
+ stats: { skillCount: 2, packCount: 0, files: ['rune-cook.md', 'rune-fix.md'] },
209
+ });
210
+ const qwenMd = extras.find((e) => e.path === 'QWEN.md');
211
+ 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'));
214
+ });
215
+
216
+ test('gemini adapter declares generateExtraFiles for bundled GEMINI.md', () => {
217
+ 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.
221
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Aider Adapter
3
+ *
4
+ * Emits per-skill convention files into aider/rules/ and a `.aider.conf.yml`
5
+ * at the project root with a `read:` array pointing to every rule file. Aider
6
+ * loads everything in `read:` automatically on each session.
7
+ *
8
+ * Aider rules dir: aider/rules/
9
+ * Aider rule file: aider/rules/rune-{name}.md
10
+ * Aider config: .aider.conf.yml (auto-discovered: home dir → repo root → cwd)
11
+ *
12
+ * @see https://aider.chat/docs/usage/conventions.html
13
+ * @see https://aider.chat/docs/config/aider_conf.html
14
+ *
15
+ * MODEL TIER MAPPING (v2.18+):
16
+ * Aider supports any provider via env vars / CLI flags. Tier hint is emitted
17
+ * as a comment — aider does not parse model directives from rule files.
18
+ */
19
+
20
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
21
+
22
+ const MODEL_MAP = {
23
+ opus: 'tier:heavy',
24
+ sonnet: 'tier:mid',
25
+ haiku: 'tier:light',
26
+ };
27
+
28
+ const TOOL_MAP = {
29
+ Read: 'read the file',
30
+ Write: 'write/create the file',
31
+ Edit: 'edit the file',
32
+ Glob: 'find files by pattern',
33
+ Grep: 'search file contents',
34
+ Bash: 'run a shell command',
35
+ TodoWrite: 'track task progress',
36
+ Skill: 'follow the referenced rune-{name} convention',
37
+ Agent: 'execute the workflow',
38
+ };
39
+
40
+ export default {
41
+ name: 'aider',
42
+ outputDir: 'aider/rules',
43
+ fileExtension: '.md',
44
+ skillPrefix: 'rune-',
45
+ skillSuffix: '',
46
+
47
+ // Aider conventions are flat per-skill markdown files.
48
+ useSkillDirectories: false,
49
+
50
+ transformReference(skillName, raw) {
51
+ const isBackticked = raw.startsWith('`') && raw.endsWith('`');
52
+ const ref = `the rune-${skillName} convention`;
53
+ return isBackticked ? `\`${ref}\`` : ref;
54
+ },
55
+
56
+ transformToolName(toolName) {
57
+ return TOOL_MAP[toolName] || toolName;
58
+ },
59
+
60
+ generateHeader(skill) {
61
+ const tierHint = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
62
+ const tierLine = tierHint ? ` | ${tierHint}` : '';
63
+ return `# rune-${skill.name}\n\n> Rune ${skill.layer} ${skill.group}${tierLine}\n> ${(skill.description || '').replace(/\n/g, ' ')}\n\n`;
64
+ },
65
+
66
+ generateFooter() {
67
+ return BRANDING_FOOTER;
68
+ },
69
+
70
+ transformSubagentInstruction(text) {
71
+ return text;
72
+ },
73
+
74
+ scriptsDir(skillName) {
75
+ return `rune-${skillName}-scripts`;
76
+ },
77
+
78
+ postProcess(content) {
79
+ return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
80
+ },
81
+
82
+ // Emit `.aider.conf.yml` with `read:` array referencing every rule file +
83
+ // a CONVENTIONS.md as the high-level entry point.
84
+ generateExtraFiles({ stats }) {
85
+ // stats.files holds entries like 'rune-cook.md' (per-skill) and 'index.md' (autogen).
86
+ // Filter to per-skill rule files; aider loads everything in read[] on every session.
87
+ const ruleFiles = stats.files
88
+ .filter((f) => f.startsWith('rune-') && f.endsWith('.md') && !f.includes('/') && !f.includes('\\'))
89
+ .sort();
90
+
91
+ const conf = [
92
+ '# Auto-generated by Rune — do not edit manually.',
93
+ '# Aider auto-loads every file listed under `read:` on each session.',
94
+ 'read:',
95
+ ...ruleFiles.map((f) => ` - aider/rules/${f}`),
96
+ '',
97
+ ].join('\n');
98
+
99
+ const conventions = [
100
+ '# Rune Conventions for Aider',
101
+ '',
102
+ `Rune compiles ${stats.skillCount} skills + ${stats.packCount} packs into per-skill convention files under \`aider/rules/\`. Aider auto-loads them via \`.aider.conf.yml\`.`,
103
+ '',
104
+ 'When a request matches a skill (e.g. "implement", "refactor", "review"), follow the matching rune-<name> convention.',
105
+ '',
106
+ '## Loaded Conventions',
107
+ '',
108
+ ...ruleFiles.map((f) => `- \`aider/rules/${f}\``),
109
+ '',
110
+ '---',
111
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
112
+ '',
113
+ ].join('\n');
114
+
115
+ return [
116
+ { path: '.aider.conf.yml', content: conf },
117
+ { path: 'CONVENTIONS.md', content: conventions },
118
+ ];
119
+ },
120
+ };
@@ -80,4 +80,37 @@ export default {
80
80
  postProcess(content) {
81
81
  return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
82
82
  },
83
+
84
+ // Codex (and OpenAI's AGENTS.md convention generally) reads AGENTS.md at the
85
+ // project root for high-level context. Migrated from emitter.js v2.18.
86
+ generateExtraFiles({ stats }) {
87
+ const agentsMd = [
88
+ '# Rune — Project Configuration',
89
+ '',
90
+ '## Overview',
91
+ '',
92
+ 'Rune is an interconnected skill ecosystem for AI coding assistants.',
93
+ `${stats.skillCount} core skills | 5-layer mesh architecture | ${stats.crossRefsResolved} connections | Multi-platform.`,
94
+ 'Philosophy: "Less skills. Deeper connections."',
95
+ '',
96
+ 'Platform: codex',
97
+ '',
98
+ '## Skills',
99
+ '',
100
+ `**${stats.skillCount} core skills** + **${stats.packCount} extension packs**`,
101
+ '',
102
+ '## Usage',
103
+ '',
104
+ 'Reference skills using the `Skill` tool or delegate to subagents using the `Agent` tool.',
105
+ '',
106
+ '## Skills Directory',
107
+ '',
108
+ 'Skills are located in: .codex/skills/',
109
+ '',
110
+ '---',
111
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
112
+ '',
113
+ ].join('\n');
114
+ return [{ path: 'AGENTS.md', content: agentsMd }];
115
+ },
83
116
  };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * GitHub Copilot CLI Adapter
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).
7
+ *
8
+ * Copilot instructions dir: .github/instructions/
9
+ * Copilot instruction file: .github/instructions/rune-{name}.instructions.md
10
+ * Copilot project context: AGENTS.md (Copilot Spaces / Coding Agent convention)
11
+ *
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
17
+ *
18
+ * MODEL TIER MAPPING (v2.18+):
19
+ * Copilot multi-model support exposes Anthropic/OpenAI/Google providers. Tier
20
+ * names are emitted as a hint comment in the body — Copilot does not parse a
21
+ * model field, so we don't fight the platform.
22
+ */
23
+
24
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
25
+
26
+ const MODEL_MAP = {
27
+ opus: 'tier:heavy',
28
+ sonnet: 'tier:mid',
29
+ haiku: 'tier:light',
30
+ };
31
+
32
+ const TOOL_MAP = {
33
+ Read: 'read the file',
34
+ Write: 'write/create the file',
35
+ Edit: 'edit the file',
36
+ Glob: 'find files by pattern',
37
+ Grep: 'search file contents',
38
+ Bash: 'run a shell command',
39
+ TodoWrite: 'track task progress',
40
+ Skill: 'follow the referenced instructions',
41
+ Agent: 'execute the workflow',
42
+ };
43
+
44
+ export default {
45
+ name: 'copilot',
46
+ outputDir: '.github/instructions',
47
+ fileExtension: '.instructions.md',
48
+ skillPrefix: 'rune-',
49
+ skillSuffix: '',
50
+
51
+ // Copilot instructions are flat per-skill files, not directories.
52
+ useSkillDirectories: false,
53
+
54
+ transformReference(skillName, raw) {
55
+ const isBackticked = raw.startsWith('`') && raw.endsWith('`');
56
+ const ref = `the rune-${skillName} instructions`;
57
+ return isBackticked ? `\`${ref}\`` : ref;
58
+ },
59
+
60
+ transformToolName(toolName) {
61
+ return TOOL_MAP[toolName] || toolName;
62
+ },
63
+
64
+ 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, ' ');
70
+ 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('', '');
74
+ return lines.join('\n');
75
+ },
76
+
77
+ generateFooter() {
78
+ return BRANDING_FOOTER;
79
+ },
80
+
81
+ transformSubagentInstruction(text) {
82
+ return text;
83
+ },
84
+
85
+ scriptsDir(skillName) {
86
+ return `rune-${skillName}-scripts`;
87
+ },
88
+
89
+ postProcess(content) {
90
+ return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
91
+ },
92
+
93
+ // Emit a top-level .github/copilot-instructions.md as the entry point + an
94
+ // AGENTS.md so Copilot Spaces / Coding Agent can find the project context.
95
+ generateExtraFiles({ stats }) {
96
+ const copilotIndex = [
97
+ '# Rune — Copilot Custom Instructions',
98
+ '',
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.`,
100
+ '',
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.',
102
+ '',
103
+ '---',
104
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
105
+ '',
106
+ ].join('\n');
107
+
108
+ const agentsMd = [
109
+ '# Rune — Project Configuration',
110
+ '',
111
+ 'Rune is an interconnected skill ecosystem for AI coding assistants.',
112
+ `${stats.skillCount} core skills + ${stats.packCount} extension packs.`,
113
+ '',
114
+ 'Per-skill custom instructions: `.github/instructions/rune-<name>.instructions.md`',
115
+ '',
116
+ '---',
117
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
118
+ '',
119
+ ].join('\n');
120
+
121
+ return [
122
+ { path: '.github/copilot-instructions.md', content: copilotIndex },
123
+ { path: 'AGENTS.md', content: agentsMd },
124
+ ];
125
+ },
126
+ };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Gemini CLI Adapter
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.
9
+ *
10
+ * Gemini context file: GEMINI.md (project root)
11
+ * Gemini per-skill files: gemini/skills/rune-{name}.md (forward-compat staging)
12
+ *
13
+ * @see https://github.com/google-gemini/gemini-cli
14
+ * @see https://geminicli.com/docs/reference/configuration/
15
+ *
16
+ * 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.
21
+ */
22
+
23
+ import { readFile } from 'node:fs/promises';
24
+ import nodePath from 'node:path';
25
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
26
+
27
+ const MODEL_MAP = {
28
+ opus: 'gemini-2.5-pro',
29
+ sonnet: 'gemini-2.5-flash',
30
+ haiku: 'gemini-2.0-flash-lite',
31
+ };
32
+
33
+ const TOOL_MAP = {
34
+ Read: 'read the file',
35
+ Write: 'write/create the file',
36
+ Edit: 'edit the file',
37
+ Glob: 'find files by pattern',
38
+ Grep: 'search file contents',
39
+ Bash: 'run a shell command',
40
+ TodoWrite: 'track task progress',
41
+ Skill: 'follow the referenced rune-{name} section in GEMINI.md',
42
+ Agent: 'execute the workflow',
43
+ };
44
+
45
+ export default {
46
+ name: 'gemini',
47
+ outputDir: 'gemini/skills',
48
+ fileExtension: '.md',
49
+ skillPrefix: 'rune-',
50
+ skillSuffix: '',
51
+
52
+ // Per-skill files staged for forward compat; canonical entry is bundled GEMINI.md.
53
+ useSkillDirectories: false,
54
+
55
+ transformReference(skillName, raw) {
56
+ const isBackticked = raw.startsWith('`') && raw.endsWith('`');
57
+ const ref = `the rune-${skillName} section in GEMINI.md`;
58
+ return isBackticked ? `\`${ref}\`` : ref;
59
+ },
60
+
61
+ transformToolName(toolName) {
62
+ return TOOL_MAP[toolName] || toolName;
63
+ },
64
+
65
+ generateHeader(skill) {
66
+ const desc = (skill.description || '').replace(/"/g, '\\"');
67
+ const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`];
68
+ const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
69
+ if (translatedModel) lines.push(`# model-hint: ${translatedModel}`);
70
+ lines.push('---', '', '');
71
+ return lines.join('\n');
72
+ },
73
+
74
+ generateFooter() {
75
+ return BRANDING_FOOTER;
76
+ },
77
+
78
+ transformSubagentInstruction(text) {
79
+ return text;
80
+ },
81
+
82
+ scriptsDir(skillName) {
83
+ return `rune-${skillName}-scripts`;
84
+ },
85
+
86
+ postProcess(content) {
87
+ return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
88
+ },
89
+
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
+
112
+ const geminiMd = [
113
+ '# Rune — Project Configuration',
114
+ '',
115
+ '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.',
119
+ '',
120
+ '> Per-skill files are staged in `gemini/skills/` for forward-compat if Gemini CLI adds @import support.',
121
+ '',
122
+ ...sections,
123
+ '',
124
+ '---',
125
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
126
+ '',
127
+ ].join('\n');
128
+
129
+ return [{ path: 'GEMINI.md', content: geminiMd }];
130
+ },
131
+ };