# Skill 自进化系统 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为 NetaClaw Agent 添加 Skill 自进化能力——后台 review agent 自动提炼/优化 Skill,skill_manage 工具支持运行时创建/编辑,SkillGuard 安全扫描保护。 **Architecture:** 4 层模块:guard_patterns(威胁规则定义)→ guard(扫描引擎)→ skill_writer(原子写入 + 验证 + DB 同步)→ tools(skill_manage/skill_list 工具)。review.ts 编排后台 review agent,chat.ts 中迭代计数触发。 **Tech Stack:** TypeScript, Node.js fs, @sinclair/typebox (tool schema), 纯正则安全扫描 **Spec:** `docs/superpowers/specs/2026-04-13-skill-evolution-design.md` --- ## File Structure ``` src/modules/netaclaw/ ├── skill_evolution/ │ ├── guard_patterns.ts # 威胁模式定义(~84 条正则规则) │ ├── guard.ts # scanSkill() 扫描引擎 │ ├── skill_writer.ts # 原子写入 + 验证 + DB 同步 + 文件锁 │ └── review.ts # spawnSkillReview() + buildSkillReviewPrompt() ├── tools/builtin/ │ ├── skill_manage.ts # skill_manage 工具(create/edit/patch/delete/write_file/remove_file) │ └── skill_list.ts # skill_list 只读工具 ``` Modified files: - `src/modules/netaclaw/service/skill_loader.ts` — 新增 reloadSkill() + getSkillSummaries() - `src/modules/netaclaw/controller/chat.ts` — 触发条件检测 + 异步 review 调用 --- ### Task 1: Create guard_patterns.ts — threat pattern definitions **Files:** - Create: `src/modules/netaclaw/skill_evolution/guard_patterns.ts` - [ ] **Step 1: Create the threat pattern definitions file** Create `src/modules/netaclaw/skill_evolution/guard_patterns.ts`: ```typescript /** * Skill Guard 威胁模式定义 * 参考 Hermes skills_guard.py,84 条正则规则按类别组织 */ export type Severity = 'critical' | 'high' | 'medium'; export interface ThreatPattern { id: string; category: string; severity: Severity; pattern: RegExp; description: string; } export const BLOCKED_EXTENSIONS = new Set([ '.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.com', '.msi', '.dmg', '.app', '.deb', '.rpm', ]); export const ALLOWED_SUBDIRS = new Set([ 'references', 'templates', 'scripts', 'assets', ]); export const LIMITS = { maxNameLength: 64, maxContentLength: 100_000, maxFileSize: 1_048_576, // 1MB per file maxTotalSize: 1_048_576, // 1MB per skill maxFileCount: 50, maxDescriptionLength: 1024, namePattern: /^[a-z0-9][a-z0-9._-]*$/, } as const; // --- 威胁模式 --- const exfiltration: ThreatPattern[] = [ { id: 'exf-01', category: 'exfiltration', severity: 'critical', pattern: /process\.env\b/, description: '访问环境变量' }, { id: 'exf-02', category: 'exfiltration', severity: 'critical', pattern: /\.ssh\//, description: '访问 SSH 目录' }, { id: 'exf-03', category: 'exfiltration', severity: 'critical', pattern: /credentials?[._-]?(json|yaml|yml|xml|conf|cfg|ini)\b/i, description: '访问凭证文件' }, { id: 'exf-04', category: 'exfiltration', severity: 'critical', pattern: /\.(aws|gcloud|azure)\//, description: '访问云凭证目录' }, { id: 'exf-05', category: 'exfiltration', severity: 'critical', pattern: /keychain|keyring|gnome-keyring/i, description: '访问系统密钥链' }, { id: 'exf-06', category: 'exfiltration', severity: 'critical', pattern: /\/etc\/(shadow|passwd|master\.passwd)/, description: '访问系统密码文件' }, { id: 'exf-07', category: 'exfiltration', severity: 'high', pattern: /dns.*exfil|exfil.*dns/i, description: 'DNS 数据泄露' }, { id: 'exf-08', category: 'exfiltration', severity: 'high', pattern: /\!\[.*\]\(https?:\/\/[^)]*\$\{/, description: 'Markdown 图片链接注入' }, { id: 'exf-09', category: 'exfiltration', severity: 'critical', pattern: /\.netrc\b/, description: '访问 .netrc 凭证' }, { id: 'exf-10', category: 'exfiltration', severity: 'critical', pattern: /\.docker\/config\.json/, description: '访问 Docker 凭证' }, { id: 'exf-11', category: 'exfiltration', severity: 'critical', pattern: /\.kube\/config/, description: '访问 Kubernetes 凭证' }, { id: 'exf-12', category: 'exfiltration', severity: 'critical', pattern: /\.npmrc\b/, description: '访问 npm 凭证' }, { id: 'exf-13', category: 'exfiltration', severity: 'critical', pattern: /\.pypirc\b/, description: '访问 PyPI 凭证' }, { id: 'exf-14', category: 'exfiltration', severity: 'high', pattern: /webhook[_.]?url/i, description: '引用 webhook URL' }, { id: 'exf-15', category: 'exfiltration', severity: 'high', pattern: /api[_.]?key\s*[:=]\s*['"][^'"]{10,}/i, description: '硬编码 API key' }, { id: 'exf-16', category: 'exfiltration', severity: 'critical', pattern: /BEGIN\s+(RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY/, description: '包含私钥' }, { id: 'exf-17', category: 'exfiltration', severity: 'high', pattern: /localStorage|sessionStorage/i, description: '访问浏览器存储' }, { id: 'exf-18', category: 'exfiltration', severity: 'high', pattern: /document\.cookie/i, description: '访问 cookie' }, ]; // PLACEHOLDER_PATTERNS_CONTINUE ``` Note: This file will be very long (~300 lines). The remaining pattern categories follow the same structure. I'll continue in Step 2. - [ ] **Step 2: Add remaining threat pattern categories** Append to `guard_patterns.ts`, replacing `// PLACEHOLDER_PATTERNS_CONTINUE`: ```typescript const promptInjection: ThreatPattern[] = [ { id: 'pi-01', category: 'prompt_injection', severity: 'critical', pattern: /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts)/i, description: '忽略指令注入' }, { id: 'pi-02', category: 'prompt_injection', severity: 'critical', pattern: /you\s+are\s+now\s+(a|an|the)\b/i, description: '角色劫持' }, { id: 'pi-03', category: 'prompt_injection', severity: 'critical', pattern: /system\s*prompt/i, description: '引用系统提示词' }, { id: 'pi-04', category: 'prompt_injection', severity: 'critical', pattern: /\bDAN\b.*mode/i, description: 'DAN 越狱' }, { id: 'pi-05', category: 'prompt_injection', severity: 'critical', pattern: /pretend\s+you\s+(are|have|can)/i, description: '伪装指令' }, { id: 'pi-06', category: 'prompt_injection', severity: 'critical', pattern: /act\s+as\s+(if|though)\s+you/i, description: '行为覆盖' }, { id: 'pi-07', category: 'prompt_injection', severity: 'critical', pattern: /override\s+(your|the|all)\s+(rules|instructions|guidelines)/i, description: '规则覆盖' }, { id: 'pi-08', category: 'prompt_injection', severity: 'critical', pattern: /jailbreak/i, description: '越狱关键词' }, { id: 'pi-09', category: 'prompt_injection', severity: 'high', pattern: /\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/i, description: '模型标记注入' }, { id: 'pi-10', category: 'prompt_injection', severity: 'high', pattern: /\bHuman:|Assistant:|<\|system\|>/i, description: '对话角色注入' }, { id: 'pi-11', category: 'prompt_injection', severity: 'critical', pattern: /forget\s+(everything|all|your)/i, description: '遗忘指令' }, { id: 'pi-12', category: 'prompt_injection', severity: 'critical', pattern: /new\s+instructions?\s*:/i, description: '新指令注入' }, { id: 'pi-13', category: 'prompt_injection', severity: 'high', pattern: /do\s+not\s+follow\s+(any|the|your)/i, description: '不遵循指令' }, { id: 'pi-14', category: 'prompt_injection', severity: 'high', pattern: /reveal\s+(your|the)\s+(system|initial|original)\s+(prompt|instructions)/i, description: '提示词泄露' }, { id: 'pi-15', category: 'prompt_injection', severity: 'high', pattern: /repeat\s+(the|your)\s+(system|initial)\s+(prompt|message)/i, description: '提示词重复' }, { id: 'pi-16', category: 'prompt_injection', severity: 'high', pattern: /bypass\s+(safety|content|security)\s+(filter|check|guard)/i, description: '安全绕过' }, ]; const destructive: ThreatPattern[] = [ { id: 'des-01', category: 'destructive', severity: 'critical', pattern: /rm\s+-rf\s+\//, description: '递归删除根目录' }, { id: 'des-02', category: 'destructive', severity: 'critical', pattern: /mkfs\b/, description: '格式化文件系统' }, { id: 'des-03', category: 'destructive', severity: 'critical', pattern: /dd\s+if=/, description: 'dd 磁盘写入' }, { id: 'des-04', category: 'destructive', severity: 'critical', pattern: /chmod\s+777\s+\//, description: '根目录权限开放' }, { id: 'des-05', category: 'destructive', severity: 'critical', pattern: /:(){ :\|:& };:/, description: 'Fork bomb' }, { id: 'des-06', category: 'destructive', severity: 'high', pattern: /truncate\s+-s\s+0/, description: '文件截断' }, { id: 'des-07', category: 'destructive', severity: 'high', pattern: />\s*\/dev\/sd[a-z]/, description: '磁盘设备写入' }, { id: 'des-08', category: 'destructive', severity: 'high', pattern: /DROP\s+(TABLE|DATABASE)/i, description: 'SQL 删除操作' }, ]; const persistence: ThreatPattern[] = [ { id: 'per-01', category: 'persistence', severity: 'high', pattern: /crontab\s+-[el]/, description: '定时任务操作' }, { id: 'per-02', category: 'persistence', severity: 'high', pattern: /authorized_keys/, description: 'SSH 授权密钥' }, { id: 'per-03', category: 'persistence', severity: 'high', pattern: /systemd|systemctl\s+enable/, description: 'systemd 服务' }, { id: 'per-04', category: 'persistence', severity: 'high', pattern: /\/etc\/sudoers/, description: 'sudoers 修改' }, { id: 'per-05', category: 'persistence', severity: 'high', pattern: /\.bashrc|\.zshrc|\.profile|\.bash_profile/, description: 'Shell 配置修改' }, { id: 'per-06', category: 'persistence', severity: 'high', pattern: /launchd|LaunchAgent|LaunchDaemon/i, description: 'macOS 启动项' }, { id: 'per-07', category: 'persistence', severity: 'high', pattern: /HKEY_|reg\s+add/i, description: 'Windows 注册表' }, { id: 'per-08', category: 'persistence', severity: 'high', pattern: /init\.d\//, description: 'init.d 服务' }, { id: 'per-09', category: 'persistence', severity: 'high', pattern: /at\s+-f\s+/, description: 'at 定时任务' }, { id: 'per-10', category: 'persistence', severity: 'high', pattern: /rc\.local/, description: 'rc.local 启动脚本' }, ]; const network: ThreatPattern[] = [ { id: 'net-01', category: 'network', severity: 'high', pattern: /\/bin\/(bash|sh)\s+-i\s+>&?\s*\/dev\/tcp/, description: '反向 shell' }, { id: 'net-02', category: 'network', severity: 'high', pattern: /nc\s+-[elp]/, description: 'netcat 监听' }, { id: 'net-03', category: 'network', severity: 'high', pattern: /socat\b.*TCP/, description: 'socat 隧道' }, { id: 'net-04', category: 'network', severity: 'high', pattern: /ssh\s+-[RLD]\s/, description: 'SSH 隧道' }, { id: 'net-05', category: 'network', severity: 'high', pattern: /ngrok|localtunnel|serveo/i, description: '隧道服务' }, { id: 'net-06', category: 'network', severity: 'high', pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5}\b/, description: '硬编码 IP:端口' }, { id: 'net-07', category: 'network', severity: 'high', pattern: /python\s+-m\s+http\.server/, description: 'Python HTTP 服务' }, { id: 'net-08', category: 'network', severity: 'high', pattern: /nmap\b/, description: '端口扫描' }, { id: 'net-09', category: 'network', severity: 'high', pattern: /tcpdump|wireshark|tshark/i, description: '网络抓包' }, ]; const obfuscation: ThreatPattern[] = [ { id: 'obf-01', category: 'obfuscation', severity: 'high', pattern: /eval\s*\(/, description: 'eval 执行' }, { id: 'obf-02', category: 'obfuscation', severity: 'high', pattern: /Buffer\.from\s*\([^)]*,\s*['"]base64['"]/, description: 'Base64 解码' }, { id: 'obf-03', category: 'obfuscation', severity: 'high', pattern: /atob\s*\(/, description: 'atob 解码' }, { id: 'obf-04', category: 'obfuscation', severity: 'high', pattern: /String\.fromCharCode/, description: 'charCode 构建' }, { id: 'obf-05', category: 'obfuscation', severity: 'high', pattern: /\\x[0-9a-f]{2}\\x[0-9a-f]{2}\\x[0-9a-f]{2}/i, description: '十六进制编码' }, { id: 'obf-06', category: 'obfuscation', severity: 'high', pattern: /\\u[0-9a-f]{4}\\u[0-9a-f]{4}/i, description: 'Unicode 编码' }, { id: 'obf-07', category: 'obfuscation', severity: 'high', pattern: /new\s+Function\s*\(/, description: 'Function 构造器' }, { id: 'obf-08', category: 'obfuscation', severity: 'high', pattern: /exec\s*\(\s*['"`]/, description: 'exec 字符串执行' }, { id: 'obf-09', category: 'obfuscation', severity: 'high', pattern: /\$\{.*`.*`.*\}/, description: '模板字符串嵌套' }, { id: 'obf-10', category: 'obfuscation', severity: 'high', pattern: /unescape\s*\(/, description: 'unescape 解码' }, { id: 'obf-11', category: 'obfuscation', severity: 'high', pattern: /decodeURIComponent\s*\(\s*['"]%/, description: 'URI 解码混淆' }, { id: 'obf-12', category: 'obfuscation', severity: 'high', pattern: /\['\\x/, description: '属性名十六进制' }, { id: 'obf-13', category: 'obfuscation', severity: 'high', pattern: /globalThis\[/, description: 'globalThis 动态访问' }, { id: 'obf-14', category: 'obfuscation', severity: 'high', pattern: /Reflect\.apply/, description: 'Reflect 调用' }, ]; const execution: ThreatPattern[] = [ { id: 'exe-01', category: 'execution', severity: 'high', pattern: /child_process/, description: 'child_process 模块' }, { id: 'exe-02', category: 'execution', severity: 'high', pattern: /require\s*\(\s*['"]child_process['"]/, description: 'require child_process' }, { id: 'exe-03', category: 'execution', severity: 'high', pattern: /execSync\s*\(/, description: 'execSync 调用' }, { id: 'exe-04', category: 'execution', severity: 'high', pattern: /spawnSync\s*\(/, description: 'spawnSync 调用' }, { id: 'exe-05', category: 'execution', severity: 'high', pattern: /os\.system\s*\(/, description: 'Python os.system' }, { id: 'exe-06', category: 'execution', severity: 'high', pattern: /subprocess\.(run|call|Popen)/, description: 'Python subprocess' }, ]; const pathTraversal: ThreatPattern[] = [ { id: 'pt-01', category: 'path_traversal', severity: 'high', pattern: /\.\.\/\.\.\//, description: '路径穿越' }, { id: 'pt-02', category: 'path_traversal', severity: 'high', pattern: /\/etc\/passwd/, description: '系统密码文件' }, { id: 'pt-03', category: 'path_traversal', severity: 'high', pattern: /\/proc\/self/, description: 'proc 自身信息' }, { id: 'pt-04', category: 'path_traversal', severity: 'high', pattern: /\/proc\/\d+\//, description: 'proc 进程信息' }, { id: 'pt-05', category: 'path_traversal', severity: 'high', pattern: /\/dev\/(tcp|udp)\//, description: '设备文件网络' }, ]; const cryptoMining: ThreatPattern[] = [ { id: 'cm-01', category: 'crypto_mining', severity: 'critical', pattern: /stratum\+tcp:\/\//, description: '矿池协议' }, { id: 'cm-02', category: 'crypto_mining', severity: 'critical', pattern: /xmrig|cpuminer|cgminer|bfgminer/i, description: '挖矿软件' }, ]; const supplyChain: ThreatPattern[] = [ { id: 'sc-01', category: 'supply_chain', severity: 'critical', pattern: /curl\s+[^|]*\|\s*(ba)?sh/, description: 'curl pipe to shell' }, { id: 'sc-02', category: 'supply_chain', severity: 'critical', pattern: /wget\s+[^|]*\|\s*(ba)?sh/, description: 'wget pipe to shell' }, { id: 'sc-03', category: 'supply_chain', severity: 'high', pattern: /pip\s+install\s+--index-url\s+http:/, description: '不安全 pip 源' }, { id: 'sc-04', category: 'supply_chain', severity: 'high', pattern: /npm\s+install\s+--registry\s+http:/, description: '不安全 npm 源' }, { id: 'sc-05', category: 'supply_chain', severity: 'high', pattern: /install.*@latest\b/, description: '未固定版本依赖' }, { id: 'sc-06', category: 'supply_chain', severity: 'high', pattern: /postinstall|preinstall.*curl|wget/i, description: '安装钩子远程获取' }, ]; const privilegeEscalation: ThreatPattern[] = [ { id: 'pe-01', category: 'privilege_escalation', severity: 'high', pattern: /sudo\s+-[sS]/, description: 'sudo shell' }, { id: 'pe-02', category: 'privilege_escalation', severity: 'high', pattern: /setuid|setgid|seteuid/, description: '设置 UID/GID' }, { id: 'pe-03', category: 'privilege_escalation', severity: 'high', pattern: /NOPASSWD/, description: '免密 sudo' }, { id: 'pe-04', category: 'privilege_escalation', severity: 'high', pattern: /chmod\s+[u+]*s\s/, description: 'setuid 位' }, { id: 'pe-05', category: 'privilege_escalation', severity: 'high', pattern: /capability.*cap_sys_admin/i, description: 'Linux capability' }, ]; const credentialExposure: ThreatPattern[] = [ { id: 'ce-01', category: 'credential_exposure', severity: 'critical', pattern: /password\s*[:=]\s*['"][^'"]{8,}/i, description: '硬编码密码' }, { id: 'ce-02', category: 'credential_exposure', severity: 'critical', pattern: /secret\s*[:=]\s*['"][^'"]{8,}/i, description: '硬编码密钥' }, { id: 'ce-03', category: 'credential_exposure', severity: 'critical', pattern: /token\s*[:=]\s*['"][A-Za-z0-9_\-]{20,}/i, description: '硬编码 token' }, { id: 'ce-04', category: 'credential_exposure', severity: 'critical', pattern: /AKIA[0-9A-Z]{16}/, description: 'AWS Access Key' }, { id: 'ce-05', category: 'credential_exposure', severity: 'critical', pattern: /ghp_[A-Za-z0-9]{36}/, description: 'GitHub PAT' }, ]; export const ALL_PATTERNS: ThreatPattern[] = [ ...exfiltration, ...promptInjection, ...destructive, ...persistence, ...network, ...obfuscation, ...execution, ...pathTraversal, ...cryptoMining, ...supplyChain, ...privilegeEscalation, ...credentialExposure, ]; ``` - [ ] **Step 3: Commit** ```bash git add src/modules/netaclaw/skill_evolution/guard_patterns.ts git commit -m "feat(skill-evolution): add 84 threat pattern definitions for SkillGuard" ``` --- ### Task 2: Create guard.ts — scan engine **Files:** - Create: `src/modules/netaclaw/skill_evolution/guard.ts` - [ ] **Step 1: Implement scanSkill()** Create `src/modules/netaclaw/skill_evolution/guard.ts`: ```typescript import * as fs from 'fs'; import * as path from 'path'; import { ALL_PATTERNS, BLOCKED_EXTENSIONS, ALLOWED_SUBDIRS, LIMITS, Severity } from './guard_patterns.js'; export interface ScanFinding { category: string; severity: Severity; pattern: string; match: string; file: string; line: number; } export interface ScanResult { verdict: 'safe' | 'caution' | 'dangerous'; findings: ScanFinding[]; summary: string; } function scanFileContent(content: string, filePath: string): ScanFinding[] { const findings: ScanFinding[] = []; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { for (const tp of ALL_PATTERNS) { const m = lines[i].match(tp.pattern); if (m) { findings.push({ category: tp.category, severity: tp.severity, pattern: tp.id, match: m[0].slice(0, 80), file: filePath, line: i + 1, }); } } } return findings; } function checkStructure(skillDir: string): ScanFinding[] { const findings: ScanFinding[] = []; let fileCount = 0; let totalSize = 0; function walk(dir: string, depth: number) { if (!fs.existsSync(dir)) return; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (depth === 0 && !ALLOWED_SUBDIRS.has(entry.name)) { findings.push({ category: 'structure', severity: 'high', pattern: 'disallowed_dir', match: entry.name, file: fullPath, line: 0, }); } walk(fullPath, depth + 1); } else { fileCount++; const ext = path.extname(entry.name).toLowerCase(); if (BLOCKED_EXTENSIONS.has(ext)) { findings.push({ category: 'structure', severity: 'critical', pattern: 'blocked_extension', match: ext, file: fullPath, line: 0, }); } const stat = fs.statSync(fullPath); totalSize += stat.size; if (stat.size > LIMITS.maxFileSize) { findings.push({ category: 'structure', severity: 'high', pattern: 'file_too_large', match: `${stat.size} bytes`, file: fullPath, line: 0, }); } } } } walk(skillDir, 0); if (fileCount > LIMITS.maxFileCount) { findings.push({ category: 'structure', severity: 'high', pattern: 'too_many_files', match: `${fileCount} files`, file: skillDir, line: 0, }); } if (totalSize > LIMITS.maxTotalSize) { findings.push({ category: 'structure', severity: 'high', pattern: 'total_size_exceeded', match: `${totalSize} bytes`, file: skillDir, line: 0, }); } return findings; } function determineVerdict(findings: ScanFinding[]): ScanResult['verdict'] { if (findings.length === 0) return 'safe'; if (findings.some(f => f.severity === 'critical')) return 'dangerous'; return 'caution'; } export function scanSkill(skillDir: string): ScanResult { const findings: ScanFinding[] = []; // 结构检查 findings.push(...checkStructure(skillDir)); // 内容扫描 function scanDir(dir: string) { if (!fs.existsSync(dir)) return; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { scanDir(fullPath); } else { const ext = path.extname(entry.name).toLowerCase(); if (BLOCKED_EXTENSIONS.has(ext)) continue; try { const content = fs.readFileSync(fullPath, 'utf-8'); const relPath = path.relative(skillDir, fullPath); findings.push(...scanFileContent(content, relPath)); } catch { // 二进制文件读取失败,跳过 } } } } scanDir(skillDir); const verdict = determineVerdict(findings); const summary = findings.length === 0 ? 'No threats detected.' : `Found ${findings.length} issue(s): ${findings.filter(f => f.severity === 'critical').length} critical, ${findings.filter(f => f.severity === 'high').length} high.`; return { verdict, findings, summary }; } ``` - [ ] **Step 2: Verify no TypeScript errors** ```bash cd packages/backend && npx tsc --noEmit ``` - [ ] **Step 3: Commit** ```bash git add src/modules/netaclaw/skill_evolution/guard.ts git commit -m "feat(skill-evolution): implement SkillGuard scan engine" ``` --- ### Task 3: Create skill_writer.ts — atomic write + validation + DB sync **Files:** - Create: `src/modules/netaclaw/skill_evolution/skill_writer.ts` - [ ] **Step 1: Implement SkillWriter** Create `src/modules/netaclaw/skill_evolution/skill_writer.ts`: ```typescript import * as fs from 'fs'; import * as path from 'path'; import { Repository } from 'typeorm'; import { NetaClawSkillEntity } from '../entity/skill.js'; import { SkillLoaderService } from '../service/skill_loader.js'; import { scanSkill, ScanResult } from './guard.js'; import { LIMITS } from './guard_patterns.js'; export interface WriteResult { success: boolean; message: string; scanResult?: ScanResult; } function validateName(name: string): string | null { if (!name || name.length > LIMITS.maxNameLength) return `名称长度须在 1-${LIMITS.maxNameLength} 字符`; if (!LIMITS.namePattern.test(name)) return '名称只允许小写字母、数字、连字符、下划线、点,且必须以字母或数字开头'; return null; } function validateFrontmatter(content: string): string | null { const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!fmMatch) return 'SKILL.md 必须包含 YAML frontmatter (--- ... ---)'; const fm = fmMatch[1]; if (!/^name:\s*.+$/m.test(fm)) return 'frontmatter 必须包含 name 字段'; if (!/^description:\s*.+$/m.test(fm)) return 'frontmatter 必须包含 description 字段'; const descMatch = fm.match(/^description:\s*(.+)$/m); if (descMatch && descMatch[1].length > LIMITS.maxDescriptionLength) { return `description 最长 ${LIMITS.maxDescriptionLength} 字符`; } return null; } function acquireLock(lockPath: string, timeoutMs = 5000): boolean { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { fs.mkdirSync(lockPath); return true; } catch { // lock exists, wait const elapsed = Date.now() - start; if (elapsed >= timeoutMs) return false; // busy wait 50ms const end = Date.now() + 50; while (Date.now() < end) { /* spin */ } } } return false; } function releaseLock(lockPath: string): void { try { fs.rmdirSync(lockPath); } catch { /* ignore */ } } export class SkillWriter { constructor( private skillsDir: string, private skillRepo: Repository, private skillLoader: SkillLoaderService, ) {} async createSkill(name: string, content: string, category?: string): Promise { const nameErr = validateName(name); if (nameErr) return { success: false, message: nameErr }; if (content.length > LIMITS.maxContentLength) { return { success: false, message: `内容超过 ${LIMITS.maxContentLength} 字符限制` }; } const fmErr = validateFrontmatter(content); if (fmErr) return { success: false, message: fmErr }; const skillDir = category ? path.join(this.skillsDir, category, name) : path.join(this.skillsDir, name); if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) { return { success: false, message: `Skill "${name}" 已存在,请用 edit 或 patch` }; } return this.atomicWrite(skillDir, name, content); } async editSkill(name: string, content: string): Promise { if (content.length > LIMITS.maxContentLength) { return { success: false, message: `内容超过 ${LIMITS.maxContentLength} 字符限制` }; } const fmErr = validateFrontmatter(content); if (fmErr) return { success: false, message: fmErr }; const skillDir = this.findSkillDir(name); if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` }; return this.atomicWrite(skillDir, name, content); } async patchSkill(name: string, oldStr: string, newStr: string, filePath?: string, replaceAll = false): Promise { const skillDir = this.findSkillDir(name); if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` }; const targetFile = filePath ? path.join(skillDir, filePath) : path.join(skillDir, 'SKILL.md'); if (!fs.existsSync(targetFile)) { return { success: false, message: `文件不存在: ${filePath ?? 'SKILL.md'}` }; } let content = fs.readFileSync(targetFile, 'utf-8'); if (!content.includes(oldStr)) { return { success: false, message: '未找到要替换的文本' }; } if (replaceAll) { content = content.split(oldStr).join(newStr); } else { const idx = content.indexOf(oldStr); content = content.slice(0, idx) + newStr + content.slice(idx + oldStr.length); } // 如果是 SKILL.md,验证 frontmatter if (!filePath || filePath === 'SKILL.md') { const fmErr = validateFrontmatter(content); if (fmErr) return { success: false, message: fmErr }; } return this.atomicWrite(skillDir, name, content, filePath); } async deleteSkill(name: string): Promise { const skillDir = this.findSkillDir(name); if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` }; fs.rmSync(skillDir, { recursive: true, force: true }); await this.skillRepo.delete({ name }); await this.skillLoader.reloadSkill(name); return { success: true, message: `Skill "${name}" 已删除` }; } async writeFile(name: string, filePath: string, fileContent: string): Promise { const skillDir = this.findSkillDir(name); if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` }; // 验证子目录 const firstSegment = filePath.split('/')[0]; const allowed = new Set(['references', 'templates', 'scripts', 'assets']); if (!allowed.has(firstSegment)) { return { success: false, message: `只允许写入 references/, templates/, scripts/, assets/ 子目录` }; } if (Buffer.byteLength(fileContent) > LIMITS.maxFileSize) { return { success: false, message: `文件超过 ${LIMITS.maxFileSize} 字节限制` }; } const lockPath = path.join(skillDir, '.lock'); if (!acquireLock(lockPath)) { return { success: false, message: '无法获取文件锁,请稍后重试' }; } try { const fullPath = path.join(skillDir, filePath); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, fileContent, 'utf-8'); // 扫描整个 skill 目录 const scanResult = scanSkill(skillDir); if (scanResult.verdict === 'dangerous') { fs.unlinkSync(fullPath); return { success: false, message: `安全扫描未通过: ${scanResult.summary}`, scanResult }; } return { success: true, message: `文件已写入: ${filePath}`, scanResult }; } finally { releaseLock(lockPath); } } async removeFile(name: string, filePath: string): Promise { const skillDir = this.findSkillDir(name); if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` }; const fullPath = path.join(skillDir, filePath); if (!fs.existsSync(fullPath)) { return { success: false, message: `文件不存在: ${filePath}` }; } fs.unlinkSync(fullPath); return { success: true, message: `文件已删除: ${filePath}` }; } private findSkillDir(name: string): string | null { // 直接查找 const direct = path.join(this.skillsDir, name); if (fs.existsSync(path.join(direct, 'SKILL.md'))) return direct; // 在分类子目录中查找 if (!fs.existsSync(this.skillsDir)) return null; for (const cat of fs.readdirSync(this.skillsDir, { withFileTypes: true })) { if (!cat.isDirectory()) continue; const nested = path.join(this.skillsDir, cat.name, name); if (fs.existsSync(path.join(nested, 'SKILL.md'))) return nested; } return null; } private async atomicWrite(skillDir: string, name: string, content: string, filePath?: string): Promise { const lockPath = path.join(skillDir, '.lock'); fs.mkdirSync(skillDir, { recursive: true }); if (!acquireLock(lockPath)) { return { success: false, message: '无法获取文件锁,请稍后重试' }; } try { const targetFile = filePath ? path.join(skillDir, filePath) : path.join(skillDir, 'SKILL.md'); const tmpFile = targetFile + '.tmp'; // 写入临时文件 fs.mkdirSync(path.dirname(targetFile), { recursive: true }); fs.writeFileSync(tmpFile, content, 'utf-8'); // 安全扫描(扫描临时文件,在 rename 之前) // 临时将 tmp 文件 rename 为正式名以便 scanSkill 扫描整个目录 // 但先备份原文件,扫描后根据结果决定保留还是回滚 const origExists = fs.existsSync(targetFile); let origContent: string | null = null; if (origExists) { origContent = fs.readFileSync(targetFile, 'utf-8'); fs.unlinkSync(targetFile); } fs.renameSync(tmpFile, targetFile); const scanResult = scanSkill(skillDir); if (scanResult.verdict === 'dangerous') { // 回滚:恢复原文件或删除新文件 if (origContent !== null) { fs.writeFileSync(targetFile, origContent, 'utf-8'); } else { fs.unlinkSync(targetFile); } return { success: false, message: `安全扫描未通过: ${scanResult.summary}`, scanResult }; } // 同步 DB await this.syncDb(name, content); // 热更新缓存 await this.skillLoader.reloadSkill(name); const level = scanResult.verdict === 'caution' ? ' (有警告)' : ''; return { success: true, message: `Skill "${name}" 已保存${level}`, scanResult }; } finally { releaseLock(lockPath); } } private async syncDb(name: string, content: string): Promise { const descMatch = content.match(/^description:\s*(.+)$/m); const description = descMatch?.[1]?.trim() ?? ''; const existing = await this.skillRepo.findOneBy({ name }); if (existing) { await this.skillRepo.update({ name }, { description }); } else { await this.skillRepo.save({ name, label: name, description, status: 1 } as any); } } } ``` - [ ] **Step 2: Verify no TypeScript errors** ```bash cd packages/backend && npx tsc --noEmit ``` - [ ] **Step 3: Commit** ```bash git add src/modules/netaclaw/skill_evolution/skill_writer.ts git commit -m "feat(skill-evolution): implement SkillWriter with atomic write, validation, and security scan" ``` --- ### Task 4: Extend SkillLoaderService — add reloadSkill() and getSkillSummaries() **Files:** - Modify: `src/modules/netaclaw/service/skill_loader.ts` - [ ] **Step 1: Add reloadSkill() method** In `src/modules/netaclaw/service/skill_loader.ts`, add the following methods inside the class body, before the closing `}` of the class (before line 137, after the `setSkillStatus` method): ```typescript /** 热更新单个 Skill(skill_manage 写入后调用) */ async reloadSkill(name: string): Promise { // 先从缓存中移除 this.skills.delete(name); // 尝试重新加载 const findInDir = async (dir: string): Promise => { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const skillMdPath = path.join(dir, entry.name, 'SKILL.md'); if (entry.name === name) { try { const raw = await fs.readFile(skillMdPath, 'utf-8'); const skill = this.parseSkillMd(raw); if (skill) { this.skills.set(skill.name, skill); this.logger.info('[SkillLoader] 已重新加载 Skill: %s', skill.name); return true; } } catch { /* file not found */ } } // 检查分类子目录 const nestedPath = path.join(dir, entry.name, name, 'SKILL.md'); try { const raw = await fs.readFile(nestedPath, 'utf-8'); const skill = this.parseSkillMd(raw); if (skill) { this.skills.set(skill.name, skill); this.logger.info('[SkillLoader] 已重新加载 Skill: %s (分类: %s)', skill.name, entry.name); return true; } } catch { /* not in this category */ } } } catch { /* dir not found */ } return false; }; await findInDir(this.skillsDir); } /** 获取 Skill 列表摘要(给 review agent 用) */ getSkillSummaries(): { name: string; description: string }[] { return Array.from(this.skills.values()).map(s => ({ name: s.name, description: s.description, })); } /** 获取 skills 目录路径 */ getSkillsDir(): string { return this.skillsDir; } ``` - [ ] **Step 2: Verify no TypeScript errors** ```bash cd packages/backend && npx tsc --noEmit ``` - [ ] **Step 3: Commit** ```bash git add src/modules/netaclaw/service/skill_loader.ts git commit -m "feat(skill-evolution): add reloadSkill(), getSkillSummaries(), getSkillsDir() to SkillLoader" ``` --- ### Task 5: Create skill_manage tool **Files:** - Create: `src/modules/netaclaw/tools/builtin/skill_manage.ts` - [ ] **Step 1: Implement createSkillManageTool** Create `src/modules/netaclaw/tools/builtin/skill_manage.ts`: ```typescript import { Type, Static } from '@sinclair/typebox'; import { AnyAgentTool } from '../common.js'; import { SkillWriter } from '../../skill_evolution/skill_writer.js'; const skillManageParams = Type.Object({ action: Type.Union([ Type.Literal('create'), Type.Literal('edit'), Type.Literal('patch'), Type.Literal('delete'), Type.Literal('write_file'), Type.Literal('remove_file'), ]), name: Type.String({ description: 'Skill 名称(小写字母数字连字符)' }), content: Type.Optional(Type.String({ description: 'SKILL.md 完整内容(create/edit 时必需)' })), old_string: Type.Optional(Type.String({ description: 'patch: 要替换的文本' })), new_string: Type.Optional(Type.String({ description: 'patch: 替换后的文本' })), replace_all: Type.Optional(Type.Boolean({ description: 'patch: 替换所有匹配', default: false })), category: Type.Optional(Type.String({ description: '可选分类目录' })), file_path: Type.Optional(Type.String({ description: '支持文件路径(write_file/remove_file)' })), file_content: Type.Optional(Type.String({ description: '支持文件内容(write_file)' })), }); type SkillManageParams = Static; export interface SkillManageToolOpts { allowedEditSkills?: string[]; // 允许编辑的已有 skill 列表 allowCreateNew?: boolean; // 是否允许创建新 skill } export function createSkillManageTool( writer: SkillWriter, opts: SkillManageToolOpts = {}, ): AnyAgentTool { const { allowedEditSkills = [], allowCreateNew = true } = opts; return { name: 'skill_manage', label: '管理 Skill', description: '创建、编辑、修补或删除 Skill。支持写入/删除支持文件。', parameters: skillManageParams, async execute(_id: string, params: SkillManageParams): Promise { const { action, name } = params; // 权限检查 if ((action === 'edit' || action === 'patch') && allowedEditSkills.length > 0) { if (!allowedEditSkills.includes(name)) { return `错误: 不允许编辑 Skill "${name}",只能编辑: ${allowedEditSkills.join(', ')}`; } } if (action === 'create' && !allowCreateNew) { return '错误: 当前配置不允许创建新 Skill'; } switch (action) { case 'create': { if (!params.content) return '错误: create 操作需要 content 参数'; const r = await writer.createSkill(name, params.content, params.category); return r.message; } case 'edit': { if (!params.content) return '错误: edit 操作需要 content 参数'; const r = await writer.editSkill(name, params.content); return r.message; } case 'patch': { if (!params.old_string || params.new_string === undefined) { return '错误: patch 操作需要 old_string 和 new_string 参数'; } const r = await writer.patchSkill(name, params.old_string, params.new_string, params.file_path, params.replace_all); return r.message; } case 'delete': { const r = await writer.deleteSkill(name); return r.message; } case 'write_file': { if (!params.file_path || !params.file_content) { return '错误: write_file 操作需要 file_path 和 file_content 参数'; } const r = await writer.writeFile(name, params.file_path, params.file_content); return r.message; } case 'remove_file': { if (!params.file_path) return '错误: remove_file 操作需要 file_path 参数'; const r = await writer.removeFile(name, params.file_path); return r.message; } default: return `错误: 未知操作 "${action}"`; } }, }; } ``` - [ ] **Step 2: Commit** ```bash git add src/modules/netaclaw/tools/builtin/skill_manage.ts git commit -m "feat(skill-evolution): add skill_manage tool with permission checks" ``` --- ### Task 6: Create skill_list tool **Files:** - Create: `src/modules/netaclaw/tools/builtin/skill_list.ts` - [ ] **Step 1: Implement createSkillListTool** Create `src/modules/netaclaw/tools/builtin/skill_list.ts`: ```typescript import { Type, Static } from '@sinclair/typebox'; import { AnyAgentTool } from '../common.js'; import { SkillLoaderService } from '../../service/skill_loader.js'; const skillListParams = Type.Object({ category: Type.Optional(Type.String({ description: '按分类过滤' })), }); type SkillListParams = Static; export function createSkillListTool(skillLoader: SkillLoaderService): AnyAgentTool { return { name: 'skill_list', label: '查看 Skill 列表', description: '列出所有已有的 Skill 及其描述,用于避免重复创建。', parameters: skillListParams, async execute(_id: string, _params: SkillListParams): Promise { const summaries = skillLoader.getSkillSummaries(); if (summaries.length === 0) return '当前没有任何 Skill。'; return summaries.map(s => `- ${s.name}: ${s.description}`).join('\n'); }, }; } ``` - [ ] **Step 2: Commit** ```bash git add src/modules/netaclaw/tools/builtin/skill_list.ts git commit -m "feat(skill-evolution): add skill_list read-only tool" ``` --- ### Task 7: Create review.ts — background review agent **Files:** - Create: `src/modules/netaclaw/skill_evolution/review.ts` - [ ] **Step 1: Implement spawnSkillReview and buildSkillReviewPrompt** Create `src/modules/netaclaw/skill_evolution/review.ts`: ```typescript import { LLMMessage } from '../plugins/plugin_entry.js'; import { AgentConfig, runAgent } from '../runtime/agent.js'; import { AnyAgentTool } from '../tools/common.js'; import { SkillLoaderService } from '../service/skill_loader.js'; import { SkillWriter } from './skill_writer.js'; import { createSkillManageTool, SkillManageToolOpts } from '../tools/builtin/skill_manage.js'; import { createSkillListTool } from '../tools/builtin/skill_list.js'; import { Repository } from 'typeorm'; import { NetaClawSkillEntity } from '../entity/skill.js'; import { NetaClawAgentEntity } from '../entity/agent.js'; const SKILL_REVIEW_SYSTEM_PROMPT = `你是一个 Skill 提炼专家。你的任务是分析对话历史,提取可复用的方法论并保存为 Skill。 Skill 格式要求: - SKILL.md 必须包含 YAML frontmatter(name + description) - 正文用 Markdown,包含:触发条件、工作流程、规则约束 - name 使用小写字母、数字、连字符(如 nginx-deploy-config) - description 一句话描述 skill 的用途 你只有 8 轮工具调用机会,请高效行动。`; export function buildSkillReviewPrompt( linkedSkills: string[], allowOptimize: boolean, allowCreateNew: boolean, ): string { let prompt = '回顾上面的对话,考虑是否需要保存或更新 skill。\n\n'; prompt += '关注:\n'; prompt += '1. 是否使用了非显而易见的方法来完成任务?\n'; prompt += '2. 是否经历了试错或因实际发现而改变了方案?\n'; prompt += '3. 用户是否期望或希望不同的方法或结果?\n\n'; if (allowOptimize && linkedSkills.length > 0) { prompt += `当前 Agent 已关联以下 skill:${linkedSkills.join(', ')}\n`; prompt += '请优先检查这些已有 skill 是否可以根据本次对话的经验进行优化(用 patch 更新)。\n\n'; } if (allowCreateNew) { prompt += '如果发现了全新的可复用方法论且没有相关 skill,请创建新 skill。\n\n'; } prompt += '操作指南:\n'; prompt += '- 先用 skill_list 查看已有 skill,避免重复创建\n'; prompt += '- 优化已有 skill 时优先用 patch(局部替换),避免 edit 全量重写\n'; prompt += '- Skill 内容应聚焦于可复用的方法论,而非具体的业务数据\n'; prompt += '- 如果没有值得保存的内容,直接说"无需保存"并停止\n'; return prompt; } export interface SkillReviewContext { conversationHistory: LLMMessage[]; parentAgentConfig: AgentConfig; skillLoader: SkillLoaderService; skillRepo: Repository; agentRepo: Repository; // 用于自动关联新 skill agentName: string; linkedSkills: string[]; allowOptimize: boolean; allowCreateNew: boolean; } export async function spawnSkillReview(ctx: SkillReviewContext): Promise { const reviewPrompt = buildSkillReviewPrompt( ctx.linkedSkills, ctx.allowOptimize, ctx.allowCreateNew, ); const reviewConfig: AgentConfig = { ...ctx.parentAgentConfig, name: `${ctx.parentAgentConfig.name}_skill_reviewer`, systemPrompt: SKILL_REVIEW_SYSTEM_PROMPT, maxToolRounds: 8, skills: [], // 清除继承的 skills,review agent 不需要父 agent 的 skill prompts }; const writer = new SkillWriter( ctx.skillLoader.getSkillsDir(), ctx.skillRepo, ctx.skillLoader, ); const toolOpts: SkillManageToolOpts = { allowedEditSkills: ctx.allowOptimize ? ctx.linkedSkills : [], allowCreateNew: ctx.allowCreateNew, }; const tools: AnyAgentTool[] = [ createSkillManageTool(writer, toolOpts), createSkillListTool(ctx.skillLoader), ]; // 快照当前 skill 列表,用于 review 后检测新增 const skillsBefore = new Set(ctx.skillLoader.getSkillSummaries().map(s => s.name)); await runAgent({ agentConfig: reviewConfig, tools, userMessage: reviewPrompt, history: ctx.conversationHistory, }); // 自动关联新创建的 skill 到当前 Agent if (ctx.allowCreateNew) { const skillsAfter = ctx.skillLoader.getSkillSummaries().map(s => s.name); const newSkills = skillsAfter.filter(s => !skillsBefore.has(s)); if (newSkills.length > 0) { const agent = await ctx.agentRepo.findOneBy({ name: ctx.agentName }); if (agent) { const updated = [...(agent.skills ?? []), ...newSkills]; await ctx.agentRepo.update({ name: ctx.agentName }, { skills: updated }); } } } } ``` - [ ] **Step 2: Verify no TypeScript errors** ```bash cd packages/backend && npx tsc --noEmit ``` - [ ] **Step 3: Commit** ```bash git add src/modules/netaclaw/skill_evolution/review.ts git commit -m "feat(skill-evolution): implement background skill review agent" ``` --- ### Task 8: Integrate skill evolution trigger into controller/chat.ts **Files:** - Modify: `src/modules/netaclaw/controller/chat.ts` - [ ] **Step 1: Add imports and inject dependencies** In `src/modules/netaclaw/controller/chat.ts`, add new imports at the top (after existing imports, avoid duplicating already-present ones): ```typescript import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { NetaClawSkillEntity } from '../entity/skill.js'; import { spawnSkillReview } from '../skill_evolution/review.js'; ``` Note: `NetaClawAgentService` and `@Inject()` are already imported in the file. Only add the above new imports. Add injections inside the class (after `skillLoader`): ```typescript @Inject() agentService: NetaClawAgentService; @InjectEntityModel(NetaClawSkillEntity) skillRepo: Repository; ``` Note: If `agentService` is already injected (check the file first), skip that injection. - [ ] **Step 2: Add skill evolution trigger after runAgent()** In the `chat` method, after `await this.sessionService.saveMessage(sessionId, { role: 'assistant', ... })` and before the `return` statement, add: ```typescript // --- Skill 进化触发 --- const agentEntity = await this.agentService.agentRepo.findOneBy({ name: agentName }); const evoConfig = (agentEntity?.config as any)?.skillEvolution; if (evoConfig?.enabled && result.toolCallCount > 0) { const session = await this.sessionService.sessionRepo.findOneBy({ sessionId }); const meta = (session?.metadata ?? {}) as Record; const totalCalls = (meta.toolCallsSinceReview ?? 0) + result.toolCallCount; const nudgeInterval = evoConfig.nudgeInterval ?? 10; if (totalCalls >= nudgeInterval) { // 重置计数器 await this.sessionService.sessionRepo.update({ sessionId }, { metadata: { ...meta, toolCallsSinceReview: 0 }, }); // 异步触发 review,不阻塞响应 spawnSkillReview({ conversationHistory: history, parentAgentConfig: agentConfig, skillLoader: this.skillLoader, skillRepo: this.skillRepo, agentRepo: this.agentService.agentRepo, agentName, linkedSkills: agentEntity?.skills ?? [], allowOptimize: evoConfig.allowOptimizeLinked ?? true, allowCreateNew: evoConfig.allowCreateNew ?? true, }).catch(err => this.logger.warn('[SkillEvolution] Review failed:', err)); } else { await this.sessionService.sessionRepo.update({ sessionId }, { metadata: { ...meta, toolCallsSinceReview: totalCalls }, }); } } ``` - [ ] **Step 3: Verify no TypeScript errors** ```bash cd packages/backend && npx tsc --noEmit ``` - [ ] **Step 4: Commit** ```bash git add src/modules/netaclaw/controller/chat.ts git commit -m "feat(skill-evolution): integrate skill review trigger into chat controller" ``` --- ### Task 9: Final verification - [ ] **Step 1: Verify full TypeScript compilation** ```bash cd packages/backend && npx tsc --noEmit ``` Expected: No errors. - [ ] **Step 2: Verify all new files exist** ```bash ls -la src/modules/netaclaw/skill_evolution/ ls -la src/modules/netaclaw/tools/builtin/skill_manage.ts ls -la src/modules/netaclaw/tools/builtin/skill_list.ts ``` Expected: guard_patterns.ts, guard.ts, skill_writer.ts, review.ts in skill_evolution/; skill_manage.ts and skill_list.ts in tools/builtin/. - [ ] **Step 3: Verify skill_loader has new methods** ```bash cd packages/backend && grep -n "reloadSkill\|getSkillSummaries\|getSkillsDir" src/modules/netaclaw/service/skill_loader.ts ``` Expected: All three methods found. - [ ] **Step 4: Final commit** ```bash git add -A && git status git commit -m "feat(skill-evolution): complete skill self-evolution system" ```