@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.
- package/README.md +22 -6
- package/compiler/__tests__/executive-dashboards.test.js +285 -0
- package/compiler/__tests__/inject.test.js +128 -0
- package/compiler/__tests__/orchestrators.test.js +151 -0
- package/compiler/__tests__/org-templates.test.js +447 -0
- package/compiler/__tests__/parser.test.js +201 -147
- package/compiler/__tests__/skill-index.test.js +218 -218
- package/compiler/__tests__/status.test.js +336 -0
- package/compiler/__tests__/templates.test.js +245 -0
- package/compiler/__tests__/visualizer.test.js +325 -0
- package/compiler/adapters/antigravity.js +71 -71
- package/compiler/bin/rune.js +444 -355
- package/compiler/doctor.js +272 -1
- package/compiler/emitter.js +939 -678
- package/compiler/parser.js +498 -267
- package/compiler/status.js +342 -0
- package/compiler/visualizer.js +622 -0
- package/package.json +1 -1
- package/skills/autopsy/SKILL.md +48 -1
- package/skills/completion-gate/SKILL.md +51 -3
- package/skills/context-engine/SKILL.md +141 -2
- package/skills/cook/SKILL.md +177 -4
- package/skills/debug/SKILL.md +50 -1
- package/skills/docs/SKILL.md +28 -3
- package/skills/fix/SKILL.md +26 -1
- package/skills/mcp-builder/SKILL.md +53 -1
- package/skills/onboard/SKILL.md +51 -1
- package/skills/perf/SKILL.md +34 -1
- package/skills/plan/SKILL.md +27 -1
- package/skills/preflight/SKILL.md +35 -1
- package/skills/research/SKILL.md +24 -1
- package/skills/retro/SKILL.md +95 -1
- package/skills/review/SKILL.md +45 -1
- package/skills/scope-guard/SKILL.md +1 -0
- package/skills/scout/SKILL.md +22 -1
- package/skills/sentinel/SKILL.md +35 -1
- package/skills/sentinel/references/policy-driven-constraints.md +424 -0
- package/skills/sentinel-env/SKILL.md +0 -2
- package/skills/session-bridge/SKILL.md +57 -2
- package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
- package/skills/team/SKILL.md +15 -1
- package/skills/verification/SKILL.md +0 -2
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visualizer — Interactive Mesh Graph
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained HTML file with a force-directed graph
|
|
5
|
+
* of all skills, connections, and signals. No CDN dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { parseSkill } from './parser.js';
|
|
12
|
+
|
|
13
|
+
// ─── Data Collection ───
|
|
14
|
+
|
|
15
|
+
export async function collectGraphData(runeRoot, tierSources = {}) {
|
|
16
|
+
const skillsDir = path.join(runeRoot, 'skills');
|
|
17
|
+
const extDir = path.join(runeRoot, 'extensions');
|
|
18
|
+
|
|
19
|
+
const nodes = [];
|
|
20
|
+
const edges = [];
|
|
21
|
+
const signalEdges = [];
|
|
22
|
+
const signalMap = {};
|
|
23
|
+
|
|
24
|
+
// Parse core skills
|
|
25
|
+
if (existsSync(skillsDir)) {
|
|
26
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory()) continue;
|
|
29
|
+
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
30
|
+
if (!existsSync(skillFile)) continue;
|
|
31
|
+
|
|
32
|
+
const content = await readFile(skillFile, 'utf-8');
|
|
33
|
+
const parsed = parseSkill(content, skillFile);
|
|
34
|
+
|
|
35
|
+
nodes.push({
|
|
36
|
+
id: parsed.name,
|
|
37
|
+
layer: parsed.layer || 'L3',
|
|
38
|
+
group: parsed.group || '',
|
|
39
|
+
description: (parsed.description || '').slice(0, 120),
|
|
40
|
+
tier: 'free',
|
|
41
|
+
connections: [...new Set(parsed.crossRefs.map((r) => r.skillName))],
|
|
42
|
+
signals: parsed.signals || null,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Cross-ref edges (skip self-references)
|
|
46
|
+
const refs = [...new Set(parsed.crossRefs.map((r) => r.skillName))];
|
|
47
|
+
for (const target of refs) {
|
|
48
|
+
if (target !== parsed.name) {
|
|
49
|
+
edges.push({ source: parsed.name, target, type: 'crossref' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Signal edges
|
|
54
|
+
if (parsed.signals) {
|
|
55
|
+
for (const sig of parsed.signals.emit) {
|
|
56
|
+
if (!signalMap[sig]) signalMap[sig] = { emitters: [], listeners: [] };
|
|
57
|
+
signalMap[sig].emitters.push(parsed.name);
|
|
58
|
+
}
|
|
59
|
+
for (const sig of parsed.signals.listen) {
|
|
60
|
+
if (!signalMap[sig]) signalMap[sig] = { emitters: [], listeners: [] };
|
|
61
|
+
signalMap[sig].listeners.push(parsed.name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Scan extension packs as L4 nodes
|
|
68
|
+
const packDirs = [
|
|
69
|
+
{ dir: extDir, tier: 'free' },
|
|
70
|
+
{ dir: tierSources.pro, tier: 'pro' },
|
|
71
|
+
{ dir: tierSources.business, tier: 'business' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const { dir, tier } of packDirs) {
|
|
75
|
+
if (!dir || !existsSync(dir)) continue;
|
|
76
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry.isDirectory()) continue;
|
|
79
|
+
const packFile = path.join(dir, entry.name, 'PACK.md');
|
|
80
|
+
if (!existsSync(packFile)) continue;
|
|
81
|
+
|
|
82
|
+
const baseName = entry.name.replace(/^(pro|business)-/, '');
|
|
83
|
+
const nodeId = `pack:${baseName}`;
|
|
84
|
+
|
|
85
|
+
// Avoid duplicate pack nodes (tier override)
|
|
86
|
+
if (!nodes.find((n) => n.id === nodeId)) {
|
|
87
|
+
nodes.push({
|
|
88
|
+
id: nodeId,
|
|
89
|
+
layer: 'L4',
|
|
90
|
+
group: 'extension',
|
|
91
|
+
description: `${tier} pack: ${baseName}`,
|
|
92
|
+
tier,
|
|
93
|
+
connections: [],
|
|
94
|
+
signals: null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Build signal edges from signal map
|
|
101
|
+
for (const [sigName, { emitters, listeners }] of Object.entries(signalMap)) {
|
|
102
|
+
for (const emitter of emitters) {
|
|
103
|
+
for (const listener of listeners) {
|
|
104
|
+
signalEdges.push({ source: emitter, target: listener, signal: sigName });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
nodes,
|
|
111
|
+
edges,
|
|
112
|
+
signalEdges,
|
|
113
|
+
signals: Object.keys(signalMap),
|
|
114
|
+
stats: {
|
|
115
|
+
nodeCount: nodes.length,
|
|
116
|
+
edgeCount: edges.length,
|
|
117
|
+
signalEdgeCount: signalEdges.length,
|
|
118
|
+
signalCount: Object.keys(signalMap).length,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── HTML Generation ───
|
|
124
|
+
|
|
125
|
+
export function generateMeshHTML(graphData) {
|
|
126
|
+
const dataJson = JSON.stringify(graphData);
|
|
127
|
+
|
|
128
|
+
return `<!DOCTYPE html>
|
|
129
|
+
<html lang="en">
|
|
130
|
+
<head>
|
|
131
|
+
<meta charset="UTF-8">
|
|
132
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
133
|
+
<title>Rune Mesh Visualizer</title>
|
|
134
|
+
<style>
|
|
135
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
136
|
+
body {
|
|
137
|
+
background: #0a0a1a;
|
|
138
|
+
color: #e2e8f0;
|
|
139
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
height: 100vh;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ─── Top Bar ─── */
|
|
145
|
+
.topbar {
|
|
146
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 10;
|
|
147
|
+
display: flex; align-items: center; gap: 12px;
|
|
148
|
+
padding: 10px 16px;
|
|
149
|
+
background: rgba(10,10,26,0.92);
|
|
150
|
+
backdrop-filter: blur(8px);
|
|
151
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
152
|
+
}
|
|
153
|
+
.topbar h1 { font-size: 14px; font-weight: 600; white-space: nowrap; }
|
|
154
|
+
.topbar .stats { font-size: 12px; color: #64748b; margin-left: 8px; }
|
|
155
|
+
.search-box {
|
|
156
|
+
padding: 5px 10px; border-radius: 6px; border: 1px solid #334155;
|
|
157
|
+
background: #1e293b; color: #e2e8f0; font-size: 12px; width: 180px;
|
|
158
|
+
outline: none;
|
|
159
|
+
}
|
|
160
|
+
.search-box:focus { border-color: #6366f1; }
|
|
161
|
+
.filter-group { display: flex; gap: 4px; margin-left: auto; }
|
|
162
|
+
.filter-btn {
|
|
163
|
+
padding: 3px 10px; border-radius: 12px; border: 1px solid #334155;
|
|
164
|
+
background: transparent; color: #94a3b8; font-size: 11px; cursor: pointer;
|
|
165
|
+
transition: all 0.15s;
|
|
166
|
+
}
|
|
167
|
+
.filter-btn:hover { border-color: #6366f1; color: #e2e8f0; }
|
|
168
|
+
.filter-btn.active { background: var(--btn-color, #6366f1); border-color: var(--btn-color, #6366f1); color: #fff; }
|
|
169
|
+
|
|
170
|
+
/* ─── Canvas ─── */
|
|
171
|
+
#canvas {
|
|
172
|
+
position: fixed; top: 44px; left: 0; right: 0; bottom: 0;
|
|
173
|
+
cursor: grab;
|
|
174
|
+
}
|
|
175
|
+
#canvas:active { cursor: grabbing; }
|
|
176
|
+
|
|
177
|
+
/* ─── Detail Panel ─── */
|
|
178
|
+
.detail-panel {
|
|
179
|
+
position: fixed; right: -320px; top: 44px; bottom: 0; width: 300px;
|
|
180
|
+
background: rgba(15,23,42,0.95); backdrop-filter: blur(12px);
|
|
181
|
+
border-left: 1px solid rgba(255,255,255,0.08);
|
|
182
|
+
padding: 20px; overflow-y: auto;
|
|
183
|
+
transition: right 0.25s ease;
|
|
184
|
+
z-index: 5;
|
|
185
|
+
}
|
|
186
|
+
.detail-panel.open { right: 0; }
|
|
187
|
+
.detail-panel h2 { font-size: 16px; margin-bottom: 4px; }
|
|
188
|
+
.detail-panel .layer-badge {
|
|
189
|
+
display: inline-block; padding: 2px 8px; border-radius: 8px;
|
|
190
|
+
font-size: 11px; font-weight: 600; margin-bottom: 12px;
|
|
191
|
+
}
|
|
192
|
+
.detail-panel .desc { font-size: 13px; color: #94a3b8; line-height: 1.5; margin-bottom: 16px; }
|
|
193
|
+
.detail-panel h3 { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin: 12px 0 6px; }
|
|
194
|
+
.detail-panel .conn-list { list-style: none; }
|
|
195
|
+
.detail-panel .conn-list li {
|
|
196
|
+
font-size: 13px; padding: 3px 0; color: #cbd5e1; cursor: pointer;
|
|
197
|
+
}
|
|
198
|
+
.detail-panel .conn-list li:hover { color: #6366f1; }
|
|
199
|
+
.detail-panel .signal-tag {
|
|
200
|
+
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
|
201
|
+
font-size: 11px; margin: 2px; background: rgba(99,102,241,0.15); color: #a5b4fc;
|
|
202
|
+
}
|
|
203
|
+
.detail-panel .close-btn {
|
|
204
|
+
position: absolute; top: 12px; right: 12px; background: none; border: none;
|
|
205
|
+
color: #64748b; font-size: 18px; cursor: pointer;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ─── Legend ─── */
|
|
209
|
+
.legend {
|
|
210
|
+
position: fixed; bottom: 12px; left: 12px; z-index: 5;
|
|
211
|
+
display: flex; gap: 12px; font-size: 11px; color: #64748b;
|
|
212
|
+
}
|
|
213
|
+
.legend-item { display: flex; align-items: center; gap: 4px; }
|
|
214
|
+
.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
215
|
+
.legend-line { width: 20px; height: 2px; }
|
|
216
|
+
.legend-line.dashed { background: repeating-linear-gradient(90deg, #f59e0b 0 4px, transparent 4px 8px); }
|
|
217
|
+
|
|
218
|
+
@media print { body { background: #fff; color: #000; } }
|
|
219
|
+
</style>
|
|
220
|
+
</head>
|
|
221
|
+
<body>
|
|
222
|
+
|
|
223
|
+
<div class="topbar">
|
|
224
|
+
<h1>Rune Mesh</h1>
|
|
225
|
+
<span class="stats" id="stats"></span>
|
|
226
|
+
<input class="search-box" id="search" placeholder="Search skills..." autocomplete="off">
|
|
227
|
+
<div class="filter-group" id="filters"></div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<canvas id="canvas"></canvas>
|
|
231
|
+
|
|
232
|
+
<div class="detail-panel" id="detail">
|
|
233
|
+
<button class="close-btn" id="detail-close">×</button>
|
|
234
|
+
<div id="detail-content"></div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div class="legend">
|
|
238
|
+
<div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div> L0 Router</div>
|
|
239
|
+
<div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div> L1 Orchestrator</div>
|
|
240
|
+
<div class="legend-item"><div class="legend-dot" style="background:#6366f1"></div> L2 Workflow</div>
|
|
241
|
+
<div class="legend-item"><div class="legend-dot" style="background:#10b981"></div> L3 Utility</div>
|
|
242
|
+
<div class="legend-item"><div class="legend-dot" style="background:#8b5cf6"></div> L4 Extension</div>
|
|
243
|
+
<div class="legend-item"><div class="legend-line" style="background:#475569"></div> Cross-ref</div>
|
|
244
|
+
<div class="legend-item"><div class="legend-line dashed"></div> Signal</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<script>
|
|
248
|
+
// ─── Data ───
|
|
249
|
+
const DATA = ${dataJson};
|
|
250
|
+
|
|
251
|
+
// ─── Config ───
|
|
252
|
+
const LAYER_COLORS = {
|
|
253
|
+
L0: '#f59e0b', L1: '#ef4444', L2: '#6366f1', L3: '#10b981', L4: '#8b5cf6'
|
|
254
|
+
};
|
|
255
|
+
const LAYER_RADIUS = { L0: 0, L1: 110, L2: 220, L3: 320, L4: 410 };
|
|
256
|
+
const TIER_BORDER = { free: null, pro: '#f59e0b', business: '#8b5cf6' };
|
|
257
|
+
const NODE_SIZES = { L0: 28, L1: 18, L2: 14, L3: 11, L4: 9 };
|
|
258
|
+
|
|
259
|
+
// ─── State ───
|
|
260
|
+
let nodes = [];
|
|
261
|
+
let cam = { x: 0, y: 0, zoom: 1 };
|
|
262
|
+
let drag = null;
|
|
263
|
+
let hoveredNode = null;
|
|
264
|
+
let selectedNode = null;
|
|
265
|
+
let activeFilters = new Set(['L0','L1','L2','L3','L4']);
|
|
266
|
+
let searchTerm = '';
|
|
267
|
+
|
|
268
|
+
// ─── Init ───
|
|
269
|
+
const canvas = document.getElementById('canvas');
|
|
270
|
+
const ctx = canvas.getContext('2d');
|
|
271
|
+
const dpr = window.devicePixelRatio || 1;
|
|
272
|
+
|
|
273
|
+
function resize() {
|
|
274
|
+
canvas.width = window.innerWidth * dpr;
|
|
275
|
+
canvas.height = (window.innerHeight - 44) * dpr;
|
|
276
|
+
canvas.style.width = window.innerWidth + 'px';
|
|
277
|
+
canvas.style.height = (window.innerHeight - 44) + 'px';
|
|
278
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
279
|
+
draw();
|
|
280
|
+
}
|
|
281
|
+
window.addEventListener('resize', resize);
|
|
282
|
+
|
|
283
|
+
// ─── Layout: Concentric Rings ───
|
|
284
|
+
function layoutNodes() {
|
|
285
|
+
const cx = window.innerWidth / 2;
|
|
286
|
+
const cy = (window.innerHeight - 44) / 2;
|
|
287
|
+
|
|
288
|
+
const byLayer = {};
|
|
289
|
+
for (const n of DATA.nodes) {
|
|
290
|
+
const l = n.layer || 'L3';
|
|
291
|
+
if (!byLayer[l]) byLayer[l] = [];
|
|
292
|
+
byLayer[l].push(n);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
nodes = [];
|
|
296
|
+
for (const [layer, layerNodes] of Object.entries(byLayer)) {
|
|
297
|
+
const r = LAYER_RADIUS[layer] || 350;
|
|
298
|
+
const count = layerNodes.length;
|
|
299
|
+
const angleStep = (2 * Math.PI) / Math.max(count, 1);
|
|
300
|
+
const startAngle = -Math.PI / 2;
|
|
301
|
+
|
|
302
|
+
layerNodes.forEach((n, i) => {
|
|
303
|
+
const angle = startAngle + i * angleStep;
|
|
304
|
+
nodes.push({
|
|
305
|
+
...n,
|
|
306
|
+
x: cx + r * Math.cos(angle),
|
|
307
|
+
y: cy + r * Math.sin(angle),
|
|
308
|
+
r: NODE_SIZES[layer] || 12,
|
|
309
|
+
color: LAYER_COLORS[layer] || '#6366f1',
|
|
310
|
+
visible: true,
|
|
311
|
+
highlighted: false,
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getNode(id) { return nodes.find(n => n.id === id); }
|
|
318
|
+
|
|
319
|
+
// ─── Filtering ───
|
|
320
|
+
function applyFilters() {
|
|
321
|
+
const term = searchTerm.toLowerCase();
|
|
322
|
+
for (const n of nodes) {
|
|
323
|
+
const layerMatch = activeFilters.has(n.layer);
|
|
324
|
+
const searchMatch = !term || n.id.toLowerCase().includes(term) ||
|
|
325
|
+
(n.description && n.description.toLowerCase().includes(term));
|
|
326
|
+
n.visible = layerMatch && searchMatch;
|
|
327
|
+
}
|
|
328
|
+
draw();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Drawing ───
|
|
332
|
+
function screenX(x) { return (x + cam.x) * cam.zoom; }
|
|
333
|
+
function screenY(y) { return (y + cam.y) * cam.zoom; }
|
|
334
|
+
|
|
335
|
+
function draw() {
|
|
336
|
+
const w = window.innerWidth;
|
|
337
|
+
const h = window.innerHeight - 44;
|
|
338
|
+
ctx.clearRect(0, 0, w, h);
|
|
339
|
+
|
|
340
|
+
ctx.save();
|
|
341
|
+
ctx.translate(cam.x * cam.zoom, cam.y * cam.zoom);
|
|
342
|
+
ctx.scale(cam.zoom, cam.zoom);
|
|
343
|
+
|
|
344
|
+
// Draw cross-ref edges
|
|
345
|
+
for (const e of DATA.edges) {
|
|
346
|
+
const src = getNode(e.source);
|
|
347
|
+
const tgt = getNode(e.target);
|
|
348
|
+
if (!src || !tgt || !src.visible || !tgt.visible) continue;
|
|
349
|
+
|
|
350
|
+
const isHighlighted = hoveredNode &&
|
|
351
|
+
(hoveredNode.id === e.source || hoveredNode.id === e.target);
|
|
352
|
+
|
|
353
|
+
ctx.beginPath();
|
|
354
|
+
ctx.moveTo(src.x, src.y);
|
|
355
|
+
// Quadratic bezier toward center for curvature
|
|
356
|
+
const mx = (src.x + tgt.x) / 2;
|
|
357
|
+
const my = (src.y + tgt.y) / 2;
|
|
358
|
+
const cx2 = mx * 0.95 + w / 2 * 0.05;
|
|
359
|
+
const cy2 = my * 0.95 + h / 2 * 0.05;
|
|
360
|
+
ctx.quadraticCurveTo(cx2, cy2, tgt.x, tgt.y);
|
|
361
|
+
ctx.strokeStyle = isHighlighted ? 'rgba(99,102,241,0.5)' : 'rgba(71,85,105,0.15)';
|
|
362
|
+
ctx.lineWidth = isHighlighted ? 1.5 : 0.5;
|
|
363
|
+
ctx.stroke();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Draw signal edges (dashed)
|
|
367
|
+
for (const e of DATA.signalEdges) {
|
|
368
|
+
const src = getNode(e.source);
|
|
369
|
+
const tgt = getNode(e.target);
|
|
370
|
+
if (!src || !tgt || !src.visible || !tgt.visible) continue;
|
|
371
|
+
|
|
372
|
+
const isHighlighted = hoveredNode &&
|
|
373
|
+
(hoveredNode.id === e.source || hoveredNode.id === e.target);
|
|
374
|
+
if (!isHighlighted) continue; // Only show signal edges on hover
|
|
375
|
+
|
|
376
|
+
ctx.beginPath();
|
|
377
|
+
ctx.setLineDash([4, 4]);
|
|
378
|
+
ctx.moveTo(src.x, src.y);
|
|
379
|
+
ctx.lineTo(tgt.x, tgt.y);
|
|
380
|
+
ctx.strokeStyle = 'rgba(245,158,11,0.6)';
|
|
381
|
+
ctx.lineWidth = 1.5;
|
|
382
|
+
ctx.stroke();
|
|
383
|
+
ctx.setLineDash([]);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Draw nodes
|
|
387
|
+
for (const n of nodes) {
|
|
388
|
+
if (!n.visible) continue;
|
|
389
|
+
|
|
390
|
+
const isDimmed = hoveredNode && !n.highlighted && n.id !== hoveredNode.id;
|
|
391
|
+
const alpha = isDimmed ? 0.15 : 1;
|
|
392
|
+
const isSelected = selectedNode && selectedNode.id === n.id;
|
|
393
|
+
|
|
394
|
+
// Glow for hovered/selected
|
|
395
|
+
if ((n.id === hoveredNode?.id || isSelected) && !isDimmed) {
|
|
396
|
+
ctx.beginPath();
|
|
397
|
+
ctx.arc(n.x, n.y, n.r + 6, 0, Math.PI * 2);
|
|
398
|
+
ctx.fillStyle = n.color + '22';
|
|
399
|
+
ctx.fill();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Node circle
|
|
403
|
+
ctx.beginPath();
|
|
404
|
+
ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
|
|
405
|
+
ctx.fillStyle = '#0f172a';
|
|
406
|
+
ctx.fill();
|
|
407
|
+
ctx.strokeStyle = isDimmed ? n.color + '33' : n.color;
|
|
408
|
+
ctx.lineWidth = isSelected ? 3 : (n.tier !== 'free' ? 2.5 : 1.5);
|
|
409
|
+
ctx.stroke();
|
|
410
|
+
ctx.globalAlpha = alpha;
|
|
411
|
+
|
|
412
|
+
// Tier indicator (double ring for paid)
|
|
413
|
+
if (TIER_BORDER[n.tier] && !isDimmed) {
|
|
414
|
+
ctx.beginPath();
|
|
415
|
+
ctx.arc(n.x, n.y, n.r + 3, 0, Math.PI * 2);
|
|
416
|
+
ctx.strokeStyle = TIER_BORDER[n.tier] + '88';
|
|
417
|
+
ctx.lineWidth = 1;
|
|
418
|
+
ctx.setLineDash([3, 3]);
|
|
419
|
+
ctx.stroke();
|
|
420
|
+
ctx.setLineDash([]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Label
|
|
424
|
+
ctx.fillStyle = isDimmed ? '#475569' : '#e2e8f0';
|
|
425
|
+
ctx.font = (n.r >= 14 ? '10px' : '8px') + ' -apple-system, sans-serif';
|
|
426
|
+
ctx.textAlign = 'center';
|
|
427
|
+
ctx.textBaseline = 'middle';
|
|
428
|
+
|
|
429
|
+
const label = n.id.replace('pack:', '');
|
|
430
|
+
if (n.r >= 14) {
|
|
431
|
+
ctx.fillText(label, n.x, n.y);
|
|
432
|
+
} else {
|
|
433
|
+
// Small nodes: label below
|
|
434
|
+
ctx.fillText(label, n.x, n.y + n.r + 10);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
ctx.globalAlpha = 1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
ctx.restore();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ─── Interaction ───
|
|
444
|
+
function worldPos(e) {
|
|
445
|
+
const rect = canvas.getBoundingClientRect();
|
|
446
|
+
return {
|
|
447
|
+
x: (e.clientX - rect.left) / cam.zoom - cam.x,
|
|
448
|
+
y: (e.clientY - rect.top) / cam.zoom - cam.y
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function hitTest(wx, wy) {
|
|
453
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
454
|
+
const n = nodes[i];
|
|
455
|
+
if (!n.visible) continue;
|
|
456
|
+
const dx = wx - n.x, dy = wy - n.y;
|
|
457
|
+
if (dx * dx + dy * dy < (n.r + 4) * (n.r + 4)) return n;
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function highlightConnected(node) {
|
|
463
|
+
const connected = new Set();
|
|
464
|
+
if (!node) { nodes.forEach(n => n.highlighted = false); return; }
|
|
465
|
+
|
|
466
|
+
connected.add(node.id);
|
|
467
|
+
for (const e of DATA.edges) {
|
|
468
|
+
if (e.source === node.id) connected.add(e.target);
|
|
469
|
+
if (e.target === node.id) connected.add(e.source);
|
|
470
|
+
}
|
|
471
|
+
for (const e of DATA.signalEdges) {
|
|
472
|
+
if (e.source === node.id) connected.add(e.target);
|
|
473
|
+
if (e.target === node.id) connected.add(e.source);
|
|
474
|
+
}
|
|
475
|
+
nodes.forEach(n => n.highlighted = connected.has(n.id));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
479
|
+
if (drag) {
|
|
480
|
+
cam.x += (e.clientX - drag.x) / cam.zoom;
|
|
481
|
+
cam.y += (e.clientY - drag.y) / cam.zoom;
|
|
482
|
+
drag = { x: e.clientX, y: e.clientY };
|
|
483
|
+
draw();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const { x, y } = worldPos(e);
|
|
488
|
+
const hit = hitTest(x, y);
|
|
489
|
+
if (hit !== hoveredNode) {
|
|
490
|
+
hoveredNode = hit;
|
|
491
|
+
highlightConnected(hit);
|
|
492
|
+
canvas.style.cursor = hit ? 'pointer' : 'grab';
|
|
493
|
+
draw();
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
498
|
+
const { x, y } = worldPos(e);
|
|
499
|
+
const hit = hitTest(x, y);
|
|
500
|
+
if (hit) {
|
|
501
|
+
selectedNode = hit;
|
|
502
|
+
showDetail(hit);
|
|
503
|
+
draw();
|
|
504
|
+
} else {
|
|
505
|
+
drag = { x: e.clientX, y: e.clientY };
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
canvas.addEventListener('mouseup', () => { drag = null; });
|
|
510
|
+
canvas.addEventListener('mouseleave', () => {
|
|
511
|
+
drag = null;
|
|
512
|
+
hoveredNode = null;
|
|
513
|
+
highlightConnected(null);
|
|
514
|
+
draw();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
canvas.addEventListener('wheel', (e) => {
|
|
518
|
+
e.preventDefault();
|
|
519
|
+
const factor = e.deltaY > 0 ? 0.9 : 1.1;
|
|
520
|
+
cam.zoom = Math.max(0.3, Math.min(3, cam.zoom * factor));
|
|
521
|
+
draw();
|
|
522
|
+
}, { passive: false });
|
|
523
|
+
|
|
524
|
+
// ─── Detail Panel ───
|
|
525
|
+
const detailPanel = document.getElementById('detail');
|
|
526
|
+
const detailContent = document.getElementById('detail-content');
|
|
527
|
+
|
|
528
|
+
function showDetail(node) {
|
|
529
|
+
const layerColor = LAYER_COLORS[node.layer] || '#6366f1';
|
|
530
|
+
|
|
531
|
+
const connections = [];
|
|
532
|
+
for (const e of DATA.edges) {
|
|
533
|
+
if (e.source === node.id && getNode(e.target)) connections.push({ dir: '→', target: e.target });
|
|
534
|
+
if (e.target === node.id && getNode(e.source)) connections.push({ dir: '←', target: e.source });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const signals = [];
|
|
538
|
+
for (const e of DATA.signalEdges) {
|
|
539
|
+
if (e.source === node.id) signals.push({ dir: 'emit', signal: e.signal, target: e.target });
|
|
540
|
+
if (e.target === node.id) signals.push({ dir: 'listen', signal: e.signal, target: e.source });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let html = '<h2>' + esc(node.id.replace('pack:', '')) + '</h2>';
|
|
544
|
+
html += '<span class="layer-badge" style="background:' + layerColor + '22;color:' + layerColor + '">' + node.layer + (node.tier !== 'free' ? ' · ' + node.tier : '') + '</span>';
|
|
545
|
+
if (node.description) html += '<p class="desc">' + esc(node.description) + '</p>';
|
|
546
|
+
|
|
547
|
+
if (connections.length > 0) {
|
|
548
|
+
html += '<h3>Connections (' + connections.length + ')</h3><ul class="conn-list">';
|
|
549
|
+
for (const c of connections) {
|
|
550
|
+
html += '<li onclick="focusNode(\\'' + esc(c.target) + '\\')">' + c.dir + ' ' + esc(c.target) + '</li>';
|
|
551
|
+
}
|
|
552
|
+
html += '</ul>';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (signals.length > 0) {
|
|
556
|
+
const uniqueSignals = [...new Set(signals.map(s => s.signal))];
|
|
557
|
+
html += '<h3>Signals (' + uniqueSignals.length + ')</h3>';
|
|
558
|
+
for (const sig of uniqueSignals) {
|
|
559
|
+
html += '<span class="signal-tag">' + esc(sig) + '</span>';
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
detailContent.innerHTML = html;
|
|
564
|
+
detailPanel.classList.add('open');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
document.getElementById('detail-close').addEventListener('click', () => {
|
|
568
|
+
detailPanel.classList.remove('open');
|
|
569
|
+
selectedNode = null;
|
|
570
|
+
draw();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
window.focusNode = function(id) {
|
|
574
|
+
const n = getNode(id);
|
|
575
|
+
if (!n) return;
|
|
576
|
+
selectedNode = n;
|
|
577
|
+
hoveredNode = n;
|
|
578
|
+
highlightConnected(n);
|
|
579
|
+
showDetail(n);
|
|
580
|
+
draw();
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
584
|
+
|
|
585
|
+
// ─── Search & Filters ───
|
|
586
|
+
document.getElementById('search').addEventListener('input', (e) => {
|
|
587
|
+
searchTerm = e.target.value;
|
|
588
|
+
applyFilters();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const filtersEl = document.getElementById('filters');
|
|
592
|
+
for (const [layer, color] of Object.entries(LAYER_COLORS)) {
|
|
593
|
+
const btn = document.createElement('button');
|
|
594
|
+
btn.className = 'filter-btn active';
|
|
595
|
+
btn.style.setProperty('--btn-color', color);
|
|
596
|
+
btn.textContent = layer;
|
|
597
|
+
btn.addEventListener('click', () => {
|
|
598
|
+
if (activeFilters.has(layer)) {
|
|
599
|
+
activeFilters.delete(layer);
|
|
600
|
+
btn.classList.remove('active');
|
|
601
|
+
} else {
|
|
602
|
+
activeFilters.add(layer);
|
|
603
|
+
btn.classList.add('active');
|
|
604
|
+
}
|
|
605
|
+
applyFilters();
|
|
606
|
+
});
|
|
607
|
+
filtersEl.appendChild(btn);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ─── Stats ───
|
|
611
|
+
document.getElementById('stats').textContent =
|
|
612
|
+
DATA.stats.nodeCount + ' nodes · ' +
|
|
613
|
+
DATA.stats.edgeCount + ' edges · ' +
|
|
614
|
+
DATA.stats.signalCount + ' signals';
|
|
615
|
+
|
|
616
|
+
// ─── Boot ───
|
|
617
|
+
layoutNodes();
|
|
618
|
+
resize();
|
|
619
|
+
</script>
|
|
620
|
+
</body>
|
|
621
|
+
</html>`;
|
|
622
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "61-skill mesh for AI coding assistants — 5-layer architecture, 200+ connections, 8 platforms (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/skills/autopsy/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: autopsy
|
|
|
3
3
|
description: Full codebase health assessment. Analyzes complexity, dependencies, dead code, tech debt, and git hotspots. Produces a health score and rescue plan.
|
|
4
4
|
metadata:
|
|
5
5
|
author: runedev
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.3.0"
|
|
7
7
|
layer: L2
|
|
8
8
|
model: opus
|
|
9
9
|
group: rescue
|
|
@@ -210,3 +210,50 @@ Known failure modes for this skill. Check these before declaring done.
|
|
|
210
210
|
~5000-10000 tokens input, ~2000-4000 tokens output. Opus for deep analysis. Most expensive L2 skill but runs once per rescue.
|
|
211
211
|
|
|
212
212
|
**Scope guardrail:** autopsy assesses — it does not refactor. All surgery is delegated to `surgeon` after the report is complete.
|
|
213
|
+
|
|
214
|
+
## Executive Mode (--executive)
|
|
215
|
+
|
|
216
|
+
When invoked as `/rune autopsy --executive`, generate a board-ready HTML health assessment. Requires Business tier.
|
|
217
|
+
|
|
218
|
+
### Executive Execution Steps
|
|
219
|
+
|
|
220
|
+
1. **Standard Autopsy**: Run Steps 1-5 (structure scan, module analysis, health scoring, risk assessment, RESCUE-REPORT.md)
|
|
221
|
+
2. **Org Context**: Read `.rune/org/org.md` for team structure and governance level
|
|
222
|
+
3. **Cross-Domain Impact**: Map module health to business domains (which team owns which modules)
|
|
223
|
+
4. **Business Risk Translation**: Convert technical health scores to business risk language:
|
|
224
|
+
- Critical modules in revenue path → "Revenue infrastructure at risk"
|
|
225
|
+
- Low test coverage on auth → "Security compliance gap"
|
|
226
|
+
- High churn in customer-facing code → "Customer experience degradation risk"
|
|
227
|
+
5. **HTML Render**: Load `report-templates/autopsy-executive.html` from Business pack and populate all `{{placeholder}}` fields:
|
|
228
|
+
- SVG health ring (score → stroke-dasharray calculation: `score / 100 * 440`)
|
|
229
|
+
- Dimension bars (6 dimensions with color coding)
|
|
230
|
+
- Module table (sorted by priority)
|
|
231
|
+
- Surgery queue (top 5 modules)
|
|
232
|
+
- Risk matrix (6 categories)
|
|
233
|
+
- Git archaeology summary
|
|
234
|
+
- Cross-domain impact table
|
|
235
|
+
- Recommended actions (numbered, prioritized)
|
|
236
|
+
6. **Save**: Write HTML to `EXECUTIVE-HEALTH.html` at project root
|
|
237
|
+
|
|
238
|
+
### Executive Output
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
EXECUTIVE-HEALTH.html — Board-ready HTML report
|
|
242
|
+
RESCUE-REPORT.md — Detailed technical report (standard autopsy)
|
|
243
|
+
.rune/retros/{date}.json — Health metrics for trend tracking
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Color Coding
|
|
247
|
+
|
|
248
|
+
| Score Range | Color | Tier |
|
|
249
|
+
|-------------|-------|------|
|
|
250
|
+
| 80-100 | var(--success) #10b981 | Healthy |
|
|
251
|
+
| 60-79 | var(--warning) #f59e0b | Watch |
|
|
252
|
+
| 40-59 | #f97316 (orange) | At-risk |
|
|
253
|
+
| 0-39 | var(--danger) #ef4444 | Critical |
|
|
254
|
+
|
|
255
|
+
### Graceful Degradation
|
|
256
|
+
|
|
257
|
+
- If no Business pack installed: skip executive mode, produce standard RESCUE-REPORT.md only
|
|
258
|
+
- If `.rune/org/org.md` missing: skip team mapping, show modules without domain ownership
|
|
259
|
+
- If org teams don't map to code modules: show "Unmapped" in cross-domain table
|