From cf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 Mon Sep 17 00:00:00 2001 From: Agando Date: Thu, 9 Jul 2026 06:00:32 +0200 Subject: auto: 2026-07-09_06:00 changes --- modules/ext/skill-pack-core.js | 298 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) (limited to 'modules/ext/skill-pack-core.js') diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js index 455a7f4..f204e70 100644 --- a/modules/ext/skill-pack-core.js +++ b/modules/ext/skill-pack-core.js @@ -35,6 +35,10 @@ class SkillPackCoreModule { 'skill-export': 'skill-export — Export a skill as JSON', 'skill-import': 'skill-import — Import a skill from JSON string', 'skill-sync': 'skill-sync — Rebuild skills index from /skills/ directory', + 'learn-skill': 'learn-skill — Reflect on topic, draft a skill from experience', + 'skill-validate': 'skill-validate — Validate skill structure and get confidence score', + 'skill-audit': 'skill-audit — Audit skills for stale, duplicate, and broken references', + 'skill-restore': 'skill-restore — Restore an archived skill', 'skill-webdav-sync': 'skill-webdav-sync — Push all skills to WebDAV as JSON files', }; } @@ -528,6 +532,11 @@ class SkillPackCoreModule { this.commands['skill-export'] = (args) => this._cmdSkillExport(args); this.commands['skill-import'] = (args) => this._cmdSkillImport(args); this.commands['skill-sync'] = () => this._cmdSkillSync(); + // v4.5.90: Agent Skills — learn, validate, audit, restore + this.commands['learn-skill'] = (args) => this._cmdLearnSkill(args); + this.commands['skill-validate'] = (args) => this._cmdSkillValidate(args); + this.commands['skill-audit'] = () => this._cmdSkillAudit(); + this.commands['skill-restore'] = (args) => this._cmdSkillRestore(args); this.commands['skill-webdav-sync'] = () => this._cmdSkillWebdavSync(); } // ── Skill Commands ── @@ -574,6 +583,9 @@ class SkillPackCoreModule { this.terminal.print(line, ''); } this.terminal.print(`=== End ${name} ===`, 'info'); + // v4.5.90: Record skill use for decay tracking + const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; + if (mg && typeof mg.recordSkillUse === 'function') mg.recordSkillUse(name, true); } _cmdSkillCreate(args) { @@ -846,6 +858,292 @@ class SkillPackCoreModule { } this._seeded = true; } + + // ── v4.5.90: Agent Skills ── + + _cmdLearnSkill(args) { + if (!args || args.length === 0) return this.terminal.print('Usage: learn-skill ', 'warning'); + const topic = args.join(' '); + this.terminal.print('🧠 Reflecting on: ' + topic + '...', 'info'); + + // 1. Pull context from memgraph + const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; + let graphNodes = []; + if (mg) { + graphNodes = mg.findNodes(topic) || []; + this.terminal.print(' 📊 Found ' + graphNodes.length + ' graph nodes matching topic', 'info'); + } + + // 2. Pull recent sessions + const fs = this.terminal.fs; + let sessionInsights = []; + try { + const memDir = fs.readDir('/memory/') || []; + // v4.5.90b fix: readDir returns objects, filter to .json files (P0 from review) + const recent = memDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.json'); }).slice(-3); + for (const f of recent) { + const data = fs.fsReadJSON('/memory/' + f.name); + if (data && data.summary) sessionInsights.push(data.summary); + } + } catch (_) {} + this.terminal.print(' 📅 Loaded ' + sessionInsights.length + ' recent sessions', 'info'); + + // 3. Pull existing skills + const index = fs.fsReadJSON('/skills/index.json') || {}; + const skillNames = Object.keys(index).filter(function(n) { + return n.toLowerCase().includes(topic.toLowerCase().split(' ')[0]); + }); + this.terminal.print(' 📚 Found ' + skillNames.length + ' related skills', 'info'); + + // 4. Draft skill + const draft = this._draftSkill(topic, graphNodes, sessionInsights, skillNames); + this.terminal.print('', ''); + this.terminal.print('📝 Draft skill:', 'header'); + const lines = draft.split('\n'); + for (const line of lines) this.terminal.print(line, ''); + this.terminal.print('', ''); + + // 5. Validate + const validation = this._validateSkillContent(draft); + if (validation.errors.length > 0) { + this.terminal.print('❌ Validation errors:', 'error'); + for (const e of validation.errors) this.terminal.print(' • ' + e, 'error'); + this.terminal.print('Fix the issues and use skill-create to save manually.', 'info'); + return; + } + this.terminal.print('✅ Validation passed. Confidence: ' + validation.confidence.toFixed(2), 'success'); + + // 6. Propose + this.terminal.print('Ship this skill? Use: skill-create ' + topic.replace(/\s+/g, '-') + ' "" ""', 'info'); + this.terminal.print('Or discard and continue.', 'text-muted'); + } + + _draftSkill(topic, graphNodes, sessions, existingSkills) { + var lines = []; + lines.push('---'); + lines.push('name: ' + topic.toLowerCase().replace(/\s+/g, '-')); + lines.push('description: ' + topic + ' — auto-drafted from agent experience'); + lines.push('tags: [auto-learned, ' + topic.toLowerCase().split(' ')[0] + ']'); + lines.push('---'); + lines.push(''); + lines.push('# ' + topic); + lines.push(''); + lines.push('## Steps'); + lines.push(''); + // Generate steps from graph node patterns + var stepNum = 1; + if (graphNodes && graphNodes.length > 0) { + for (var n of graphNodes.slice(0, 5)) { + lines.push(stepNum + '. ' + n.label + ' (type: ' + n.type + ', uses: ' + (n.access_count || 1) + ')'); + stepNum++; + } + } else { + lines.push('1. Identify the core ' + topic + ' workflow'); + lines.push('2. Check for existing tools and modules'); + lines.push('3. Execute the task step by step'); + lines.push('4. Record results in memory graph'); + } + if (sessions && sessions.length > 0) { + lines.push(''); + lines.push('## Insights from Recent Sessions'); + for (var s of sessions.slice(0, 3)) { + lines.push('- ' + s.substring(0, 100)); + } + } + if (existingSkills && existingSkills.length > 0) { + lines.push(''); + lines.push('## Related Skills'); + for (var sk of existingSkills) lines.push('- skill-view ' + sk); + } + lines.push(''); + lines.push('## Pitfalls'); + lines.push('- This skill was auto-drafted — validate before relying on it'); + return lines.join('\n'); + } + + _cmdSkillValidate(args) { + if (!args || args.length === 0) return this.terminal.print('Usage: skill-validate ', 'warning'); + const name = args[0].replace(/\.md$/, ''); + const fs = this.terminal.fs; + const content = fs.readFile('/skills/' + name + '.md'); + if (!content) return this.terminal.print('Skill not found: ' + name, 'error'); + + const result = this._validateSkillContent(content); + this.terminal.print('📊 Validation: ' + name, 'header'); + if (result.errors.length === 0) { + this.terminal.print(' ✅ Valid', 'success'); + } else { + this.terminal.print(' ❌ ' + result.errors.length + ' error(s):', 'error'); + for (var e of result.errors) this.terminal.print(' • ' + e, 'error'); + } + if (result.warnings.length > 0) { + this.terminal.print(' ⚠️ ' + result.warnings.length + ' warning(s):', 'warning'); + for (var w of result.warnings) this.terminal.print(' • ' + w, 'warning'); + } + this.terminal.print(' 📈 Confidence: ' + result.confidence.toFixed(2), 'info'); + } + + _validateSkillContent(content) { + var errors = []; + var warnings = []; + var confidence = 0; + + // v4.5.90b fix: Normalize CRLF to LF (P2 from review) + content = content.replace(/\r\n/g, '\n'); + + // Check YAML frontmatter + var fmMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!fmMatch) { + errors.push('Missing YAML frontmatter (--- ... ---)'); + } else { + var fm = fmMatch[1]; + if (!fm.match(/^name:\s*\S+/m)) errors.push('Missing "name" field in frontmatter'); + if (!fm.match(/^description:\s*\S+/m)) errors.push('Missing "description" field in frontmatter'); + var nameMatch = fm.match(/^name:\s*(\S+)/m); + if (nameMatch && !nameMatch[1].match(/^[a-z0-9-]+$/)) { + errors.push('Name must be kebab-case (lowercase, hyphens, numbers only)'); + } + var tagsMatch = fm.match(/^tags:\s*\[(.+)\]/m); + if (tagsMatch && !tagsMatch[1].trim()) warnings.push('Tags field is empty'); + } + + // Check for ## Steps heading + if (!content.match(/##\s+Steps/i)) { + errors.push('Missing "## Steps" heading'); + } else { + // Check for at least 1 numbered step + var steps = content.match(/^\d+\.\s+/gm); + if (!steps || steps.length === 0) errors.push('No numbered steps found under ## Steps'); + } + + // Check broken skill-view references + var refMatches = content.match(/skill-view\s+(\S+)/g) || []; + var fs = this.terminal.fs; + for (var ref of refMatches) { + var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, ''); + if (fs && !fs.readFile('/skills/' + refName + '.md')) { + warnings.push('Broken skill-view reference: ' + refName); + } + } + + // v4.5.90b fix: Confidence = success_rate × (uses / (uses + 5)) — saturates at ~5 perfect uses, discriminates better (P2 from review) + var index = fs ? (fs.fsReadJSON('/skills/index.json') || {}) : {}; + var fmName = fmMatch ? fmMatch[1].match(/^name:\s*(\S+)/m) : null; + var skillName = fmName ? fmName[1] : ''; + var stats = index[skillName] || {}; + var uses = stats.useCount || 0; + var successes = stats.successCount || 0; + if (uses > 0) { + var successRate = successes / uses; + confidence = successRate * (uses / (uses + 5)); + } + + return { valid: errors.length === 0, errors: errors, warnings: warnings, confidence: Math.min(confidence, 1.0) }; + } + + _cmdSkillAudit() { + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + const allNames = Object.keys(index); + const now = Date.now(); + + var stale = []; + var duplicates = []; + var brokenRefs = 0; + + // 1. Stale check + const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; + var staleSkills = []; + if (mg && typeof mg.getStaleSkills === 'function') { + staleSkills = mg.getStaleSkills(); + } + for (var s of staleSkills) { + if (!index[s.name] || index[s.name].immune) continue; + stale.push(s.name + ' (' + s.daysUnused + 'd unused)'); + } + + // 2. Duplicate check (simple Levenshtein on name) + const auditCap = 200; + const auditNames = allNames.slice(0, auditCap); + for (var i = 0; i < auditNames.length; i++) { + for (var j = i + 1; j < auditNames.length; j++) { + if (this._levenshtein(auditNames[i], auditNames[j]) <= 2) { + duplicates.push(auditNames[i] + ' ≈ ' + auditNames[j]); + } + } + } + if (allNames.length > auditCap) { + warnings.push('Duplicate check only scanned first ' + auditCap + ' of ' + allNames.length + ' skills'); + } + + // 3. Reference check + for (var name of allNames) { + var content = fs.readFile('/skills/' + name + '.md'); + if (!content) continue; + var refs = content.match(/skill-view\s+(\S+)/g) || []; + for (var ref of refs) { + var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, ''); + if (!fs.readFile('/skills/' + refName + '.md')) brokenRefs++; + } + } + + var healthy = allNames.length - stale.length; + this.terminal.print('📊 Skill Audit', 'header'); + this.terminal.print('────────────────────────────────', 'text-muted'); + this.terminal.print('Total skills: ' + allNames.length, 'info'); + this.terminal.print('Healthy: ' + healthy, 'success'); + this.terminal.print('Stale: ' + stale.length + (stale.length > 0 ? ' (' + stale.join(', ') + ')' : ''), stale.length > 0 ? 'warning' : 'info'); + this.terminal.print('Duplicates: ' + duplicates.length + (duplicates.length > 0 ? ' (' + duplicates.join(', ') + ')' : ''), duplicates.length > 0 ? 'warning' : 'info'); + this.terminal.print('Broken refs: ' + brokenRefs, brokenRefs > 0 ? 'error' : 'info'); + this.terminal.print('', ''); + + if (stale.length > 0 || duplicates.length > 0 || brokenRefs > 0) { + this.terminal.print('Suggestions:', 'info'); + for (var st of stale) this.terminal.print('• Archive ' + st, 'warning'); + for (var dup of duplicates) this.terminal.print('• Merge ' + dup, 'warning'); + if (brokenRefs > 0) this.terminal.print('• Fix ' + brokenRefs + ' broken reference(s)', 'warning'); + } else { + this.terminal.print('✅ All skills healthy', 'success'); + } + } + + _cmdSkillRestore(args) { + if (!args || args.length === 0) return this.terminal.print('Usage: skill-restore ', 'warning'); + const name = args[0].replace(/\.md$/, ''); + const fs = this.terminal.fs; + if (!fs.readDir('/skills/.archive/')) return this.terminal.print('No archive directory found', 'error'); + const content = fs.readFile('/skills/.archive/' + name + '.md'); + if (!content) return this.terminal.print('Skill not found in archive: ' + name, 'error'); + // v4.5.90b fix: Don't overwrite a live skill (P1 from review) + const existing = fs.readFile('/skills/' + name + '.md'); + if (existing) return this.terminal.print('Skill "' + name + '" already exists live. Use skill-patch or delete it first.', 'warning'); + fs.writeFile('/skills/' + name + '.md', content); + fs.deleteFile('/skills/.archive/' + name + '.md'); + const index = fs.fsReadJSON('/skills/index.json') || {}; + index[name] = index[name] || {}; + index[name].lastUsed = new Date().toISOString(); + index[name].useCount = index[name].useCount || 0; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print('✅ Restored: ' + name, 'success'); + } + + _levenshtein(a, b) { + if (a.length === 0) return b.length; + if (b.length === 0) return a.length; + var matrix = []; + for (var i = 0; i <= b.length; i++) matrix[i] = [i]; + for (var j = 0; j <= a.length; j++) matrix[0][j] = j; + for (var i = 1; i <= b.length; i++) { + for (var j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); + } + } + } + return matrix[b.length][a.length]; + } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new SkillPackCoreModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/skill-pack-core.js'); -- cgit v1.2.3