@rune-kit/rune 2.16.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.
Files changed (48) hide show
  1. package/README.md +68 -16
  2. package/compiler/__tests__/adapter-model-mapping.test.js +80 -0
  3. package/compiler/__tests__/adapters.test.js +115 -3
  4. package/compiler/__tests__/doctor-mesh.test.js +5 -0
  5. package/compiler/__tests__/hooks-drift.test.js +156 -0
  6. package/compiler/__tests__/hooks-merge.test.js +2 -1
  7. package/compiler/__tests__/setup.test.js +152 -0
  8. package/compiler/adapters/aider.js +120 -0
  9. package/compiler/adapters/codex.js +33 -0
  10. package/compiler/adapters/copilot.js +126 -0
  11. package/compiler/adapters/gemini.js +131 -0
  12. package/compiler/adapters/index.js +10 -0
  13. package/compiler/adapters/openclaw.js +1 -1
  14. package/compiler/adapters/qoder.js +102 -0
  15. package/compiler/adapters/qwen.js +111 -0
  16. package/compiler/bin/rune.js +65 -4
  17. package/compiler/commands/hooks/drift.js +170 -0
  18. package/compiler/commands/hooks/presets.js +11 -1
  19. package/compiler/commands/setup.js +242 -0
  20. package/compiler/doctor.js +48 -2
  21. package/compiler/emitter.js +38 -41
  22. package/compiler/transforms/branding.js +1 -1
  23. package/compiler/transforms/hooks.js +6 -0
  24. package/hooks/hooks.json +10 -0
  25. package/hooks/quarantine/index.cjs +256 -0
  26. package/hooks/session-start/index.cjs +91 -0
  27. package/package.json +2 -2
  28. package/skills/asset-creator/SKILL.md +1 -1
  29. package/skills/audit/SKILL.md +20 -2
  30. package/skills/autopsy/SKILL.md +173 -2
  31. package/skills/brainstorm/SKILL.md +24 -1
  32. package/skills/browser-pilot/SKILL.md +16 -1
  33. package/skills/debug/SKILL.md +4 -2
  34. package/skills/deploy/SKILL.md +72 -2
  35. package/skills/design/SKILL.md +50 -3
  36. package/skills/integrity-check/SKILL.md +2 -0
  37. package/skills/launch/SKILL.md +11 -1
  38. package/skills/marketing/SKILL.md +1 -1
  39. package/skills/neural-memory/SKILL.md +1 -1
  40. package/skills/perf/SKILL.md +93 -2
  41. package/skills/quarantine/SKILL.md +173 -0
  42. package/skills/quarantine/references/quarantine-discipline.md +97 -0
  43. package/skills/quarantine/references/trusted-mcp-allowlist.md +77 -0
  44. package/skills/sentinel/SKILL.md +2 -1
  45. package/skills/sentinel-env/SKILL.md +2 -2
  46. package/skills/skill-forge/SKILL.md +47 -1
  47. package/skills/surgeon/SKILL.md +1 -1
  48. package/skills/team/SKILL.md +27 -1
@@ -0,0 +1,152 @@
1
+ import assert from 'node:assert';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { afterEach, beforeEach, describe, test } from 'node:test';
6
+ import { detectTiers, formatSetupResult, runSetup } from '../commands/setup.js';
7
+
8
+ let tmpRoot;
9
+
10
+ async function seedClaude(root) {
11
+ await mkdir(path.join(root, '.claude'), { recursive: true });
12
+ }
13
+
14
+ async function seedTier(root, tier) {
15
+ const dir = path.join(root, '..', tier === 'pro' ? 'Pro' : 'Business', 'hooks');
16
+ await mkdir(dir, { recursive: true });
17
+ await writeFile(
18
+ path.join(dir, 'manifest.json'),
19
+ JSON.stringify({
20
+ tier,
21
+ version: '1.0.0',
22
+ minFreeVersion: '2.0.0',
23
+ requires: [tier === 'pro' ? 'RUNE_PRO_ROOT' : 'RUNE_BUSINESS_ROOT'],
24
+ entries: [],
25
+ }),
26
+ );
27
+ }
28
+
29
+ beforeEach(async () => {
30
+ tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-setup-'));
31
+ await mkdir(path.join(tmpRoot, 'project'), { recursive: true });
32
+ });
33
+
34
+ afterEach(async () => {
35
+ if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
36
+ delete process.env.RUNE_PRO_ROOT;
37
+ delete process.env.RUNE_BUSINESS_ROOT;
38
+ });
39
+
40
+ describe('detectTiers', () => {
41
+ test('returns null for both when no tier present', () => {
42
+ const projectRoot = path.join(tmpRoot, 'project');
43
+ const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
44
+ assert.strictEqual(result.pro, null);
45
+ assert.strictEqual(result.business, null);
46
+ });
47
+
48
+ test('detects Pro via sibling path', async () => {
49
+ const projectRoot = path.join(tmpRoot, 'project');
50
+ await seedTier(projectRoot, 'pro');
51
+ const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
52
+ assert.ok(result.pro);
53
+ assert.match(result.pro.source, /sibling/);
54
+ assert.strictEqual(result.pro.version, '1.0.0');
55
+ assert.strictEqual(result.business, null);
56
+ });
57
+
58
+ test('detects Business via sibling path', async () => {
59
+ const projectRoot = path.join(tmpRoot, 'project');
60
+ await seedTier(projectRoot, 'business');
61
+ const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
62
+ assert.ok(result.business);
63
+ assert.match(result.business.source, /sibling/);
64
+ assert.strictEqual(result.pro, null);
65
+ });
66
+
67
+ test('env var takes precedence over sibling', async () => {
68
+ const projectRoot = path.join(tmpRoot, 'project');
69
+ await seedTier(projectRoot, 'pro');
70
+ // Env var pointing at a different location
71
+ const envRoot = path.join(tmpRoot, 'env-pro');
72
+ await mkdir(path.join(envRoot, 'hooks'), { recursive: true });
73
+ await writeFile(
74
+ path.join(envRoot, 'hooks', 'manifest.json'),
75
+ JSON.stringify({ tier: 'pro', version: '2.0.0', minFreeVersion: '2.0.0', requires: [], entries: [] }),
76
+ );
77
+ process.env.RUNE_PRO_ROOT = envRoot;
78
+ const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
79
+ assert.match(result.pro.source, /\$RUNE_PRO_ROOT/);
80
+ assert.strictEqual(result.pro.version, '2.0.0');
81
+ });
82
+ });
83
+
84
+ describe('runSetup (non-interactive)', () => {
85
+ test('--here installs to current project, no tiers when none detected', async () => {
86
+ const projectRoot = path.join(tmpRoot, 'project');
87
+ await seedClaude(projectRoot);
88
+ const result = await runSetup({
89
+ projectRoot,
90
+ runeRoot: path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..'),
91
+ args: { here: true, preset: 'gentle', 'no-tier': true },
92
+ });
93
+ assert.strictEqual(result.scope, 'current');
94
+ assert.strictEqual(result.preset, 'gentle');
95
+ assert.deepStrictEqual(result.tiers, []);
96
+ assert.strictEqual(result.targetRoot, projectRoot);
97
+ });
98
+
99
+ test('--tier flag bypasses prompt', async () => {
100
+ const projectRoot = path.join(tmpRoot, 'project');
101
+ await seedClaude(projectRoot);
102
+ await seedTier(projectRoot, 'pro');
103
+ const result = await runSetup({
104
+ projectRoot,
105
+ runeRoot: path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..'),
106
+ args: { here: true, preset: 'gentle', tier: 'pro' },
107
+ });
108
+ assert.deepStrictEqual(result.tiers, ['pro']);
109
+ });
110
+
111
+ test('--dry does not write files', async () => {
112
+ const projectRoot = path.join(tmpRoot, 'project');
113
+ await seedClaude(projectRoot);
114
+ const result = await runSetup({
115
+ projectRoot,
116
+ runeRoot: path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..'),
117
+ args: { here: true, preset: 'gentle', 'no-tier': true, dry: true },
118
+ });
119
+ assert.strictEqual(result.written, false);
120
+ });
121
+ });
122
+
123
+ describe('formatSetupResult', () => {
124
+ test('renders summary with scope and tiers', () => {
125
+ const out = formatSetupResult({
126
+ scope: 'current',
127
+ targetRoot: '/path/to/project',
128
+ tiers: ['pro'],
129
+ preset: 'gentle',
130
+ platforms: ['claude'],
131
+ written: true,
132
+ notes: [],
133
+ });
134
+ assert.match(out, /Setup Complete/);
135
+ assert.match(out, /Tiers:.*Free.*pro/);
136
+ assert.match(out, /current project/);
137
+ assert.match(out, /rune doctor --hooks/);
138
+ });
139
+
140
+ test('renders global scope label when scope=global', () => {
141
+ const out = formatSetupResult({
142
+ scope: 'global',
143
+ targetRoot: '/home/user',
144
+ tiers: [],
145
+ preset: 'gentle',
146
+ platforms: ['claude'],
147
+ written: true,
148
+ notes: [],
149
+ });
150
+ assert.match(out, /GLOBAL/);
151
+ });
152
+ });
@@ -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
+ };
@@ -4,13 +4,18 @@
4
4
  * Central registry for all platform adapters.
5
5
  */
6
6
 
7
+ import aider from './aider.js';
7
8
  import antigravity from './antigravity.js';
8
9
  import claude from './claude.js';
9
10
  import codex from './codex.js';
11
+ import copilot from './copilot.js';
10
12
  import cursor from './cursor.js';
13
+ import gemini from './gemini.js';
11
14
  import generic from './generic.js';
12
15
  import openclaw from './openclaw.js';
13
16
  import opencode from './opencode.js';
17
+ import qoder from './qoder.js';
18
+ import qwen from './qwen.js';
14
19
  import windsurf from './windsurf.js';
15
20
 
16
21
  const adapters = {
@@ -22,6 +27,11 @@ const adapters = {
22
27
  openclaw,
23
28
  codex,
24
29
  opencode,
30
+ aider,
31
+ copilot,
32
+ gemini,
33
+ qoder,
34
+ qwen,
25
35
  };
26
36
 
27
37
  /**
@@ -105,7 +105,7 @@ export default {
105
105
  id: 'rune',
106
106
  name: 'Rune',
107
107
  kind: 'skills',
108
- description: `${skills.length}-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 215+ connections, 14 extension packs.`,
108
+ description: `${skills.length}-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 203 sync connections + 40 async signals, 14 extension packs.`,
109
109
  version: pluginJson.version || '0.0.0',
110
110
  skills: ['./skills'],
111
111
  artifactConvention: {