@rune-kit/rune 2.7.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.
- package/README.md +78 -41
- package/agents/adversary.md +27 -0
- package/agents/architect.md +19 -29
- package/agents/asset-creator.md +18 -4
- package/agents/audit.md +25 -4
- package/agents/autopsy.md +19 -4
- package/agents/ba.md +35 -0
- package/agents/brainstorm.md +31 -4
- package/agents/browser-pilot.md +21 -4
- package/agents/coder.md +21 -29
- package/agents/completion-gate.md +20 -4
- package/agents/constraint-check.md +18 -4
- package/agents/context-engine.md +22 -4
- package/agents/context-pack.md +32 -0
- package/agents/cook.md +41 -4
- package/agents/db.md +19 -4
- package/agents/debug.md +33 -4
- package/agents/dependency-doctor.md +20 -4
- package/agents/deploy.md +27 -4
- package/agents/design.md +22 -4
- package/agents/doc-processor.md +27 -0
- package/agents/docs-seeker.md +19 -4
- package/agents/docs.md +31 -0
- package/agents/fix.md +37 -4
- package/agents/git.md +29 -0
- package/agents/hallucination-guard.md +20 -4
- package/agents/incident.md +21 -4
- package/agents/integrity-check.md +18 -4
- package/agents/journal.md +19 -4
- package/agents/launch.md +32 -4
- package/agents/logic-guardian.md +26 -11
- package/agents/marketing.md +23 -4
- package/agents/mcp-builder.md +26 -0
- package/agents/neural-memory.md +30 -0
- package/agents/onboard.md +22 -4
- package/agents/perf.md +21 -4
- package/agents/plan.md +29 -4
- package/agents/preflight.md +22 -4
- package/agents/problem-solver.md +20 -4
- package/agents/rescue.md +23 -4
- package/agents/research.md +19 -4
- package/agents/researcher.md +19 -29
- package/agents/retro.md +32 -0
- package/agents/review-intake.md +20 -4
- package/agents/review.md +32 -4
- package/agents/reviewer.md +20 -28
- package/agents/safeguard.md +19 -4
- package/agents/sast.md +18 -4
- package/agents/scaffold.md +41 -0
- package/agents/scanner.md +19 -28
- package/agents/scope-guard.md +18 -4
- package/agents/scout.md +23 -4
- package/agents/sentinel-env.md +26 -0
- package/agents/sentinel.md +33 -4
- package/agents/sequential-thinking.md +20 -4
- package/agents/session-bridge.md +24 -4
- package/agents/skill-forge.md +22 -4
- package/agents/skill-router.md +26 -4
- package/agents/slides.md +24 -0
- package/agents/surgeon.md +19 -4
- package/agents/team.md +30 -4
- package/agents/test.md +36 -4
- package/agents/trend-scout.md +17 -4
- package/agents/verification.md +20 -4
- package/agents/video-creator.md +20 -4
- package/agents/watchdog.md +19 -4
- package/agents/worktree.md +17 -4
- package/compiler/__tests__/analytics.test.js +370 -0
- package/compiler/adapters/openclaw.js +2 -2
- package/compiler/analytics.js +385 -0
- package/compiler/bin/rune.js +68 -2
- package/compiler/dashboard.js +883 -0
- package/compiler/transforms/branding.js +1 -1
- package/extensions/ui/skills/animation-patterns.md +21 -0
- package/extensions/ui/skills/component-patterns.md +25 -0
- package/extensions/ui/skills/landing-patterns.md +1 -1
- package/hooks/context-watch/index.cjs +95 -68
- package/hooks/metrics-collector/index.cjs +86 -42
- package/hooks/post-session-reflect/index.cjs +41 -5
- package/hooks/session-start/index.cjs +8 -2
- package/package.json +2 -2
- package/skills/audit/SKILL.md +1 -0
- package/skills/autopsy/SKILL.md +78 -2
- package/skills/autopsy/references/repo-analysis-patterns.md +113 -0
- package/skills/ba/SKILL.md +72 -2
- package/skills/brainstorm/SKILL.md +1 -0
- package/skills/completion-gate/SKILL.md +26 -3
- package/skills/context-engine/SKILL.md +49 -1
- package/skills/cook/SKILL.md +72 -3
- package/skills/cook/references/output-format.md +33 -0
- package/skills/db/SKILL.md +1 -0
- package/skills/debug/SKILL.md +57 -1
- package/skills/deploy/SKILL.md +1 -1
- package/skills/design/SKILL.md +110 -3
- package/skills/docs/SKILL.md +2 -1
- package/skills/fix/SKILL.md +47 -1
- package/skills/graft/SKILL.md +352 -0
- package/skills/graft/references/challenge-framework.md +98 -0
- package/skills/graft/references/mode-decision.md +44 -0
- package/skills/incident/SKILL.md +2 -0
- package/skills/journal/SKILL.md +2 -0
- package/skills/launch/SKILL.md +2 -0
- package/skills/marketing/SKILL.md +46 -2
- package/skills/mcp-builder/SKILL.md +3 -1
- package/skills/mcp-builder/references/auto-discovery-pattern.md +169 -0
- package/skills/plan/SKILL.md +60 -4
- package/skills/plan/references/feature-map.md +84 -0
- package/skills/preflight/SKILL.md +20 -1
- package/skills/rescue/SKILL.md +25 -1
- package/skills/research/SKILL.md +24 -1
- package/skills/retro/SKILL.md +2 -0
- package/skills/review/SKILL.md +55 -2
- package/skills/scaffold/SKILL.md +1 -0
- package/skills/scope-guard/SKILL.md +35 -17
- package/skills/scout/SKILL.md +22 -1
- package/skills/sentinel/SKILL.md +29 -1
- package/skills/sentinel-env/SKILL.md +0 -2
- package/skills/session-bridge/SKILL.md +155 -9
- package/skills/skill-forge/SKILL.md +43 -1
- package/skills/skill-router/{skill.md → SKILL.md} +27 -2
- package/skills/team/SKILL.md +24 -1
- package/skills/test/SKILL.md +29 -2
- package/skills/verification/SKILL.md +1 -2
package/agents/video-creator.md
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: video-creator
|
|
3
|
-
description: "Video content
|
|
3
|
+
description: "Video content PLANNING — narration scripts with timing, storyboards, shot lists, asset checklists. Plans only, not actual video production. Platform-aware (YouTube/Twitter/TikTok/Loom)."
|
|
4
4
|
model: sonnet
|
|
5
5
|
subagent_type: general-purpose
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are the **video-creator** skill
|
|
8
|
+
You are the **video-creator** skill — Rune's video content planner.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
**Workflow:**
|
|
13
|
+
1. Accept brief: topic, audience, duration, platform
|
|
14
|
+
2. Apply platform constraints (YouTube/Twitter/TikTok/Loom duration/format limits)
|
|
15
|
+
3. Write script with timing marks: hook (0-5s), setup, demo/body, CTA (last 10s)
|
|
16
|
+
4. Create storyboard: scene-by-scene with duration, visuals, transitions
|
|
17
|
+
5. Define shot list: categorized by type (screen recording, terminal, code, diagram)
|
|
18
|
+
6. List assets needed (screenshots, diagrams, slides, thumbnail)
|
|
19
|
+
7. Save to `marketing/video-plan.md`
|
|
20
|
+
|
|
21
|
+
**Critical Rules:**
|
|
22
|
+
- This creates PLANS ONLY — actual recording/editing is human work
|
|
23
|
+
- MUST apply platform constraints immediately
|
|
24
|
+
- MUST include CTA in every script (last 10s required)
|
|
25
|
+
- MUST save to file (not verbal only)
|
|
26
|
+
|
|
27
|
+
Read `skills/video-creator/SKILL.md` for the full specification.
|
package/agents/watchdog.md
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: watchdog
|
|
3
|
-
description: "Post-deploy monitoring
|
|
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
|
|
8
|
+
You are the **watchdog** skill — Rune's deployment health monitor.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
package/agents/worktree.md
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: worktree
|
|
3
|
-
description: "Git worktree lifecycle
|
|
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
|
|
8
|
+
You are the **worktree** skill — Rune's parallel workspace manager.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
'
|
|
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,
|
|
119
|
+
**${skills.length}-skill mesh** for AI coding assistants — 5-layer architecture, 215+ connections, 14 extension packs.
|
|
120
120
|
|
|
121
121
|
## Install
|
|
122
122
|
|