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,583 @@
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
+ * Autonomous security research engine for self-improving skill generation.
7
+ *
8
+ * @module autonomous/researcher
9
+ */
10
+
11
+ import { randomUUID } from 'node:crypto';
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, chmodSync } from 'node:fs';
13
+ import { join, resolve, isAbsolute } from 'node:path';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Data classes
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export class ResearchHypothesis {
20
+ constructor({
21
+ hypothesisId = randomUUID(),
22
+ domain = '',
23
+ techniqueName = '',
24
+ description = '',
25
+ skillContent = '',
26
+ agentScript = '',
27
+ referenceContent = '',
28
+ confidence = 0,
29
+ status = 'proposed',
30
+ testResults = {},
31
+ createdAt = new Date().toISOString(),
32
+ } = {}) {
33
+ this.hypothesisId = hypothesisId;
34
+ this.domain = domain;
35
+ this.techniqueName = techniqueName;
36
+ this.description = description;
37
+ this.skillContent = skillContent;
38
+ this.agentScript = agentScript;
39
+ this.referenceContent = referenceContent;
40
+ this.confidence = confidence;
41
+ this.status = status;
42
+ this.testResults = testResults;
43
+ this.createdAt = createdAt;
44
+ }
45
+ }
46
+
47
+ export class ResearchExperiment {
48
+ constructor({
49
+ experimentId = randomUUID(),
50
+ hypothesis = new ResearchHypothesis(),
51
+ validationChecks = [],
52
+ checkResults = {},
53
+ overallPass = false,
54
+ createdAt = new Date().toISOString(),
55
+ } = {}) {
56
+ this.experimentId = experimentId;
57
+ this.hypothesis = hypothesis;
58
+ this.validationChecks = validationChecks;
59
+ this.checkResults = checkResults;
60
+ this.overallPass = overallPass;
61
+ this.createdAt = createdAt;
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // MITRE ATT&CK tactics mapped to technique IDs
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const _MITRE_TACTICS = {
70
+ 'reconnaissance': ['T1595', 'T1592', 'T1589', 'T1590', 'T1591', 'T1596', 'T1597', 'T1598'],
71
+ 'resource-development': ['T1583', 'T1584', 'T1585', 'T1586', 'T1587', 'T1588'],
72
+ 'initial-access': ['T1189', 'T1190', 'T1133', 'T1200', 'T1566', 'T1195', 'T1199', 'T1078'],
73
+ 'execution': ['T1059', 'T1203', 'T1047', 'T1053', 'T1129', 'T1106', 'T1204'],
74
+ 'persistence': ['T1098', 'T1197', 'T1547', 'T1136', 'T1543', 'T1546', 'T1053', 'T1078'],
75
+ 'privilege-escalation': ['T1548', 'T1134', 'T1547', 'T1484', 'T1055', 'T1053', 'T1078'],
76
+ 'defense-evasion': ['T1140', 'T1027', 'T1036', 'T1070', 'T1562', 'T1202', 'T1218', 'T1222'],
77
+ 'credential-access': ['T1110', 'T1003', 'T1555', 'T1552', 'T1187', 'T1557', 'T1558'],
78
+ 'discovery': ['T1087', 'T1482', 'T1083', 'T1135', 'T1046', 'T1057', 'T1018', 'T1082'],
79
+ 'lateral-movement': ['T1021', 'T1210', 'T1534', 'T1570', 'T1563', 'T1080'],
80
+ 'collection': ['T1560', 'T1123', 'T1119', 'T1005', 'T1039', 'T1025', 'T1074', 'T1114'],
81
+ 'command-and-control': ['T1071', 'T1132', 'T1001', 'T1568', 'T1573', 'T1095', 'T1572', 'T1090'],
82
+ 'exfiltration': ['T1041', 'T1048', 'T1567', 'T1029', 'T1030', 'T1537'],
83
+ 'impact': ['T1485', 'T1486', 'T1565', 'T1491', 'T1561', 'T1499', 'T1529'],
84
+ };
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // SkillGapAnalyzer
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export class SkillGapAnalyzer {
91
+ /**
92
+ * @param {string} skillsDir
93
+ */
94
+ constructor(skillsDir) {
95
+ this.skillsDir = resolve(String(skillsDir));
96
+ }
97
+
98
+ /** @returns {string[]} */
99
+ _listDomains() {
100
+ if (!existsSync(this.skillsDir)) return [];
101
+ try {
102
+ return readdirSync(this.skillsDir)
103
+ .filter(d => !d.startsWith('.') && statSync(join(this.skillsDir, d)).isDirectory())
104
+ .sort();
105
+ } catch { return []; }
106
+ }
107
+
108
+ /**
109
+ * @param {string} domain
110
+ * @returns {string[]}
111
+ */
112
+ _listTechniques(domain) {
113
+ const techDir = join(this.skillsDir, domain, 'techniques');
114
+ if (!existsSync(techDir)) return [];
115
+ try {
116
+ return readdirSync(techDir)
117
+ .filter(d => !d.startsWith('.') && statSync(join(techDir, d)).isDirectory())
118
+ .sort();
119
+ } catch { return []; }
120
+ }
121
+
122
+ /**
123
+ * Parse YAML frontmatter from a SKILL.md file.
124
+ * @param {string} skillMdPath
125
+ * @returns {object}
126
+ */
127
+ _parseSkillFrontmatter(skillMdPath) {
128
+ if (!existsSync(skillMdPath)) return {};
129
+ try {
130
+ const text = readFileSync(skillMdPath, 'utf-8');
131
+ const parts = text.split('---');
132
+ for (let i = 1; i < parts.length; i++) {
133
+ const candidate = parts[i].trim();
134
+ if (!candidate) continue;
135
+ // Lazy import yaml
136
+ try {
137
+ const YAML = require('yaml');
138
+ const data = YAML.parse(candidate);
139
+ if (data && typeof data === 'object') return data;
140
+ } catch { continue; }
141
+ }
142
+ } catch { /* ok */ }
143
+ return {};
144
+ }
145
+
146
+ /**
147
+ * Map MITRE ATT&CK tactics to covered/uncovered techniques.
148
+ */
149
+ analyzeCoverage() {
150
+ const domains = this._listDomains();
151
+ const coveredIds = new Set();
152
+
153
+ for (const domain of domains) {
154
+ const domainSkill = join(this.skillsDir, domain, 'SKILL.md');
155
+ const meta = this._parseSkillFrontmatter(domainSkill);
156
+ const mitre = meta.metadata || {};
157
+ if (typeof mitre === 'object' && Array.isArray(mitre['mitre-attack'])) {
158
+ for (const id of mitre['mitre-attack']) coveredIds.add(String(id));
159
+ }
160
+ for (const tech of this._listTechniques(domain)) {
161
+ const techSkill = join(this.skillsDir, domain, 'techniques', tech, 'SKILL.md');
162
+ const techMeta = this._parseSkillFrontmatter(techSkill);
163
+ const techMitre = techMeta.metadata || {};
164
+ if (typeof techMitre === 'object' && Array.isArray(techMitre['mitre-attack'])) {
165
+ for (const id of techMitre['mitre-attack']) coveredIds.add(String(id));
166
+ }
167
+ }
168
+ }
169
+
170
+ const coverage = {};
171
+ for (const [tactic, techniqueIds] of Object.entries(_MITRE_TACTICS)) {
172
+ const covered = techniqueIds.filter(t => coveredIds.has(t));
173
+ const uncovered = techniqueIds.filter(t => !coveredIds.has(t));
174
+ coverage[tactic] = {
175
+ total: techniqueIds.length,
176
+ covered,
177
+ uncovered,
178
+ pct: Math.round((covered.length / Math.max(techniqueIds.length, 1)) * 1000) / 10,
179
+ };
180
+ }
181
+ return coverage;
182
+ }
183
+
184
+ /** Identify domains with fewer than 24 techniques. */
185
+ findGaps() {
186
+ const gaps = [];
187
+ for (const domain of this._listDomains()) {
188
+ const techniques = this._listTechniques(domain);
189
+ if (techniques.length < 24) {
190
+ gaps.push({
191
+ domain,
192
+ techniqueCount: techniques.length,
193
+ deficit: 24 - techniques.length,
194
+ existing: techniques,
195
+ });
196
+ }
197
+ }
198
+ return gaps;
199
+ }
200
+
201
+ /**
202
+ * Suggest new technique names based on existing patterns.
203
+ * @param {string} domain
204
+ * @param {number} [count=5]
205
+ */
206
+ suggestNewTechniques(domain, count = 5) {
207
+ const existing = this._listTechniques(domain);
208
+ const verbCounts = {};
209
+ for (const d of this._listDomains()) {
210
+ for (const tech of this._listTechniques(d)) {
211
+ const verb = tech.includes('-') ? tech.split('-')[0] : tech;
212
+ verbCounts[verb] = (verbCounts[verb] || 0) + 1;
213
+ }
214
+ }
215
+ const popularVerbs = Object.keys(verbCounts).sort((a, b) => verbCounts[b] - verbCounts[a]);
216
+ const domainWords = domain.replace(/-/g, ' ').split(' ');
217
+ const suggestions = [];
218
+ const existingSet = new Set(existing);
219
+
220
+ for (const verb of popularVerbs) {
221
+ if (suggestions.length >= count) break;
222
+ for (const kw of domainWords) {
223
+ const candidate = `${verb}-${kw}-operations`;
224
+ if (!existingSet.has(candidate) && !suggestions.includes(candidate)) {
225
+ suggestions.push(candidate);
226
+ if (suggestions.length >= count) break;
227
+ }
228
+ }
229
+ const candidateAlt = `${verb}-${domain}-analysis`;
230
+ if (!existingSet.has(candidateAlt) && !suggestions.includes(candidateAlt)) {
231
+ suggestions.push(candidateAlt);
232
+ if (suggestions.length >= count) break;
233
+ }
234
+ }
235
+ return suggestions.slice(0, count);
236
+ }
237
+
238
+ /** Get full coverage report. */
239
+ getCoverageReport() {
240
+ const domains = this._listDomains();
241
+ let totalTechniques = 0;
242
+ const domainStats = [];
243
+ for (const domain of domains) {
244
+ const techs = this._listTechniques(domain);
245
+ totalTechniques += techs.length;
246
+ domainStats.push({ domain, techniqueCount: techs.length });
247
+ }
248
+ return {
249
+ totalDomains: domains.length,
250
+ totalTechniques,
251
+ domainStats,
252
+ mitreCoverage: this.analyzeCoverage(),
253
+ gapCount: this.findGaps().length,
254
+ };
255
+ }
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Content generators
260
+ // ---------------------------------------------------------------------------
261
+
262
+ function _titleCase(slug) {
263
+ return slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
264
+ }
265
+
266
+ function _generateSkillMd(domain, techniqueName, description) {
267
+ return `<!-- Copyright (c) 2026 defconxt. All rights reserved. -->
268
+ <!-- Licensed under AGPL-3.0 — see LICENSE file for details. -->
269
+ ---
270
+ name: ${techniqueName}
271
+ description: >-
272
+ ${description}
273
+ domain: cybersecurity
274
+ subdomain: ${domain}
275
+ tags:
276
+ - ${domain}
277
+ - ${techniqueName.split('-')[0]}
278
+ - security-automation
279
+ version: "1.0"
280
+ author: defconxt
281
+ license: AGPL-3.0
282
+ ---
283
+
284
+ # ${_titleCase(techniqueName)}
285
+
286
+ ## Quick Reference
287
+
288
+ | Item | Detail |
289
+ |---|---|
290
+ | Domain | ${domain} |
291
+ | Technique | ${techniqueName} |
292
+ | ATT&CK | See metadata |
293
+ | Difficulty | Intermediate |
294
+
295
+ ## Overview
296
+
297
+ ${description}
298
+
299
+ ## Workflow
300
+
301
+ 1. **Preparation** — Gather required tooling and access.
302
+ 2. **Execution** — Run the technique using the agent script.
303
+ 3. **Analysis** — Review output and correlate findings.
304
+ 4. **Reporting** — Document results and remediation steps.
305
+
306
+ ## Verification
307
+
308
+ \`\`\`bash
309
+ python3 scripts/agent.py analyze --target example
310
+ python3 scripts/agent.py report --format json
311
+ \`\`\`
312
+
313
+ ## References
314
+
315
+ - MITRE ATT&CK: https://attack.mitre.org/
316
+ - See \`references/api-reference.md\` for command details.
317
+ `;
318
+ }
319
+
320
+ function _generateAgentPy(domain, techniqueName) {
321
+ const funcPrefix = techniqueName.replace(/-/g, '_').slice(0, 20);
322
+ return `#!/usr/bin/env python3
323
+ """Agent tooling for ${_titleCase(techniqueName).toLowerCase()}."""
324
+
325
+ import argparse
326
+ import json
327
+ import sys
328
+
329
+
330
+ def ${funcPrefix}_analyze(target, verbose=False):
331
+ """Analyze a target for ${techniqueName} indicators."""
332
+ results = {
333
+ "action": "analyze",
334
+ "technique": "${techniqueName}",
335
+ "domain": "${domain}",
336
+ "target": target,
337
+ "findings": [],
338
+ "risk_level": "medium",
339
+ }
340
+ if verbose:
341
+ results["debug"] = {"verbose": True}
342
+ return results
343
+
344
+
345
+ def main():
346
+ parser = argparse.ArgumentParser(description="${techniqueName} agent")
347
+ parser.add_argument("--target", required=True)
348
+ parser.add_argument("--verbose", action="store_true")
349
+ parser.add_argument("--format", choices=["json", "text"], default="json")
350
+ args = parser.parse_args()
351
+
352
+ result = ${funcPrefix}_analyze(args.target, args.verbose)
353
+ if args.format == "json":
354
+ print(json.dumps(result, indent=2))
355
+ else:
356
+ for k, v in result.items():
357
+ print(f"{k}: {v}")
358
+
359
+
360
+ if __name__ == "__main__":
361
+ main()
362
+ `;
363
+ }
364
+
365
+ function _generateApiReference(domain, techniqueName) {
366
+ const title = _titleCase(techniqueName);
367
+ return `<!-- Copyright (c) 2026 defconxt. All rights reserved. -->
368
+ <!-- Licensed under AGPL-3.0 — see LICENSE file for details. -->
369
+
370
+ # API Reference — ${title}
371
+
372
+ ## Agent Script
373
+
374
+ | Command | Description |
375
+ |---|---|
376
+ | \`python3 agent.py analyze --target <t>\` | Analyze a target for ${techniqueName} indicators |
377
+
378
+ ## Options
379
+
380
+ | Flag | Type | Default | Description |
381
+ |---|---|---|---|
382
+ | \`--target\` | str | *(required)* | Target identifier for analysis |
383
+ | \`--verbose\` | flag | false | Enable verbose output |
384
+ | \`--format\` | str | json | Output format |
385
+
386
+ ## Output Format
387
+
388
+ All commands return JSON.
389
+ `;
390
+ }
391
+
392
+ // ---------------------------------------------------------------------------
393
+ // AutonomousResearcher
394
+ // ---------------------------------------------------------------------------
395
+
396
+ export class AutonomousResearcher {
397
+ /**
398
+ * @param {string} skillsDir
399
+ * @param {object} [memory] - CipherMemory instance (optional, lazy-loaded)
400
+ */
401
+ constructor(skillsDir, memory = null) {
402
+ this.skillsDir = resolve(String(skillsDir));
403
+ this.memory = memory;
404
+ this._history = [];
405
+ this._analyzer = new SkillGapAnalyzer(this.skillsDir);
406
+ }
407
+
408
+ /**
409
+ * Create a complete hypothesis with SKILL.md, agent.py, and api-reference.md.
410
+ * @param {string} domain
411
+ * @param {string} techniqueName
412
+ * @returns {ResearchHypothesis}
413
+ */
414
+ generateHypothesis(domain, techniqueName) {
415
+ const description =
416
+ `Automated security technique for ${_titleCase(techniqueName).toLowerCase()} ` +
417
+ `within the ${domain} domain. Provides analysis, scanning, and reporting ` +
418
+ `capabilities for defensive and offensive security workflows.`;
419
+
420
+ return new ResearchHypothesis({
421
+ domain,
422
+ techniqueName,
423
+ description,
424
+ skillContent: _generateSkillMd(domain, techniqueName, description),
425
+ agentScript: _generateAgentPy(domain, techniqueName),
426
+ referenceContent: _generateApiReference(domain, techniqueName),
427
+ confidence: 0.75,
428
+ status: 'proposed',
429
+ });
430
+ }
431
+
432
+ /**
433
+ * Run validation checks against a hypothesis.
434
+ * @param {ResearchHypothesis} hypothesis
435
+ * @returns {ResearchExperiment}
436
+ */
437
+ validateHypothesis(hypothesis) {
438
+ const checks = ['yaml_valid', 'script_compiles', 'references_exist', 'skill_has_sections', 'content_length'];
439
+ const results = {};
440
+
441
+ results.yaml_valid = AutonomousResearcher._checkYaml(hypothesis.skillContent);
442
+ results.script_compiles = AutonomousResearcher._checkCompiles(hypothesis.agentScript);
443
+ results.references_exist = AutonomousResearcher._checkReferences(hypothesis.referenceContent);
444
+ results.skill_has_sections = AutonomousResearcher._checkSkillSections(hypothesis.skillContent);
445
+ results.content_length = AutonomousResearcher._checkContentLength(
446
+ hypothesis.skillContent, hypothesis.agentScript, hypothesis.referenceContent
447
+ );
448
+
449
+ const overall = Object.values(results).every(v => v);
450
+ hypothesis.testResults = { ...results };
451
+ hypothesis.status = 'testing';
452
+
453
+ return new ResearchExperiment({
454
+ hypothesis,
455
+ validationChecks: checks,
456
+ checkResults: results,
457
+ overallPass: overall,
458
+ });
459
+ }
460
+
461
+ static _checkYaml(skillContent) {
462
+ const parts = skillContent.split('---');
463
+ for (let i = 1; i < parts.length; i++) {
464
+ const candidate = parts[i].trim();
465
+ if (!candidate) continue;
466
+ try {
467
+ const YAML = require('yaml');
468
+ const data = YAML.parse(candidate);
469
+ return data && typeof data === 'object' && 'name' in data;
470
+ } catch { continue; }
471
+ }
472
+ return false;
473
+ }
474
+
475
+ static _checkCompiles(script) {
476
+ // For JS port: just verify it's a non-empty string with valid Python syntax indicators
477
+ // We can't actually parse Python in Node.js, but we can check for structure
478
+ return typeof script === 'string' && script.length > 100 && script.includes('def ') && script.includes('import ');
479
+ }
480
+
481
+ static _checkReferences(refContent) {
482
+ return !!refContent && (refContent.trim().startsWith('#') || refContent.includes('# '));
483
+ }
484
+
485
+ static _checkSkillSections(skillContent) {
486
+ const required = ['Quick Reference', 'Workflow', 'Verification', 'References'];
487
+ const lower = skillContent.toLowerCase();
488
+ return required.every(section => lower.includes(section.toLowerCase()));
489
+ }
490
+
491
+ static _checkContentLength(skill, script, reference) {
492
+ return skill.length >= 200 && script.length >= 200 && reference.length >= 100;
493
+ }
494
+
495
+ /**
496
+ * Write hypothesis files to disk.
497
+ * @param {ResearchHypothesis} hypothesis
498
+ * @returns {boolean}
499
+ */
500
+ applyHypothesis(hypothesis) {
501
+ try {
502
+ const base = join(this.skillsDir, hypothesis.domain, 'techniques', hypothesis.techniqueName);
503
+ const scriptsDir = join(base, 'scripts');
504
+ const refsDir = join(base, 'references');
505
+
506
+ mkdirSync(scriptsDir, { recursive: true });
507
+ mkdirSync(refsDir, { recursive: true });
508
+
509
+ writeFileSync(join(base, 'SKILL.md'), hypothesis.skillContent, 'utf-8');
510
+ const agentPath = join(scriptsDir, 'agent.py');
511
+ writeFileSync(agentPath, hypothesis.agentScript, 'utf-8');
512
+ try { chmodSync(agentPath, 0o755); } catch { /* ok */ }
513
+ writeFileSync(join(refsDir, 'api-reference.md'), hypothesis.referenceContent, 'utf-8');
514
+
515
+ hypothesis.status = 'accepted';
516
+
517
+ // Store in memory if available
518
+ if (this.memory) {
519
+ try {
520
+ // Lazy import to avoid circular deps
521
+ this.memory.store({
522
+ content: `Generated technique ${hypothesis.techniqueName} in ${hypothesis.domain}`,
523
+ keywords: [hypothesis.domain, hypothesis.techniqueName, hypothesis.hypothesisId],
524
+ });
525
+ } catch { /* memory is optional */ }
526
+ }
527
+
528
+ return true;
529
+ } catch {
530
+ hypothesis.status = 'rejected';
531
+ return false;
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Full cycle: generate → validate → apply if passing.
537
+ * @param {string} domain
538
+ * @param {string} techniqueName
539
+ * @returns {ResearchExperiment}
540
+ */
541
+ runExperiment(domain, techniqueName) {
542
+ const hypothesis = this.generateHypothesis(domain, techniqueName);
543
+ const experiment = this.validateHypothesis(hypothesis);
544
+
545
+ if (experiment.overallPass) {
546
+ const applied = this.applyHypothesis(hypothesis);
547
+ if (!applied) {
548
+ hypothesis.status = 'rejected';
549
+ experiment.overallPass = false;
550
+ }
551
+ } else {
552
+ hypothesis.status = 'rejected';
553
+ }
554
+
555
+ this._history.push({
556
+ experimentId: experiment.experimentId,
557
+ hypothesisId: hypothesis.hypothesisId,
558
+ domain,
559
+ techniqueName,
560
+ overallPass: experiment.overallPass,
561
+ status: hypothesis.status,
562
+ checks: { ...experiment.checkResults },
563
+ createdAt: experiment.createdAt,
564
+ });
565
+
566
+ return experiment;
567
+ }
568
+
569
+ /**
570
+ * Run experiments for multiple techniques in a domain.
571
+ * @param {string} domain
572
+ * @param {string[]} techniques
573
+ * @returns {ResearchExperiment[]}
574
+ */
575
+ batchExpand(domain, techniques) {
576
+ return techniques.map(tech => this.runExperiment(domain, tech));
577
+ }
578
+
579
+ /** Return all experiments with results. */
580
+ getResearchHistory() {
581
+ return [...this._history];
582
+ }
583
+ }