GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-13-skill-evolution.md
2026-05-20 21:39:12 +08:00

50 KiB
Raw Blame History

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 自动提炼/优化 Skillskill_manage 工具支持运行时创建/编辑SkillGuard 安全扫描保护。

Architecture: 4 层模块guard_patterns威胁规则定义→ guard扫描引擎→ skill_writer原子写入 + 验证 + DB 同步)→ toolsskill_manage/skill_list 工具。review.ts 编排后台 review agentchat.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.py84 条正则规则按类别组织
 */

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):

  /** 热更新单个 Skillskill_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 frontmattername + 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: [],  // 清除继承的 skillsreview 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"