@rune-kit/rune 2.4.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 (49) hide show
  1. package/README.md +47 -17
  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__/pack-split.test.js +141 -1
  7. package/compiler/__tests__/parser.test.js +147 -1
  8. package/compiler/__tests__/scripts-bundling.test.js +10 -11
  9. package/compiler/__tests__/skill-index.test.js +218 -0
  10. package/compiler/__tests__/status.test.js +336 -0
  11. package/compiler/__tests__/templates.test.js +245 -0
  12. package/compiler/__tests__/visualizer.test.js +325 -0
  13. package/compiler/adapters/antigravity.js +18 -4
  14. package/compiler/bin/rune.js +90 -1
  15. package/compiler/doctor.js +283 -2
  16. package/compiler/emitter.js +490 -17
  17. package/compiler/parser.js +255 -4
  18. package/compiler/status.js +342 -0
  19. package/compiler/visualizer.js +622 -0
  20. package/hooks/hooks.json +12 -0
  21. package/hooks/intent-router/index.cjs +108 -0
  22. package/hooks/pre-tool-guard/index.cjs +177 -68
  23. package/package.json +63 -63
  24. package/skills/autopsy/SKILL.md +48 -1
  25. package/skills/brainstorm/SKILL.md +2 -0
  26. package/skills/completion-gate/SKILL.md +26 -1
  27. package/skills/context-engine/SKILL.md +93 -2
  28. package/skills/cook/SKILL.md +794 -648
  29. package/skills/debug/SKILL.md +409 -392
  30. package/skills/deploy/SKILL.md +2 -0
  31. package/skills/docs/SKILL.md +28 -3
  32. package/skills/fix/SKILL.md +284 -281
  33. package/skills/mcp-builder/SKILL.md +53 -1
  34. package/skills/onboard/SKILL.md +58 -1
  35. package/skills/perf/SKILL.md +34 -1
  36. package/skills/plan/SKILL.md +372 -342
  37. package/skills/preflight/SKILL.md +396 -360
  38. package/skills/retro/SKILL.md +95 -1
  39. package/skills/review/SKILL.md +535 -489
  40. package/skills/scope-guard/SKILL.md +1 -0
  41. package/skills/scout/SKILL.md +1 -0
  42. package/skills/sentinel/SKILL.md +353 -299
  43. package/skills/sentinel/references/policy-driven-constraints.md +424 -0
  44. package/skills/session-bridge/SKILL.md +58 -2
  45. package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
  46. package/skills/team/SKILL.md +16 -1
  47. package/skills/test/SKILL.md +587 -585
  48. package/skills/verification/SKILL.md +1 -0
  49. package/skills/watchdog/SKILL.md +2 -0
@@ -7,7 +7,7 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { readdir, readFile } from 'node:fs/promises';
9
9
  import path from 'node:path';
10
- import { parsePack } from './parser.js';
10
+ import { parseOrgConfig, parsePack, parseTemplate } from './parser.js';
11
11
 
12
12
  /**
13
13
  * Run doctor checks on compiled output
@@ -61,7 +61,17 @@ export async function runDoctor({ outputRoot, adapter, config, runeRoot }) {
61
61
  // Check 3: Count skill files
62
62
  const files = await readdir(outputDir);
63
63
  const skillFiles = files.filter((f) => f.startsWith('rune-') && f !== `rune-index${adapter.fileExtension}`);
64
- const expectedSkillCount = 55 - (config.skills?.disabled?.length || 0);
64
+
65
+ // Dynamic expected count: scan source skills/ directory
66
+ const sourceSkillsDir = path.join(runeRoot, 'skills');
67
+ let sourceSkillCount = 0;
68
+ if (existsSync(sourceSkillsDir)) {
69
+ const entries = await readdir(sourceSkillsDir, { withFileTypes: true });
70
+ sourceSkillCount = entries.filter(
71
+ (e) => e.isDirectory() && existsSync(path.join(sourceSkillsDir, e.name, 'SKILL.md')),
72
+ ).length;
73
+ }
74
+ const expectedSkillCount = sourceSkillCount - (config.skills?.disabled?.length || 0);
65
75
 
66
76
  if (skillFiles.length >= expectedSkillCount) {
67
77
  results.checks.push({ name: 'Skill files', status: 'pass', detail: `${skillFiles.length}/${expectedSkillCount}` });
@@ -114,6 +124,45 @@ export async function runDoctor({ outputRoot, adapter, config, runeRoot }) {
114
124
  }
115
125
  }
116
126
 
127
+ // Check 8: Reference injection rule validation
128
+ const injectionErrors = await checkInjectionRules(runeRoot, config);
129
+ if (injectionErrors.length === 0) {
130
+ results.checks.push({ name: 'Injection rules', status: 'pass' });
131
+ } else {
132
+ results.checks.push({
133
+ name: 'Injection rules',
134
+ status: 'warn',
135
+ detail: `${injectionErrors.length} issues`,
136
+ });
137
+ results.warnings.push(...injectionErrors);
138
+ }
139
+
140
+ // Check 9: Template signal validation (Pro/Business packs)
141
+ const templateErrors = await checkTemplateSignals(runeRoot, config);
142
+ if (templateErrors.length === 0) {
143
+ results.checks.push({ name: 'Template signals', status: 'pass' });
144
+ } else {
145
+ results.checks.push({
146
+ name: 'Template signals',
147
+ status: 'warn',
148
+ detail: `${templateErrors.length} issues`,
149
+ });
150
+ results.warnings.push(...templateErrors);
151
+ }
152
+
153
+ // Check 10: Org template validation (Business)
154
+ const orgErrors = await checkOrgTemplates(runeRoot, config);
155
+ if (orgErrors.length === 0) {
156
+ results.checks.push({ name: 'Org templates', status: 'pass' });
157
+ } else {
158
+ results.checks.push({
159
+ name: 'Org templates',
160
+ status: 'warn',
161
+ detail: `${orgErrors.length} issues`,
162
+ });
163
+ results.warnings.push(...orgErrors);
164
+ }
165
+
117
166
  if (results.errors.length > 0) results.healthy = false;
118
167
 
119
168
  return results;
@@ -173,6 +222,238 @@ async function checkSplitPacks(extensionsDir) {
173
222
  return errors;
174
223
  }
175
224
 
225
+ /**
226
+ * Check that inject.json rules reference existing files and valid skills.
227
+ */
228
+ async function checkInjectionRules(runeRoot, _config) {
229
+ const errors = [];
230
+ const parentDir = path.resolve(runeRoot, '..');
231
+
232
+ // Collect known skill names from Free core
233
+ const knownSkills = new Set();
234
+ const skillsDir = path.join(runeRoot, 'skills');
235
+ if (existsSync(skillsDir)) {
236
+ for (const entry of await readdir(skillsDir, { withFileTypes: true })) {
237
+ if (entry.isDirectory() && existsSync(path.join(skillsDir, entry.name, 'SKILL.md'))) {
238
+ knownSkills.add(entry.name);
239
+ }
240
+ }
241
+ }
242
+
243
+ // Scan tier directories for inject.json files
244
+ const tierDirs = [
245
+ path.join(runeRoot, 'extensions'),
246
+ path.join(parentDir, 'Pro', 'extensions'),
247
+ path.join(parentDir, 'Business', 'extensions'),
248
+ ];
249
+
250
+ for (const extDir of tierDirs) {
251
+ if (!existsSync(extDir)) continue;
252
+ for (const packEntry of await readdir(extDir, { withFileTypes: true })) {
253
+ if (!packEntry.isDirectory()) continue;
254
+ const injectFile = path.join(extDir, packEntry.name, 'inject.json');
255
+ if (!existsSync(injectFile)) continue;
256
+
257
+ try {
258
+ const raw = await readFile(injectFile, 'utf-8');
259
+ const config = JSON.parse(raw);
260
+ const packDir = path.join(extDir, packEntry.name);
261
+
262
+ for (const rule of config.injections || []) {
263
+ // Check target skill exists
264
+ if (rule.skill && !knownSkills.has(rule.skill)) {
265
+ errors.push(`${packEntry.name}/inject.json: target skill "${rule.skill}" not found in Free core`);
266
+ }
267
+ // Check reference file exists
268
+ if (rule.ref) {
269
+ const refPath = path.join(packDir, rule.ref);
270
+ if (!existsSync(refPath)) {
271
+ errors.push(`${packEntry.name}/inject.json: reference file "${rule.ref}" not found`);
272
+ }
273
+ }
274
+ }
275
+ } catch (e) {
276
+ errors.push(`${packEntry.name}/inject.json: invalid JSON — ${e.message}`);
277
+ }
278
+ }
279
+ }
280
+
281
+ return errors;
282
+ }
283
+
284
+ /**
285
+ * Check that template signal references exist in the skill ecosystem.
286
+ * Scans templates/ in Pro/Business extension dirs (relative to runeRoot parent).
287
+ */
288
+ async function checkTemplateSignals(runeRoot, _config) {
289
+ const errors = [];
290
+ const parentDir = path.resolve(runeRoot, '..');
291
+
292
+ // Collect all known signals from core + pack skills
293
+ const knownSignals = new Set();
294
+ const skillsDir = path.join(runeRoot, 'skills');
295
+ if (existsSync(skillsDir)) {
296
+ for (const entry of await readdir(skillsDir, { withFileTypes: true })) {
297
+ if (!entry.isDirectory()) continue;
298
+ const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
299
+ if (!existsSync(skillFile)) continue;
300
+ const content = await readFile(skillFile, 'utf-8');
301
+ for (const signal of extractFrontmatterSignals(content)) {
302
+ knownSignals.add(signal);
303
+ }
304
+ }
305
+ }
306
+
307
+ // Scan tier extension dirs for signals + templates
308
+ const tierDirs = [
309
+ path.join(runeRoot, 'extensions'),
310
+ path.join(parentDir, 'Pro', 'extensions'),
311
+ path.join(parentDir, 'Business', 'extensions'),
312
+ ];
313
+
314
+ const templateFiles = [];
315
+
316
+ for (const extDir of tierDirs) {
317
+ if (!existsSync(extDir)) continue;
318
+ for (const packEntry of await readdir(extDir, { withFileTypes: true })) {
319
+ if (!packEntry.isDirectory()) continue;
320
+ const packDir = path.join(extDir, packEntry.name);
321
+
322
+ // Collect signals from pack skills
323
+ const packSkillsDir = path.join(packDir, 'skills');
324
+ if (existsSync(packSkillsDir)) {
325
+ for (const sf of await readdir(packSkillsDir)) {
326
+ if (!sf.endsWith('.md')) continue;
327
+ const content = await readFile(path.join(packSkillsDir, sf), 'utf-8');
328
+ for (const signal of extractFrontmatterSignals(content)) {
329
+ knownSignals.add(signal);
330
+ }
331
+ }
332
+ }
333
+
334
+ // Collect template files
335
+ const templatesDir = path.join(packDir, 'templates');
336
+ if (existsSync(templatesDir)) {
337
+ for (const tf of await readdir(templatesDir)) {
338
+ if (!tf.endsWith('.md')) continue;
339
+ templateFiles.push(path.join(templatesDir, tf));
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ // Validate each template's signals
346
+ for (const templatePath of templateFiles) {
347
+ const content = await readFile(templatePath, 'utf-8');
348
+ const parsed = parseTemplate(content, templatePath);
349
+ const templateName = parsed.name || path.basename(templatePath, '.md');
350
+
351
+ for (const signal of parsed.signals.listen) {
352
+ if (!knownSignals.has(signal)) {
353
+ errors.push(`template "${templateName}": listens to "${signal}" but no skill emits it`);
354
+ }
355
+ }
356
+ }
357
+
358
+ return errors;
359
+ }
360
+
361
+ /**
362
+ * Extract emit/listen signal names from a file's frontmatter
363
+ */
364
+ function extractFrontmatterSignals(content) {
365
+ const signals = [];
366
+ const normalized = content.replace(/\r\n/g, '\n');
367
+ const fmMatch = normalized.match(/^---\n([\s\S]*?)\n---/);
368
+ if (!fmMatch) return signals;
369
+
370
+ const raw = fmMatch[1];
371
+ for (const key of ['emit', 'listen']) {
372
+ const match = raw.match(new RegExp(`^\\s*${key}:\\s*(.+)$`, 'm'));
373
+ if (match) {
374
+ for (const s of match[1].split(',')) {
375
+ const trimmed = s.trim();
376
+ if (trimmed) signals.push(trimmed);
377
+ }
378
+ }
379
+ }
380
+ return signals;
381
+ }
382
+
383
+ /**
384
+ * Validate org templates in Business tier.
385
+ * Checks: valid frontmatter, required sections, policy structure.
386
+ */
387
+ async function checkOrgTemplates(_runeRoot, config) {
388
+ const errors = [];
389
+ const tierSources = config.tierSources || {};
390
+ if (!tierSources.business) return errors;
391
+
392
+ const orgTemplatesDir = path.join(tierSources.business, 'org-templates');
393
+ if (!existsSync(orgTemplatesDir)) return errors;
394
+
395
+ let entries;
396
+ try {
397
+ entries = await readdir(orgTemplatesDir);
398
+ } catch {
399
+ return errors;
400
+ }
401
+
402
+ const mdFiles = entries.filter((f) => f.endsWith('.md'));
403
+ if (mdFiles.length === 0) {
404
+ errors.push('org-templates/: directory exists but contains no .md files');
405
+ return errors;
406
+ }
407
+
408
+ const requiredSections = ['Teams', 'Roles', 'Policies', 'Governance Level'];
409
+
410
+ for (const file of mdFiles) {
411
+ const filePath = path.join(orgTemplatesDir, file);
412
+ try {
413
+ const content = await readFile(filePath, 'utf-8');
414
+ const parsed = parseOrgConfig(content, filePath);
415
+
416
+ // Check required frontmatter
417
+ if (!parsed.name) {
418
+ errors.push(`org-templates/${file}: missing 'name' in frontmatter`);
419
+ }
420
+ if (!parsed.description) {
421
+ errors.push(`org-templates/${file}: missing 'description' in frontmatter`);
422
+ }
423
+
424
+ // Check required sections exist
425
+ for (const section of requiredSections) {
426
+ const pattern = new RegExp(`## ${section}`);
427
+ if (!pattern.test(content)) {
428
+ errors.push(`org-templates/${file}: missing required section '## ${section}'`);
429
+ }
430
+ }
431
+
432
+ // Check teams table has entries
433
+ if (parsed.teams.length === 0) {
434
+ errors.push(`org-templates/${file}: ## Teams table is empty`);
435
+ }
436
+
437
+ // Check roles table has entries
438
+ if (parsed.roles.length === 0) {
439
+ errors.push(`org-templates/${file}: ## Roles table is empty`);
440
+ }
441
+
442
+ // Check governance level is valid
443
+ const validLevels = ['minimal', 'moderate', 'maximum'];
444
+ if (!validLevels.includes(parsed.governanceLevel.level)) {
445
+ errors.push(
446
+ `org-templates/${file}: governance level '${parsed.governanceLevel.level}' not in [${validLevels.join(', ')}]`,
447
+ );
448
+ }
449
+ } catch (err) {
450
+ errors.push(`org-templates/${file}: parse error — ${err.message}`);
451
+ }
452
+ }
453
+
454
+ return errors;
455
+ }
456
+
176
457
  /**
177
458
  * Format doctor results for console output
178
459
  */