cipher-security 5.0.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 (75) hide show
  1. package/bin/cipher.js +465 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +130 -0
  45. package/lib/commands.js +99 -0
  46. package/lib/complexity.js +377 -0
  47. package/lib/config.js +213 -0
  48. package/lib/gateway/client.js +309 -0
  49. package/lib/gateway/commands.js +830 -0
  50. package/lib/gateway/config-validate.js +109 -0
  51. package/lib/gateway/gateway.js +367 -0
  52. package/lib/gateway/index.js +62 -0
  53. package/lib/gateway/mode.js +309 -0
  54. package/lib/gateway/plugins.js +222 -0
  55. package/lib/gateway/prompt.js +214 -0
  56. package/lib/mcp/server.js +262 -0
  57. package/lib/memory/compressor.js +425 -0
  58. package/lib/memory/engine.js +763 -0
  59. package/lib/memory/evolution.js +668 -0
  60. package/lib/memory/index.js +58 -0
  61. package/lib/memory/orchestrator.js +506 -0
  62. package/lib/memory/retriever.js +515 -0
  63. package/lib/memory/synthesizer.js +333 -0
  64. package/lib/pipeline/async-scanner.js +510 -0
  65. package/lib/pipeline/binary-analysis.js +1043 -0
  66. package/lib/pipeline/dom-xss-scanner.js +435 -0
  67. package/lib/pipeline/github-actions.js +792 -0
  68. package/lib/pipeline/index.js +124 -0
  69. package/lib/pipeline/osint.js +498 -0
  70. package/lib/pipeline/sarif.js +373 -0
  71. package/lib/pipeline/scanner.js +880 -0
  72. package/lib/pipeline/template-manager.js +525 -0
  73. package/lib/pipeline/xss-scanner.js +353 -0
  74. package/lib/setup-wizard.js +229 -0
  75. package/package.json +30 -0
@@ -0,0 +1,554 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * Leaderboard → Researcher Feedback Loop.
7
+ *
8
+ * Connects skill performance metrics to autonomous improvement:
9
+ * - Identifies bottom-performing skills from leaderboard
10
+ * - Triggers skill regeneration via quality analysis
11
+ * - Tracks improvement cycles and validates quality gains
12
+ *
13
+ * @module autonomous/feedback-loop
14
+ */
15
+
16
+ import { randomBytes } from 'node:crypto';
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, chmodSync } from 'node:fs';
18
+ import { join, resolve } from 'node:path';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // ImprovementStatus
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export const ImprovementStatus = Object.freeze({
25
+ IDENTIFIED: 'identified',
26
+ ANALYZING: 'analyzing',
27
+ REGENERATING: 'regenerating',
28
+ VALIDATING: 'validating',
29
+ COMPLETED: 'completed',
30
+ FAILED: 'failed',
31
+ SKIPPED: 'skipped',
32
+ });
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // ImprovementCandidate
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export class ImprovementCandidate {
39
+ constructor({
40
+ skillPath = '',
41
+ domain = '',
42
+ currentScore = 0,
43
+ invocations = 0,
44
+ trend = '',
45
+ issues = [],
46
+ status = ImprovementStatus.IDENTIFIED,
47
+ improvementScore = 0,
48
+ cycleId = '',
49
+ startedAt = 0,
50
+ completedAt = 0,
51
+ } = {}) {
52
+ this.skillPath = skillPath;
53
+ this.domain = domain;
54
+ this.currentScore = currentScore;
55
+ this.invocations = invocations;
56
+ this.trend = trend;
57
+ this.issues = [...issues];
58
+ this.status = status;
59
+ this.improvementScore = improvementScore;
60
+ this.cycleId = cycleId;
61
+ this.startedAt = startedAt;
62
+ this.completedAt = completedAt;
63
+ }
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // ImprovementCycle
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export class ImprovementCycle {
71
+ constructor({
72
+ cycleId = '',
73
+ startedAt = 0,
74
+ candidates = [],
75
+ completedAt = 0,
76
+ skillsImproved = 0,
77
+ skillsFailed = 0,
78
+ skillsSkipped = 0,
79
+ avgScoreBefore = 0,
80
+ avgScoreAfter = 0,
81
+ } = {}) {
82
+ this.cycleId = cycleId;
83
+ this.startedAt = startedAt;
84
+ this.candidates = candidates;
85
+ this.completedAt = completedAt;
86
+ this.skillsImproved = skillsImproved;
87
+ this.skillsFailed = skillsFailed;
88
+ this.skillsSkipped = skillsSkipped;
89
+ this.avgScoreBefore = avgScoreBefore;
90
+ this.avgScoreAfter = avgScoreAfter;
91
+ }
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // SkillQualityAnalyzer
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export class SkillQualityAnalyzer {
99
+ static MIN_SKILL_MD_LINES = 20;
100
+ static MIN_AGENT_PY_LINES = 15;
101
+ static MIN_CODE_BLOCKS = 1;
102
+ static REQUIRED_SECTIONS = new Set(['overview', 'techniques', 'best practices', 'references']);
103
+
104
+ /**
105
+ * @param {string} [skillsDir='skills']
106
+ */
107
+ constructor(skillsDir = 'skills') {
108
+ this.skillsDir = resolve(String(skillsDir));
109
+ }
110
+
111
+ /**
112
+ * Analyze a skill's quality and return findings.
113
+ * @param {string} skillPath
114
+ * @returns {object}
115
+ */
116
+ analyzeSkill(skillPath) {
117
+ let fullPath;
118
+ if (isAbsolute(skillPath)) {
119
+ fullPath = skillPath;
120
+ } else {
121
+ // Try relative to parent of skillsDir
122
+ fullPath = resolve(join(this.skillsDir, '..'), skillPath);
123
+ if (!existsSync(fullPath)) {
124
+ const cleaned = skillPath.startsWith('skills/') ? skillPath.slice(7) : skillPath;
125
+ fullPath = join(this.skillsDir, cleaned);
126
+ }
127
+ }
128
+
129
+ const issues = [];
130
+ const scores = {};
131
+
132
+ // Check SKILL.md
133
+ const skillMd = join(fullPath, 'SKILL.md');
134
+ if (existsSync(skillMd)) {
135
+ const content = readFileSync(skillMd, 'utf-8');
136
+ const lines = content.trim().split('\n');
137
+
138
+ if (lines.length < SkillQualityAnalyzer.MIN_SKILL_MD_LINES) {
139
+ issues.push(`SKILL.md too short (${lines.length} lines, min ${SkillQualityAnalyzer.MIN_SKILL_MD_LINES})`);
140
+ }
141
+ scores.content_length = Math.min(lines.length / 50, 1.0);
142
+
143
+ const lower = content.toLowerCase();
144
+ const foundSections = new Set();
145
+ for (const s of SkillQualityAnalyzer.REQUIRED_SECTIONS) {
146
+ if (lower.includes(`## ${s}`) || lower.includes(`# ${s}`)) {
147
+ foundSections.add(s);
148
+ }
149
+ }
150
+ const missing = [...SkillQualityAnalyzer.REQUIRED_SECTIONS].filter(s => !foundSections.has(s));
151
+ if (missing.length > 0) {
152
+ issues.push(`Missing sections: ${missing.sort().join(', ')}`);
153
+ }
154
+ scores.section_coverage = foundSections.size / SkillQualityAnalyzer.REQUIRED_SECTIONS.size;
155
+
156
+ const codeBlocks = (content.match(/```/g) || []).length;
157
+ if (codeBlocks < SkillQualityAnalyzer.MIN_CODE_BLOCKS * 2) {
158
+ issues.push('No code blocks found');
159
+ }
160
+ scores.code_examples = Math.min(codeBlocks / 6, 1.0);
161
+
162
+ if (!content.slice(0, 200).includes('Copyright')) {
163
+ issues.push('Missing copyright header');
164
+ }
165
+ scores.frontmatter = content.slice(0, 10).includes('---') ? 1.0 : 0.0;
166
+ if (!content.slice(0, 10).includes('---')) {
167
+ issues.push('Missing YAML frontmatter');
168
+ }
169
+ } else {
170
+ issues.push('SKILL.md missing');
171
+ scores.content_length = 0;
172
+ scores.section_coverage = 0;
173
+ }
174
+
175
+ // Check agent.py
176
+ const agentPy = join(fullPath, 'scripts', 'agent.py');
177
+ if (existsSync(agentPy)) {
178
+ const agentContent = readFileSync(agentPy, 'utf-8');
179
+ const agentLines = agentContent.trim().split('\n');
180
+ if (agentLines.length < SkillQualityAnalyzer.MIN_AGENT_PY_LINES) {
181
+ issues.push(`agent.py too short (${agentLines.length} lines)`);
182
+ }
183
+ scores.agent_quality = Math.min(agentLines.length / 50, 1.0);
184
+ if (!agentContent.includes('argparse')) issues.push('agent.py missing argparse');
185
+ if (!agentContent.includes('json')) issues.push('agent.py missing JSON output');
186
+ if (!agentContent.includes('__main__')) issues.push('agent.py missing __main__ guard');
187
+ } else {
188
+ issues.push('scripts/agent.py missing');
189
+ scores.agent_quality = 0;
190
+ }
191
+
192
+ // Check references
193
+ const refDir = join(fullPath, 'references');
194
+ if (existsSync(refDir)) {
195
+ try {
196
+ const refs = readdirSync(refDir).filter(f => f.endsWith('.md'));
197
+ scores.references = refs.length > 0 ? 1.0 : 0.0;
198
+ if (refs.length === 0) issues.push('No reference documentation');
199
+ } catch {
200
+ scores.references = 0;
201
+ issues.push('No reference documentation');
202
+ }
203
+ } else {
204
+ issues.push('No reference documentation');
205
+ scores.references = 0;
206
+ }
207
+
208
+ const vals = Object.values(scores);
209
+ const overall = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
210
+
211
+ return {
212
+ path: fullPath,
213
+ issues,
214
+ scores,
215
+ overallQuality: Math.round(overall * 1000) / 1000,
216
+ needsImprovement: issues.length > 2 || overall < 0.6,
217
+ };
218
+ }
219
+ }
220
+
221
+ /** Helper to check if a path is absolute. */
222
+ function isAbsolute(p) {
223
+ return p.startsWith('/');
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // FeedbackLoop
228
+ // ---------------------------------------------------------------------------
229
+
230
+ export class FeedbackLoop {
231
+ static DEFAULT_BATCH_SIZE = 10;
232
+ static MIN_INVOCATIONS_FOR_FEEDBACK = 1;
233
+ static SCORE_THRESHOLD = 0.5;
234
+
235
+ /**
236
+ * @param {object} [opts]
237
+ * @param {string} [opts.skillsDir='skills']
238
+ * @param {number} [opts.maxImprovementsPerCycle=10]
239
+ */
240
+ constructor({ skillsDir = 'skills', maxImprovementsPerCycle = 10 } = {}) {
241
+ this.skillsDir = resolve(String(skillsDir));
242
+ this.maxPerCycle = maxImprovementsPerCycle;
243
+ this.analyzer = new SkillQualityAnalyzer(this.skillsDir);
244
+ /** @type {ImprovementCycle[]} */
245
+ this._history = [];
246
+ }
247
+
248
+ /**
249
+ * Identify skills that need improvement.
250
+ * @param {object} [opts]
251
+ * @param {object[]|null} [opts.bottomSkills]
252
+ * @param {number} [opts.scoreThreshold]
253
+ * @returns {ImprovementCandidate[]}
254
+ */
255
+ identifyCandidates({ bottomSkills = null, scoreThreshold } = {}) {
256
+ const threshold = scoreThreshold ?? FeedbackLoop.SCORE_THRESHOLD;
257
+ const candidates = [];
258
+
259
+ if (!bottomSkills) {
260
+ return this._identifyByQuality();
261
+ }
262
+
263
+ for (const skill of bottomSkills) {
264
+ const path = skill.skillPath || skill.path || '';
265
+ const score = skill.score ?? 0;
266
+ const invocations = skill.invocations ?? 0;
267
+ const trend = skill.trend ?? 'stable';
268
+
269
+ if (score >= threshold && trend !== 'declining') continue;
270
+
271
+ const analysis = this.analyzer.analyzeSkill(path);
272
+ candidates.push(new ImprovementCandidate({
273
+ skillPath: path,
274
+ domain: FeedbackLoop._extractDomain(path),
275
+ currentScore: score,
276
+ invocations,
277
+ trend,
278
+ issues: analysis.issues || [],
279
+ }));
280
+ }
281
+
282
+ // Sort: declining first, then lowest score, then most used
283
+ candidates.sort((a, b) => {
284
+ const aDecl = a.trend === 'declining' ? 1 : 0;
285
+ const bDecl = b.trend === 'declining' ? 1 : 0;
286
+ if (bDecl !== aDecl) return bDecl - aDecl;
287
+ if (a.currentScore !== b.currentScore) return a.currentScore - b.currentScore;
288
+ return b.invocations - a.invocations;
289
+ });
290
+
291
+ return candidates.slice(0, this.maxPerCycle);
292
+ }
293
+
294
+ /** @private */
295
+ _identifyByQuality() {
296
+ const candidates = [];
297
+ if (!existsSync(this.skillsDir)) return candidates;
298
+
299
+ try {
300
+ for (const domainName of readdirSync(this.skillsDir).sort()) {
301
+ const domainDir = join(this.skillsDir, domainName);
302
+ if (domainName.startsWith('.') || !statSync(domainDir).isDirectory()) continue;
303
+ const techniquesDir = join(domainDir, 'techniques');
304
+ if (!existsSync(techniquesDir)) continue;
305
+
306
+ for (const techName of readdirSync(techniquesDir).sort()) {
307
+ const techDir = join(techniquesDir, techName);
308
+ if (!statSync(techDir).isDirectory()) continue;
309
+ const relPath = `skills/${domainName}/techniques/${techName}`;
310
+ const analysis = this.analyzer.analyzeSkill(relPath);
311
+ if (analysis.needsImprovement) {
312
+ candidates.push(new ImprovementCandidate({
313
+ skillPath: relPath,
314
+ domain: domainName,
315
+ currentScore: analysis.overallQuality,
316
+ invocations: 0,
317
+ trend: 'unknown',
318
+ issues: analysis.issues,
319
+ }));
320
+ }
321
+ }
322
+ }
323
+ } catch { /* ok */ }
324
+
325
+ candidates.sort((a, b) => a.currentScore - b.currentScore);
326
+ return candidates.slice(0, this.maxPerCycle);
327
+ }
328
+
329
+ /**
330
+ * Run a full improvement cycle.
331
+ * @param {object} [opts]
332
+ * @param {ImprovementCandidate[]|null} [opts.candidates]
333
+ * @param {boolean} [opts.dryRun=false]
334
+ * @returns {ImprovementCycle}
335
+ */
336
+ runImprovementCycle({ candidates = null, dryRun = false } = {}) {
337
+ const cycleId = `cycle-${randomBytes(4).toString('hex')}`;
338
+ const cycle = new ImprovementCycle({
339
+ cycleId,
340
+ startedAt: Date.now() / 1000,
341
+ });
342
+
343
+ if (!candidates) {
344
+ candidates = this.identifyCandidates();
345
+ }
346
+
347
+ cycle.candidates = candidates;
348
+ const scoresBefore = [];
349
+
350
+ for (const candidate of candidates) {
351
+ candidate.cycleId = cycleId;
352
+ candidate.startedAt = Date.now() / 1000;
353
+ scoresBefore.push(candidate.currentScore);
354
+
355
+ if (dryRun) {
356
+ candidate.status = ImprovementStatus.SKIPPED;
357
+ cycle.skillsSkipped += 1;
358
+ continue;
359
+ }
360
+
361
+ try {
362
+ candidate.status = ImprovementStatus.ANALYZING;
363
+ const analysis = this.analyzer.analyzeSkill(candidate.skillPath);
364
+
365
+ if (!analysis.needsImprovement) {
366
+ candidate.status = ImprovementStatus.SKIPPED;
367
+ cycle.skillsSkipped += 1;
368
+ continue;
369
+ }
370
+
371
+ candidate.status = ImprovementStatus.REGENERATING;
372
+ const improved = this._improveSkill(candidate, analysis);
373
+
374
+ if (improved) {
375
+ candidate.status = ImprovementStatus.VALIDATING;
376
+ const validation = this.analyzer.analyzeSkill(candidate.skillPath);
377
+ candidate.improvementScore = validation.overallQuality;
378
+ candidate.status = ImprovementStatus.COMPLETED;
379
+ cycle.skillsImproved += 1;
380
+ } else {
381
+ candidate.status = ImprovementStatus.FAILED;
382
+ cycle.skillsFailed += 1;
383
+ }
384
+ } catch {
385
+ candidate.status = ImprovementStatus.FAILED;
386
+ cycle.skillsFailed += 1;
387
+ }
388
+
389
+ candidate.completedAt = Date.now() / 1000;
390
+ }
391
+
392
+ cycle.completedAt = Date.now() / 1000;
393
+ cycle.avgScoreBefore = scoresBefore.length > 0
394
+ ? scoresBefore.reduce((a, b) => a + b, 0) / scoresBefore.length : 0;
395
+ const scoresAfter = candidates
396
+ .filter(c => c.improvementScore > 0)
397
+ .map(c => c.improvementScore);
398
+ cycle.avgScoreAfter = scoresAfter.length > 0
399
+ ? scoresAfter.reduce((a, b) => a + b, 0) / scoresAfter.length : 0;
400
+
401
+ this._history.push(cycle);
402
+ return cycle;
403
+ }
404
+
405
+ /**
406
+ * Improve a single skill based on analysis findings.
407
+ * @private
408
+ * @param {ImprovementCandidate} candidate
409
+ * @param {object} analysis
410
+ * @returns {boolean}
411
+ */
412
+ _improveSkill(candidate, analysis) {
413
+ let skillDir = candidate.skillPath;
414
+ if (!isAbsolute(skillDir)) {
415
+ skillDir = resolve(join(this.skillsDir, '..'), candidate.skillPath);
416
+ }
417
+ if (!existsSync(skillDir)) return false;
418
+
419
+ let improved = false;
420
+
421
+ // Fix missing sections in SKILL.md
422
+ const skillMd = join(skillDir, 'SKILL.md');
423
+ if (existsSync(skillMd)) {
424
+ let content = readFileSync(skillMd, 'utf-8');
425
+ const lower = content.toLowerCase();
426
+ const additions = [];
427
+
428
+ if (!lower.includes('## overview') && !lower.includes('# overview')) {
429
+ additions.push('\n## Overview\nThis technique provides security analysis capabilities.\n');
430
+ }
431
+ if (!lower.includes('## techniques') && !lower.includes('# techniques')) {
432
+ additions.push('\n## Techniques\n- Analysis and detection methods\n- Tool configuration and usage\n- Threat indicator identification\n');
433
+ }
434
+ if (!lower.includes('## best practices') && !lower.includes('# best practices')) {
435
+ additions.push('\n## Best Practices\n- Follow defense-in-depth principles\n- Validate against known-good baselines\n- Document all findings with evidence\n');
436
+ }
437
+ if (!lower.includes('## references') && !lower.includes('# references')) {
438
+ additions.push('\n## References\n- MITRE ATT&CK Framework\n- CIS Controls v8\n- NIST SP 800-53 Rev. 5\n');
439
+ }
440
+ if (!content.includes('```')) {
441
+ additions.push('\n## Example\n\n```yaml\n# Configuration example\nenabled: true\nlevel: high\n```\n');
442
+ }
443
+
444
+ if (additions.length > 0) {
445
+ content += additions.join('');
446
+ writeFileSync(skillMd, content, 'utf-8');
447
+ improved = true;
448
+ }
449
+ }
450
+
451
+ return improved;
452
+ }
453
+
454
+ /**
455
+ * Generate a report for an improvement cycle.
456
+ * @param {ImprovementCycle} cycle
457
+ * @returns {object}
458
+ */
459
+ getCycleReport(cycle) {
460
+ return {
461
+ cycleId: cycle.cycleId,
462
+ durationS: cycle.completedAt
463
+ ? Math.round((cycle.completedAt - cycle.startedAt) * 100) / 100
464
+ : 0,
465
+ totalCandidates: cycle.candidates.length,
466
+ improved: cycle.skillsImproved,
467
+ failed: cycle.skillsFailed,
468
+ skipped: cycle.skillsSkipped,
469
+ avgScoreBefore: Math.round(cycle.avgScoreBefore * 1000) / 1000,
470
+ avgScoreAfter: Math.round(cycle.avgScoreAfter * 1000) / 1000,
471
+ candidates: cycle.candidates.map(c => ({
472
+ path: c.skillPath,
473
+ domain: c.domain,
474
+ status: c.status,
475
+ scoreBefore: c.currentScore,
476
+ scoreAfter: c.improvementScore,
477
+ issues: c.issues,
478
+ })),
479
+ };
480
+ }
481
+
482
+ /** Return all cycle reports. */
483
+ getHistory() {
484
+ return this._history.map(c => this.getCycleReport(c));
485
+ }
486
+
487
+ /**
488
+ * Extract domain name from skill path.
489
+ * @param {string} skillPath
490
+ * @returns {string}
491
+ */
492
+ static _extractDomain(skillPath) {
493
+ const parts = skillPath.split('/').filter(Boolean);
494
+ for (let i = 0; i < parts.length; i++) {
495
+ if (parts[i] === 'skills' && i + 1 < parts.length) {
496
+ return parts[i + 1];
497
+ }
498
+ }
499
+ if (parts.length >= 2) {
500
+ return parts[0] === 'skills' ? parts[1] : parts[0];
501
+ }
502
+ return 'unknown';
503
+ }
504
+ }
505
+
506
+ // ---------------------------------------------------------------------------
507
+ // Convenience function
508
+ // ---------------------------------------------------------------------------
509
+
510
+ /**
511
+ * Run a complete feedback cycle: leaderboard → analysis → improvement.
512
+ * @param {object} [opts]
513
+ * @param {string} [opts.skillsDir='skills']
514
+ * @param {number} [opts.maxImprovements=10]
515
+ * @param {number} [opts.scoreThreshold=0.5]
516
+ * @param {boolean} [opts.dryRun=false]
517
+ * @returns {object}
518
+ */
519
+ export function connectLeaderboardToResearcher({
520
+ skillsDir = 'skills',
521
+ maxImprovements = 10,
522
+ scoreThreshold = 0.5,
523
+ dryRun = false,
524
+ } = {}) {
525
+ const loop = new FeedbackLoop({
526
+ skillsDir,
527
+ maxImprovementsPerCycle: maxImprovements,
528
+ });
529
+
530
+ // Try to get leaderboard data
531
+ let bottomSkills = null;
532
+ try {
533
+ const { SkillLeaderboard } = require('./leaderboard.js');
534
+ const lb = new SkillLeaderboard();
535
+ const bottomEntries = lb.getBottomSkills(maxImprovements * 2);
536
+ bottomSkills = bottomEntries.map(e => ({
537
+ skillPath: e.skillPath,
538
+ score: e.score,
539
+ invocations: e.invocations,
540
+ trend: e.trend,
541
+ }));
542
+ lb.close();
543
+ } catch {
544
+ // Leaderboard not available — quality-only analysis
545
+ }
546
+
547
+ const candidates = loop.identifyCandidates({
548
+ bottomSkills,
549
+ scoreThreshold,
550
+ });
551
+
552
+ const cycle = loop.runImprovementCycle({ candidates, dryRun });
553
+ return loop.getCycleReport(cycle);
554
+ }