@rune-kit/rune 2.6.0 → 2.7.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 (38) hide show
  1. package/README.md +17 -6
  2. package/compiler/__tests__/executive-dashboards.test.js +285 -0
  3. package/compiler/__tests__/inject.test.js +128 -0
  4. package/compiler/__tests__/orchestrators.test.js +151 -0
  5. package/compiler/__tests__/org-templates.test.js +447 -0
  6. package/compiler/__tests__/parser.test.js +201 -147
  7. package/compiler/__tests__/skill-index.test.js +218 -218
  8. package/compiler/__tests__/status.test.js +336 -0
  9. package/compiler/__tests__/templates.test.js +245 -0
  10. package/compiler/__tests__/visualizer.test.js +325 -0
  11. package/compiler/adapters/antigravity.js +71 -71
  12. package/compiler/bin/rune.js +444 -355
  13. package/compiler/doctor.js +272 -1
  14. package/compiler/emitter.js +939 -678
  15. package/compiler/parser.js +498 -267
  16. package/compiler/status.js +342 -0
  17. package/compiler/visualizer.js +622 -0
  18. package/package.json +1 -1
  19. package/skills/autopsy/SKILL.md +48 -1
  20. package/skills/completion-gate/SKILL.md +26 -1
  21. package/skills/context-engine/SKILL.md +93 -2
  22. package/skills/cook/SKILL.md +137 -4
  23. package/skills/debug/SKILL.md +16 -1
  24. package/skills/docs/SKILL.md +28 -3
  25. package/skills/fix/SKILL.md +2 -1
  26. package/skills/mcp-builder/SKILL.md +53 -1
  27. package/skills/onboard/SKILL.md +51 -1
  28. package/skills/perf/SKILL.md +34 -1
  29. package/skills/plan/SKILL.md +29 -1
  30. package/skills/preflight/SKILL.md +35 -1
  31. package/skills/retro/SKILL.md +95 -1
  32. package/skills/review/SKILL.md +45 -1
  33. package/skills/scope-guard/SKILL.md +1 -0
  34. package/skills/sentinel/SKILL.md +35 -1
  35. package/skills/sentinel/references/policy-driven-constraints.md +424 -0
  36. package/skills/session-bridge/SKILL.md +57 -2
  37. package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
  38. package/skills/team/SKILL.md +15 -1
package/README.md CHANGED
@@ -12,6 +12,7 @@
12
12
  <a href="https://rune-kit.github.io/rune"><img src="https://img.shields.io/badge/Landing_Page-rune--kit.github.io-blue?style=for-the-badge" alt="Landing Page"></a>
13
13
  <a href="https://rune-kit.github.io/rune#pricing"><img src="https://img.shields.io/badge/Pro_%2449-lifetime-blueviolet?style=for-the-badge" alt="Rune Pro $49"></a>
14
14
  <a href="https://rune-kit.github.io/rune#pricing"><img src="https://img.shields.io/badge/Business_%24149-lifetime-orange?style=for-the-badge" alt="Rune Business $149"></a>
15
+ <a href="https://t.me/xlabs_updates"><img src="https://img.shields.io/badge/Telegram-Updates-26A5E4?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram Updates"></a>
15
16
  </p>
16
17
 
17
18
  <p align="center">
@@ -82,15 +83,25 @@ _Methodology: Claude Code CLI headless mode (`claude -p --output-format json`),
82
83
 
83
84
  ---
84
85
 
85
- ## What's New (v2.6.0)
86
+ ## What's New (v2.7.0)
86
87
 
87
- - **Mesh Signals** — event-driven skill communication via frontmatter. Skills declare `emit` and `listen` signals; compiler builds a signal graph in `skill-index.json`. 17 signals across 15 core skills. Completes the spec-kit trilogy: Tier Override Mesh Contract → Mesh Signals
88
+ - **Deep Knowledge** — 8 core skills enriched with battle-tested patterns: context compaction, structured cumulative memory, milestone analysis, multi-provider adapters, AI-driven interview, prompt-as-API-contract, token budget tracking, incremental stream processing
89
+ - **context-engine v0.8.0** — structured compaction summaries with continuation point anchoring + sentence-level stream processing pattern
90
+ - **session-bridge v0.5.0** — cumulative project notes (`.rune/cumulative-notes.md`) for living institutional memory across sessions
91
+ - **retro v0.3.0** — milestone progressive analysis at project thresholds (4/12/24/50 retros) with focal-point depth scaling
92
+ - **mcp-builder v0.4.0** — multi-provider adapter pattern reference with discriminated StreamChunk union + dual-model cost config
93
+ - **onboard v0.4.0** — AI-driven conversational interview mode (`--interview`) for ambiguous or complex projects
94
+ - **perf v0.3.0** — token budget tracking for AI-powered apps (loop detection, model mismatch, unbounded tokens, duplicate calls)
95
+ - **cook v2.0.0** — prompt-as-API-contract pattern for structured sub-skill output
96
+ - **946 Tests** — compiler + signals + hooks + scripts + status + visualizer validation
97
+
98
+ ### Previous (v2.6.0)
99
+
100
+ - **Mesh Signals** — event-driven skill communication via frontmatter. Skills declare `emit` and `listen` signals; compiler builds a signal graph in `skill-index.json`. 17 signals across 15 core skills
88
101
  - **Signal Validation** — `scripts/validate-signals.js` checks orphan listeners (hard error), unlistened emitters (warning), signal naming conventions
89
102
  - **Mesh Contract** (v2.5.0) — `.rune/contract.md` project-level invariants enforced by cook + sentinel as hard gates
90
- - **Compiled Intent Mesh** (v2.5.0) — compile-time `skill-index.json` with intent keywords, mesh graph, chain predictions
91
103
  - **Tier Override** — Pro/Business packs override Free packs with skill-level merging
92
104
  - **Scripts Bundling** — compiler copies `scripts/` directories, resolves `{scripts_dir}` placeholders
93
- - **566 Tests** — compiler + signals + hooks + scripts validation
94
105
 
95
106
  ### Signal Graph
96
107
 
@@ -447,11 +458,11 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
447
458
  Core Skills: 61 (L0: 1 │ L1: 5 │ L2: 28 │ L3: 27)
448
459
  Extension Packs: 14 free + 4 pro + 4 business
449
460
  Mesh Connections: 200+ cross-references
450
- Mesh Signals: 17 signals across 15 skills (emit/listen graph)
461
+ Mesh Signals: 57 signals across 66 skills (emit/listen graph)
451
462
  Connections/Skill: 3.4 avg
452
463
  Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
453
464
  Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
454
- Tests: 566 (compiler + signals + hooks + scripts)
465
+ Tests: 946 (compiler + signals + status + visualizer + hooks + scripts)
455
466
  Pack Depth: 22 packs total (14 free + 4 pro + 4 business, all free packs rated Deep)
456
467
  ```
457
468
 
@@ -0,0 +1,285 @@
1
+ import assert from 'node:assert';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { describe, test } from 'node:test';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const BUSINESS_DIR = path.resolve(__dirname, '../../../Business');
9
+ const REPORT_TEMPLATES_DIR = path.join(BUSINESS_DIR, 'report-templates');
10
+ const SKILLS_DIR = path.resolve(__dirname, '../../skills');
11
+
12
+ // ─── HTML report template validation ──────────────────────────────
13
+
14
+ // Skip Business tier tests when Business repo is not available (CI)
15
+ const HAS_BUSINESS = existsSync(BUSINESS_DIR);
16
+
17
+ (HAS_BUSINESS ? describe : describe.skip)('Business report-templates directory', () => {
18
+ test('report-templates directory exists', () => {
19
+ assert.ok(existsSync(REPORT_TEMPLATES_DIR), 'Expected Business/report-templates/ to exist');
20
+ });
21
+
22
+ test('retro-business.html exists', () => {
23
+ assert.ok(
24
+ existsSync(path.join(REPORT_TEMPLATES_DIR, 'retro-business.html')),
25
+ 'Expected retro-business.html template',
26
+ );
27
+ });
28
+
29
+ test('autopsy-executive.html exists', () => {
30
+ assert.ok(
31
+ existsSync(path.join(REPORT_TEMPLATES_DIR, 'autopsy-executive.html')),
32
+ 'Expected autopsy-executive.html template',
33
+ );
34
+ });
35
+ });
36
+
37
+ describe('retro-business.html template', () => {
38
+ const filePath = path.join(REPORT_TEMPLATES_DIR, 'retro-business.html');
39
+ if (!existsSync(filePath)) return;
40
+ const content = readFileSync(filePath, 'utf-8');
41
+
42
+ test('is valid HTML with doctype', () => {
43
+ assert.ok(content.startsWith('<!DOCTYPE html>'), 'Must start with DOCTYPE');
44
+ assert.ok(content.includes('<html'), 'Must have html tag');
45
+ assert.ok(content.includes('</html>'), 'Must close html tag');
46
+ });
47
+
48
+ test('is self-contained (no external CSS/JS)', () => {
49
+ assert.ok(!content.includes('href="http'), 'No external CSS links');
50
+ assert.ok(!content.includes('src="http'), 'No external JS scripts');
51
+ assert.ok(!content.includes('cdn.'), 'No CDN references');
52
+ });
53
+
54
+ test('has inline styles', () => {
55
+ assert.ok(content.includes('<style>'), 'Must have inline style block');
56
+ });
57
+
58
+ test('has print-friendly styles', () => {
59
+ assert.ok(content.includes('@media print'), 'Must have print media query');
60
+ assert.ok(content.includes('@page'), 'Must have @page rule for PDF export');
61
+ });
62
+
63
+ test('has KPI card placeholders', () => {
64
+ assert.ok(content.includes('{{sprint_velocity}}'), 'Missing sprint_velocity placeholder');
65
+ assert.ok(content.includes('{{mrr}}'), 'Missing MRR placeholder');
66
+ assert.ok(content.includes('{{sla_pct}}'), 'Missing SLA placeholder');
67
+ assert.ok(content.includes('{{compliance_score}}'), 'Missing compliance_score placeholder');
68
+ });
69
+
70
+ test('has domain performance sections', () => {
71
+ assert.ok(content.includes('Engineering'), 'Missing Engineering domain');
72
+ assert.ok(content.includes('Revenue'), 'Missing Revenue domain');
73
+ assert.ok(content.includes('Support'), 'Missing Support domain');
74
+ assert.ok(content.includes('Finance'), 'Missing Finance domain');
75
+ });
76
+
77
+ test('has team health table', () => {
78
+ assert.ok(content.includes('Team Health'), 'Missing Team Health section');
79
+ assert.ok(content.includes('{{team_name}}'), 'Missing team_name placeholder');
80
+ assert.ok(content.includes('{{team_lead}}'), 'Missing team_lead placeholder');
81
+ });
82
+
83
+ test('has compliance status section', () => {
84
+ assert.ok(content.includes('Compliance Status'), 'Missing Compliance Status section');
85
+ assert.ok(content.includes('{{framework}}'), 'Missing framework placeholder');
86
+ });
87
+
88
+ test('has key insights section (wins + risks)', () => {
89
+ assert.ok(content.includes('Key Insights'), 'Missing Key Insights section');
90
+ assert.ok(content.includes('Wins'), 'Missing Wins subsection');
91
+ assert.ok(content.includes('Risks'), 'Missing Risks subsection');
92
+ });
93
+
94
+ test('has Rune Business footer', () => {
95
+ assert.ok(content.includes('Rune Business'), 'Missing Rune Business branding');
96
+ assert.ok(content.includes('retro --business'), 'Missing retro --business reference');
97
+ });
98
+
99
+ test('has trend delta placeholders for KPIs', () => {
100
+ assert.ok(content.includes('{{velocity_delta}}'), 'Missing velocity trend');
101
+ assert.ok(content.includes('{{mrr_delta}}'), 'Missing MRR trend');
102
+ });
103
+
104
+ test('uses CSS custom properties for theming', () => {
105
+ assert.ok(content.includes(':root'), 'Must use CSS custom properties');
106
+ assert.ok(content.includes('--bg'), 'Missing --bg variable');
107
+ assert.ok(content.includes('--accent'), 'Missing --accent variable');
108
+ });
109
+ });
110
+
111
+ describe('autopsy-executive.html template', () => {
112
+ const filePath = path.join(REPORT_TEMPLATES_DIR, 'autopsy-executive.html');
113
+ if (!existsSync(filePath)) return;
114
+ const content = readFileSync(filePath, 'utf-8');
115
+
116
+ test('is valid HTML with doctype', () => {
117
+ assert.ok(content.startsWith('<!DOCTYPE html>'), 'Must start with DOCTYPE');
118
+ assert.ok(content.includes('<html'), 'Must have html tag');
119
+ assert.ok(content.includes('</html>'), 'Must close html tag');
120
+ });
121
+
122
+ test('is self-contained (no external CSS/JS)', () => {
123
+ assert.ok(!content.includes('href="http'), 'No external CSS links');
124
+ assert.ok(!content.includes('src="http'), 'No external JS scripts');
125
+ assert.ok(!content.includes('cdn.'), 'No CDN references');
126
+ });
127
+
128
+ test('has print-friendly styles', () => {
129
+ assert.ok(content.includes('@media print'), 'Must have print media query');
130
+ assert.ok(content.includes('@page'), 'Must have @page rule');
131
+ });
132
+
133
+ test('has SVG health ring', () => {
134
+ assert.ok(content.includes('<svg'), 'Must have inline SVG');
135
+ assert.ok(content.includes('<circle'), 'Must have SVG circle for health ring');
136
+ assert.ok(content.includes('stroke-dasharray'), 'Must use dasharray for progress ring');
137
+ });
138
+
139
+ test('has health score placeholder', () => {
140
+ assert.ok(content.includes('{{health_score}}'), 'Missing health_score placeholder');
141
+ assert.ok(content.includes('{{risk_tier}}'), 'Missing risk_tier placeholder');
142
+ assert.ok(content.includes('{{score_color}}'), 'Missing score_color placeholder');
143
+ });
144
+
145
+ test('has six health dimensions', () => {
146
+ assert.ok(content.includes('Complexity'), 'Missing Complexity dimension');
147
+ assert.ok(content.includes('Test Coverage'), 'Missing Test Coverage dimension');
148
+ assert.ok(content.includes('Documentation'), 'Missing Documentation dimension');
149
+ assert.ok(content.includes('Dependencies'), 'Missing Dependencies dimension');
150
+ assert.ok(content.includes('Code Smells'), 'Missing Code Smells dimension');
151
+ assert.ok(content.includes('Maintenance'), 'Missing Maintenance dimension');
152
+ });
153
+
154
+ test('has module health table', () => {
155
+ assert.ok(content.includes('Module Health'), 'Missing Module Health section');
156
+ assert.ok(content.includes('{{module_name}}'), 'Missing module_name placeholder');
157
+ assert.ok(content.includes('{{module_score}}'), 'Missing module_score placeholder');
158
+ });
159
+
160
+ test('has surgery queue section', () => {
161
+ assert.ok(content.includes('Surgery Queue'), 'Missing Surgery Queue section');
162
+ assert.ok(content.includes('{{surgery_module}}'), 'Missing surgery_module placeholder');
163
+ assert.ok(content.includes('{{surgery_pattern}}'), 'Missing surgery_pattern placeholder');
164
+ });
165
+
166
+ test('has risk assessment section', () => {
167
+ assert.ok(content.includes('Risk Assessment'), 'Missing Risk Assessment section');
168
+ assert.ok(content.includes('Git Archaeology'), 'Missing Git Archaeology section');
169
+ });
170
+
171
+ test('has cross-domain impact table', () => {
172
+ assert.ok(content.includes('Cross-Domain Impact'), 'Missing Cross-Domain Impact section');
173
+ assert.ok(content.includes('{{domain_name}}'), 'Missing domain_name placeholder');
174
+ });
175
+
176
+ test('has recommended actions', () => {
177
+ assert.ok(content.includes('Recommended Actions'), 'Missing Recommended Actions section');
178
+ });
179
+
180
+ test('has Rune Business footer', () => {
181
+ assert.ok(content.includes('Rune Business'), 'Missing Rune Business branding');
182
+ assert.ok(content.includes('autopsy --executive'), 'Missing autopsy --executive reference');
183
+ });
184
+
185
+ test('has board-ready label', () => {
186
+ assert.ok(content.includes('Board-ready'), 'Should mention board-ready format');
187
+ });
188
+ });
189
+
190
+ // ─── Skill integration validation ─────────────────────────────────
191
+
192
+ describe('retro skill --business integration', () => {
193
+ const retroPath = path.join(SKILLS_DIR, 'retro', 'SKILL.md');
194
+ const content = readFileSync(retroPath, 'utf-8');
195
+
196
+ test('retro has --business trigger', () => {
197
+ assert.ok(content.includes('retro --business'), 'retro should document --business mode');
198
+ });
199
+
200
+ test('retro --business references HTML report', () => {
201
+ assert.ok(content.includes('.html'), 'retro --business should reference HTML output');
202
+ });
203
+
204
+ test('retro --business references cross-domain data', () => {
205
+ assert.ok(
206
+ content.includes('Revenue') || content.includes('revenue'),
207
+ 'retro --business should reference revenue domain',
208
+ );
209
+ assert.ok(
210
+ content.includes('Support') || content.includes('support'),
211
+ 'retro --business should reference support domain',
212
+ );
213
+ assert.ok(
214
+ content.includes('Finance') || content.includes('finance'),
215
+ 'retro --business should reference finance domain',
216
+ );
217
+ });
218
+
219
+ test('retro --business references org config', () => {
220
+ assert.ok(content.includes('.rune/org/org.md'), 'retro --business should reference org config');
221
+ });
222
+
223
+ test('retro --business has graceful degradation', () => {
224
+ assert.ok(
225
+ content.includes('Graceful Degradation') || content.includes('graceful'),
226
+ 'retro --business should handle missing Business pack gracefully',
227
+ );
228
+ });
229
+
230
+ test('retro --business references report template', () => {
231
+ assert.ok(content.includes('retro-business.html'), 'retro should reference the HTML template file');
232
+ });
233
+ });
234
+
235
+ describe('autopsy skill --executive integration', () => {
236
+ const autopsyPath = path.join(SKILLS_DIR, 'autopsy', 'SKILL.md');
237
+ const content = readFileSync(autopsyPath, 'utf-8');
238
+
239
+ test('autopsy has --executive mode', () => {
240
+ assert.ok(content.includes('--executive'), 'autopsy should document --executive mode');
241
+ });
242
+
243
+ test('autopsy --executive references HTML report', () => {
244
+ assert.ok(
245
+ content.includes('EXECUTIVE-HEALTH.html'),
246
+ 'autopsy --executive should reference EXECUTIVE-HEALTH.html output',
247
+ );
248
+ });
249
+
250
+ test('autopsy --executive references SVG health ring', () => {
251
+ assert.ok(
252
+ content.includes('SVG') || content.includes('svg'),
253
+ 'autopsy --executive should reference SVG health ring',
254
+ );
255
+ });
256
+
257
+ test('autopsy --executive has color coding table', () => {
258
+ assert.ok(
259
+ content.includes('Color Coding') || content.includes('color'),
260
+ 'autopsy --executive should define color coding',
261
+ );
262
+ });
263
+
264
+ test('autopsy --executive references org config', () => {
265
+ assert.ok(content.includes('.rune/org/org.md'), 'autopsy --executive should reference org config for team mapping');
266
+ });
267
+
268
+ test('autopsy --executive references report template', () => {
269
+ assert.ok(content.includes('autopsy-executive.html'), 'autopsy should reference the HTML template file');
270
+ });
271
+
272
+ test('autopsy --executive has cross-domain impact', () => {
273
+ assert.ok(
274
+ content.includes('Cross-Domain') || content.includes('cross-domain'),
275
+ 'autopsy --executive should map to business domains',
276
+ );
277
+ });
278
+
279
+ test('autopsy --executive has graceful degradation', () => {
280
+ assert.ok(
281
+ content.includes('Graceful Degradation') || content.includes('graceful'),
282
+ 'autopsy --executive should handle missing Business pack',
283
+ );
284
+ });
285
+ });
@@ -0,0 +1,128 @@
1
+ import assert from 'node:assert';
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { describe, test } from 'node:test';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const SKILLS_DIR = path.resolve(__dirname, '../../skills');
9
+ const PRO_DIR = path.resolve(__dirname, '../../../Pro/extensions');
10
+ const BUSINESS_DIR = path.resolve(__dirname, '../../../Business/extensions');
11
+
12
+ // Skip Pro/Business tier tests when repos are not available (CI)
13
+ const HAS_PRO = existsSync(PRO_DIR);
14
+
15
+ // Collect known Free core skill names
16
+ const knownSkills = new Set();
17
+ if (existsSync(SKILLS_DIR)) {
18
+ for (const entry of readdirSync(SKILLS_DIR, { withFileTypes: true })) {
19
+ if (entry.isDirectory() && existsSync(path.join(SKILLS_DIR, entry.name, 'SKILL.md'))) {
20
+ knownSkills.add(entry.name);
21
+ }
22
+ }
23
+ }
24
+
25
+ (HAS_PRO ? describe : describe.skip)('inject.json format validation', () => {
26
+ const packDirs = [];
27
+
28
+ // Collect all extension dirs that might have inject.json
29
+ for (const extDir of [PRO_DIR, BUSINESS_DIR]) {
30
+ if (!existsSync(extDir)) continue;
31
+ for (const entry of readdirSync(extDir, { withFileTypes: true })) {
32
+ if (!entry.isDirectory()) continue;
33
+ const injectPath = path.join(extDir, entry.name, 'inject.json');
34
+ if (existsSync(injectPath)) {
35
+ packDirs.push({ name: entry.name, dir: path.join(extDir, entry.name), injectPath });
36
+ }
37
+ }
38
+ }
39
+
40
+ test('at least one inject.json exists in Pro packs', () => {
41
+ assert.ok(packDirs.length > 0, 'Expected at least one pack with inject.json');
42
+ });
43
+
44
+ for (const pack of packDirs) {
45
+ describe(`${pack.name}/inject.json`, () => {
46
+ let config;
47
+
48
+ test('is valid JSON', () => {
49
+ const raw = readFileSync(pack.injectPath, 'utf-8');
50
+ config = JSON.parse(raw);
51
+ assert.ok(config, 'inject.json should parse as valid JSON');
52
+ });
53
+
54
+ test('has injections array', () => {
55
+ if (!config) return;
56
+ assert.ok(Array.isArray(config.injections), 'must have injections array');
57
+ assert.ok(config.injections.length > 0, 'must have at least one injection rule');
58
+ });
59
+
60
+ test('has description field', () => {
61
+ if (!config) return;
62
+ assert.ok(config.description, 'should have a description field');
63
+ });
64
+
65
+ test('each rule has required fields', () => {
66
+ if (!config) return;
67
+ for (const rule of config.injections) {
68
+ assert.ok(rule.skill, `rule must have "skill" field: ${JSON.stringify(rule)}`);
69
+ assert.ok(rule.ref, `rule must have "ref" field: ${JSON.stringify(rule)}`);
70
+ assert.ok(rule.context, `rule must have "context" field: ${JSON.stringify(rule)}`);
71
+ }
72
+ });
73
+
74
+ test('target skills exist in Free core', () => {
75
+ if (!config) return;
76
+ for (const rule of config.injections) {
77
+ assert.ok(knownSkills.has(rule.skill), `target skill "${rule.skill}" not found in Free core skills`);
78
+ }
79
+ });
80
+
81
+ test('reference files exist', () => {
82
+ if (!config) return;
83
+ for (const rule of config.injections) {
84
+ const refPath = path.join(pack.dir, rule.ref);
85
+ assert.ok(existsSync(refPath), `reference file "${rule.ref}" not found at ${refPath}`);
86
+ }
87
+ });
88
+
89
+ test('reference files are .md', () => {
90
+ if (!config) return;
91
+ for (const rule of config.injections) {
92
+ assert.ok(rule.ref.endsWith('.md'), `reference "${rule.ref}" should be a .md file`);
93
+ }
94
+ });
95
+
96
+ test('no duplicate skill targets within same pack', () => {
97
+ if (!config) return;
98
+ const seen = new Set();
99
+ for (const rule of config.injections) {
100
+ assert.ok(!seen.has(rule.skill), `duplicate injection target "${rule.skill}" in ${pack.name}`);
101
+ seen.add(rule.skill);
102
+ }
103
+ });
104
+ });
105
+ }
106
+ });
107
+
108
+ (HAS_PRO ? describe : describe.skip)('injection rule coverage', () => {
109
+ const expectedPacks = ['pro-product', 'pro-data-science', 'pro-sales', 'pro-support'];
110
+
111
+ for (const packName of expectedPacks) {
112
+ test(`${packName} has inject.json`, () => {
113
+ const injectPath = path.join(PRO_DIR, packName, 'inject.json');
114
+ assert.ok(existsSync(injectPath), `${packName} should have inject.json`);
115
+ });
116
+ }
117
+
118
+ test('total injection rules across all packs', () => {
119
+ let totalRules = 0;
120
+ for (const packName of expectedPacks) {
121
+ const injectPath = path.join(PRO_DIR, packName, 'inject.json');
122
+ if (!existsSync(injectPath)) continue;
123
+ const config = JSON.parse(readFileSync(injectPath, 'utf-8'));
124
+ totalRules += (config.injections || []).length;
125
+ }
126
+ assert.ok(totalRules >= 8, `should have at least 8 injection rules total, got ${totalRules}`);
127
+ });
128
+ });
@@ -0,0 +1,151 @@
1
+ import assert from 'node:assert';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { describe, test } from 'node:test';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { parseSkill } from '../parser.js';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const BUSINESS_SKILLS_DIR = path.resolve(__dirname, '../../../Business/skills');
10
+
11
+ // Signal naming pattern
12
+ const SIGNAL_NAME_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/;
13
+
14
+ const ORCHESTRATORS = ['launch-product', 'quarterly-review', 'compliance-audit', 'customer-lifecycle'];
15
+
16
+ // Skip Business tier tests when Business repo is not available (CI)
17
+ const HAS_BUSINESS = existsSync(BUSINESS_SKILLS_DIR);
18
+
19
+ (HAS_BUSINESS ? describe : describe.skip)('Business orchestrators', () => {
20
+ test('skills directory exists', () => {
21
+ assert.ok(existsSync(BUSINESS_SKILLS_DIR), `${BUSINESS_SKILLS_DIR} should exist`);
22
+ });
23
+
24
+ for (const name of ORCHESTRATORS) {
25
+ const skillPath = path.join(BUSINESS_SKILLS_DIR, name, 'SKILL.md');
26
+
27
+ describe(name, () => {
28
+ test('SKILL.md exists', () => {
29
+ assert.ok(existsSync(skillPath), `${skillPath} should exist`);
30
+ });
31
+
32
+ // Skip remaining tests if file doesn't exist (CI without Business repo)
33
+ if (!existsSync(skillPath)) return;
34
+
35
+ const content = readFileSync(skillPath, 'utf-8');
36
+ const parsed = parseSkill(content, skillPath);
37
+
38
+ test('has correct name', () => {
39
+ assert.strictEqual(parsed.name, name);
40
+ });
41
+
42
+ test('is L1 layer', () => {
43
+ assert.strictEqual(parsed.layer, 'L1', `${name} must be L1 orchestrator`);
44
+ });
45
+
46
+ test('uses opus model', () => {
47
+ assert.strictEqual(parsed.model, 'opus', `${name} should use opus for complex orchestration`);
48
+ });
49
+
50
+ test('has description', () => {
51
+ assert.ok(parsed.description.length > 50, `${name} should have a meaningful description`);
52
+ });
53
+
54
+ test('is context fork', () => {
55
+ assert.strictEqual(parsed.contextFork, true, `${name} should fork context`);
56
+ });
57
+
58
+ test('has emit signals', () => {
59
+ assert.ok(parsed.signals, `${name} must have signals`);
60
+ assert.ok(parsed.signals.emit.length > 0, `${name} must emit at least one signal`);
61
+ });
62
+
63
+ test('has listen signals', () => {
64
+ assert.ok(parsed.signals.listen.length > 0, `${name} must listen to at least one signal`);
65
+ });
66
+
67
+ test('all signals follow naming convention', () => {
68
+ for (const signal of [...parsed.signals.emit, ...parsed.signals.listen]) {
69
+ assert.ok(SIGNAL_NAME_PATTERN.test(signal), `signal "${signal}" must match lowercase.dot.notation`);
70
+ }
71
+ });
72
+
73
+ test('has HARD-GATE block', () => {
74
+ assert.ok(parsed.hardGates.length > 0, `${name} must have at least one HARD-GATE`);
75
+ });
76
+
77
+ test('has cross-references to other skills', () => {
78
+ assert.ok(parsed.crossRefs.length >= 3, `${name} should reference at least 3 other skills`);
79
+ });
80
+
81
+ test('has Workflow Phases section', () => {
82
+ assert.ok(parsed.sections.has('Workflow Phases'), `${name} must have ## Workflow Phases`);
83
+ });
84
+
85
+ test('has Signal Map section', () => {
86
+ assert.ok(parsed.sections.has('Signal Map'), `${name} must have ## Signal Map`);
87
+ });
88
+
89
+ test('has Cross-Domain Connections section', () => {
90
+ assert.ok(parsed.sections.has('Cross-Domain Connections'), `${name} must have ## Cross-Domain Connections`);
91
+ });
92
+
93
+ test('has Connections section', () => {
94
+ assert.ok(parsed.sections.has('Connections'), `${name} must have ## Connections`);
95
+ });
96
+
97
+ test('has Constraints section', () => {
98
+ assert.ok(parsed.sections.has('Constraints'), `${name} must have ## Constraints`);
99
+ });
100
+
101
+ test('references multiple domain packs (cross-domain)', () => {
102
+ const body = parsed.body;
103
+ const domains = ['finance', 'legal', 'hr', 'product', 'sales', 'support', 'enterprise-search', 'data-science'];
104
+ const referencedDomains = domains.filter((d) => body.toLowerCase().includes(d));
105
+ assert.ok(
106
+ referencedDomains.length >= 3,
107
+ `${name} must reference at least 3 domains, found: ${referencedDomains.join(', ')}`,
108
+ );
109
+ });
110
+
111
+ test('emits business.* signals', () => {
112
+ const businessSignals = parsed.signals.emit.filter((s) => s.startsWith('business.'));
113
+ assert.ok(businessSignals.length > 0, `${name} must emit at least one business.* signal`);
114
+ });
115
+
116
+ test('body is substantial (300-500+ lines)', () => {
117
+ const lineCount = content.split('\n').length;
118
+ assert.ok(lineCount >= 100, `${name} should be at least 100 lines, got ${lineCount}`);
119
+ });
120
+ });
121
+ }
122
+ });
123
+
124
+ (HAS_BUSINESS ? describe : describe.skip)('orchestrator signal consistency', () => {
125
+ // Verify orchestrators don't declare signals that overlap with each other
126
+ const allEmitted = new Set();
127
+ const duplicates = [];
128
+
129
+ for (const name of ORCHESTRATORS) {
130
+ const skillPath = path.join(BUSINESS_SKILLS_DIR, name, 'SKILL.md');
131
+ if (!existsSync(skillPath)) continue;
132
+
133
+ const content = readFileSync(skillPath, 'utf-8');
134
+ const parsed = parseSkill(content, skillPath);
135
+
136
+ for (const signal of parsed.signals.emit) {
137
+ if (allEmitted.has(signal)) {
138
+ duplicates.push({ signal, skill: name });
139
+ }
140
+ allEmitted.add(signal);
141
+ }
142
+ }
143
+
144
+ test('no duplicate emit signals across orchestrators', () => {
145
+ assert.strictEqual(
146
+ duplicates.length,
147
+ 0,
148
+ `Duplicate signals: ${duplicates.map((d) => `${d.signal} (${d.skill})`).join(', ')}`,
149
+ );
150
+ });
151
+ });