@rune-kit/rune 2.6.0 → 2.8.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 (42) hide show
  1. package/README.md +22 -6
  2. package/compiler/__tests__/executive-dashboards.test.js +285 -0
  3. package/compiler/__tests__/inject.test.js +128 -0
  4. package/compiler/__tests__/orchestrators.test.js +151 -0
  5. package/compiler/__tests__/org-templates.test.js +447 -0
  6. package/compiler/__tests__/parser.test.js +201 -147
  7. package/compiler/__tests__/skill-index.test.js +218 -218
  8. package/compiler/__tests__/status.test.js +336 -0
  9. package/compiler/__tests__/templates.test.js +245 -0
  10. package/compiler/__tests__/visualizer.test.js +325 -0
  11. package/compiler/adapters/antigravity.js +71 -71
  12. package/compiler/bin/rune.js +444 -355
  13. package/compiler/doctor.js +272 -1
  14. package/compiler/emitter.js +939 -678
  15. package/compiler/parser.js +498 -267
  16. package/compiler/status.js +342 -0
  17. package/compiler/visualizer.js +622 -0
  18. package/package.json +1 -1
  19. package/skills/autopsy/SKILL.md +48 -1
  20. package/skills/completion-gate/SKILL.md +51 -3
  21. package/skills/context-engine/SKILL.md +141 -2
  22. package/skills/cook/SKILL.md +177 -4
  23. package/skills/debug/SKILL.md +50 -1
  24. package/skills/docs/SKILL.md +28 -3
  25. package/skills/fix/SKILL.md +26 -1
  26. package/skills/mcp-builder/SKILL.md +53 -1
  27. package/skills/onboard/SKILL.md +51 -1
  28. package/skills/perf/SKILL.md +34 -1
  29. package/skills/plan/SKILL.md +27 -1
  30. package/skills/preflight/SKILL.md +35 -1
  31. package/skills/research/SKILL.md +24 -1
  32. package/skills/retro/SKILL.md +95 -1
  33. package/skills/review/SKILL.md +45 -1
  34. package/skills/scope-guard/SKILL.md +1 -0
  35. package/skills/scout/SKILL.md +22 -1
  36. package/skills/sentinel/SKILL.md +35 -1
  37. package/skills/sentinel/references/policy-driven-constraints.md +424 -0
  38. package/skills/sentinel-env/SKILL.md +0 -2
  39. package/skills/session-bridge/SKILL.md +57 -2
  40. package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
  41. package/skills/team/SKILL.md +15 -1
  42. package/skills/verification/SKILL.md +0 -2
@@ -0,0 +1,325 @@
1
+ import assert from 'node:assert';
2
+ import path from 'node:path';
3
+ import { describe, test } from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { collectGraphData, generateMeshHTML } from '../visualizer.js';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const RUNE_ROOT = path.resolve(__dirname, '../..');
9
+ const PRO_DIR = path.resolve(RUNE_ROOT, '../../Pro/extensions');
10
+ const BIZ_DIR = path.resolve(RUNE_ROOT, '../../Business/extensions');
11
+
12
+ // ─── collectGraphData ───
13
+
14
+ describe('collectGraphData', () => {
15
+ test('collects nodes for all core skills', async () => {
16
+ const data = await collectGraphData(RUNE_ROOT);
17
+ const coreNodes = data.nodes.filter((n) => n.layer !== 'L4');
18
+ assert.ok(coreNodes.length >= 60, `Expected >= 60 core nodes, got ${coreNodes.length}`);
19
+ });
20
+
21
+ test('collects L4 pack nodes', async () => {
22
+ const data = await collectGraphData(RUNE_ROOT);
23
+ const packNodes = data.nodes.filter((n) => n.layer === 'L4');
24
+ assert.ok(packNodes.length >= 14, `Expected >= 14 pack nodes, got ${packNodes.length}`);
25
+ });
26
+
27
+ test('nodes have required fields', async () => {
28
+ const data = await collectGraphData(RUNE_ROOT);
29
+ for (const n of data.nodes) {
30
+ assert.ok(n.id, 'Node must have id');
31
+ assert.ok(n.layer, 'Node must have layer');
32
+ assert.ok(n.tier, 'Node must have tier');
33
+ }
34
+ });
35
+
36
+ test('nodes have correct layer values', async () => {
37
+ const data = await collectGraphData(RUNE_ROOT);
38
+ const validLayers = new Set(['L0', 'L1', 'L2', 'L3', 'L4']);
39
+ for (const n of data.nodes) {
40
+ assert.ok(validLayers.has(n.layer), `Invalid layer ${n.layer} for ${n.id}`);
41
+ }
42
+ });
43
+
44
+ test('collects cross-ref edges', async () => {
45
+ const data = await collectGraphData(RUNE_ROOT);
46
+ assert.ok(data.edges.length > 100, `Expected > 100 edges, got ${data.edges.length}`);
47
+ });
48
+
49
+ test('edges reference valid nodes', async () => {
50
+ const data = await collectGraphData(RUNE_ROOT);
51
+ const nodeIds = new Set(data.nodes.map((n) => n.id));
52
+ for (const e of data.edges) {
53
+ assert.ok(nodeIds.has(e.source), `Edge source ${e.source} not in nodes`);
54
+ }
55
+ });
56
+
57
+ test('edges have type field', async () => {
58
+ const data = await collectGraphData(RUNE_ROOT);
59
+ for (const e of data.edges) {
60
+ assert.strictEqual(e.type, 'crossref');
61
+ }
62
+ });
63
+
64
+ test('collects signal edges', async () => {
65
+ const data = await collectGraphData(RUNE_ROOT);
66
+ assert.ok(data.signalEdges.length > 0, 'Should have signal edges');
67
+ });
68
+
69
+ test('signal edges have signal name', async () => {
70
+ const data = await collectGraphData(RUNE_ROOT);
71
+ for (const e of data.signalEdges) {
72
+ assert.ok(e.signal, 'Signal edge must have signal name');
73
+ assert.ok(e.source, 'Signal edge must have source');
74
+ assert.ok(e.target, 'Signal edge must have target');
75
+ }
76
+ });
77
+
78
+ test('counts unique signals', async () => {
79
+ const data = await collectGraphData(RUNE_ROOT);
80
+ assert.ok(data.signals.length >= 10, `Expected >= 10 signals, got ${data.signals.length}`);
81
+ });
82
+
83
+ test('stats are accurate', async () => {
84
+ const data = await collectGraphData(RUNE_ROOT);
85
+ assert.strictEqual(data.stats.nodeCount, data.nodes.length);
86
+ assert.strictEqual(data.stats.edgeCount, data.edges.length);
87
+ assert.strictEqual(data.stats.signalEdgeCount, data.signalEdges.length);
88
+ assert.strictEqual(data.stats.signalCount, data.signals.length);
89
+ });
90
+
91
+ test('includes Pro pack nodes when tier provided', async () => {
92
+ const data = await collectGraphData(RUNE_ROOT, { pro: PRO_DIR });
93
+ const proNodes = data.nodes.filter((n) => n.tier === 'pro');
94
+ if (proNodes.length > 0) {
95
+ assert.ok(proNodes.length >= 4, `Expected >= 4 pro nodes`);
96
+ for (const n of proNodes) {
97
+ assert.strictEqual(n.layer, 'L4');
98
+ assert.ok(n.id.startsWith('pack:'));
99
+ }
100
+ }
101
+ });
102
+
103
+ test('includes Business pack nodes when tier provided', async () => {
104
+ const data = await collectGraphData(RUNE_ROOT, { business: BIZ_DIR });
105
+ const bizNodes = data.nodes.filter((n) => n.tier === 'business');
106
+ if (bizNodes.length > 0) {
107
+ assert.ok(bizNodes.length >= 4);
108
+ for (const n of bizNodes) {
109
+ assert.strictEqual(n.layer, 'L4');
110
+ }
111
+ }
112
+ });
113
+
114
+ test('handles non-existent tier paths', async () => {
115
+ const data = await collectGraphData(RUNE_ROOT, {
116
+ pro: '/nonexistent',
117
+ business: '/nonexistent',
118
+ });
119
+ const proNodes = data.nodes.filter((n) => n.tier === 'pro');
120
+ assert.strictEqual(proNodes.length, 0);
121
+ });
122
+
123
+ test('no duplicate node IDs', async () => {
124
+ const data = await collectGraphData(RUNE_ROOT, { pro: PRO_DIR, business: BIZ_DIR });
125
+ const ids = data.nodes.map((n) => n.id);
126
+ const unique = new Set(ids);
127
+ assert.strictEqual(ids.length, unique.size, 'Duplicate node IDs found');
128
+ });
129
+
130
+ test('L0 has exactly 1 node (skill-router)', async () => {
131
+ const data = await collectGraphData(RUNE_ROOT);
132
+ const l0 = data.nodes.filter((n) => n.layer === 'L0');
133
+ assert.strictEqual(l0.length, 1);
134
+ assert.strictEqual(l0[0].id, 'skill-router');
135
+ });
136
+
137
+ test('L1 has orchestrator nodes', async () => {
138
+ const data = await collectGraphData(RUNE_ROOT);
139
+ const l1 = data.nodes.filter((n) => n.layer === 'L1');
140
+ assert.ok(l1.length >= 4);
141
+ const names = l1.map((n) => n.id);
142
+ assert.ok(names.includes('cook'));
143
+ assert.ok(names.includes('team'));
144
+ });
145
+ });
146
+
147
+ // ─── generateMeshHTML ───
148
+
149
+ describe('generateMeshHTML', () => {
150
+ test('generates valid HTML document', async () => {
151
+ const data = await collectGraphData(RUNE_ROOT);
152
+ const html = generateMeshHTML(data);
153
+ assert.ok(html.startsWith('<!DOCTYPE html>'));
154
+ assert.ok(html.includes('<html'));
155
+ assert.ok(html.includes('</html>'));
156
+ });
157
+
158
+ test('is self-contained (no CDN links)', async () => {
159
+ const data = await collectGraphData(RUNE_ROOT);
160
+ const html = generateMeshHTML(data);
161
+ assert.ok(!html.includes('cdn.'), 'Should not reference CDN');
162
+ assert.ok(!html.includes('unpkg.com'), 'Should not reference unpkg');
163
+ assert.ok(!html.includes('cdnjs.'), 'Should not reference cdnjs');
164
+ });
165
+
166
+ test('embeds graph data as JSON', async () => {
167
+ const data = await collectGraphData(RUNE_ROOT);
168
+ const html = generateMeshHTML(data);
169
+ assert.ok(html.includes('const DATA ='));
170
+ assert.ok(html.includes('"nodeCount"'));
171
+ });
172
+
173
+ test('includes inline CSS', async () => {
174
+ const data = await collectGraphData(RUNE_ROOT);
175
+ const html = generateMeshHTML(data);
176
+ assert.ok(html.includes('<style>'));
177
+ assert.ok(html.includes('</style>'));
178
+ });
179
+
180
+ test('includes inline JavaScript', async () => {
181
+ const data = await collectGraphData(RUNE_ROOT);
182
+ const html = generateMeshHTML(data);
183
+ assert.ok(html.includes('<script>'));
184
+ assert.ok(html.includes('</script>'));
185
+ });
186
+
187
+ test('includes search input', async () => {
188
+ const data = await collectGraphData(RUNE_ROOT);
189
+ const html = generateMeshHTML(data);
190
+ assert.ok(html.includes('id="search"'));
191
+ assert.ok(html.includes('Search skills'));
192
+ });
193
+
194
+ test('includes layer filter buttons', async () => {
195
+ const data = await collectGraphData(RUNE_ROOT);
196
+ const html = generateMeshHTML(data);
197
+ assert.ok(html.includes('id="filters"'));
198
+ assert.ok(html.includes('filter-btn'));
199
+ });
200
+
201
+ test('includes detail panel', async () => {
202
+ const data = await collectGraphData(RUNE_ROOT);
203
+ const html = generateMeshHTML(data);
204
+ assert.ok(html.includes('detail-panel'));
205
+ assert.ok(html.includes('detail-content'));
206
+ });
207
+
208
+ test('includes legend', async () => {
209
+ const data = await collectGraphData(RUNE_ROOT);
210
+ const html = generateMeshHTML(data);
211
+ assert.ok(html.includes('L0 Router'));
212
+ assert.ok(html.includes('L1 Orchestrator'));
213
+ assert.ok(html.includes('L2 Workflow'));
214
+ assert.ok(html.includes('L3 Utility'));
215
+ assert.ok(html.includes('L4 Extension'));
216
+ assert.ok(html.includes('Cross-ref'));
217
+ assert.ok(html.includes('Signal'));
218
+ });
219
+
220
+ test('includes canvas element', async () => {
221
+ const data = await collectGraphData(RUNE_ROOT);
222
+ const html = generateMeshHTML(data);
223
+ assert.ok(html.includes('<canvas id="canvas"'));
224
+ });
225
+
226
+ test('includes layer colors config', async () => {
227
+ const data = await collectGraphData(RUNE_ROOT);
228
+ const html = generateMeshHTML(data);
229
+ assert.ok(html.includes('#f59e0b')); // L0
230
+ assert.ok(html.includes('#ef4444')); // L1
231
+ assert.ok(html.includes('#6366f1')); // L2
232
+ assert.ok(html.includes('#10b981')); // L3
233
+ assert.ok(html.includes('#8b5cf6')); // L4
234
+ });
235
+
236
+ test('includes tier border colors for paid packs', async () => {
237
+ const data = await collectGraphData(RUNE_ROOT);
238
+ const html = generateMeshHTML(data);
239
+ assert.ok(html.includes('TIER_BORDER'));
240
+ assert.ok(html.includes('pro'));
241
+ assert.ok(html.includes('business'));
242
+ });
243
+
244
+ test('handles interaction code', async () => {
245
+ const data = await collectGraphData(RUNE_ROOT);
246
+ const html = generateMeshHTML(data);
247
+ assert.ok(html.includes('mousemove'));
248
+ assert.ok(html.includes('mousedown'));
249
+ assert.ok(html.includes('wheel'));
250
+ assert.ok(html.includes('hitTest'));
251
+ });
252
+
253
+ test('has zoom/pan support', async () => {
254
+ const data = await collectGraphData(RUNE_ROOT);
255
+ const html = generateMeshHTML(data);
256
+ assert.ok(html.includes('cam.zoom'));
257
+ assert.ok(html.includes('cam.x'));
258
+ });
259
+
260
+ test('HTML escapes data to prevent XSS', async () => {
261
+ const data = await collectGraphData(RUNE_ROOT);
262
+ const html = generateMeshHTML(data);
263
+ assert.ok(html.includes('function esc('));
264
+ assert.ok(html.includes('&amp;'));
265
+ assert.ok(html.includes('&lt;'));
266
+ });
267
+ });
268
+
269
+ // ─── Graph Properties ───
270
+
271
+ describe('graph properties', () => {
272
+ test('graph is connected (most nodes reachable from cook)', async () => {
273
+ const data = await collectGraphData(RUNE_ROOT);
274
+ const adj = {};
275
+ for (const n of data.nodes) adj[n.id] = [];
276
+ for (const e of data.edges) {
277
+ if (adj[e.source]) adj[e.source].push(e.target);
278
+ if (adj[e.target]) adj[e.target].push(e.source);
279
+ }
280
+
281
+ // BFS from cook
282
+ const visited = new Set();
283
+ const queue = ['cook'];
284
+ visited.add('cook');
285
+ while (queue.length > 0) {
286
+ const current = queue.shift();
287
+ for (const neighbor of adj[current] || []) {
288
+ if (!visited.has(neighbor)) {
289
+ visited.add(neighbor);
290
+ queue.push(neighbor);
291
+ }
292
+ }
293
+ }
294
+
295
+ // Most core skills should be reachable
296
+ const coreNodes = data.nodes.filter((n) => n.layer !== 'L4');
297
+ const reachable = coreNodes.filter((n) => visited.has(n.id));
298
+ const ratio = reachable.length / coreNodes.length;
299
+ assert.ok(ratio > 0.5, `Only ${(ratio * 100).toFixed(0)}% of core skills reachable from cook`);
300
+ });
301
+
302
+ test('no self-referencing edges', async () => {
303
+ const data = await collectGraphData(RUNE_ROOT);
304
+ for (const e of data.edges) {
305
+ assert.notStrictEqual(e.source, e.target, `Self-edge found: ${e.source}`);
306
+ }
307
+ });
308
+
309
+ test('signal edges connect different nodes', async () => {
310
+ const data = await collectGraphData(RUNE_ROOT);
311
+ for (const e of data.signalEdges) {
312
+ assert.notStrictEqual(e.source, e.target, `Signal self-edge: ${e.source} via ${e.signal}`);
313
+ }
314
+ });
315
+
316
+ test('all layers represented', async () => {
317
+ const data = await collectGraphData(RUNE_ROOT);
318
+ const layers = new Set(data.nodes.map((n) => n.layer));
319
+ assert.ok(layers.has('L0'));
320
+ assert.ok(layers.has('L1'));
321
+ assert.ok(layers.has('L2'));
322
+ assert.ok(layers.has('L3'));
323
+ assert.ok(layers.has('L4'));
324
+ });
325
+ });
@@ -1,71 +1,71 @@
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
- };
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
+ };