50 KiB
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:
/**
* 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:
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
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:
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
cd packages/backend && npx tsc --noEmit
- Step 3: Commit
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:
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<NetaClawSkillEntity>,
private skillLoader: SkillLoaderService,
) {}
async createSkill(name: string, content: string, category?: string): Promise<WriteResult> {
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<WriteResult> {
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<WriteResult> {
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<WriteResult> {
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<WriteResult> {
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<WriteResult> {
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<WriteResult> {
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<void> {
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
cd packages/backend && npx tsc --noEmit
- Step 3: Commit
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):
/** 热更新单个 Skill(skill_manage 写入后调用) */
async reloadSkill(name: string): Promise<void> {
// 先从缓存中移除
this.skills.delete(name);
// 尝试重新加载
const findInDir = async (dir: string): Promise<boolean> => {
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
cd packages/backend && npx tsc --noEmit
- Step 3: Commit
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:
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<typeof skillManageParams>;
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<string> {
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
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:
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<typeof skillListParams>;
export function createSkillListTool(skillLoader: SkillLoaderService): AnyAgentTool {
return {
name: 'skill_list',
label: '查看 Skill 列表',
description: '列出所有已有的 Skill 及其描述,用于避免重复创建。',
parameters: skillListParams,
async execute(_id: string, _params: SkillListParams): Promise<string> {
const summaries = skillLoader.getSkillSummaries();
if (summaries.length === 0) return '当前没有任何 Skill。';
return summaries.map(s => `- ${s.name}: ${s.description}`).join('\n');
},
};
}
- Step 2: Commit
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:
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<NetaClawSkillEntity>;
agentRepo: Repository<NetaClawAgentEntity>; // 用于自动关联新 skill
agentName: string;
linkedSkills: string[];
allowOptimize: boolean;
allowCreateNew: boolean;
}
export async function spawnSkillReview(ctx: SkillReviewContext): Promise<void> {
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
cd packages/backend && npx tsc --noEmit
- Step 3: Commit
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):
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):
@Inject()
agentService: NetaClawAgentService;
@InjectEntityModel(NetaClawSkillEntity)
skillRepo: Repository<NetaClawSkillEntity>;
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:
// --- 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<string, any>;
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
cd packages/backend && npx tsc --noEmit
- Step 4: Commit
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
cd packages/backend && npx tsc --noEmit
Expected: No errors.
- Step 2: Verify all new files exist
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
cd packages/backend && grep -n "reloadSkill\|getSkillSummaries\|getSkillsDir" src/modules/netaclaw/service/skill_loader.ts
Expected: All three methods found.
- Step 4: Final commit
git add -A && git status
git commit -m "feat(skill-evolution): complete skill self-evolution system"