@rune-kit/rune 2.8.0 → 2.10.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 (118) hide show
  1. package/README.md +66 -34
  2. package/agents/adversary.md +27 -0
  3. package/agents/architect.md +19 -29
  4. package/agents/asset-creator.md +18 -4
  5. package/agents/audit.md +25 -4
  6. package/agents/autopsy.md +19 -4
  7. package/agents/ba.md +35 -0
  8. package/agents/brainstorm.md +31 -4
  9. package/agents/browser-pilot.md +21 -4
  10. package/agents/coder.md +21 -29
  11. package/agents/completion-gate.md +20 -4
  12. package/agents/constraint-check.md +18 -4
  13. package/agents/context-engine.md +22 -4
  14. package/agents/context-pack.md +32 -0
  15. package/agents/cook.md +41 -4
  16. package/agents/db.md +19 -4
  17. package/agents/debug.md +33 -4
  18. package/agents/dependency-doctor.md +20 -4
  19. package/agents/deploy.md +27 -4
  20. package/agents/design.md +22 -4
  21. package/agents/doc-processor.md +27 -0
  22. package/agents/docs-seeker.md +19 -4
  23. package/agents/docs.md +31 -0
  24. package/agents/fix.md +37 -4
  25. package/agents/git.md +29 -0
  26. package/agents/hallucination-guard.md +20 -4
  27. package/agents/incident.md +21 -4
  28. package/agents/integrity-check.md +18 -4
  29. package/agents/journal.md +19 -4
  30. package/agents/launch.md +32 -4
  31. package/agents/logic-guardian.md +26 -11
  32. package/agents/marketing.md +23 -4
  33. package/agents/mcp-builder.md +26 -0
  34. package/agents/neural-memory.md +30 -0
  35. package/agents/onboard.md +22 -4
  36. package/agents/perf.md +21 -4
  37. package/agents/plan.md +29 -4
  38. package/agents/preflight.md +22 -4
  39. package/agents/problem-solver.md +20 -4
  40. package/agents/rescue.md +23 -4
  41. package/agents/research.md +19 -4
  42. package/agents/researcher.md +19 -29
  43. package/agents/retro.md +32 -0
  44. package/agents/review-intake.md +20 -4
  45. package/agents/review.md +32 -4
  46. package/agents/reviewer.md +20 -28
  47. package/agents/safeguard.md +19 -4
  48. package/agents/sast.md +18 -4
  49. package/agents/scaffold.md +41 -0
  50. package/agents/scanner.md +19 -28
  51. package/agents/scope-guard.md +18 -4
  52. package/agents/scout.md +23 -4
  53. package/agents/sentinel-env.md +26 -0
  54. package/agents/sentinel.md +33 -4
  55. package/agents/sequential-thinking.md +20 -4
  56. package/agents/session-bridge.md +24 -4
  57. package/agents/skill-forge.md +22 -4
  58. package/agents/skill-router.md +26 -4
  59. package/agents/slides.md +24 -0
  60. package/agents/surgeon.md +19 -4
  61. package/agents/team.md +30 -4
  62. package/agents/test.md +36 -4
  63. package/agents/trend-scout.md +17 -4
  64. package/agents/verification.md +20 -4
  65. package/agents/video-creator.md +20 -4
  66. package/agents/watchdog.md +19 -4
  67. package/agents/worktree.md +17 -4
  68. package/compiler/__tests__/analytics.test.js +370 -0
  69. package/compiler/adapters/openclaw.js +2 -2
  70. package/compiler/analytics.js +385 -0
  71. package/compiler/bin/rune.js +68 -2
  72. package/compiler/dashboard.js +883 -0
  73. package/compiler/transforms/branding.js +1 -1
  74. package/extensions/ui/skills/animation-patterns.md +21 -0
  75. package/extensions/ui/skills/component-patterns.md +25 -0
  76. package/extensions/ui/skills/landing-patterns.md +1 -1
  77. package/hooks/context-watch/index.cjs +95 -68
  78. package/hooks/metrics-collector/index.cjs +86 -42
  79. package/hooks/post-session-reflect/index.cjs +41 -5
  80. package/hooks/session-start/index.cjs +8 -2
  81. package/package.json +2 -2
  82. package/skills/audit/SKILL.md +1 -0
  83. package/skills/autopsy/SKILL.md +78 -2
  84. package/skills/autopsy/references/repo-analysis-patterns.md +113 -0
  85. package/skills/ba/SKILL.md +72 -2
  86. package/skills/brainstorm/SKILL.md +1 -0
  87. package/skills/cook/SKILL.md +32 -3
  88. package/skills/cook/references/output-format.md +33 -0
  89. package/skills/db/SKILL.md +1 -0
  90. package/skills/debug/SKILL.md +23 -1
  91. package/skills/deploy/SKILL.md +1 -1
  92. package/skills/design/SKILL.md +110 -3
  93. package/skills/docs/SKILL.md +2 -1
  94. package/skills/fix/SKILL.md +23 -1
  95. package/skills/graft/SKILL.md +352 -0
  96. package/skills/graft/references/challenge-framework.md +98 -0
  97. package/skills/graft/references/mode-decision.md +44 -0
  98. package/skills/incident/SKILL.md +2 -0
  99. package/skills/journal/SKILL.md +2 -0
  100. package/skills/launch/SKILL.md +2 -0
  101. package/skills/marketing/SKILL.md +46 -2
  102. package/skills/mcp-builder/SKILL.md +3 -1
  103. package/skills/mcp-builder/references/auto-discovery-pattern.md +169 -0
  104. package/skills/plan/SKILL.md +60 -2
  105. package/skills/plan/references/feature-map.md +84 -0
  106. package/skills/preflight/SKILL.md +20 -1
  107. package/skills/rescue/SKILL.md +25 -1
  108. package/skills/retro/SKILL.md +2 -0
  109. package/skills/review/SKILL.md +55 -2
  110. package/skills/scaffold/SKILL.md +1 -0
  111. package/skills/scope-guard/SKILL.md +35 -17
  112. package/skills/sentinel/SKILL.md +29 -1
  113. package/skills/session-bridge/SKILL.md +155 -9
  114. package/skills/skill-forge/SKILL.md +43 -1
  115. package/skills/skill-router/{skill.md → SKILL.md} +27 -2
  116. package/skills/team/SKILL.md +24 -1
  117. package/skills/test/SKILL.md +29 -2
  118. package/skills/verification/SKILL.md +1 -0
@@ -1,11 +1,26 @@
1
1
  ---
2
2
  name: watchdog
3
- description: "Post-deploy monitoring. Checks HTTP status, response times, error detection."
3
+ description: "Post-deploy monitoring HTTP health checks, response time measurement, error detection. Called by deploy/launch to verify live site. Precision over recall (no false alarms)."
4
4
  model: haiku
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
8
- You are the **watchdog** skill of the Rune plugin.
8
+ You are the **watchdog** skill Rune's deployment health monitor.
9
9
 
10
- Your full skill definition is in `skills/watchdog/SKILL.md` relative to the plugin root.
11
- Read that file and execute every step precisely. Do not skip steps or improvise beyond what the SKILL.md specifies.
10
+ ## Quick Reference
11
+
12
+ **Workflow:**
13
+ 1. Accept base_url + endpoints (default: `/`, `/health`, `/ready`)
14
+ 2. HTTP status checks: classify 2xx/3xx/4xx/5xx
15
+ 3. Response time: <500ms=FAST, 500-2000ms=ACCEPTABLE, >2000ms=SLOW
16
+ 4. Detect errors: 4xx/5xx, timeouts, empty responses
17
+ 5. Performance patterns: consistent slowness, cluster degradation, spikes
18
+ 6. Report: smoke test results + alerts + performance signals + overall status
19
+
20
+ **Critical Rules:**
21
+ - Report specific metrics (not vague "seems slow")
22
+ - Include baseline comparison when available
23
+ - Separate perf signals from error alerts (different severity)
24
+ - Precision > recall — no false alarms
25
+
26
+ Read `skills/watchdog/SKILL.md` for the full specification.
@@ -1,11 +1,24 @@
1
1
  ---
2
2
  name: worktree
3
- description: "Git worktree lifecycle management."
3
+ description: "Git worktree lifecycle — create isolated workspaces for parallel development, manage branches, cleanup. Max 3 active worktrees. Called by team for parallel streams."
4
4
  model: sonnet
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
8
- You are the **worktree** skill of the Rune plugin.
8
+ You are the **worktree** skill Rune's parallel workspace manager.
9
9
 
10
- Your full skill definition is in `skills/worktree/SKILL.md` relative to the plugin root.
11
- Read that file and execute every step precisely. Do not skip steps or improvise beyond what the SKILL.md specifies.
10
+ ## Quick Reference
11
+
12
+ **Operations:**
13
+ - **Create:** `git worktree add .claude/worktrees/<name> -b rune/<name>`
14
+ - **List:** active worktrees, verify branch merge status
15
+ - **Cleanup:** remove if merged OR with force (checks uncommitted changes)
16
+ - **Cleanup all:** prune stale branches
17
+
18
+ **Critical Rules:**
19
+ - MUST use `.claude/worktrees/` directory (never project root)
20
+ - MUST prefix branches with `rune/` (easy cleanup identification)
21
+ - MAX 3 active worktrees enforced
22
+ - MUST check for uncommitted changes before deletion
23
+
24
+ Read `skills/worktree/SKILL.md` for the full specification.
@@ -0,0 +1,370 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { afterEach, beforeEach, describe, it } from 'node:test';
6
+ import {
7
+ getAllAnalytics,
8
+ getModelDistribution,
9
+ getSessionOverview,
10
+ getSessionTrend,
11
+ getSkillChains,
12
+ getSkillFrequency,
13
+ getToolDistribution,
14
+ } from '../analytics.js';
15
+
16
+ // ─── Test Helpers ───
17
+
18
+ function today() {
19
+ return new Date().toISOString().slice(0, 10);
20
+ }
21
+
22
+ function daysAgo(n) {
23
+ const d = new Date();
24
+ d.setDate(d.getDate() - n);
25
+ return d.toISOString().slice(0, 10);
26
+ }
27
+
28
+ function sessionEntry(overrides = {}) {
29
+ return {
30
+ id: `s-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
31
+ date: today(),
32
+ duration_min: 15,
33
+ tool_calls: 42,
34
+ tool_distribution: { Read: 15, Edit: 12, Bash: 8, Grep: 7 },
35
+ skill_invocations: 5,
36
+ skills_used: ['cook', 'plan', 'scout'],
37
+ primary_skill: 'cook',
38
+ models_used: { opus: 1, sonnet: 2 },
39
+ skill_durations: { cook: 3000, plan: 1500 },
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ function chainEntry(chain, session = 'test-session') {
45
+ return { session, chain, depth: Array.isArray(chain) ? chain.length : 0 };
46
+ }
47
+
48
+ async function setupMetrics(tmpDir, sessions = [], chains = [], skillTotals = null) {
49
+ const metricsDir = path.join(tmpDir, '.rune', 'metrics');
50
+ await mkdir(metricsDir, { recursive: true });
51
+
52
+ if (sessions.length > 0) {
53
+ const lines = `${sessions.map((s) => JSON.stringify(s)).join('\n')}\n`;
54
+ await writeFile(path.join(metricsDir, 'sessions.jsonl'), lines);
55
+ }
56
+
57
+ if (chains.length > 0) {
58
+ const lines = `${chains.map((c) => JSON.stringify(c)).join('\n')}\n`;
59
+ await writeFile(path.join(metricsDir, 'chains.jsonl'), lines);
60
+ }
61
+
62
+ if (skillTotals) {
63
+ await writeFile(
64
+ path.join(metricsDir, 'skills.json'),
65
+ JSON.stringify({ version: 1, updated: new Date().toISOString(), skills: skillTotals }),
66
+ );
67
+ }
68
+ }
69
+
70
+ // ─── Tests ───
71
+
72
+ describe('analytics — empty state', () => {
73
+ let tmpDir;
74
+ beforeEach(async () => {
75
+ tmpDir = await mkdtemp(path.join(os.tmpdir(), 'rune-analytics-test-'));
76
+ });
77
+ afterEach(async () => {
78
+ await rm(tmpDir, { recursive: true, force: true });
79
+ });
80
+
81
+ it('returns zeros when no .rune/metrics/ exists', async () => {
82
+ const overview = await getSessionOverview(tmpDir, 30);
83
+ assert.equal(overview.total_sessions, 0);
84
+ assert.equal(overview.avg_duration_min, 0);
85
+ assert.equal(overview.total_tool_calls, 0);
86
+ });
87
+
88
+ it('returns empty arrays for frequency/distribution', async () => {
89
+ const skills = await getSkillFrequency(tmpDir, 30);
90
+ assert.deepEqual(skills, []);
91
+ const models = await getModelDistribution(tmpDir, 30);
92
+ assert.deepEqual(models, []);
93
+ const tools = await getToolDistribution(tmpDir, 30);
94
+ assert.deepEqual(tools, []);
95
+ const chains = await getSkillChains(tmpDir, 30);
96
+ assert.deepEqual(chains, []);
97
+ });
98
+
99
+ it('returns empty trend', async () => {
100
+ const trend = await getSessionTrend(tmpDir, 30);
101
+ assert.deepEqual(trend, []);
102
+ });
103
+
104
+ it('getAllAnalytics works with empty data', async () => {
105
+ const data = await getAllAnalytics(tmpDir, 30);
106
+ assert.equal(data.overview.total_sessions, 0);
107
+ assert.ok(data.generated);
108
+ assert.equal(data.days, 30);
109
+ });
110
+ });
111
+
112
+ describe('analytics — with data', () => {
113
+ let tmpDir;
114
+ beforeEach(async () => {
115
+ tmpDir = await mkdtemp(path.join(os.tmpdir(), 'rune-analytics-test-'));
116
+ });
117
+ afterEach(async () => {
118
+ await rm(tmpDir, { recursive: true, force: true });
119
+ });
120
+
121
+ it('getSessionOverview aggregates correctly', async () => {
122
+ await setupMetrics(tmpDir, [
123
+ sessionEntry({ duration_min: 10, tool_calls: 30, skill_invocations: 3 }),
124
+ sessionEntry({ duration_min: 20, tool_calls: 50, skill_invocations: 7 }),
125
+ ]);
126
+ const overview = await getSessionOverview(tmpDir, 30);
127
+ assert.equal(overview.total_sessions, 2);
128
+ assert.equal(overview.avg_duration_min, 15);
129
+ assert.equal(overview.total_tool_calls, 80);
130
+ assert.equal(overview.total_skill_invocations, 10);
131
+ assert.equal(overview.active_days, 1);
132
+ });
133
+
134
+ it('getSkillFrequency counts sessions per skill', async () => {
135
+ await setupMetrics(tmpDir, [
136
+ sessionEntry({ skills_used: ['cook', 'plan'] }),
137
+ sessionEntry({ skills_used: ['cook', 'debug'] }),
138
+ sessionEntry({ skills_used: ['plan'] }),
139
+ ]);
140
+ const freq = await getSkillFrequency(tmpDir, 30);
141
+ assert.equal(freq[0].skill, 'cook');
142
+ assert.equal(freq[0].sessions_count, 2);
143
+ assert.equal(freq[1].skill, 'plan');
144
+ assert.equal(freq[1].sessions_count, 2);
145
+ assert.equal(freq[2].skill, 'debug');
146
+ assert.equal(freq[2].sessions_count, 1);
147
+ });
148
+
149
+ it('getModelDistribution aggregates model counts', async () => {
150
+ await setupMetrics(tmpDir, [
151
+ sessionEntry({ models_used: { opus: 2, sonnet: 3 } }),
152
+ sessionEntry({ models_used: { sonnet: 1, haiku: 5 } }),
153
+ ]);
154
+ const models = await getModelDistribution(tmpDir, 30);
155
+ const map = Object.fromEntries(models.map((m) => [m.model, m.skill_count]));
156
+ assert.equal(map.sonnet, 4);
157
+ assert.equal(map.haiku, 5);
158
+ assert.equal(map.opus, 2);
159
+ });
160
+
161
+ it('getSessionTrend groups by date', async () => {
162
+ await setupMetrics(tmpDir, [
163
+ sessionEntry({ date: daysAgo(1), skill_invocations: 3 }),
164
+ sessionEntry({ date: daysAgo(1), skill_invocations: 5 }),
165
+ sessionEntry({ date: today(), skill_invocations: 2 }),
166
+ ]);
167
+ const trend = await getSessionTrend(tmpDir, 30);
168
+ assert.equal(trend.length, 2);
169
+ assert.equal(trend[0].sessions, 2);
170
+ assert.equal(trend[0].skill_invocations, 8);
171
+ assert.equal(trend[1].sessions, 1);
172
+ });
173
+
174
+ it('getSkillChains counts pattern frequency', async () => {
175
+ await setupMetrics(
176
+ tmpDir,
177
+ [sessionEntry({ id: 's1' }), sessionEntry({ id: 's2' })],
178
+ [
179
+ chainEntry(['cook', 'plan', 'scout'], 's1'),
180
+ chainEntry(['cook', 'plan', 'scout'], 's2'),
181
+ chainEntry(['debug', 'fix'], 's1'),
182
+ ],
183
+ );
184
+ const chains = await getSkillChains(tmpDir, 30);
185
+ assert.equal(chains[0].chain, 'cook → plan → scout');
186
+ assert.equal(chains[0].count, 2);
187
+ assert.equal(chains[1].chain, 'debug → fix');
188
+ assert.equal(chains[1].count, 1);
189
+ });
190
+
191
+ it('getToolDistribution aggregates tool counts', async () => {
192
+ await setupMetrics(tmpDir, [
193
+ sessionEntry({ tool_distribution: { Read: 10, Edit: 5 } }),
194
+ sessionEntry({ tool_distribution: { Read: 3, Bash: 7 } }),
195
+ ]);
196
+ const tools = await getToolDistribution(tmpDir, 30);
197
+ const map = Object.fromEntries(tools.map((t) => [t.tool, t.count]));
198
+ assert.equal(map.Read, 13);
199
+ assert.equal(map.Edit, 5);
200
+ assert.equal(map.Bash, 7);
201
+ });
202
+ });
203
+
204
+ describe('analytics — date filtering', () => {
205
+ let tmpDir;
206
+ beforeEach(async () => {
207
+ tmpDir = await mkdtemp(path.join(os.tmpdir(), 'rune-analytics-test-'));
208
+ });
209
+ afterEach(async () => {
210
+ await rm(tmpDir, { recursive: true, force: true });
211
+ });
212
+
213
+ it('filters sessions by day range', async () => {
214
+ await setupMetrics(tmpDir, [
215
+ sessionEntry({ date: today() }),
216
+ sessionEntry({ date: daysAgo(5) }),
217
+ sessionEntry({ date: daysAgo(15) }),
218
+ sessionEntry({ date: daysAgo(45) }),
219
+ ]);
220
+ const all = await getSessionOverview(tmpDir, 0);
221
+ assert.equal(all.total_sessions, 4);
222
+
223
+ const week = await getSessionOverview(tmpDir, 7);
224
+ assert.equal(week.total_sessions, 2);
225
+
226
+ const month = await getSessionOverview(tmpDir, 30);
227
+ assert.equal(month.total_sessions, 3);
228
+ });
229
+
230
+ it('days=0 returns all sessions', async () => {
231
+ await setupMetrics(tmpDir, [sessionEntry({ date: daysAgo(100) }), sessionEntry({ date: today() })]);
232
+ const all = await getSessionOverview(tmpDir, 0);
233
+ assert.equal(all.total_sessions, 2);
234
+ });
235
+ });
236
+
237
+ describe('analytics — edge cases', () => {
238
+ let tmpDir;
239
+ beforeEach(async () => {
240
+ tmpDir = await mkdtemp(path.join(os.tmpdir(), 'rune-analytics-test-'));
241
+ });
242
+ afterEach(async () => {
243
+ await rm(tmpDir, { recursive: true, force: true });
244
+ });
245
+
246
+ it('handles malformed JSONL lines gracefully', async () => {
247
+ const metricsDir = path.join(tmpDir, '.rune', 'metrics');
248
+ await mkdir(metricsDir, { recursive: true });
249
+ await writeFile(
250
+ path.join(metricsDir, 'sessions.jsonl'),
251
+ `${JSON.stringify(sessionEntry())}\nNOT VALID JSON\n${JSON.stringify(sessionEntry())}\n`,
252
+ );
253
+ const overview = await getSessionOverview(tmpDir, 30);
254
+ assert.equal(overview.total_sessions, 2); // skips malformed line
255
+ });
256
+
257
+ it('handles empty JSONL files', async () => {
258
+ const metricsDir = path.join(tmpDir, '.rune', 'metrics');
259
+ await mkdir(metricsDir, { recursive: true });
260
+ await writeFile(path.join(metricsDir, 'sessions.jsonl'), '');
261
+ const overview = await getSessionOverview(tmpDir, 30);
262
+ assert.equal(overview.total_sessions, 0);
263
+ });
264
+
265
+ it('excludes orphaned chains (no matching session)', async () => {
266
+ await setupMetrics(
267
+ tmpDir,
268
+ [sessionEntry({ id: 's1' })],
269
+ [
270
+ chainEntry(['cook', 'plan'], 's1'),
271
+ chainEntry(['debug', 'fix'], 'orphaned-session'), // no matching session
272
+ chainEntry(null, 's1'), // malformed chain
273
+ ],
274
+ );
275
+ const chains = await getSkillChains(tmpDir, 30);
276
+ assert.equal(chains.length, 1);
277
+ assert.equal(chains[0].chain, 'cook → plan');
278
+ });
279
+
280
+ it('handles sessions without optional fields', async () => {
281
+ await setupMetrics(tmpDir, [
282
+ { id: 's1', date: today(), duration_min: 5, tool_calls: 10 },
283
+ // Missing: skill_invocations, skills_used, primary_skill, models_used, skill_durations
284
+ ]);
285
+ const overview = await getSessionOverview(tmpDir, 30);
286
+ assert.equal(overview.total_sessions, 1);
287
+ const models = await getModelDistribution(tmpDir, 30);
288
+ assert.deepEqual(models, []);
289
+ });
290
+
291
+ it('handles corrupted skills.json', async () => {
292
+ const metricsDir = path.join(tmpDir, '.rune', 'metrics');
293
+ await mkdir(metricsDir, { recursive: true });
294
+ await writeFile(path.join(metricsDir, 'skills.json'), 'NOT JSON');
295
+ // Should not throw
296
+ const data = await getAllAnalytics(tmpDir, 30);
297
+ assert.ok(data);
298
+ });
299
+ });
300
+
301
+ describe('analytics — dashboard HTML generation', () => {
302
+ it('generates valid HTML from analytics data', async () => {
303
+ const { generateDashboardHTML } = await import('../dashboard.js');
304
+
305
+ const data = {
306
+ overview: {
307
+ total_sessions: 5,
308
+ avg_duration_min: 12,
309
+ total_tool_calls: 200,
310
+ total_skill_invocations: 30,
311
+ active_days: 3,
312
+ },
313
+ skillFrequency: [
314
+ { skill: 'cook', sessions_count: 4 },
315
+ { skill: 'plan', sessions_count: 2 },
316
+ ],
317
+ modelDistribution: [
318
+ { model: 'sonnet', skill_count: 8 },
319
+ { model: 'opus', skill_count: 3 },
320
+ ],
321
+ sessionTrend: [{ date: '2026-03-30', sessions: 2, duration_min: 20, skill_invocations: 10, tool_calls: 80 }],
322
+ skillChains: [{ chain: 'cook → plan → scout', count: 3 }],
323
+ toolDistribution: [
324
+ { tool: 'Read', count: 50 },
325
+ { tool: 'Edit', count: 30 },
326
+ ],
327
+ skillHeatmap: { heatmap: [], dates: [], maxCount: 1 },
328
+ sessionTimeline: [],
329
+ skillMesh: { nodes: [], edges: [], maxCount: 1 },
330
+ generated: new Date().toISOString(),
331
+ days: 30,
332
+ };
333
+
334
+ const html = generateDashboardHTML(data);
335
+ assert.ok(html.includes('<!DOCTYPE html>'));
336
+ assert.ok(html.includes('Rune Analytics'));
337
+ assert.ok(html.includes('Skill Mesh'));
338
+ assert.ok(html.includes('Models'));
339
+ assert.ok(html.includes('Workflow Chains'));
340
+ assert.ok(html.includes('Recent Sessions'));
341
+ assert.ok(html.includes('cook'));
342
+ });
343
+
344
+ it('generates empty state HTML when no data', async () => {
345
+ const { generateDashboardHTML } = await import('../dashboard.js');
346
+
347
+ const data = {
348
+ overview: {
349
+ total_sessions: 0,
350
+ avg_duration_min: 0,
351
+ total_tool_calls: 0,
352
+ total_skill_invocations: 0,
353
+ active_days: 0,
354
+ },
355
+ skillFrequency: [],
356
+ modelDistribution: [],
357
+ sessionTrend: [],
358
+ skillChains: [],
359
+ toolDistribution: [],
360
+ skillHeatmap: { heatmap: [], dates: [], maxCount: 1 },
361
+ sessionTimeline: [],
362
+ skillMesh: { nodes: [], edges: [], maxCount: 1 },
363
+ generated: new Date().toISOString(),
364
+ days: 30,
365
+ };
366
+
367
+ const html = generateDashboardHTML(data);
368
+ assert.ok(html.includes('No analytics data yet'));
369
+ });
370
+ });
@@ -73,7 +73,7 @@ export default {
73
73
  name: 'Rune',
74
74
  kind: 'skills',
75
75
  description:
76
- '59-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 200+ connections, 14 extension packs.',
76
+ '62-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 215+ connections, 14 extension packs.',
77
77
  version: pluginJson.version || '0.0.0',
78
78
  skills: ['./skills'],
79
79
  configSchema: {
@@ -116,7 +116,7 @@ export default {
116
116
 
117
117
  > Less skills. Deeper connections.
118
118
 
119
- **${skills.length}-skill mesh** for AI coding assistants — 5-layer architecture, 200+ connections, 14 extension packs.
119
+ **${skills.length}-skill mesh** for AI coding assistants — 5-layer architecture, 215+ connections, 14 extension packs.
120
120
 
121
121
  ## Install
122
122