@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.
- package/README.md +86 -17
- package/compiler/__tests__/pack-split.test.js +141 -1
- package/compiler/__tests__/parser.test.js +147 -55
- package/compiler/__tests__/scripts-bundling.test.js +283 -0
- package/compiler/__tests__/skill-index.test.js +218 -0
- package/compiler/__tests__/tier-override.test.js +41 -0
- package/compiler/adapters/antigravity.js +71 -53
- package/compiler/adapters/codex.js +4 -0
- package/compiler/adapters/cursor.js +4 -0
- package/compiler/adapters/generic.js +4 -0
- package/compiler/adapters/openclaw.js +4 -0
- package/compiler/adapters/opencode.js +4 -0
- package/compiler/adapters/windsurf.js +4 -0
- package/compiler/bin/rune.js +355 -355
- package/compiler/doctor.js +11 -1
- package/compiler/emitter.js +678 -386
- package/compiler/parser.js +267 -247
- package/compiler/transforms/scripts-path.js +18 -0
- package/extensions/zalo/PACK.md +20 -1
- package/extensions/zalo/references/conversation-management.md +214 -0
- package/extensions/zalo/references/eval-scenarios.md +157 -0
- package/extensions/zalo/references/listen-mode.md +237 -0
- package/extensions/zalo/references/mcp-production.md +274 -0
- package/extensions/zalo/references/multi-account-proxy.md +224 -0
- package/extensions/zalo/references/vietqr-banking.md +160 -0
- package/hooks/hooks.json +12 -0
- package/hooks/intent-router/index.cjs +108 -0
- package/hooks/pre-tool-guard/index.cjs +177 -68
- package/package.json +63 -64
- package/skills/brainstorm/SKILL.md +2 -0
- package/skills/cook/SKILL.md +661 -648
- package/skills/debug/SKILL.md +394 -392
- package/skills/deploy/SKILL.md +2 -0
- package/skills/fix/SKILL.md +283 -281
- package/skills/marketing/SKILL.md +3 -0
- package/skills/onboard/SKILL.md +7 -0
- package/skills/plan/SKILL.md +344 -342
- package/skills/preflight/SKILL.md +362 -360
- package/skills/review/SKILL.md +491 -489
- package/skills/scout/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +319 -296
- package/skills/sentinel/references/auth-crypto-reference.md +192 -0
- package/skills/sentinel/references/desktop-security.md +201 -0
- package/skills/sentinel/references/supply-chain.md +160 -0
- package/skills/session-bridge/SKILL.md +1 -0
- package/skills/slides/SKILL.md +142 -0
- package/skills/slides/scripts/build-deck.js +158 -0
- package/skills/team/SKILL.md +1 -0
- package/skills/test/SKILL.md +587 -585
- package/skills/verification/SKILL.md +1 -0
- package/skills/watchdog/SKILL.md +2 -0
- package/docs/ANTIGRAVITY-GAP-ANALYSIS.md +0 -369
- package/docs/ARCHITECTURE.md +0 -332
- package/docs/COMMUNITY-PACKS.md +0 -109
- package/docs/CONTRIBUTING-L4.md +0 -215
- package/docs/CROSS-IDE-ANALYSIS.md +0 -164
- package/docs/EXTENSION-TEMPLATE.md +0 -126
- package/docs/MESH-RULES.md +0 -34
- package/docs/MULTI-PLATFORM.md +0 -804
- package/docs/SKILL-DEPTH-AUDIT.md +0 -191
- package/docs/SKILL-TEMPLATE.md +0 -118
- package/docs/TRADE-MATRIX.md +0 -327
- package/docs/VERSIONING.md +0 -91
- package/docs/VISION.md +0 -263
- package/docs/assets/demo-subtitles.srt +0 -215
- package/docs/assets/end-card.html +0 -276
- package/docs/assets/mesh-diagram.html +0 -654
- package/docs/assets/thumbnail.html +0 -295
- package/docs/guides/cli.md +0 -403
- package/docs/guides/index.html +0 -1450
- package/docs/index.html +0 -1005
- package/docs/references/claudekit-analysis.md +0 -414
- package/docs/references/voltagent-analysis.md +0 -189
- package/docs/script.js +0 -495
- package/docs/skills/index.html +0 -832
- package/docs/style.css +0 -958
- 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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
};
|