@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
@@ -2,13 +2,13 @@
2
2
  * Emitter
3
3
  *
4
4
  * Writes transformed skill files to the platform's output directory.
5
- * Handles file naming, directory creation, and index generation.
5
+ * Handles file naming, directory creation, index generation, and AGENTS.md creation.
6
6
  */
7
7
 
8
8
  import { existsSync } from 'node:fs';
9
9
  import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
10
10
  import path from 'node:path';
11
- import { extractCrossRefs, extractToolRefs, parsePack, parseSkill } from './parser.js';
11
+ import { extractCrossRefs, extractToolRefs, parseOrgConfig, parsePack, parseSkill, parseTemplate } from './parser.js';
12
12
  import { transformSkill } from './transformer.js';
13
13
  import { resolveScriptsPath } from './transforms/scripts-path.js';
14
14
 
@@ -60,6 +60,33 @@ async function discoverPacks(extensionsDir, enabledPacks = null) {
60
60
  return paths.sort();
61
61
  }
62
62
 
63
+ /**
64
+ * Copy extra directories from skill source to output
65
+ * Copies directories except SKILL.md (already processed) and denylisted dirs
66
+ *
67
+ * @param {string} sourceSkillDir - e.g. skills/cook/
68
+ * @param {string} outputSkillDir - e.g. .codex/skills/rune-cook/
69
+ * @returns {Promise<string[]>} list of copied directory names
70
+ */
71
+ const COPY_DENYLIST = new Set(['.git', 'node_modules', '__pycache__', '.DS_Store', '.venv', '.env']);
72
+
73
+ async function copySkillExtraDirs(sourceSkillDir, outputSkillDir) {
74
+ if (!existsSync(sourceSkillDir)) return [];
75
+
76
+ const entries = await readdir(sourceSkillDir, { withFileTypes: true });
77
+ const dirs = entries.filter((e) => e.isDirectory() && !COPY_DENYLIST.has(e.name));
78
+
79
+ const copied = [];
80
+ for (const dir of dirs) {
81
+ const sourcePath = path.join(sourceSkillDir, dir.name);
82
+ const outputPath = path.join(outputSkillDir, dir.name);
83
+ await cp(sourcePath, outputPath, { recursive: true });
84
+ copied.push(dir.name);
85
+ }
86
+
87
+ return copied;
88
+ }
89
+
63
90
  /**
64
91
  * Copy scripts directory from skill source to output.
65
92
  *
@@ -151,6 +178,217 @@ export async function discoverTieredPacks(freeExtDir, tierSources = {}, enabledP
151
178
  return [...packMap.values()].sort((a, b) => a.dirName.localeCompare(b.dirName));
152
179
  }
153
180
 
181
+ /**
182
+ * Discover workflow templates in a pack's templates/ directory
183
+ *
184
+ * @param {string} packDir - path to the pack directory (e.g. extensions/pro-product/)
185
+ * @returns {Promise<Array<{name: string, file: string, parsed: object}>>} array of parsed templates
186
+ */
187
+ async function discoverTemplates(packDir) {
188
+ const templatesDir = path.join(packDir, 'templates');
189
+ if (!existsSync(templatesDir)) return [];
190
+
191
+ const entries = await readdir(templatesDir, { withFileTypes: true });
192
+ const templates = [];
193
+
194
+ for (const entry of entries) {
195
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
196
+ const filePath = path.join(templatesDir, entry.name);
197
+ const content = await readFile(filePath, 'utf-8');
198
+ const parsed = parseTemplate(content, filePath);
199
+ templates.push({
200
+ name: parsed.name || entry.name.replace(/\.md$/, ''),
201
+ file: `templates/${entry.name}`,
202
+ parsed,
203
+ });
204
+ }
205
+
206
+ return templates.sort((a, b) => a.name.localeCompare(b.name));
207
+ }
208
+
209
+ /**
210
+ * Load reference injection rules from all tier extension directories.
211
+ * Each pack can have an inject.json file defining which references to inject
212
+ * into Free core skills when that pack is installed.
213
+ *
214
+ * @param {string} freeExtDir - path to free extensions/ directory
215
+ * @param {Object<string, string>} tierSources - { pro: "/path", business: "/path" }
216
+ * @returns {Promise<Map<string, Array<{ref: string, context: string, packDir: string}>>>}
217
+ * Map of skillName → array of injections
218
+ */
219
+ async function loadInjectionRules(freeExtDir, tierSources) {
220
+ const injections = new Map(); // skillName → [{ref, context, packDir}]
221
+
222
+ const extDirs = [freeExtDir, tierSources.pro, tierSources.business].filter(Boolean);
223
+
224
+ for (const extDir of extDirs) {
225
+ if (!existsSync(extDir)) continue;
226
+ const entries = await readdir(extDir, { withFileTypes: true });
227
+
228
+ for (const entry of entries) {
229
+ if (!entry.isDirectory()) continue;
230
+ const injectFile = path.join(extDir, entry.name, 'inject.json');
231
+ if (!existsSync(injectFile)) continue;
232
+
233
+ try {
234
+ const raw = await readFile(injectFile, 'utf-8');
235
+ const config = JSON.parse(raw);
236
+ const packDir = path.join(extDir, entry.name);
237
+
238
+ for (const rule of config.injections || []) {
239
+ if (!rule.skill || !rule.ref) continue;
240
+ if (!injections.has(rule.skill)) {
241
+ injections.set(rule.skill, []);
242
+ }
243
+ injections.get(rule.skill).push({
244
+ ref: rule.ref,
245
+ context: rule.context || '',
246
+ packDir,
247
+ pack: entry.name,
248
+ });
249
+ }
250
+ } catch {
251
+ // Invalid JSON — skip gracefully
252
+ }
253
+ }
254
+ }
255
+
256
+ return injections;
257
+ }
258
+
259
+ /**
260
+ * Apply reference injections to a skill's body.
261
+ * Reads the referenced files and appends them as context blocks.
262
+ *
263
+ * @param {string} body - original skill body
264
+ * @param {Array<{ref: string, context: string, packDir: string}>} rules - injection rules
265
+ * @returns {Promise<{body: string, count: number}>} injected body + count of injections applied
266
+ */
267
+ async function applyInjections(body, rules) {
268
+ const blocks = [];
269
+
270
+ for (const rule of rules) {
271
+ const refPath = path.join(rule.packDir, rule.ref);
272
+ if (!existsSync(refPath)) continue;
273
+
274
+ try {
275
+ const content = await readFile(refPath, 'utf-8');
276
+ // Strip frontmatter if present
277
+ const cleaned = content
278
+ .replace(/\r\n/g, '\n')
279
+ .replace(/^---\n[\s\S]*?\n---\n?/, '')
280
+ .trim();
281
+ blocks.push(
282
+ `\n\n<!-- Pro Reference: ${rule.pack}/${rule.ref} -->\n` +
283
+ `<CONTEXT-INJECTION source="${rule.pack}" context="${rule.context}">\n` +
284
+ `${cleaned}\n` +
285
+ `</CONTEXT-INJECTION>`,
286
+ );
287
+ } catch {
288
+ // Unreadable reference — skip
289
+ }
290
+ }
291
+
292
+ return { body: body + blocks.join(''), count: blocks.length };
293
+ }
294
+
295
+ /**
296
+ * Load org config from .rune/org/org.md if it exists.
297
+ * Returns parsed org config or null if not present.
298
+ */
299
+ async function loadOrgConfig(runeRoot, tierSources) {
300
+ // Org templates are a Business feature — only load if business tier is available
301
+ if (!tierSources || !tierSources.business) return null;
302
+
303
+ const orgPath = path.join(runeRoot, '.rune', 'org', 'org.md');
304
+ if (!existsSync(orgPath)) return null;
305
+
306
+ try {
307
+ const content = await readFile(orgPath, 'utf-8');
308
+ return parseOrgConfig(content, orgPath);
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Generate <ORG-POLICY> block from org config for injection into sentinel/preflight.
316
+ */
317
+ function buildOrgPolicyBlock(orgConfig, targetSkill) {
318
+ if (!orgConfig) return null;
319
+
320
+ const lines = [];
321
+ lines.push(`\n\n<!-- Business: Organization Policy (${orgConfig.name}) -->`);
322
+ lines.push(`<ORG-POLICY template="${orgConfig.name}" governance="${orgConfig.governanceLevel.level}">`);
323
+
324
+ if (targetSkill === 'sentinel') {
325
+ // Inject security + code review + deployment policies
326
+ const security = orgConfig.policies.security || [];
327
+ const codeReview = orgConfig.policies.code_review || [];
328
+ const deployment = orgConfig.policies.deployment || [];
329
+
330
+ if (security.length > 0) {
331
+ lines.push('\n### Security Policies');
332
+ for (const rule of security) {
333
+ lines.push(`- **${rule.key}**: ${rule.value}`);
334
+ }
335
+ }
336
+ if (codeReview.length > 0) {
337
+ lines.push('\n### Code Review Policies');
338
+ for (const rule of codeReview) {
339
+ lines.push(`- **${rule.key}**: ${rule.value}`);
340
+ }
341
+ }
342
+ if (deployment.length > 0) {
343
+ lines.push('\n### Deployment Policies');
344
+ for (const rule of deployment) {
345
+ lines.push(`- **${rule.key}**: ${rule.value}`);
346
+ }
347
+ }
348
+ } else if (targetSkill === 'preflight') {
349
+ // Inject code review + deployment + approval flows
350
+ const codeReview = orgConfig.policies.code_review || [];
351
+ const deployment = orgConfig.policies.deployment || [];
352
+
353
+ if (codeReview.length > 0) {
354
+ lines.push('\n### Code Review Requirements');
355
+ for (const rule of codeReview) {
356
+ lines.push(`- **${rule.key}**: ${rule.value}`);
357
+ }
358
+ }
359
+ if (deployment.length > 0) {
360
+ lines.push('\n### Deployment Requirements');
361
+ for (const rule of deployment) {
362
+ lines.push(`- **${rule.key}**: ${rule.value}`);
363
+ }
364
+ }
365
+
366
+ // Include approval flows
367
+ const flows = orgConfig.approvalFlows;
368
+ if (Object.keys(flows).length > 0) {
369
+ lines.push('\n### Approval Flows');
370
+ for (const [name, flow] of Object.entries(flows)) {
371
+ lines.push(`\n**${name}**:`);
372
+ lines.push('```');
373
+ lines.push(flow);
374
+ lines.push('```');
375
+ }
376
+ }
377
+ }
378
+
379
+ // Governance settings
380
+ const gov = orgConfig.governanceLevel;
381
+ if (gov.settings.length > 0) {
382
+ lines.push('\n### Governance Settings');
383
+ for (const setting of gov.settings) {
384
+ lines.push(`- ${setting}`);
385
+ }
386
+ }
387
+
388
+ lines.push('</ORG-POLICY>');
389
+ return lines.join('\n');
390
+ }
391
+
154
392
  /**
155
393
  * Generate output filename for a skill
156
394
  */
@@ -215,13 +453,23 @@ export async function buildAll({
215
453
  crossRefsResolved: 0,
216
454
  toolRefsResolved: 0,
217
455
  scriptsCopied: 0,
456
+ templateCount: 0,
457
+ injectionsApplied: 0,
218
458
  files: [],
219
459
  skipped: [],
220
460
  errors: [],
221
461
  tierOverrides: [],
222
462
  };
223
463
 
224
- // Build skills
464
+ // Load reference injection rules from Pro/Business packs
465
+ const injectionRules = hasTiers ? await loadInjectionRules(extensionsDir, tierSources) : new Map();
466
+
467
+ // Load org config from .rune/org/org.md (Business feature)
468
+ const orgConfig = await loadOrgConfig(runeRoot, tierSources);
469
+
470
+ // Build skills — collect parsed data for skill-index + openclaw reuse
471
+ const parsedSkills = [];
472
+
225
473
  for (const skillPath of skillPaths) {
226
474
  try {
227
475
  const content = await readFile(skillPath, 'utf-8');
@@ -252,15 +500,33 @@ export async function buildAll({
252
500
  });
253
501
  }
254
502
 
255
- const output = [header, body, footer].filter(Boolean).join('\n');
503
+ // Apply reference injections from Pro/Business packs
504
+ let finalBody = body;
505
+ if (injectionRules.has(parsed.name)) {
506
+ const { body: injectedBody, count } = await applyInjections(body, injectionRules.get(parsed.name));
507
+ finalBody = injectedBody;
508
+ stats.injectionsApplied += count;
509
+ }
510
+
511
+ // Inject org policies into sentinel/preflight (Business feature)
512
+ if (orgConfig && (parsed.name === 'sentinel' || parsed.name === 'preflight')) {
513
+ const orgBlock = buildOrgPolicyBlock(orgConfig, parsed.name);
514
+ if (orgBlock) {
515
+ finalBody += orgBlock;
516
+ stats.orgPoliciesInjected = (stats.orgPoliciesInjected || 0) + 1;
517
+ }
518
+ }
519
+
520
+ const output = [header, finalBody, footer].filter(Boolean).join('\n');
256
521
 
257
522
  let outputPath;
258
523
  let displayName;
524
+ let skillDir = null;
259
525
 
260
526
  if (adapter.useSkillDirectories) {
261
527
  // Directory-per-skill: .codex/skills/rune-{name}/SKILL.md
262
528
  const dirName = `${adapter.skillPrefix}${parsed.name}`;
263
- const skillDir = path.join(outputDir, dirName);
529
+ skillDir = path.join(outputDir, dirName);
264
530
  await mkdir(skillDir, { recursive: true });
265
531
  outputPath = path.join(skillDir, adapter.skillFileName || 'SKILL.md');
266
532
  displayName = `${dirName}/${adapter.skillFileName || 'SKILL.md'}`;
@@ -272,6 +538,11 @@ export async function buildAll({
272
538
 
273
539
  await writeFile(outputPath, output, 'utf-8');
274
540
 
541
+ // Copy extra directories (references/, etc.) from skill source
542
+ if (adapter.useSkillDirectories && skillDir) {
543
+ await copySkillExtraDirs(skillSourceDir, skillDir);
544
+ }
545
+
275
546
  // Copy scripts/ directory if present
276
547
  if (hasScripts) {
277
548
  const scriptsOutput = path.join(outputDir, adapter.scriptsDir(parsed.name));
@@ -279,6 +550,7 @@ export async function buildAll({
279
550
  stats.scriptsCopied += copied.length;
280
551
  }
281
552
 
553
+ parsedSkills.push(parsed);
282
554
  stats.skillCount++;
283
555
  stats.crossRefsResolved += parsed.crossRefs.length;
284
556
  stats.toolRefsResolved += parsed.toolRefs.length;
@@ -327,6 +599,17 @@ export async function buildAll({
327
599
  }
328
600
  }
329
601
 
602
+ // For split packs: auto-discover skill files from skills/ subdir when manifest is empty
603
+ if (parsed.isSplit && parsed.skillManifest.length === 0) {
604
+ const skillsSubdir = path.join(packDir, 'skills');
605
+ if (existsSync(skillsSubdir)) {
606
+ const skillFiles = (await readdir(skillsSubdir)).filter((f) => f.endsWith('.md')).sort();
607
+ for (const sf of skillFiles) {
608
+ parsed.skillManifest.push({ name: sf.replace(/\.md$/, ''), file: `skills/${sf}` });
609
+ }
610
+ }
611
+ }
612
+
330
613
  // For split packs, load individual skill files and concatenate into body
331
614
  if (parsed.isSplit && parsed.skillManifest.length > 0) {
332
615
  const skillBodies = [];
@@ -351,6 +634,31 @@ export async function buildAll({
351
634
  parsed.toolRefs = extractToolRefs(parsed.body);
352
635
  }
353
636
 
637
+ // Discover and append workflow templates (Pro/Business packs)
638
+ const templates = await discoverTemplates(packDir);
639
+ if (templates.length > 0) {
640
+ const templateSection = [
641
+ '',
642
+ '## Workflow Templates',
643
+ '',
644
+ 'Pre-built multi-phase workflows for `cook --template <name>`:',
645
+ '',
646
+ ...templates.map(
647
+ (t) => `| \`${t.name}\` | ${t.parsed.description.slice(0, 100)} | chain: ${t.parsed.chain} |`,
648
+ ),
649
+ '',
650
+ ...templates.map((t) => {
651
+ const header = `### Template: ${t.name}`;
652
+ return `${header}\n\n${t.parsed.body}`;
653
+ }),
654
+ ];
655
+ parsed.body = `${parsed.body}\n\n${templateSection.join('\n')}`;
656
+ // Re-extract refs after template injection
657
+ parsed.crossRefs = extractCrossRefs(parsed.body);
658
+ parsed.toolRefs = extractToolRefs(parsed.body);
659
+ stats.templateCount = (stats.templateCount || 0) + templates.length;
660
+ }
661
+
354
662
  // Normalize pack name for headers (ext-trading instead of @rune/trading)
355
663
  parsed.name = `ext-${packName}`;
356
664
 
@@ -387,6 +695,18 @@ export async function buildAll({
387
695
  await writeFile(path.join(outputDir, indexFileName), indexContent, 'utf-8');
388
696
  stats.files.push(indexFileName);
389
697
 
698
+ // Generate skill-index.json — compiled intent mesh for auto-trigger hooks
699
+ const skillIndex = generateSkillIndex(parsedSkills);
700
+ await writeFile(path.join(outputDir, 'skill-index.json'), `${JSON.stringify(skillIndex, null, 2)}\n`, 'utf-8');
701
+ stats.files.push('skill-index.json');
702
+
703
+ // Generate AGENTS.md for Codex (OpenAI convention — not used by other platforms)
704
+ if (adapter.name === 'codex') {
705
+ const agentsMdContent = generateAgentsMd(stats, adapter);
706
+ await writeFile(path.join(outputRoot, 'AGENTS.md'), agentsMdContent, 'utf-8');
707
+ stats.files.push('AGENTS.md');
708
+ }
709
+
390
710
  // OpenClaw adapter: generate manifest + TypeScript entry point
391
711
  if (adapter.name === 'openclaw' && adapter.generateManifest && adapter.generateEntryPoint) {
392
712
  const pluginJsonPath = path.join(runeRoot, '.claude-plugin', 'plugin.json');
@@ -395,17 +715,6 @@ export async function buildAll({
395
715
  pluginJson = JSON.parse(await readFile(pluginJsonPath, 'utf-8'));
396
716
  }
397
717
 
398
- // Collect parsed skills for manifest/entry generation
399
- const parsedSkills = [];
400
- for (const sp of skillPaths) {
401
- try {
402
- const c = await readFile(sp, 'utf-8');
403
- parsedSkills.push(parseSkill(c, sp));
404
- } catch {
405
- /* skip on error */
406
- }
407
- }
408
-
409
718
  // Read skill-router content for system prompt injection
410
719
  const routerPath = path.join(runeRoot, 'skills', 'skill-router', 'SKILL.md');
411
720
  let routerContent = '';
@@ -447,7 +756,7 @@ function generateIndex(stats, adapter) {
447
756
  const lines = [
448
757
  '# Rune Skill Index',
449
758
  '',
450
- `> Platform: ${adapter.name} | Skills: ${stats.skillCount} | Extensions: ${stats.packCount}`,
759
+ `> Platform: ${adapter.name} | Skills: ${stats.skillCount} | Extensions: ${stats.packCount}${stats.templateCount > 0 ? ` | Templates: ${stats.templateCount}` : ''}${stats.injectionsApplied > 0 ? ` | Injections: ${stats.injectionsApplied}` : ''}`,
451
760
  '',
452
761
  '## Core Skills',
453
762
  '',
@@ -464,3 +773,167 @@ function generateIndex(stats, adapter) {
464
773
 
465
774
  return lines.join('\n');
466
775
  }
776
+
777
+ /**
778
+ * Generate AGENTS.md for Codex (OpenAI convention)
779
+ * Uses dynamic counts from build stats — no hardcoded skill lists
780
+ */
781
+ function generateAgentsMd(stats, adapter) {
782
+ const lines = [
783
+ '# Rune — Project Configuration',
784
+ '',
785
+ '## Overview',
786
+ '',
787
+ 'Rune is an interconnected skill ecosystem for AI coding assistants.',
788
+ `${stats.skillCount} core skills | 5-layer mesh architecture | ${stats.crossRefsResolved} connections | Multi-platform.`,
789
+ 'Philosophy: "Less skills. Deeper connections."',
790
+ '',
791
+ `Platform: ${adapter.name}`,
792
+ '',
793
+ '## Skills',
794
+ '',
795
+ `**${stats.skillCount} core skills** + **${stats.packCount} extension packs**`,
796
+ '',
797
+ '## Usage',
798
+ '',
799
+ 'Reference skills using the `Skill` tool or delegate to subagents using the `Agent` tool.',
800
+ '',
801
+ '## Skills Directory',
802
+ '',
803
+ `Skills are located in: ${adapter.outputDir}/`,
804
+ '',
805
+ '---',
806
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
807
+ '',
808
+ ];
809
+
810
+ return lines.join('\n');
811
+ }
812
+
813
+ /**
814
+ * Intent keyword patterns for each skill — extracted from description + Triggers section
815
+ * Maps common user intent words to the skill that handles them
816
+ */
817
+ const INTENT_KEYWORDS = {
818
+ cook: ['implement', 'build', 'create', 'add', 'feature', 'fix', 'code', 'write', 'make', 'develop'],
819
+ team: ['parallel', 'split', 'multiple', 'large', 'many files', 'multi-module'],
820
+ launch: ['deploy', 'launch', 'release', 'ship', 'publish', 'production'],
821
+ rescue: ['legacy', 'refactor', 'modernize', 'rescue', 'clean up', 'old code', 'messy'],
822
+ scaffold: ['new project', 'bootstrap', 'scaffold', 'init', 'greenfield', 'starter'],
823
+ plan: ['plan', 'architect', 'design system', 'roadmap', 'strategy'],
824
+ brainstorm: ['brainstorm', 'explore', 'ideas', 'alternatives', 'approaches'],
825
+ debug: ['debug', 'error', 'bug', 'broken', 'trace', 'diagnose', 'crash', 'fail'],
826
+ fix: ['fix', 'patch', 'hotfix', 'resolve', 'repair'],
827
+ test: ['test', 'tdd', 'coverage', 'unit test', 'e2e', 'spec'],
828
+ review: ['review', 'code review', 'check quality', 'audit code'],
829
+ sentinel: ['security', 'vulnerability', 'owasp', 'secret', 'audit security'],
830
+ preflight: ['pre-commit', 'quality gate', 'check before'],
831
+ deploy: ['deploy', 'ci/cd', 'pipeline', 'kubernetes', 'docker'],
832
+ design: ['ui', 'ux', 'design', 'layout', 'component design', 'wireframe'],
833
+ perf: ['performance', 'slow', 'optimize', 'n+1', 'memory leak', 'bundle size'],
834
+ db: ['database', 'migration', 'schema', 'sql', 'query', 'index'],
835
+ audit: ['audit', 'health check', 'project assessment', 'codebase review'],
836
+ onboard: ['onboard', 'setup', 'configure project', 'get started'],
837
+ docs: ['document', 'readme', 'api docs', 'changelog'],
838
+ ba: ['requirements', 'business analysis', 'user stories', 'stakeholder'],
839
+ adversary: ['red team', 'challenge', 'stress test', 'edge case'],
840
+ incident: ['incident', 'outage', 'downtime', 'postmortem'],
841
+ surgeon: ['refactor', 'extract', 'strangler', 'decompose'],
842
+ 'mcp-builder': ['mcp', 'mcp server', 'tool server', 'model context'],
843
+ 'skill-forge': ['new skill', 'create skill', 'edit skill'],
844
+ 'review-intake': ['pr feedback', 'review comments', 'received review'],
845
+ 'logic-guardian': ['business logic', 'protect logic', 'critical path'],
846
+ marketing: ['marketing', 'landing page', 'seo', 'social media', 'copy'],
847
+ retro: ['retrospective', 'sprint review', 'velocity', 'team health'],
848
+ };
849
+
850
+ /**
851
+ * Generate skill-index.json — compiled intent mesh for runtime auto-trigger
852
+ *
853
+ * Extracts from parsed skills: name, description, layer, model, group,
854
+ * cross-references (connections), and maps intent keywords to skill chains.
855
+ *
856
+ * @param {Array} parsedSkills - array of parsed skill objects
857
+ * @returns {object} skill index with graph + intents
858
+ */
859
+ function generateSkillIndex(parsedSkills) {
860
+ // Build adjacency graph from cross-references
861
+ const graph = {};
862
+ const skills = {};
863
+
864
+ for (const skill of parsedSkills) {
865
+ const outbound = [...new Set(skill.crossRefs.map((r) => r.skillName))];
866
+ graph[skill.name] = outbound;
867
+ skills[skill.name] = {
868
+ layer: skill.layer,
869
+ model: skill.model,
870
+ group: skill.group,
871
+ description: skill.description.slice(0, 200),
872
+ connections: outbound,
873
+ ...(skill.signals ? { signals: skill.signals } : {}),
874
+ };
875
+ }
876
+
877
+ // Build signal graph — maps each signal to its emitters and listeners
878
+ const signalGraph = buildSignalGraph(parsedSkills);
879
+
880
+ // Build intent patterns from INTENT_KEYWORDS + skill descriptions
881
+ const intents = {};
882
+ for (const [skillName, keywords] of Object.entries(INTENT_KEYWORDS)) {
883
+ if (!skills[skillName]) continue;
884
+ const skill = skills[skillName];
885
+
886
+ // Build chain: primary skill + its direct connections (1-hop)
887
+ const chain = [skillName, ...graph[skillName].filter((c) => skills[c]).slice(0, 5)];
888
+
889
+ intents[skillName] = {
890
+ keywords,
891
+ layer: skill.layer,
892
+ model: skill.model,
893
+ chain,
894
+ };
895
+ }
896
+
897
+ return {
898
+ version: 2,
899
+ generated: new Date().toISOString(),
900
+ skillCount: parsedSkills.length,
901
+ skills,
902
+ graph,
903
+ signals: signalGraph,
904
+ intents,
905
+ };
906
+ }
907
+
908
+ /**
909
+ * Build signal graph from parsed skills' emit/listen declarations.
910
+ * Maps each signal name to its emitters and listeners.
911
+ *
912
+ * @param {Array} parsedSkills
913
+ * @returns {object} { "code.changed": { emitters: ["fix"], listeners: ["test", "review"] } }
914
+ */
915
+ function buildSignalGraph(parsedSkills) {
916
+ const signals = {};
917
+
918
+ for (const skill of parsedSkills) {
919
+ if (!skill.signals) continue;
920
+
921
+ for (const signal of skill.signals.emit) {
922
+ if (!signals[signal]) signals[signal] = { emitters: [], listeners: [] };
923
+ signals[signal].emitters.push(skill.name);
924
+ }
925
+
926
+ for (const signal of skill.signals.listen) {
927
+ if (!signals[signal]) signals[signal] = { emitters: [], listeners: [] };
928
+ signals[signal].listeners.push(skill.name);
929
+ }
930
+ }
931
+
932
+ // Sort for deterministic output
933
+ for (const entry of Object.values(signals)) {
934
+ entry.emitters.sort();
935
+ entry.listeners.sort();
936
+ }
937
+
938
+ return signals;
939
+ }