@rune-kit/rune 2.6.0 → 2.8.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 +22 -6
- package/compiler/__tests__/executive-dashboards.test.js +285 -0
- package/compiler/__tests__/inject.test.js +128 -0
- package/compiler/__tests__/orchestrators.test.js +151 -0
- package/compiler/__tests__/org-templates.test.js +447 -0
- package/compiler/__tests__/parser.test.js +201 -147
- package/compiler/__tests__/skill-index.test.js +218 -218
- package/compiler/__tests__/status.test.js +336 -0
- package/compiler/__tests__/templates.test.js +245 -0
- package/compiler/__tests__/visualizer.test.js +325 -0
- package/compiler/adapters/antigravity.js +71 -71
- package/compiler/bin/rune.js +444 -355
- package/compiler/doctor.js +272 -1
- package/compiler/emitter.js +939 -678
- package/compiler/parser.js +498 -267
- package/compiler/status.js +342 -0
- package/compiler/visualizer.js +622 -0
- package/package.json +1 -1
- package/skills/autopsy/SKILL.md +48 -1
- package/skills/completion-gate/SKILL.md +51 -3
- package/skills/context-engine/SKILL.md +141 -2
- package/skills/cook/SKILL.md +177 -4
- package/skills/debug/SKILL.md +50 -1
- package/skills/docs/SKILL.md +28 -3
- package/skills/fix/SKILL.md +26 -1
- package/skills/mcp-builder/SKILL.md +53 -1
- package/skills/onboard/SKILL.md +51 -1
- package/skills/perf/SKILL.md +34 -1
- package/skills/plan/SKILL.md +27 -1
- package/skills/preflight/SKILL.md +35 -1
- package/skills/research/SKILL.md +24 -1
- package/skills/retro/SKILL.md +95 -1
- package/skills/review/SKILL.md +45 -1
- package/skills/scope-guard/SKILL.md +1 -0
- package/skills/scout/SKILL.md +22 -1
- package/skills/sentinel/SKILL.md +35 -1
- package/skills/sentinel/references/policy-driven-constraints.md +424 -0
- package/skills/sentinel-env/SKILL.md +0 -2
- package/skills/session-bridge/SKILL.md +57 -2
- package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
- package/skills/team/SKILL.md +15 -1
- package/skills/verification/SKILL.md +0 -2
package/compiler/doctor.js
CHANGED
|
@@ -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
|
|
@@ -124,6 +124,45 @@ export async function runDoctor({ outputRoot, adapter, config, runeRoot }) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
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
|
+
|
|
127
166
|
if (results.errors.length > 0) results.healthy = false;
|
|
128
167
|
|
|
129
168
|
return results;
|
|
@@ -183,6 +222,238 @@ async function checkSplitPacks(extensionsDir) {
|
|
|
183
222
|
return errors;
|
|
184
223
|
}
|
|
185
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
|
+
|
|
186
457
|
/**
|
|
187
458
|
* Format doctor results for console output
|
|
188
459
|
*/
|