@blamejs/exceptd-skills 0.9.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 (136) hide show
  1. package/AGENTS.md +232 -0
  2. package/ARCHITECTURE.md +267 -0
  3. package/CHANGELOG.md +616 -0
  4. package/CONTEXT.md +203 -0
  5. package/LICENSE +200 -0
  6. package/NOTICE +82 -0
  7. package/README.md +307 -0
  8. package/SECURITY.md +73 -0
  9. package/agents/README.md +81 -0
  10. package/agents/report-generator.md +156 -0
  11. package/agents/skill-updater.md +102 -0
  12. package/agents/source-validator.md +119 -0
  13. package/agents/threat-researcher.md +149 -0
  14. package/bin/exceptd.js +183 -0
  15. package/data/_indexes/_meta.json +88 -0
  16. package/data/_indexes/activity-feed.json +362 -0
  17. package/data/_indexes/catalog-summaries.json +229 -0
  18. package/data/_indexes/chains.json +7135 -0
  19. package/data/_indexes/currency.json +359 -0
  20. package/data/_indexes/did-ladders.json +451 -0
  21. package/data/_indexes/frequency.json +2072 -0
  22. package/data/_indexes/handoff-dag.json +476 -0
  23. package/data/_indexes/jurisdiction-clocks.json +967 -0
  24. package/data/_indexes/jurisdiction-map.json +536 -0
  25. package/data/_indexes/recipes.json +319 -0
  26. package/data/_indexes/section-offsets.json +3656 -0
  27. package/data/_indexes/stale-content.json +14 -0
  28. package/data/_indexes/summary-cards.json +1736 -0
  29. package/data/_indexes/theater-fingerprints.json +381 -0
  30. package/data/_indexes/token-budget.json +2137 -0
  31. package/data/_indexes/trigger-table.json +1374 -0
  32. package/data/_indexes/xref.json +818 -0
  33. package/data/atlas-ttps.json +282 -0
  34. package/data/cve-catalog.json +496 -0
  35. package/data/cwe-catalog.json +1017 -0
  36. package/data/d3fend-catalog.json +738 -0
  37. package/data/dlp-controls.json +1039 -0
  38. package/data/exploit-availability.json +67 -0
  39. package/data/framework-control-gaps.json +1255 -0
  40. package/data/global-frameworks.json +2913 -0
  41. package/data/rfc-references.json +324 -0
  42. package/data/zeroday-lessons.json +377 -0
  43. package/keys/public.pem +3 -0
  44. package/lib/framework-gap.js +328 -0
  45. package/lib/job-queue.js +195 -0
  46. package/lib/lint-skills.js +536 -0
  47. package/lib/prefetch.js +372 -0
  48. package/lib/refresh-external.js +713 -0
  49. package/lib/schemas/cve-catalog.schema.json +151 -0
  50. package/lib/schemas/manifest.schema.json +106 -0
  51. package/lib/schemas/skill-frontmatter.schema.json +113 -0
  52. package/lib/scoring.js +149 -0
  53. package/lib/sign.js +197 -0
  54. package/lib/ttp-mapper.js +80 -0
  55. package/lib/validate-catalog-meta.js +198 -0
  56. package/lib/validate-cve-catalog.js +213 -0
  57. package/lib/validate-indexes.js +83 -0
  58. package/lib/validate-package.js +162 -0
  59. package/lib/validate-vendor.js +85 -0
  60. package/lib/verify.js +216 -0
  61. package/lib/worker-pool.js +84 -0
  62. package/manifest-snapshot.json +1833 -0
  63. package/manifest.json +2108 -0
  64. package/orchestrator/README.md +124 -0
  65. package/orchestrator/dispatcher.js +140 -0
  66. package/orchestrator/event-bus.js +146 -0
  67. package/orchestrator/index.js +874 -0
  68. package/orchestrator/pipeline.js +201 -0
  69. package/orchestrator/scanner.js +327 -0
  70. package/orchestrator/scheduler.js +137 -0
  71. package/package.json +113 -0
  72. package/sbom.cdx.json +158 -0
  73. package/scripts/audit-cross-skill.js +261 -0
  74. package/scripts/audit-perf.js +160 -0
  75. package/scripts/bootstrap.js +205 -0
  76. package/scripts/build-indexes.js +721 -0
  77. package/scripts/builders/activity-feed.js +79 -0
  78. package/scripts/builders/catalog-summaries.js +67 -0
  79. package/scripts/builders/currency.js +109 -0
  80. package/scripts/builders/cwe-chains.js +105 -0
  81. package/scripts/builders/did-ladders.js +149 -0
  82. package/scripts/builders/frequency.js +89 -0
  83. package/scripts/builders/jurisdiction-clocks.js +126 -0
  84. package/scripts/builders/recipes.js +159 -0
  85. package/scripts/builders/section-offsets.js +162 -0
  86. package/scripts/builders/stale-content.js +171 -0
  87. package/scripts/builders/summary-cards.js +166 -0
  88. package/scripts/builders/theater-fingerprints.js +198 -0
  89. package/scripts/builders/token-budget.js +96 -0
  90. package/scripts/check-manifest-snapshot.js +217 -0
  91. package/scripts/predeploy.js +267 -0
  92. package/scripts/refresh-manifest-snapshot.js +57 -0
  93. package/scripts/refresh-sbom.js +222 -0
  94. package/skills/age-gates-child-safety/skill.md +456 -0
  95. package/skills/ai-attack-surface/skill.md +282 -0
  96. package/skills/ai-c2-detection/skill.md +440 -0
  97. package/skills/ai-risk-management/skill.md +311 -0
  98. package/skills/api-security/skill.md +287 -0
  99. package/skills/attack-surface-pentest/skill.md +381 -0
  100. package/skills/cloud-security/skill.md +384 -0
  101. package/skills/compliance-theater/skill.md +365 -0
  102. package/skills/container-runtime-security/skill.md +379 -0
  103. package/skills/coordinated-vuln-disclosure/skill.md +473 -0
  104. package/skills/defensive-countermeasure-mapping/skill.md +300 -0
  105. package/skills/dlp-gap-analysis/skill.md +337 -0
  106. package/skills/email-security-anti-phishing/skill.md +206 -0
  107. package/skills/exploit-scoring/skill.md +331 -0
  108. package/skills/framework-gap-analysis/skill.md +374 -0
  109. package/skills/fuzz-testing-strategy/skill.md +313 -0
  110. package/skills/global-grc/skill.md +564 -0
  111. package/skills/identity-assurance/skill.md +272 -0
  112. package/skills/incident-response-playbook/skill.md +546 -0
  113. package/skills/kernel-lpe-triage/skill.md +303 -0
  114. package/skills/mcp-agent-trust/skill.md +326 -0
  115. package/skills/mlops-security/skill.md +325 -0
  116. package/skills/ot-ics-security/skill.md +340 -0
  117. package/skills/policy-exception-gen/skill.md +437 -0
  118. package/skills/pqc-first/skill.md +546 -0
  119. package/skills/rag-pipeline-security/skill.md +294 -0
  120. package/skills/researcher/skill.md +310 -0
  121. package/skills/sector-energy/skill.md +409 -0
  122. package/skills/sector-federal-government/skill.md +302 -0
  123. package/skills/sector-financial/skill.md +398 -0
  124. package/skills/sector-healthcare/skill.md +373 -0
  125. package/skills/security-maturity-tiers/skill.md +464 -0
  126. package/skills/skill-update-loop/skill.md +463 -0
  127. package/skills/supply-chain-integrity/skill.md +318 -0
  128. package/skills/threat-model-currency/skill.md +404 -0
  129. package/skills/threat-modeling-methodology/skill.md +312 -0
  130. package/skills/webapp-security/skill.md +281 -0
  131. package/skills/zeroday-gap-learn/skill.md +350 -0
  132. package/vendor/blamejs/LICENSE +201 -0
  133. package/vendor/blamejs/README.md +54 -0
  134. package/vendor/blamejs/_PROVENANCE.json +54 -0
  135. package/vendor/blamejs/retry.js +335 -0
  136. package/vendor/blamejs/worker-pool.js +418 -0
@@ -0,0 +1,124 @@
1
+ # Orchestrator
2
+
3
+ The scanning and orchestration layer that ties all exceptd skills together. It scans an environment, routes findings to relevant skills, coordinates the multi-agent pipeline, and generates structured reports.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌──────────────────────────────────────────────────────┐
9
+ │ orchestrator/ │
10
+ │ │
11
+ │ scanner.js → dispatcher.js → pipeline.js │
12
+ │ │ │ │ │
13
+ │ ↓ ↓ ↓ │
14
+ │ findings.json matched skills agent handoffs │
15
+ │ │
16
+ │ event-bus.js ← external events (KEV, ATLAS, etc) │
17
+ │ scheduler.js ← weekly/annual currency checks │
18
+ └──────────────────────────────────────────────────────┘
19
+ ```
20
+
21
+ ## Components
22
+
23
+ ### scanner.js
24
+ Discovers the security posture of the current environment:
25
+ - Kernel version and patch status
26
+ - MCP server configurations across AI coding assistants
27
+ - Cryptographic posture (TLS versions, algorithm inventory)
28
+ - Framework compliance claims
29
+ - AI API dependencies in use
30
+
31
+ Outputs: `findings.json` — a structured list of signals, each with a severity and a hint toward which skills apply.
32
+
33
+ ### dispatcher.js
34
+ Routes scanner findings to relevant skills. Matches findings against skill `triggers` in `manifest.json`. Returns an ordered list of skills to invoke, sorted by RWEP urgency.
35
+
36
+ ### pipeline.js
37
+ Coordinates the multi-agent research → validation → update → report pipeline:
38
+ 1. **threat-researcher** — investigates new CVEs and TTPs
39
+ 2. **source-validator** — gates data quality before it enters the catalog
40
+ 3. **skill-updater** — applies validated findings to skill files and data
41
+ 4. **report-generator** — produces structured output for the target audience
42
+
43
+ ### event-bus.js
44
+ Event-driven trigger system. Fires when:
45
+ - CISA KEV catalog adds a new entry
46
+ - MITRE ATLAS publishes a new version
47
+ - A kernel CVE with RWEP > 80 is added to the catalog
48
+ - An AI/MCP platform CVE drops
49
+ - A compliance framework publishes an amendment
50
+
51
+ Each event triggers `skill-update-loop` and marks affected skills for review.
52
+
53
+ ### scheduler.js
54
+ Scheduled tasks:
55
+ - **Weekly**: currency check on all skills (any `last_threat_review` > 30 days gets flagged)
56
+ - **Monthly**: full CVE catalog validation against NVD
57
+ - **Annual**: full skill audit — all skills reviewed against current threat landscape
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ # Scan current environment and produce findings
63
+ node orchestrator/index.js scan
64
+
65
+ # Route findings to relevant skills
66
+ node orchestrator/index.js dispatch
67
+
68
+ # Run a specific skill programmatically
69
+ node orchestrator/index.js skill kernel-lpe-triage
70
+
71
+ # Run the full agent pipeline (threat-researcher → report)
72
+ node orchestrator/index.js pipeline
73
+
74
+ # Check skill currency scores
75
+ node orchestrator/index.js currency
76
+
77
+ # Generate an executive report from current findings
78
+ node orchestrator/index.js report --format executive
79
+
80
+ # Watch for events and trigger updates automatically
81
+ node orchestrator/index.js watch
82
+ ```
83
+
84
+ ## Output Formats
85
+
86
+ The orchestrator produces output in three formats:
87
+
88
+ | Format | Audience | File |
89
+ |--------|----------|------|
90
+ | `executive` | CISO / Board | `reports/templates/executive-summary.md` |
91
+ | `technical` | Security Engineers | `reports/templates/technical-assessment.md` |
92
+ | `compliance` | Auditors / GRC | `reports/templates/compliance-gap-report.md` |
93
+ | `zero-day` | Incident Response | `reports/templates/zero-day-response.md` |
94
+
95
+ ## Agent Handoff Protocol
96
+
97
+ When `pipeline.js` hands off between agents, it passes a structured JSON package:
98
+
99
+ ```json
100
+ {
101
+ "handoff_id": "uuid",
102
+ "from_agent": "threat-researcher",
103
+ "to_agent": "source-validator",
104
+ "timestamp": "2026-05-11T00:00:00Z",
105
+ "payload": {
106
+ "cve_id": "CVE-XXXX-XXXXX",
107
+ "findings": {},
108
+ "confidence": "high|medium|low",
109
+ "primary_sources": [],
110
+ "flags": []
111
+ }
112
+ }
113
+ ```
114
+
115
+ Source-validator either approves (passes to skill-updater), returns for revision, or rejects with reason.
116
+
117
+ ## Environment Variables
118
+
119
+ ```bash
120
+ EXCEPTD_SCAN_TARGETS=./ # Directories to scan for MCP configs
121
+ EXCEPTD_REPORT_FORMAT=technical # Default report format
122
+ EXCEPTD_KEV_CHECK_INTERVAL=3600 # KEV polling interval in seconds (default: 1h)
123
+ EXCEPTD_DATA_DIR=./data # Path to data directory
124
+ ```
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Skill dispatcher. Routes scanner findings to relevant skills via manifest trigger matching.
5
+ * Returns an ordered dispatch plan sorted by RWEP urgency.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const MANIFEST_PATH = process.env.EXCEPTD_MANIFEST || path.join(__dirname, '..', 'manifest.json');
12
+ const SKILLS_DIR = process.env.EXCEPTD_SKILLS_DIR || path.join(__dirname, '..', 'skills');
13
+
14
+ // --- public API ---
15
+
16
+ /**
17
+ * Route findings to skills and return a dispatch plan.
18
+ *
19
+ * @param {object[]} findings - From scanner.scan()
20
+ * @returns {{ plan: object[], unmatched: object[], summary: object }}
21
+ */
22
+ function dispatch(findings) {
23
+ const manifest = loadManifest();
24
+ const plan = [];
25
+ const unmatched = [];
26
+ const seen = new Set();
27
+
28
+ for (const finding of findings) {
29
+ const matched = matchFinding(finding, manifest.skills);
30
+
31
+ if (matched.length === 0) {
32
+ unmatched.push(finding);
33
+ continue;
34
+ }
35
+
36
+ for (const skill of matched) {
37
+ if (seen.has(skill.name)) continue;
38
+ seen.add(skill.name);
39
+
40
+ plan.push({
41
+ skill_name: skill.name,
42
+ skill_path: path.join(SKILLS_DIR, skill.name, 'skill.md'),
43
+ triggered_by: finding.signal,
44
+ finding_domain: finding.domain,
45
+ finding_severity: finding.severity,
46
+ action_required: finding.action_required,
47
+ priority: severityToPriority(finding.severity),
48
+ last_threat_review: skill.last_threat_review || 'unknown'
49
+ });
50
+ }
51
+ }
52
+
53
+ plan.sort((a, b) => a.priority - b.priority);
54
+
55
+ return {
56
+ plan,
57
+ unmatched,
58
+ summary: {
59
+ total_findings: findings.length,
60
+ matched_findings: findings.length - unmatched.length,
61
+ skills_to_invoke: plan.length,
62
+ critical_priority: plan.filter(p => p.priority === 1).length,
63
+ high_priority: plan.filter(p => p.priority === 2).length
64
+ }
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Route a single natural language query to matching skills.
70
+ *
71
+ * @param {string} query - Free text query
72
+ * @returns {object[]} Matched skills from manifest
73
+ */
74
+ function routeQuery(query) {
75
+ const manifest = loadManifest();
76
+ const q = query.toLowerCase();
77
+
78
+ return manifest.skills.filter(skill => {
79
+ const triggers = skill.triggers || [];
80
+ return triggers.some(t => q.includes(t.toLowerCase()) || t.toLowerCase().includes(q));
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Get the full dispatch context for a specific skill (data deps, frontmatter).
86
+ *
87
+ * @param {string} skillName
88
+ * @returns {{ skill: object, data_paths: object, skill_content: string|null }}
89
+ */
90
+ function getSkillContext(skillName) {
91
+ const manifest = loadManifest();
92
+ const skill = manifest.skills.find(s => s.name === skillName);
93
+ if (!skill) return null;
94
+
95
+ const DATA_DIR = path.join(__dirname, '..', 'data');
96
+ const dataPaths = {};
97
+ for (const dep of skill.data_deps || []) {
98
+ const fullPath = path.join(DATA_DIR, dep);
99
+ dataPaths[dep] = { path: fullPath, exists: fs.existsSync(fullPath) };
100
+ }
101
+
102
+ const skillPath = path.join(SKILLS_DIR, skillName, 'skill.md');
103
+ let skillContent = null;
104
+ try {
105
+ skillContent = fs.readFileSync(skillPath, 'utf8');
106
+ } catch (_) {}
107
+
108
+ return { skill, data_paths: dataPaths, skill_content: skillContent };
109
+ }
110
+
111
+ // --- private helpers ---
112
+
113
+ function matchFinding(finding, skills) {
114
+ if (finding.skill_hint) {
115
+ const direct = skills.find(s => s.name === finding.skill_hint);
116
+ if (direct) return [direct];
117
+ }
118
+
119
+ const domainToSkills = {
120
+ kernel: ['kernel-lpe-triage', 'exploit-scoring', 'compliance-theater'],
121
+ mcp: ['mcp-agent-trust', 'ai-attack-surface', 'security-maturity-tiers'],
122
+ crypto: ['pqc-first', 'framework-gap-analysis'],
123
+ ai_api: ['ai-c2-detection', 'ai-attack-surface', 'threat-model-currency'],
124
+ framework: ['framework-gap-analysis', 'compliance-theater', 'global-grc']
125
+ };
126
+
127
+ const candidateNames = domainToSkills[finding.domain] || [];
128
+ return skills.filter(s => candidateNames.includes(s.name));
129
+ }
130
+
131
+ function severityToPriority(severity) {
132
+ const map = { critical: 1, high: 2, medium: 3, low: 4, info: 5 };
133
+ return map[severity] || 5;
134
+ }
135
+
136
+ function loadManifest() {
137
+ return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
138
+ }
139
+
140
+ module.exports = { dispatch, routeQuery, getSkillContext };
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Event bus for trigger-driven skill updates.
5
+ *
6
+ * Events are typed and carry structured payloads. Handlers are registered per event type.
7
+ * This is an in-process event bus — no external message queue required.
8
+ * For production deployments, swap the internal emitter for a durable queue (Redis, SQS, etc.)
9
+ * without changing the event schema.
10
+ */
11
+
12
+ const { EventEmitter } = require('events');
13
+
14
+ const EVENT_TYPES = {
15
+ CISA_KEV_ADDED: 'cisa.kev.added',
16
+ ATLAS_VERSION_RELEASED: 'atlas.version.released',
17
+ KERNEL_CVE_HIGH_RWEP: 'cve.kernel.high_rwep',
18
+ AI_PLATFORM_CVE: 'cve.ai_platform',
19
+ FRAMEWORK_AMENDMENT: 'framework.amendment',
20
+ PQC_STANDARD_UPDATE: 'pqc.standard.update',
21
+ EXPLOIT_STATUS_CHANGE: 'exploit.status.change',
22
+ NEW_ATTACK_CLASS: 'attack_class.new',
23
+ SKILL_CURRENCY_LOW: 'skill.currency.low'
24
+ };
25
+
26
+ // Maps event types to the skills they should trigger for review
27
+ const EVENT_SKILL_MAP = {
28
+ [EVENT_TYPES.CISA_KEV_ADDED]: ['kernel-lpe-triage', 'exploit-scoring', 'compliance-theater', 'skill-update-loop'],
29
+ [EVENT_TYPES.ATLAS_VERSION_RELEASED]: ['ai-attack-surface', 'mcp-agent-trust', 'rag-pipeline-security', 'ai-c2-detection', 'skill-update-loop'],
30
+ [EVENT_TYPES.KERNEL_CVE_HIGH_RWEP]: ['kernel-lpe-triage', 'exploit-scoring', 'zeroday-gap-learn', 'framework-gap-analysis'],
31
+ [EVENT_TYPES.AI_PLATFORM_CVE]: ['mcp-agent-trust', 'ai-attack-surface', 'zeroday-gap-learn'],
32
+ [EVENT_TYPES.FRAMEWORK_AMENDMENT]: ['framework-gap-analysis', 'compliance-theater', 'global-grc', 'policy-exception-gen'],
33
+ [EVENT_TYPES.PQC_STANDARD_UPDATE]: ['pqc-first', 'framework-gap-analysis'],
34
+ [EVENT_TYPES.EXPLOIT_STATUS_CHANGE]: ['exploit-scoring', 'kernel-lpe-triage', 'compliance-theater'],
35
+ [EVENT_TYPES.NEW_ATTACK_CLASS]: ['threat-model-currency', 'ai-attack-surface', 'skill-update-loop'],
36
+ [EVENT_TYPES.SKILL_CURRENCY_LOW]: ['skill-update-loop']
37
+ };
38
+
39
+ class ExceptdEventBus extends EventEmitter {
40
+ constructor() {
41
+ super();
42
+ this.eventLog = [];
43
+ }
44
+
45
+ /**
46
+ * Emit a typed event with structured payload.
47
+ *
48
+ * @param {string} eventType - One of EVENT_TYPES
49
+ * @param {object} payload - Event-specific data
50
+ */
51
+ emit(eventType, payload) {
52
+ const event = {
53
+ event_id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
54
+ type: eventType,
55
+ timestamp: new Date().toISOString(),
56
+ payload,
57
+ affected_skills: EVENT_SKILL_MAP[eventType] || []
58
+ };
59
+
60
+ this.eventLog.push(event);
61
+ super.emit(eventType, event);
62
+ super.emit('*', event);
63
+ return event;
64
+ }
65
+
66
+ /**
67
+ * Register a handler for an event type.
68
+ *
69
+ * @param {string} eventType
70
+ * @param {function} handler - (event) => void
71
+ */
72
+ on(eventType, handler) {
73
+ super.on(eventType, handler);
74
+ return this;
75
+ }
76
+
77
+ /**
78
+ * Register a handler for all events.
79
+ *
80
+ * @param {function} handler - (event) => void
81
+ */
82
+ onAny(handler) {
83
+ return this.on('*', handler);
84
+ }
85
+
86
+ /**
87
+ * Get all events that affected a specific skill.
88
+ *
89
+ * @param {string} skillName
90
+ * @returns {object[]}
91
+ */
92
+ getSkillEvents(skillName) {
93
+ return this.eventLog.filter(e => e.affected_skills.includes(skillName));
94
+ }
95
+
96
+ /**
97
+ * Get the event log, optionally filtered by type.
98
+ *
99
+ * @param {string} [eventType]
100
+ * @returns {object[]}
101
+ */
102
+ getLog(eventType) {
103
+ if (eventType) return this.eventLog.filter(e => e.type === eventType);
104
+ return [...this.eventLog];
105
+ }
106
+
107
+ /**
108
+ * Fire a CISA KEV addition event.
109
+ *
110
+ * @param {{ cve_id: string, kev_date: string, rwep_score: number }} params
111
+ */
112
+ kevAdded({ cve_id, kev_date, rwep_score }) {
113
+ return this.emit(EVENT_TYPES.CISA_KEV_ADDED, { cve_id, kev_date, rwep_score });
114
+ }
115
+
116
+ /**
117
+ * Fire an ATLAS version release event.
118
+ *
119
+ * @param {{ old_version: string, new_version: string, release_date: string }} params
120
+ */
121
+ atlasReleased({ old_version, new_version, release_date }) {
122
+ return this.emit(EVENT_TYPES.ATLAS_VERSION_RELEASED, { old_version, new_version, release_date });
123
+ }
124
+
125
+ /**
126
+ * Fire an exploit status change event.
127
+ *
128
+ * @param {{ cve_id: string, old_status: string, new_status: string }} params
129
+ */
130
+ exploitStatusChanged({ cve_id, old_status, new_status }) {
131
+ return this.emit(EVENT_TYPES.EXPLOIT_STATUS_CHANGE, { cve_id, old_status, new_status });
132
+ }
133
+
134
+ /**
135
+ * Fire a skill currency low event (emitted by scheduler).
136
+ *
137
+ * @param {{ skill_name: string, currency_score: number, days_since_review: number }} params
138
+ */
139
+ skillCurrencyLow({ skill_name, currency_score, days_since_review }) {
140
+ return this.emit(EVENT_TYPES.SKILL_CURRENCY_LOW, { skill_name, currency_score, days_since_review });
141
+ }
142
+ }
143
+
144
+ const bus = new ExceptdEventBus();
145
+
146
+ module.exports = { bus, EVENT_TYPES, EVENT_SKILL_MAP };