@rune-kit/rune 2.3.0 → 2.3.1

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 (46) hide show
  1. package/README.md +395 -389
  2. package/compiler/__tests__/tier-override.test.js +158 -0
  3. package/compiler/adapters/antigravity.js +3 -8
  4. package/compiler/adapters/codex.js +3 -8
  5. package/compiler/adapters/cursor.js +3 -8
  6. package/compiler/adapters/generic.js +3 -8
  7. package/compiler/adapters/openclaw.js +4 -9
  8. package/compiler/adapters/opencode.js +3 -8
  9. package/compiler/adapters/windsurf.js +3 -8
  10. package/compiler/bin/rune.js +34 -1
  11. package/compiler/emitter.js +94 -5
  12. package/compiler/transforms/branding.js +10 -3
  13. package/docs/ARCHITECTURE.md +3 -3
  14. package/docs/VISION.md +3 -3
  15. package/docs/guides/index.html +14 -14
  16. package/docs/index.html +7 -7
  17. package/docs/skills/index.html +832 -832
  18. package/extensions/ai-ml/PACK.md +7 -0
  19. package/extensions/content/PACK.md +7 -0
  20. package/extensions/mobile/PACK.md +9 -9
  21. package/extensions/zalo/PACK.md +9 -0
  22. package/package.json +2 -2
  23. package/skills/audit/SKILL.md +526 -529
  24. package/skills/ba/SKILL.md +349 -351
  25. package/skills/completion-gate/SKILL.md +260 -263
  26. package/skills/context-engine/SKILL.md +0 -6
  27. package/skills/cook/SKILL.md +2 -11
  28. package/skills/debug/SKILL.md +392 -394
  29. package/skills/deploy/references/post-deploy-integration.md +192 -0
  30. package/skills/fix/SKILL.md +281 -282
  31. package/skills/onboard/SKILL.md +0 -4
  32. package/skills/plan/references/completeness-scoring.md +0 -1
  33. package/skills/plan/references/outcome-block.md +0 -1
  34. package/skills/plan/references/workflow-registry.md +0 -1
  35. package/skills/preflight/SKILL.md +360 -365
  36. package/skills/rescue/SKILL.md +1 -0
  37. package/skills/research/SKILL.md +149 -150
  38. package/skills/review/SKILL.md +489 -495
  39. package/skills/sentinel/SKILL.md +0 -11
  40. package/skills/sentinel/references/destructive-commands.md +0 -1
  41. package/skills/sentinel/references/skill-content-guard.md +0 -1
  42. package/skills/session-bridge/SKILL.md +0 -4
  43. package/skills/skill-router/{SKILL.md → skill.md} +446 -397
  44. package/skills/team/SKILL.md +1 -2
  45. package/skills/test/SKILL.md +585 -593
  46. package/skills/watchdog/references/webhook-health-checks.md +243 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Tier Override Tests
3
+ *
4
+ * Tests the tier resolution logic: Business > Pro > Free.
5
+ * When the same normalized pack name exists in multiple tiers,
6
+ * the highest-priority tier wins.
7
+ */
8
+
9
+ import assert from 'node:assert';
10
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import path from 'node:path';
13
+ import { describe, test } from 'node:test';
14
+ import { discoverTieredPacks } from '../emitter.js';
15
+
16
+ /**
17
+ * Create a temp directory structure with PACK.md files for testing
18
+ */
19
+ function createTierFixture() {
20
+ const root = mkdtempSync(path.join(tmpdir(), 'rune-tier-'));
21
+
22
+ const freePacks = path.join(root, 'free');
23
+ const proPacks = path.join(root, 'pro');
24
+ const bizPacks = path.join(root, 'business');
25
+
26
+ // Free: saas, trading, ui
27
+ for (const pack of ['saas', 'trading', 'ui']) {
28
+ const dir = path.join(freePacks, pack);
29
+ mkdirSync(dir, { recursive: true });
30
+ writeFileSync(path.join(dir, 'PACK.md'), `---\nname: "@rune/${pack}"\n---\n\nFree ${pack} pack.\n`);
31
+ }
32
+
33
+ // Pro: pro-product, pro-saas (overrides free saas)
34
+ for (const pack of ['pro-product', 'pro-saas']) {
35
+ const dir = path.join(proPacks, pack);
36
+ mkdirSync(dir, { recursive: true });
37
+ writeFileSync(path.join(dir, 'PACK.md'), `---\nname: "@rune-pro/${pack}"\n---\n\nPro ${pack} pack.\n`);
38
+ }
39
+
40
+ // Business: business-saas (overrides pro-saas AND free saas), business-finance
41
+ for (const pack of ['business-saas', 'business-finance']) {
42
+ const dir = path.join(bizPacks, pack);
43
+ mkdirSync(dir, { recursive: true });
44
+ writeFileSync(path.join(dir, 'PACK.md'), `---\nname: "@rune-biz/${pack}"\n---\n\nBusiness ${pack} pack.\n`);
45
+ }
46
+
47
+ return { root, freePacks, proPacks, bizPacks };
48
+ }
49
+
50
+ // --- discoverTieredPacks ---
51
+
52
+ describe('discoverTieredPacks', () => {
53
+ test('returns free packs when no tier sources provided', async () => {
54
+ const { freePacks, root } = createTierFixture();
55
+ try {
56
+ const packs = await discoverTieredPacks(freePacks, {});
57
+ assert.strictEqual(packs.length, 3);
58
+ assert.ok(packs.every((p) => p.tier === 'free'));
59
+ const names = packs.map((p) => p.dirName).sort();
60
+ assert.deepStrictEqual(names, ['saas', 'trading', 'ui']);
61
+ } finally {
62
+ rmSync(root, { recursive: true, force: true });
63
+ }
64
+ });
65
+
66
+ test('pro pack overrides free pack with same normalized name', async () => {
67
+ const { freePacks, proPacks, root } = createTierFixture();
68
+ try {
69
+ const packs = await discoverTieredPacks(freePacks, { pro: proPacks });
70
+
71
+ // "saas" normalized name should come from pro (pro-saas), not free (saas)
72
+ const saasPack = packs.find((p) => p.dirName === 'pro-saas' || p.dirName === 'saas');
73
+ assert.ok(saasPack, 'saas pack should exist');
74
+ assert.strictEqual(saasPack.tier, 'pro');
75
+ assert.strictEqual(saasPack.dirName, 'pro-saas');
76
+
77
+ // pro-product should be added (no free equivalent)
78
+ const productPack = packs.find((p) => p.dirName === 'pro-product');
79
+ assert.ok(productPack, 'pro-product should exist');
80
+ assert.strictEqual(productPack.tier, 'pro');
81
+
82
+ // trading and ui should remain free
83
+ const tradingPack = packs.find((p) => p.dirName === 'trading');
84
+ assert.ok(tradingPack, 'trading should exist');
85
+ assert.strictEqual(tradingPack.tier, 'free');
86
+ } finally {
87
+ rmSync(root, { recursive: true, force: true });
88
+ }
89
+ });
90
+
91
+ test('business pack overrides both pro and free', async () => {
92
+ const { freePacks, proPacks, bizPacks, root } = createTierFixture();
93
+ try {
94
+ const packs = await discoverTieredPacks(freePacks, { pro: proPacks, business: bizPacks });
95
+
96
+ // "saas" should come from business (highest tier)
97
+ const saasPack = packs.find((p) => p.dirName.includes('saas'));
98
+ assert.ok(saasPack, 'saas pack should exist');
99
+ assert.strictEqual(saasPack.tier, 'business');
100
+ assert.strictEqual(saasPack.dirName, 'business-saas');
101
+
102
+ // business-finance should be added
103
+ const financePack = packs.find((p) => p.dirName === 'business-finance');
104
+ assert.ok(financePack, 'business-finance should exist');
105
+ assert.strictEqual(financePack.tier, 'business');
106
+
107
+ // pro-product should still exist (no business override)
108
+ const productPack = packs.find((p) => p.dirName === 'pro-product');
109
+ assert.ok(productPack, 'pro-product should exist');
110
+ assert.strictEqual(productPack.tier, 'pro');
111
+
112
+ // trading and ui should remain free
113
+ const tradingPack = packs.find((p) => p.dirName === 'trading');
114
+ assert.strictEqual(tradingPack.tier, 'free');
115
+ } finally {
116
+ rmSync(root, { recursive: true, force: true });
117
+ }
118
+ });
119
+
120
+ test('handles missing tier directories gracefully', async () => {
121
+ const { freePacks, root } = createTierFixture();
122
+ try {
123
+ const packs = await discoverTieredPacks(freePacks, {
124
+ pro: '/nonexistent/pro/extensions',
125
+ business: '/nonexistent/business/extensions',
126
+ });
127
+ assert.strictEqual(packs.length, 3);
128
+ assert.ok(packs.every((p) => p.tier === 'free'));
129
+ } finally {
130
+ rmSync(root, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ test('respects enabledPacks filter across tiers', async () => {
135
+ const { freePacks, proPacks, root } = createTierFixture();
136
+ try {
137
+ const packs = await discoverTieredPacks(freePacks, { pro: proPacks }, ['trading', 'pro-product']);
138
+
139
+ assert.strictEqual(packs.length, 2);
140
+ const names = packs.map((p) => p.dirName).sort();
141
+ assert.deepStrictEqual(names, ['pro-product', 'trading']);
142
+ } finally {
143
+ rmSync(root, { recursive: true, force: true });
144
+ }
145
+ });
146
+
147
+ test('output is sorted by dirName for deterministic builds', async () => {
148
+ const { freePacks, proPacks, bizPacks, root } = createTierFixture();
149
+ try {
150
+ const packs = await discoverTieredPacks(freePacks, { pro: proPacks, business: bizPacks });
151
+ const names = packs.map((p) => p.dirName);
152
+ const sorted = [...names].sort();
153
+ assert.deepStrictEqual(names, sorted);
154
+ } finally {
155
+ rmSync(root, { recursive: true, force: true });
156
+ }
157
+ });
158
+ });
@@ -4,6 +4,8 @@
4
4
  * Emits .md rule files for .agent/rules/ directory.
5
5
  */
6
6
 
7
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
8
+
7
9
  const TOOL_MAP = {
8
10
  Read: 'read the file',
9
11
  Write: 'write/create the file',
@@ -38,14 +40,7 @@ export default {
38
40
  },
39
41
 
40
42
  generateFooter() {
41
- return [
42
- '',
43
- '---',
44
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
45
- '> Source: https://github.com/rune-kit/rune (MIT)',
46
- '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
47
- '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
48
- ].join('\n');
43
+ return BRANDING_FOOTER;
49
44
  },
50
45
 
51
46
  transformSubagentInstruction(text) {
@@ -10,6 +10,8 @@
10
10
  * Codex skill format: .codex/skills/{name}/SKILL.md
11
11
  */
12
12
 
13
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
14
+
13
15
  const TOOL_MAP = {
14
16
  Read: 'read the file',
15
17
  Write: 'write/create the file',
@@ -49,14 +51,7 @@ export default {
49
51
  },
50
52
 
51
53
  generateFooter() {
52
- return [
53
- '',
54
- '---',
55
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
56
- '> Source: https://github.com/rune-kit/rune (MIT)',
57
- '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
58
- '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
59
- ].join('\n');
54
+ return BRANDING_FOOTER;
60
55
  },
61
56
 
62
57
  transformSubagentInstruction(text) {
@@ -5,6 +5,8 @@
5
5
  * Uses @file references for cross-skill mesh.
6
6
  */
7
7
 
8
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
9
+
8
10
  const TOOL_MAP = {
9
11
  Read: 'read the file',
10
12
  Write: 'write/create the file',
@@ -46,14 +48,7 @@ export default {
46
48
  },
47
49
 
48
50
  generateFooter() {
49
- return [
50
- '',
51
- '---',
52
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
53
- '> Source: https://github.com/rune-kit/rune (MIT)',
54
- '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
55
- '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
56
- ].join('\n');
51
+ return BRANDING_FOOTER;
57
52
  },
58
53
 
59
54
  transformSubagentInstruction(text) {
@@ -5,6 +5,8 @@
5
5
  * Uses the most portable format possible.
6
6
  */
7
7
 
8
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
9
+
8
10
  const TOOL_MAP = {
9
11
  Read: 'read the file',
10
12
  Write: 'write/create the file',
@@ -39,14 +41,7 @@ export default {
39
41
  },
40
42
 
41
43
  generateFooter() {
42
- return [
43
- '',
44
- '---',
45
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
46
- '> Source: https://github.com/rune-kit/rune (MIT)',
47
- '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
48
- '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
49
- ].join('\n');
44
+ return BRANDING_FOOTER;
50
45
  },
51
46
 
52
47
  transformSubagentInstruction(text) {
@@ -9,6 +9,8 @@
9
9
  * Follows the NeuralMemory OpenClaw plugin pattern.
10
10
  */
11
11
 
12
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
13
+
12
14
  const TOOL_MAP = {
13
15
  Read: 'read_file',
14
16
  Write: 'write_file',
@@ -43,14 +45,7 @@ export default {
43
45
  },
44
46
 
45
47
  generateFooter() {
46
- return [
47
- '',
48
- '---',
49
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
50
- '> Source: https://github.com/rune-kit/rune (MIT)',
51
- '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
52
- '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
53
- ].join('\n');
48
+ return BRANDING_FOOTER;
54
49
  },
55
50
 
56
51
  transformSubagentInstruction(text) {
@@ -74,7 +69,7 @@ export default {
74
69
  name: 'Rune',
75
70
  kind: 'skills',
76
71
  description:
77
- '58-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 200+ connections, 14 extension packs.',
72
+ '59-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 200+ connections, 14 extension packs.',
78
73
  version: pluginJson.version || '0.0.0',
79
74
  skills: ['./skills'],
80
75
  configSchema: {
@@ -18,6 +18,8 @@
18
18
  * @see https://opencode.ai/docs/agents/
19
19
  */
20
20
 
21
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
22
+
21
23
  const TOOL_MAP = {
22
24
  Read: 'read the file',
23
25
  Write: 'write/create the file',
@@ -57,14 +59,7 @@ export default {
57
59
  },
58
60
 
59
61
  generateFooter() {
60
- return [
61
- '',
62
- '---',
63
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
64
- '> Source: https://github.com/rune-kit/rune (MIT)',
65
- '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
66
- '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
67
- ].join('\n');
62
+ return BRANDING_FOOTER;
68
63
  },
69
64
 
70
65
  transformSubagentInstruction(text) {
@@ -5,6 +5,8 @@
5
5
  * Uses prose references for cross-skill mesh (no @file support).
6
6
  */
7
7
 
8
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
9
+
8
10
  const TOOL_MAP = {
9
11
  Read: 'read the file',
10
12
  Write: 'write/create the file',
@@ -39,14 +41,7 @@ export default {
39
41
  },
40
42
 
41
43
  generateFooter() {
42
- return [
43
- '',
44
- '---',
45
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
46
- '> Source: https://github.com/rune-kit/rune (MIT)',
47
- '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
48
- '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
49
- ].join('\n');
44
+ return BRANDING_FOOTER;
50
45
  },
51
46
 
52
47
  transformSubagentInstruction(text) {
@@ -65,6 +65,28 @@ async function prompt(question) {
65
65
  });
66
66
  }
67
67
 
68
+ /**
69
+ * Resolve tier source paths from config.
70
+ * Paths can be absolute or relative to projectRoot.
71
+ *
72
+ * Config format:
73
+ * "tiers": { "pro": "../Pro/extensions", "business": "../Business/extensions" }
74
+ *
75
+ * @param {Object} tiers - tier config object
76
+ * @param {string} projectRoot - base for relative paths
77
+ * @returns {Object} resolved { pro?: string, business?: string }
78
+ */
79
+ function resolveTierSources(tiers, projectRoot) {
80
+ if (!tiers) return {};
81
+ const resolved = {};
82
+ for (const tier of ['pro', 'business']) {
83
+ if (tiers[tier]) {
84
+ resolved[tier] = path.resolve(projectRoot, tiers[tier]);
85
+ }
86
+ }
87
+ return resolved;
88
+ }
89
+
68
90
  // ─── Commands ───
69
91
 
70
92
  async function cmdInit(projectRoot, args) {
@@ -120,12 +142,14 @@ async function cmdInit(projectRoot, args) {
120
142
 
121
143
  // Auto-build
122
144
  const adapter = getAdapter(platform);
145
+ const tierSources = resolveTierSources(config.tiers, projectRoot);
123
146
  const stats = await buildAll({
124
147
  runeRoot: RUNE_ROOT,
125
148
  outputRoot: projectRoot,
126
149
  adapter,
127
150
  disabledSkills: config.skills.disabled,
128
151
  enabledPacks: config.extensions.enabled,
152
+ tierSources,
129
153
  });
130
154
 
131
155
  logStep('✓', `Built ${stats.skillCount} skills + ${stats.packCount} extensions to ${adapter.outputDir}/`);
@@ -140,7 +164,7 @@ async function cmdInit(projectRoot, args) {
140
164
  log(' Next steps:');
141
165
  log(' 1. /rune onboard Generate project context (CLAUDE.md + .rune/)');
142
166
  log(' 2. /rune cook "..." Build a feature (full TDD cycle)');
143
- log(' 3. /rune help See all 58 skills');
167
+ log(' 3. /rune help See all 59 skills');
144
168
  log('');
145
169
  }
146
170
 
@@ -163,6 +187,7 @@ async function cmdBuild(projectRoot, args) {
163
187
  const outputRoot = typeof args.output === 'string' ? args.output : projectRoot;
164
188
  const disabledSkills = config?.skills?.disabled || [];
165
189
  const enabledPacks = config?.extensions?.enabled || null;
190
+ const tierSources = resolveTierSources(config?.tiers, projectRoot);
166
191
 
167
192
  log('');
168
193
  log(` [parse] Discovering skills...`);
@@ -173,6 +198,7 @@ async function cmdBuild(projectRoot, args) {
173
198
  adapter,
174
199
  disabledSkills,
175
200
  enabledPacks,
201
+ tierSources,
176
202
  });
177
203
 
178
204
  log(` [transform] Platform: ${stats.platform}`);
@@ -180,6 +206,13 @@ async function cmdBuild(projectRoot, args) {
180
206
  log(` [transform] Resolved ${stats.toolRefsResolved} tool-name references`);
181
207
  log(` [emit] ${stats.skillCount} skills + ${stats.packCount} extensions`);
182
208
 
209
+ if (stats.tierOverrides?.length > 0) {
210
+ log(` [tier] ${stats.tierOverrides.length} pack(s) resolved from higher tiers:`);
211
+ for (const override of stats.tierOverrides) {
212
+ log(` → ${override.pack} (${override.tier})`);
213
+ }
214
+ }
215
+
183
216
  if (stats.skipped.length > 0) {
184
217
  log(` [skip] ${stats.skipped.length} disabled: ${stats.skipped.join(', ')}`);
185
218
  }
@@ -59,6 +59,71 @@ async function discoverPacks(extensionsDir, enabledPacks = null) {
59
59
  return paths.sort();
60
60
  }
61
61
 
62
+ /**
63
+ * Tier priority: higher number = higher priority (wins override)
64
+ */
65
+ const TIER_PRIORITY = { free: 0, pro: 1, business: 2 };
66
+
67
+ /**
68
+ * Normalize pack name for tier comparison.
69
+ * Strips tier prefixes (pro-, business-) so packs can be compared across tiers.
70
+ * e.g. "pro-product" → "product", "saas" → "saas"
71
+ */
72
+ function normalizePackName(dirName) {
73
+ return dirName.replace(/^(pro|business)-/, '');
74
+ }
75
+
76
+ /**
77
+ * Discover packs across multiple tier sources and resolve overrides.
78
+ * Business > Pro > Free: if the same normalized pack name exists in multiple tiers,
79
+ * the highest-priority tier wins.
80
+ *
81
+ * @param {string} freeExtDir - path to free extensions/ directory
82
+ * @param {Object<string, string>} [tierSources] - { pro: "/path/to/pro/extensions", business: "/path/to/business/extensions" }
83
+ * @param {string[]} [enabledPacks] - list of enabled pack names (null = all)
84
+ * @returns {Promise<Array<{path: string, tier: string, dirName: string}>>} resolved pack entries
85
+ */
86
+ export async function discoverTieredPacks(freeExtDir, tierSources = {}, enabledPacks = null) {
87
+ // Collect all packs with their tier info: Map<normalizedName, {path, tier, priority, dirName}>
88
+ const packMap = new Map();
89
+
90
+ // Helper: scan one extensions directory for packs
91
+ async function scanDir(extDir, tier) {
92
+ if (!existsSync(extDir)) return;
93
+ const entries = await readdir(extDir, { withFileTypes: true });
94
+
95
+ for (const entry of entries) {
96
+ if (!entry.isDirectory()) continue;
97
+ if (enabledPacks && !enabledPacks.includes(entry.name) && !enabledPacks.includes(`@rune/${entry.name}`)) {
98
+ continue;
99
+ }
100
+ const packFile = path.join(extDir, entry.name, 'PACK.md');
101
+ if (!existsSync(packFile)) continue;
102
+
103
+ const normalized = normalizePackName(entry.name);
104
+ const priority = TIER_PRIORITY[tier] ?? 0;
105
+ const existing = packMap.get(normalized);
106
+
107
+ // Higher priority tier wins
108
+ if (!existing || priority > existing.priority) {
109
+ packMap.set(normalized, { path: packFile, tier, priority, dirName: entry.name });
110
+ }
111
+ }
112
+ }
113
+
114
+ // Scan free first (lowest priority), then pro, then business
115
+ await scanDir(freeExtDir, 'free');
116
+ if (tierSources.pro) {
117
+ await scanDir(tierSources.pro, 'pro');
118
+ }
119
+ if (tierSources.business) {
120
+ await scanDir(tierSources.business, 'business');
121
+ }
122
+
123
+ // Return sorted by dirName for deterministic output
124
+ return [...packMap.values()].sort((a, b) => a.dirName.localeCompare(b.dirName));
125
+ }
126
+
62
127
  /**
63
128
  * Generate output filename for a skill
64
129
  */
@@ -75,9 +140,17 @@ function outputFileName(skillName, adapter) {
75
140
  * @param {object} options.adapter - platform adapter
76
141
  * @param {string[]} [options.disabledSkills] - skills to skip
77
142
  * @param {string[]} [options.enabledPacks] - extension packs to include (null = all)
143
+ * @param {Object<string, string>} [options.tierSources] - tier extension dirs { pro: "path", business: "path" }
78
144
  * @returns {Promise<object>} build result stats
79
145
  */
80
- export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills = [], enabledPacks = null }) {
146
+ export async function buildAll({
147
+ runeRoot,
148
+ outputRoot,
149
+ adapter,
150
+ disabledSkills = [],
151
+ enabledPacks = null,
152
+ tierSources = {},
153
+ }) {
81
154
  // Claude Code = passthrough, no build needed
82
155
  if (adapter.name === 'claude') {
83
156
  return {
@@ -97,7 +170,16 @@ export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills =
97
170
  await mkdir(outputDir, { recursive: true });
98
171
 
99
172
  const skillPaths = await discoverSkills(skillsDir);
100
- const packPaths = await discoverPacks(extensionsDir, enabledPacks);
173
+
174
+ // Tier-aware pack discovery: if tierSources provided, resolve overrides
175
+ const hasTiers = tierSources && (tierSources.pro || tierSources.business);
176
+ const packEntries = hasTiers
177
+ ? await discoverTieredPacks(extensionsDir, tierSources, enabledPacks)
178
+ : (await discoverPacks(extensionsDir, enabledPacks)).map((p) => ({
179
+ path: p,
180
+ tier: 'free',
181
+ dirName: path.basename(path.dirname(p)),
182
+ }));
101
183
 
102
184
  const stats = {
103
185
  platform: adapter.name,
@@ -108,6 +190,7 @@ export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills =
108
190
  files: [],
109
191
  skipped: [],
110
192
  errors: [],
193
+ tierOverrides: [],
111
194
  };
112
195
 
113
196
  // Build skills
@@ -152,14 +235,20 @@ export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills =
152
235
  }
153
236
  }
154
237
 
155
- // Build extension packs
156
- for (const packPath of packPaths) {
238
+ // Build extension packs (tier-aware)
239
+ for (const packEntry of packEntries) {
157
240
  try {
241
+ const packPath = packEntry.path;
158
242
  const content = await readFile(packPath, 'utf-8');
159
243
  const parsed = parsePack(content, packPath);
160
- const packName = path.basename(path.dirname(packPath));
244
+ const packName = packEntry.dirName;
161
245
  const packDir = path.dirname(packPath);
162
246
 
247
+ // Track tier overrides for reporting
248
+ if (packEntry.tier !== 'free') {
249
+ stats.tierOverrides.push({ pack: packName, tier: packEntry.tier });
250
+ }
251
+
163
252
  // For split packs, load individual skill files and concatenate into body
164
253
  if (parsed.isSplit && parsed.skillManifest.length > 0) {
165
254
  const skillBodies = [];
@@ -2,17 +2,24 @@
2
2
  * Branding Transform
3
3
  *
4
4
  * Adds Rune attribution footer to compiled skill files.
5
+ * All adapters MUST import BRANDING_FOOTER instead of hardcoding stats.
5
6
  */
6
7
 
7
- const DEFAULT_FOOTER = [
8
+ /**
9
+ * Single source of truth for branding footer.
10
+ * Update these numbers here — all adapters inherit automatically.
11
+ */
12
+ export const BRANDING_FOOTER = [
8
13
  '',
9
14
  '---',
10
- '> **Rune Skill Mesh** — 58 skills, 200+ connections, 14 extension packs',
11
- '> Source: https://github.com/rune-kit/rune (MIT)',
15
+ '> **Rune Skill Mesh** — 59 skills, 200+ connections, 14 extension packs',
16
+ '> [Landing Page](https://rune-kit.github.io/rune) · [Source](https://github.com/rune-kit/rune) (MIT)',
12
17
  '> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
13
18
  '> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
14
19
  ].join('\n');
15
20
 
21
+ const DEFAULT_FOOTER = BRANDING_FOOTER;
22
+
16
23
  /**
17
24
  * Add branding footer to skill output
18
25
  *
@@ -6,9 +6,9 @@
6
6
  |-------|------|-------|----------|----------|-------|
7
7
  | **L0** | **Router** | **1** | **L1-L3 (routing)** | **Every message** | **Stateless (rule-based)** |
8
8
  | L1 | Orchestrators | 5 | L2, L3 | L0, User | Stateful (workflow) |
9
- | L2 | Workflow Hubs | 27 | L2 (cross-hub), L3 | L1, L2 | Stateful (task) |
10
- | L3 | Utilities | 25 | Nothing (pure)* | L1, L2 | Stateless |
11
- | L4 | Extension Packs | 14 free + 4 pro + 2 business | L3 | L2 (domain match) | Config-based |
9
+ | L2 | Workflow Hubs | 28 | L2 (cross-hub), L3 | L1, L2 | Stateful (task) |
10
+ | L3 | Utilities | 26 | Nothing (pure)* | L1, L2 | Stateless |
11
+ | L4 | Extension Packs | 14 free + 4 pro + 4 business | L3 | L2 (domain match) | Config-based |
12
12
 
13
13
  ### L0 — The Enforcement Layer
14
14
 
package/docs/VISION.md CHANGED
@@ -30,7 +30,7 @@ Rune is a **skill mesh** — not a skill collection, not a pipeline, not an AI a
30
30
 
31
31
  **Technical definition:**
32
32
 
33
- > **Rune = 58 skills × 200+ bidirectional connections × cross-session memory × multi-platform compiler**
33
+ > **Rune = 59 skills × 200+ bidirectional connections × cross-session memory × multi-platform compiler**
34
34
 
35
35
  All three components are equally essential:
36
36
  - Remove connections → becomes a collection (The Bloat wins)
@@ -211,7 +211,7 @@ Rune operates on three time horizons. The roadmap is intentionally non-prescript
211
211
 
212
212
  **Implementation**: Zero new L1-L3 skills added. 1 new hook (`metrics-collector`), 3 modified hooks, 4 extended skills (`audit`, `cook`, `skill-router`, `onboard`), 2 new commands (`/rune metrics`, `/rune pack`).
213
213
 
214
- **Constraint:** Core mesh expanded to 58 skills (v2.1.0+). Further growth happens in L4 and community packs.
214
+ **Constraint:** Core mesh expanded to 59 skills (v2.1.0+). Further growth happens in L4 and community packs.
215
215
 
216
216
  ---
217
217
 
@@ -220,7 +220,7 @@ Rune operates on three time horizons. The roadmap is intentionally non-prescript
220
220
  *Rune is successful when these are true. Not when the feature list is long.*
221
221
 
222
222
  ### Mesh Health
223
- - **Connection density** ≥ 3.0 connections/skill (currently: 3.4 at 200+ connections / 58 skills) — do not let this drop below 2.5
223
+ - **Connection density** ≥ 3.0 connections/skill (currently: 3.4 at 200+ connections / 59 skills) — do not let this drop below 2.5
224
224
  - **Dead nodes** = 0 — every skill has ≥1 inbound and ≥1 outbound connection
225
225
  - **Max chain depth used** < 6 in practice (ceiling is 8) — if chains regularly hit 8, the mesh needs restructuring
226
226
  - **Bloat Index** = 0.00 — dead nodes / total skills