@rune-kit/rune 2.3.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +86 -17
  2. package/compiler/__tests__/pack-split.test.js +141 -1
  3. package/compiler/__tests__/parser.test.js +147 -55
  4. package/compiler/__tests__/scripts-bundling.test.js +283 -0
  5. package/compiler/__tests__/skill-index.test.js +218 -0
  6. package/compiler/__tests__/tier-override.test.js +41 -0
  7. package/compiler/adapters/antigravity.js +71 -53
  8. package/compiler/adapters/codex.js +4 -0
  9. package/compiler/adapters/cursor.js +4 -0
  10. package/compiler/adapters/generic.js +4 -0
  11. package/compiler/adapters/openclaw.js +4 -0
  12. package/compiler/adapters/opencode.js +4 -0
  13. package/compiler/adapters/windsurf.js +4 -0
  14. package/compiler/bin/rune.js +355 -355
  15. package/compiler/doctor.js +11 -1
  16. package/compiler/emitter.js +678 -386
  17. package/compiler/parser.js +267 -247
  18. package/compiler/transforms/scripts-path.js +18 -0
  19. package/extensions/zalo/PACK.md +20 -1
  20. package/extensions/zalo/references/conversation-management.md +214 -0
  21. package/extensions/zalo/references/eval-scenarios.md +157 -0
  22. package/extensions/zalo/references/listen-mode.md +237 -0
  23. package/extensions/zalo/references/mcp-production.md +274 -0
  24. package/extensions/zalo/references/multi-account-proxy.md +224 -0
  25. package/extensions/zalo/references/vietqr-banking.md +160 -0
  26. package/hooks/hooks.json +12 -0
  27. package/hooks/intent-router/index.cjs +108 -0
  28. package/hooks/pre-tool-guard/index.cjs +177 -68
  29. package/package.json +63 -64
  30. package/skills/brainstorm/SKILL.md +2 -0
  31. package/skills/cook/SKILL.md +661 -648
  32. package/skills/debug/SKILL.md +394 -392
  33. package/skills/deploy/SKILL.md +2 -0
  34. package/skills/fix/SKILL.md +283 -281
  35. package/skills/marketing/SKILL.md +3 -0
  36. package/skills/onboard/SKILL.md +7 -0
  37. package/skills/plan/SKILL.md +344 -342
  38. package/skills/preflight/SKILL.md +362 -360
  39. package/skills/review/SKILL.md +491 -489
  40. package/skills/scout/SKILL.md +1 -0
  41. package/skills/sentinel/SKILL.md +319 -296
  42. package/skills/sentinel/references/auth-crypto-reference.md +192 -0
  43. package/skills/sentinel/references/desktop-security.md +201 -0
  44. package/skills/sentinel/references/supply-chain.md +160 -0
  45. package/skills/session-bridge/SKILL.md +1 -0
  46. package/skills/slides/SKILL.md +142 -0
  47. package/skills/slides/scripts/build-deck.js +158 -0
  48. package/skills/team/SKILL.md +1 -0
  49. package/skills/test/SKILL.md +587 -585
  50. package/skills/verification/SKILL.md +1 -0
  51. package/skills/watchdog/SKILL.md +2 -0
  52. package/docs/ANTIGRAVITY-GAP-ANALYSIS.md +0 -369
  53. package/docs/ARCHITECTURE.md +0 -332
  54. package/docs/COMMUNITY-PACKS.md +0 -109
  55. package/docs/CONTRIBUTING-L4.md +0 -215
  56. package/docs/CROSS-IDE-ANALYSIS.md +0 -164
  57. package/docs/EXTENSION-TEMPLATE.md +0 -126
  58. package/docs/MESH-RULES.md +0 -34
  59. package/docs/MULTI-PLATFORM.md +0 -804
  60. package/docs/SKILL-DEPTH-AUDIT.md +0 -191
  61. package/docs/SKILL-TEMPLATE.md +0 -118
  62. package/docs/TRADE-MATRIX.md +0 -327
  63. package/docs/VERSIONING.md +0 -91
  64. package/docs/VISION.md +0 -263
  65. package/docs/assets/demo-subtitles.srt +0 -215
  66. package/docs/assets/end-card.html +0 -276
  67. package/docs/assets/mesh-diagram.html +0 -654
  68. package/docs/assets/thumbnail.html +0 -295
  69. package/docs/guides/cli.md +0 -403
  70. package/docs/guides/index.html +0 -1450
  71. package/docs/index.html +0 -1005
  72. package/docs/references/claudekit-analysis.md +0 -414
  73. package/docs/references/voltagent-analysis.md +0 -189
  74. package/docs/script.js +0 -495
  75. package/docs/skills/index.html +0 -832
  76. package/docs/style.css +0 -958
  77. package/docs/video-demo-plan.md +0 -172
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Scripts Bundling Tests
3
+ *
4
+ * Tests the scripts/ directory copy pipeline:
5
+ * - copyScriptsDir helper
6
+ * - scriptsDir adapter method
7
+ * - {scripts_dir} placeholder resolution
8
+ * - End-to-end buildAll with scripts
9
+ */
10
+
11
+ import assert from 'node:assert';
12
+ import { existsSync } from 'node:fs';
13
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
14
+ import { tmpdir } from 'node:os';
15
+ import path from 'node:path';
16
+ import { describe, test } from 'node:test';
17
+ import { getAdapter } from '../adapters/index.js';
18
+ import { buildAll } from '../emitter.js';
19
+ import { resolveScriptsPath } from '../transforms/scripts-path.js';
20
+
21
+ // --- resolveScriptsPath transform ---
22
+
23
+ describe('resolveScriptsPath', () => {
24
+ test('replaces {scripts_dir} with resolved path', () => {
25
+ const body = 'Run: `node {scripts_dir}/build-deck.js`';
26
+ const result = resolveScriptsPath(body, '.cursor/rules/rune-slides-scripts');
27
+ assert.strictEqual(result, 'Run: `node .cursor/rules/rune-slides-scripts/build-deck.js`');
28
+ });
29
+
30
+ test('replaces multiple occurrences', () => {
31
+ const body = '{scripts_dir}/a.js and {scripts_dir}/b.js';
32
+ const result = resolveScriptsPath(body, 'scripts');
33
+ assert.strictEqual(result, 'scripts/a.js and scripts/b.js');
34
+ });
35
+
36
+ test('returns body unchanged when no placeholder', () => {
37
+ const body = 'No placeholders here';
38
+ const result = resolveScriptsPath(body, 'some/path');
39
+ assert.strictEqual(result, body);
40
+ });
41
+
42
+ test('returns body unchanged when scriptsPath is null', () => {
43
+ const body = 'Has {scripts_dir} but no path';
44
+ const result = resolveScriptsPath(body, null);
45
+ assert.strictEqual(result, body);
46
+ });
47
+
48
+ test('returns body unchanged when body is empty', () => {
49
+ assert.strictEqual(resolveScriptsPath('', 'path'), '');
50
+ });
51
+ });
52
+
53
+ // --- scriptsDir adapter method ---
54
+
55
+ describe('adapter scriptsDir', () => {
56
+ test('flat-file adapters return sibling dir pattern', () => {
57
+ for (const name of ['cursor', 'windsurf', 'antigravity', 'generic', 'openclaw']) {
58
+ const adapter = getAdapter(name);
59
+ assert.ok(adapter.scriptsDir, `${name} should have scriptsDir`);
60
+ assert.strictEqual(adapter.scriptsDir('slides'), 'rune-slides-scripts', `${name} scriptsDir mismatch`);
61
+ }
62
+ });
63
+
64
+ test('directory-per-skill adapters return nested dir pattern', () => {
65
+ for (const name of ['codex', 'opencode']) {
66
+ const adapter = getAdapter(name);
67
+ assert.ok(adapter.scriptsDir, `${name} should have scriptsDir`);
68
+ assert.strictEqual(adapter.scriptsDir('slides'), 'rune-slides/scripts', `${name} scriptsDir mismatch`);
69
+ }
70
+ });
71
+
72
+ test('claude adapter has no scriptsDir (passthrough)', () => {
73
+ const adapter = getAdapter('claude');
74
+ assert.ok(!adapter.scriptsDir, 'claude should not have scriptsDir');
75
+ });
76
+ });
77
+
78
+ // --- Integration: buildAll with scripts ---
79
+
80
+ describe('buildAll with scripts', () => {
81
+ async function createTempSkillTree() {
82
+ const tmp = path.join(tmpdir(), `rune-scripts-test-${Date.now()}`);
83
+ const skillsDir = path.join(tmp, 'skills');
84
+ const slideSkillDir = path.join(skillsDir, 'test-slide');
85
+ const scriptsDir = path.join(slideSkillDir, 'scripts');
86
+ const plainSkillDir = path.join(skillsDir, 'test-plain');
87
+
88
+ await mkdir(scriptsDir, { recursive: true });
89
+ await mkdir(plainSkillDir, { recursive: true });
90
+
91
+ // Skill with scripts/
92
+ await writeFile(
93
+ path.join(slideSkillDir, 'SKILL.md'),
94
+ [
95
+ '---',
96
+ 'name: test-slide',
97
+ 'description: "Test slide skill"',
98
+ 'layer: L3',
99
+ 'group: utility',
100
+ 'connections: []',
101
+ 'tags: [test]',
102
+ '---',
103
+ '',
104
+ '# test-slide',
105
+ '',
106
+ 'Run: `node {scripts_dir}/build-deck.js --input slides.json`',
107
+ '',
108
+ 'Also: `{scripts_dir}/helper.py`',
109
+ ].join('\n'),
110
+ 'utf-8',
111
+ );
112
+
113
+ await writeFile(path.join(scriptsDir, 'build-deck.js'), '// build deck script\nconsole.log("hello");\n', 'utf-8');
114
+ await writeFile(path.join(scriptsDir, 'helper.py'), '# helper\nprint("hi")\n', 'utf-8');
115
+
116
+ // Skill without scripts/
117
+ await writeFile(
118
+ path.join(plainSkillDir, 'SKILL.md'),
119
+ [
120
+ '---',
121
+ 'name: test-plain',
122
+ 'description: "Test plain skill"',
123
+ 'layer: L3',
124
+ 'group: utility',
125
+ 'connections: []',
126
+ 'tags: [test]',
127
+ '---',
128
+ '',
129
+ '# test-plain',
130
+ '',
131
+ 'No scripts here.',
132
+ ].join('\n'),
133
+ 'utf-8',
134
+ );
135
+
136
+ // Minimal extensions dir (empty — no packs needed for test)
137
+ await mkdir(path.join(tmp, 'extensions'), { recursive: true });
138
+
139
+ // Minimal .claude-plugin for openclaw
140
+ await mkdir(path.join(tmp, '.claude-plugin'), { recursive: true });
141
+ await writeFile(path.join(tmp, '.claude-plugin', 'plugin.json'), JSON.stringify({ version: '0.0.1' }), 'utf-8');
142
+
143
+ return tmp;
144
+ }
145
+
146
+ test('cursor: scripts copied to sibling dir, placeholder resolved', async () => {
147
+ const tmp = await createTempSkillTree();
148
+ try {
149
+ const outputRoot = path.join(tmp, 'out');
150
+ const adapter = getAdapter('cursor');
151
+ const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
152
+
153
+ // Scripts copied
154
+ assert.ok(stats.scriptsCopied >= 2, `expected 2+ scripts copied, got ${stats.scriptsCopied}`);
155
+
156
+ // Scripts dir exists with files
157
+ const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide-scripts');
158
+ assert.ok(existsSync(path.join(scriptsOut, 'build-deck.js')), 'build-deck.js missing in output');
159
+ assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing in output');
160
+
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');
165
+
166
+ // Plain skill: no scripts dir created
167
+ assert.ok(
168
+ !existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain-scripts')),
169
+ 'plain skill should not have scripts dir',
170
+ );
171
+ } finally {
172
+ await rm(tmp, { recursive: true, force: true });
173
+ }
174
+ });
175
+
176
+ test('codex: scripts inside skill directory', async () => {
177
+ const tmp = await createTempSkillTree();
178
+ try {
179
+ const outputRoot = path.join(tmp, 'out');
180
+ const adapter = getAdapter('codex');
181
+ const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
182
+
183
+ assert.ok(stats.scriptsCopied >= 2);
184
+
185
+ // Scripts inside skill dir: .codex/skills/rune-test-slide/scripts/
186
+ const scriptsOut = path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'scripts');
187
+ assert.ok(existsSync(path.join(scriptsOut, 'build-deck.js')), 'build-deck.js missing');
188
+ assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing');
189
+
190
+ // Placeholder resolved
191
+ const md = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'), 'utf-8');
192
+ 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');
194
+ } finally {
195
+ await rm(tmp, { recursive: true, force: true });
196
+ }
197
+ });
198
+
199
+ test('skill without scripts: no error, no empty dirs', async () => {
200
+ const tmp = await createTempSkillTree();
201
+ try {
202
+ const outputRoot = path.join(tmp, 'out');
203
+ const adapter = getAdapter('windsurf');
204
+ const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
205
+
206
+ // Should still build both skills
207
+ assert.strictEqual(stats.skillCount, 2);
208
+
209
+ // No scripts dir for plain skill
210
+ assert.ok(!existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain-scripts')));
211
+ } finally {
212
+ await rm(tmp, { recursive: true, force: true });
213
+ }
214
+ });
215
+
216
+ test('stats include scriptsCopied count', async () => {
217
+ const tmp = await createTempSkillTree();
218
+ try {
219
+ const outputRoot = path.join(tmp, 'out');
220
+ const adapter = getAdapter('generic');
221
+ const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
222
+
223
+ assert.strictEqual(typeof stats.scriptsCopied, 'number');
224
+ assert.strictEqual(stats.scriptsCopied, 2);
225
+ } finally {
226
+ await rm(tmp, { recursive: true, force: true });
227
+ }
228
+ });
229
+
230
+ test('claude adapter: passthrough, no scripts processing', async () => {
231
+ const tmp = await createTempSkillTree();
232
+ try {
233
+ const outputRoot = path.join(tmp, 'out');
234
+ const adapter = getAdapter('claude');
235
+ const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
236
+
237
+ // Claude is passthrough — no build
238
+ assert.strictEqual(stats.platform, 'claude');
239
+ assert.strictEqual(stats.skillCount, 0);
240
+ } finally {
241
+ await rm(tmp, { recursive: true, force: true });
242
+ }
243
+ });
244
+
245
+ test('scripts content copied as-is (byte-for-byte)', async () => {
246
+ const tmp = await createTempSkillTree();
247
+ try {
248
+ const outputRoot = path.join(tmp, 'out');
249
+ const adapter = getAdapter('cursor');
250
+ await buildAll({ runeRoot: tmp, outputRoot, adapter });
251
+
252
+ const original = await readFile(path.join(tmp, 'skills', 'test-slide', 'scripts', 'build-deck.js'), 'utf-8');
253
+ const copied = await readFile(
254
+ path.join(outputRoot, adapter.outputDir, 'rune-test-slide-scripts', 'build-deck.js'),
255
+ 'utf-8',
256
+ );
257
+ assert.strictEqual(copied, original, 'script content should be identical');
258
+ } finally {
259
+ await rm(tmp, { recursive: true, force: true });
260
+ }
261
+ });
262
+
263
+ test('multiple adapters resolve different paths', async () => {
264
+ const tmp = await createTempSkillTree();
265
+ try {
266
+ // Cursor (flat)
267
+ const cursorOut = path.join(tmp, 'out-cursor');
268
+ const cursorAdapter = getAdapter('cursor');
269
+ 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');
272
+
273
+ // Codex (directory)
274
+ const codexOut = path.join(tmp, 'out-codex');
275
+ const codexAdapter = getAdapter('codex');
276
+ 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');
279
+ } finally {
280
+ await rm(tmp, { recursive: true, force: true });
281
+ }
282
+ });
283
+ });
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Skill Index Generation Tests
3
+ *
4
+ * Tests that buildAll generates a valid skill-index.json with:
5
+ * - Intent patterns mapped to skills
6
+ * - Mesh-aware chain prediction from connections
7
+ * - Complete skill graph
8
+ */
9
+
10
+ import assert from 'node:assert';
11
+ import { existsSync } from 'node:fs';
12
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
13
+ import { tmpdir } from 'node:os';
14
+ import path from 'node:path';
15
+ import { describe, test } from 'node:test';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { getAdapter } from '../adapters/index.js';
18
+ import { buildAll } from '../emitter.js';
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const RUNE_ROOT = path.resolve(__dirname, '../..');
22
+
23
+ describe('skill-index.json generation', () => {
24
+ test('buildAll emits skill-index.json with correct structure', async () => {
25
+ const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
26
+ try {
27
+ const adapter = getAdapter('cursor');
28
+ await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
29
+
30
+ const indexPath = path.join(tmp, adapter.outputDir, 'skill-index.json');
31
+ assert.ok(existsSync(indexPath), 'skill-index.json not found in output');
32
+
33
+ const index = JSON.parse(await readFile(indexPath, 'utf-8'));
34
+
35
+ // Structure checks
36
+ assert.strictEqual(index.version, 2);
37
+ assert.ok(index.generated, 'missing generated timestamp');
38
+ assert.ok(index.skillCount >= 50, `too few skills: ${index.skillCount}`);
39
+ assert.ok(typeof index.skills === 'object', 'missing skills object');
40
+ assert.ok(typeof index.graph === 'object', 'missing graph object');
41
+ assert.ok(typeof index.signals === 'object', 'missing signals object');
42
+ assert.ok(typeof index.intents === 'object', 'missing intents object');
43
+ } finally {
44
+ await rm(tmp, { recursive: true, force: true });
45
+ }
46
+ });
47
+
48
+ test('skill-index contains intent patterns with chains', async () => {
49
+ const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
50
+ try {
51
+ const adapter = getAdapter('cursor');
52
+ await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
53
+
54
+ const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
55
+
56
+ // cook intent should exist with keywords and chain
57
+ assert.ok(index.intents.cook, 'missing cook intent');
58
+ assert.ok(Array.isArray(index.intents.cook.keywords), 'cook keywords not array');
59
+ assert.ok(index.intents.cook.keywords.includes('implement'), 'cook missing "implement" keyword');
60
+ assert.ok(Array.isArray(index.intents.cook.chain), 'cook chain not array');
61
+ assert.strictEqual(index.intents.cook.chain[0], 'cook', 'cook chain should start with cook');
62
+ assert.ok(index.intents.cook.chain.length > 1, 'cook chain should have connected skills');
63
+
64
+ // debug intent
65
+ assert.ok(index.intents.debug, 'missing debug intent');
66
+ assert.ok(index.intents.debug.keywords.includes('bug'), 'debug missing "bug" keyword');
67
+
68
+ // sentinel intent
69
+ assert.ok(index.intents.sentinel, 'missing sentinel intent');
70
+ assert.ok(index.intents.sentinel.keywords.includes('security'), 'sentinel missing "security" keyword');
71
+ } finally {
72
+ await rm(tmp, { recursive: true, force: true });
73
+ }
74
+ });
75
+
76
+ test('skill-index graph has connections from cross-refs', async () => {
77
+ const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
78
+ try {
79
+ const adapter = getAdapter('cursor');
80
+ await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
81
+
82
+ const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
83
+
84
+ // cook should have outbound connections
85
+ assert.ok(index.graph.cook, 'cook not in graph');
86
+ assert.ok(index.graph.cook.length > 3, `cook should have many connections, got ${index.graph.cook.length}`);
87
+
88
+ // Skills entry should have layer and description
89
+ assert.ok(index.skills.cook, 'cook not in skills');
90
+ assert.strictEqual(index.skills.cook.layer, 'L1');
91
+ assert.ok(index.skills.cook.description.length > 20, 'cook description too short');
92
+ } finally {
93
+ await rm(tmp, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ test('skill-index contains signal graph', async () => {
98
+ const tmp = path.join(tmpdir(), `rune-idx-test-${Date.now()}`);
99
+ try {
100
+ const adapter = getAdapter('cursor');
101
+ await buildAll({ runeRoot: RUNE_ROOT, outputRoot: tmp, adapter });
102
+
103
+ const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
104
+
105
+ // Signal graph should exist
106
+ assert.ok(typeof index.signals === 'object', 'missing signals object');
107
+ assert.ok(Object.keys(index.signals).length >= 10, `too few signals: ${Object.keys(index.signals).length}`);
108
+
109
+ // code.changed should be a well-connected signal
110
+ assert.ok(index.signals['code.changed'], 'missing code.changed signal');
111
+ assert.ok(index.signals['code.changed'].emitters.includes('fix'), 'fix should emit code.changed');
112
+ assert.ok(index.signals['code.changed'].listeners.length >= 3, 'code.changed should have 3+ listeners');
113
+
114
+ // Per-skill signals should be included
115
+ assert.ok(index.skills.test.signals, 'test skill should have signals');
116
+ assert.ok(index.skills.test.signals.emit.includes('tests.passed'), 'test should emit tests.passed');
117
+ assert.ok(index.skills.test.signals.listen.includes('code.changed'), 'test should listen to code.changed');
118
+
119
+ // Skills without signals should not have the field
120
+ const noSignalSkill = Object.values(index.skills).find((s) => !s.signals);
121
+ assert.ok(noSignalSkill, 'at least one skill should have no signals');
122
+ } finally {
123
+ await rm(tmp, { recursive: true, force: true });
124
+ }
125
+ });
126
+
127
+ test('skill-index signal graph with synthetic skills', async () => {
128
+ const tmp = path.join(tmpdir(), `rune-idx-sig-${Date.now()}`);
129
+ const skillsDir = path.join(tmp, 'skills');
130
+ await mkdir(path.join(skillsDir, 'emitter'), { recursive: true });
131
+ await mkdir(path.join(skillsDir, 'listener'), { recursive: true });
132
+ await mkdir(path.join(tmp, 'extensions'), { recursive: true });
133
+
134
+ await writeFile(
135
+ path.join(skillsDir, 'emitter', 'SKILL.md'),
136
+ [
137
+ '---',
138
+ 'name: emitter',
139
+ 'description: "Emits signals"',
140
+ 'metadata:',
141
+ ' layer: L2',
142
+ ' emit: data.ready',
143
+ '---',
144
+ '',
145
+ '# emitter',
146
+ ].join('\n'),
147
+ 'utf-8',
148
+ );
149
+
150
+ await writeFile(
151
+ path.join(skillsDir, 'listener', 'SKILL.md'),
152
+ [
153
+ '---',
154
+ 'name: listener',
155
+ 'description: "Listens to signals"',
156
+ 'metadata:',
157
+ ' layer: L3',
158
+ ' listen: data.ready',
159
+ '---',
160
+ '',
161
+ '# listener',
162
+ ].join('\n'),
163
+ 'utf-8',
164
+ );
165
+
166
+ try {
167
+ const adapter = getAdapter('generic');
168
+ await buildAll({ runeRoot: tmp, outputRoot: tmp, adapter });
169
+
170
+ const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
171
+
172
+ assert.ok(index.signals['data.ready'], 'data.ready signal should exist');
173
+ assert.deepStrictEqual(index.signals['data.ready'].emitters, ['emitter']);
174
+ assert.deepStrictEqual(index.signals['data.ready'].listeners, ['listener']);
175
+ assert.ok(index.skills.emitter.signals);
176
+ assert.deepStrictEqual(index.skills.emitter.signals.emit, ['data.ready']);
177
+ } finally {
178
+ await rm(tmp, { recursive: true, force: true });
179
+ }
180
+ });
181
+
182
+ test('skill-index works with minimal skill tree', async () => {
183
+ const tmp = path.join(tmpdir(), `rune-idx-min-${Date.now()}`);
184
+ const skillsDir = path.join(tmp, 'skills', 'alpha');
185
+ await mkdir(skillsDir, { recursive: true });
186
+ await mkdir(path.join(tmp, 'extensions'), { recursive: true });
187
+
188
+ await writeFile(
189
+ path.join(skillsDir, 'SKILL.md'),
190
+ [
191
+ '---',
192
+ 'name: alpha',
193
+ 'description: "Test skill"',
194
+ 'metadata:',
195
+ ' layer: L3',
196
+ ' group: utility',
197
+ '---',
198
+ '',
199
+ '# alpha',
200
+ '',
201
+ 'Body.',
202
+ ].join('\n'),
203
+ 'utf-8',
204
+ );
205
+
206
+ try {
207
+ const adapter = getAdapter('generic');
208
+ await buildAll({ runeRoot: tmp, outputRoot: tmp, adapter });
209
+
210
+ const index = JSON.parse(await readFile(path.join(tmp, adapter.outputDir, 'skill-index.json'), 'utf-8'));
211
+ assert.strictEqual(index.skillCount, 1);
212
+ assert.ok(index.skills.alpha, 'alpha not in skills');
213
+ assert.deepStrictEqual(index.graph.alpha, [], 'alpha should have no connections');
214
+ } finally {
215
+ await rm(tmp, { recursive: true, force: true });
216
+ }
217
+ });
218
+ });
@@ -155,4 +155,45 @@ describe('discoverTieredPacks', () => {
155
155
  rmSync(root, { recursive: true, force: true });
156
156
  }
157
157
  });
158
+
159
+ test('pro override tracks free pack in overrides array', async () => {
160
+ const { freePacks, proPacks, root } = createTierFixture();
161
+ try {
162
+ const packs = await discoverTieredPacks(freePacks, { pro: proPacks });
163
+ const saasPack = packs.find((p) => p.dirName === 'pro-saas');
164
+ assert.ok(saasPack, 'pro-saas should exist');
165
+ assert.strictEqual(saasPack.overrides.length, 1);
166
+ assert.strictEqual(saasPack.overrides[0].tier, 'free');
167
+ assert.strictEqual(saasPack.overrides[0].dirName, 'saas');
168
+ } finally {
169
+ rmSync(root, { recursive: true, force: true });
170
+ }
171
+ });
172
+
173
+ test('business override tracks both pro and free in overrides array', async () => {
174
+ const { freePacks, proPacks, bizPacks, root } = createTierFixture();
175
+ try {
176
+ const packs = await discoverTieredPacks(freePacks, { pro: proPacks, business: bizPacks });
177
+ const saasPack = packs.find((p) => p.dirName === 'business-saas');
178
+ assert.ok(saasPack, 'business-saas should exist');
179
+ // Should have both free and pro overrides (free was overridden by pro, then pro by business)
180
+ assert.strictEqual(saasPack.overrides.length, 2);
181
+ const overrideTiers = saasPack.overrides.map((o) => o.tier).sort();
182
+ assert.deepStrictEqual(overrideTiers, ['free', 'pro']);
183
+ } finally {
184
+ rmSync(root, { recursive: true, force: true });
185
+ }
186
+ });
187
+
188
+ test('free-only packs have empty overrides array', async () => {
189
+ const { freePacks, proPacks, root } = createTierFixture();
190
+ try {
191
+ const packs = await discoverTieredPacks(freePacks, { pro: proPacks });
192
+ const tradingPack = packs.find((p) => p.dirName === 'trading');
193
+ assert.ok(tradingPack, 'trading should exist');
194
+ assert.deepStrictEqual(tradingPack.overrides, []);
195
+ } finally {
196
+ rmSync(root, { recursive: true, force: true });
197
+ }
198
+ });
158
199
  });
@@ -1,53 +1,71 @@
1
- /**
2
- * Google Antigravity (Jules) Adapter
3
- *
4
- * Emits .md rule files for .agent/rules/ directory.
5
- */
6
-
7
- import { BRANDING_FOOTER } from '../transforms/branding.js';
8
-
9
- const TOOL_MAP = {
10
- Read: 'read the file',
11
- Write: 'write/create the file',
12
- Edit: 'edit the file',
13
- Glob: 'find files by pattern',
14
- Grep: 'search file contents',
15
- Bash: 'run a shell command',
16
- TodoWrite: 'track task progress',
17
- Skill: 'follow the referenced skill',
18
- Agent: 'execute the workflow',
19
- };
20
-
21
- export default {
22
- name: 'antigravity',
23
- outputDir: '.agent/rules',
24
- fileExtension: '.md',
25
- skillPrefix: 'rune-',
26
- skillSuffix: '',
27
-
28
- transformReference(skillName, raw) {
29
- const isBackticked = raw.startsWith('`') && raw.endsWith('`');
30
- const ref = `the rune-${skillName} rule`;
31
- return isBackticked ? `\`${ref}\`` : ref;
32
- },
33
-
34
- transformToolName(toolName) {
35
- return TOOL_MAP[toolName] || toolName;
36
- },
37
-
38
- generateHeader(skill) {
39
- return `# rune-${skill.name}\n\n> Rune ${skill.layer} Skill | ${skill.group}\n\n`;
40
- },
41
-
42
- generateFooter() {
43
- return BRANDING_FOOTER;
44
- },
45
-
46
- transformSubagentInstruction(text) {
47
- return text;
48
- },
49
-
50
- postProcess(content) {
51
- return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
52
- },
53
- };
1
+ /**
2
+ * Google Antigravity (Jules) Adapter
3
+ *
4
+ * Emits SKILL.md files into .agents/skills/{name}/ directories.
5
+ * Uses the same SKILL.md frontmatter format (name, description)
6
+ * with markdown body — identical to Codex pattern.
7
+ *
8
+ * Antigravity project context: AGENTS.md (+ CLAUDE.md fallback)
9
+ * Antigravity skills dir: .agents/skills/
10
+ * Antigravity skill format: .agents/skills/{name}/SKILL.md
11
+ */
12
+
13
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
14
+
15
+ const TOOL_MAP = {
16
+ Read: 'read the file',
17
+ Write: 'write/create the file',
18
+ Edit: 'edit the file',
19
+ Glob: 'find files by pattern',
20
+ Grep: 'search file contents',
21
+ Bash: 'run a shell command',
22
+ TodoWrite: 'track task progress',
23
+ Skill: 'follow the referenced skill',
24
+ Agent: 'execute the workflow',
25
+ };
26
+
27
+ export default {
28
+ name: 'antigravity',
29
+ outputDir: '.agents/skills',
30
+ fileExtension: '.md',
31
+ skillPrefix: 'rune-',
32
+ skillSuffix: '',
33
+
34
+ useSkillDirectories: true,
35
+ skillFileName: 'SKILL.md',
36
+
37
+ transformReference(skillName, raw) {
38
+ const isBackticked = raw.startsWith('`') && raw.endsWith('`');
39
+ const ref = `the rune-${skillName} skill`;
40
+ return isBackticked ? `\`${ref}\`` : ref;
41
+ },
42
+
43
+ transformToolName(toolName) {
44
+ return TOOL_MAP[toolName] || toolName;
45
+ },
46
+
47
+ generateHeader(skill) {
48
+ const desc = (skill.description || '').replace(/"/g, '\\"');
49
+ return ['---', `name: rune-${skill.name}`, `description: "${desc}"`, '---', '', ''].join('\n');
50
+ },
51
+
52
+ generateFooter() {
53
+ return BRANDING_FOOTER;
54
+ },
55
+
56
+ transformSubagentInstruction(text) {
57
+ return text;
58
+ },
59
+
60
+ scriptsDir(skillName) {
61
+ return `rune-${skillName}-scripts`;
62
+ },
63
+
64
+ referencesDir(skillName) {
65
+ return `rune-${skillName}-references`;
66
+ },
67
+
68
+ postProcess(content) {
69
+ return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
70
+ },
71
+ };
@@ -58,6 +58,10 @@ export default {
58
58
  return text;
59
59
  },
60
60
 
61
+ scriptsDir(skillName) {
62
+ return `rune-${skillName}/scripts`;
63
+ },
64
+
61
65
  postProcess(content) {
62
66
  return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
63
67
  },