# P2: Agent Skills 标准兼容 + References 加载 实施计划

> **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:** 兼容 Agent Skills 社区标准（名称验证、hidden skill、字段映射），改造 prompt 索引区分三种 skill 类型，改造 read_skill 返回格式强制 Agent 读取 required references。

**Architecture:** 新增 `validateSkillName()` 验证函数。`buildSkillsPrompt()` 输出增加 type 属性和 compute-entry 的 interface 摘要。`read_skill` 工具返回区分 `<skill_required_references>` 和 `<skill_optional_references>`。基础设施文件从 collectFiles 中过滤。

**Tech Stack:** TypeBox, XML 模板, Midway.js

**Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 5 + 10.5-10.7 + 10.10

**Depends on:** P1 (SkillConfigService, SkillClassification must be integrated into SkillLoaderService before P2 Task 2 can work. Execute P1 Tasks 1-2 first.)

---

### Task 1: 名称验证函数（无依赖，可独立执行）

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`

- [ ] **Step 1: 在 skill_loader.ts 顶部新增 validateSkillName 函数**

在 class 定义之前添加：

```typescript
const MAX_NAME_LENGTH = 64;

export function validateSkillName(name: string, parentDirName?: string): string[] {
  const errors: string[] = [];
  if (!name) { errors.push('name is required'); return errors; }
  if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters`);
  if (!/^[a-z0-9-]+$/.test(name)) errors.push('name must be lowercase a-z, 0-9, hyphens only');
  if (name.startsWith('-') || name.endsWith('-')) errors.push('name must not start or end with hyphen');
  if (name.includes('--')) errors.push('name must not contain consecutive hyphens');
  if (parentDirName && name !== parentDirName) errors.push(`name "${name}" does not match directory "${parentDirName}"`);
  return errors;
}
```

- [ ] **Step 2: 在 parseSkillMd 中调用验证（warning 级别，不阻塞加载）**

在 `parseSkillMd` 方法中，解析出 name 后添加：

```typescript
const nameErrors = validateSkillName(name);
if (nameErrors.length > 0) {
  this.logger.warn('[SkillLoader] Skill "%s" name validation: %s', name, nameErrors.join(', '));
}
```

- [ ] **Step 3: 在 skill_manage.ts 和 skill_installer.ts 的创建/安装流程中调用验证（error 级别，阻塞）**

在 `skill_manage.ts` 的 `execute` 方法中，`create` 分支的 `parseSkillMd` 之后添加：

```typescript
const nameErrors = validateSkillName(parsed.name, params.name);
if (nameErrors.length > 0) {
  return textResult(`名称不合规: ${nameErrors.join('; ')}`);
}
```

在 `skill_installer.ts` 的 `installFromGitHub` 和 `installFromZip` 中，`parseSkillMd` 之后添加类似校验。

- [ ] **Step 4: Commit**

```bash
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts \
       packages/backend/src/modules/netaclaw/tools/builtin/skill_manage.ts \
       packages/backend/src/modules/netaclaw/service/skill_installer.ts
git commit -m "feat(skill): add Agent Skills standard name validation"
```

---

### Task 2: buildSkillsPrompt 输出改造

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`

- [ ] **Step 1: 改造 buildSkillsPrompt 方法**

替换现有 `buildSkillsPrompt` 方法：

```typescript
buildSkillsPrompt(skillNames: string[], availableTools: string[]): string {
  const filtered = this.filterByConditions(skillNames, availableTools);
  if (filtered.length === 0) return '';

  const lines: string[] = [];
  let totalChars = 0;

  for (const s of filtered) {
    // 跳过 hidden skill
    const fm = s.metadata as any;
    if (fm?.['disable-model-invocation'] === true) continue;

    const classification = this.skillConfig.classify(s.name);
    const category = fm?.category || '通用';
    const tags = Array.isArray(fm?.tags) ? fm.tags.join(',') : '';

    let body = `    ${s.description}`;

    // compute-entry: 附带 interface.input 摘要
    if (classification === 'compute-entry') {
      const config = this.skillConfig.getConfig(s.name);
      if (config?.interface?.input) {
        const inputDesc = Object.entries(config.interface.input)
          .map(([k, v]) => `${k}(${v.type}${v.required ? ',必填' : ''}${v.default ? ',默认' + v.default : ''})`)
          .join(', ');
        body += `\n    输入: ${inputDesc}`;
      }
    }

    const line = `  <skill name="${s.name}" type="${classification}" category="${category}" tags="${tags}">\n${body}\n  </skill>`;
    if (totalChars + line.length > SkillLoaderService.MAX_SKILLS_PROMPT_CHARS) break;
    lines.push(line);
    totalChars += line.length;
  }

  if (lines.length === 0) return '';

  return `\n\n## 技能（必须扫描）\n回复前，扫描以下技能列表。prompt 类型用 read_skill 加载指令，compute-entry 类型用 execute_skill 直接调用，compute-toolkit 类型用 read_skill 加载指令后通过 bash 执行。\n读取 skill 后，如果返回中包含 <skill_required_references>，你必须在执行任何操作前先用 read_skill_file 逐一读取列出的所有文档。这不是建议，是强制要求。\n\n<available_skills>\n${lines.join('\n')}\n</available_skills>`;
}
```

- [ ] **Step 2: 验证编译通过**

Run: `cd packages/backend && npx tsc --noEmit`

- [ ] **Step 3: Commit**

```bash
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
git commit -m "feat(skill): enhance buildSkillsPrompt with type classification and required references guidance"
```

---

### Task 3: read_skill 返回格式改造

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts`

- [ ] **Step 1: 改造 read_skill 的 execute 方法**

替换现有 `execute` 方法体：

```typescript
async execute(_id, params) {
  const skill = skillLoader.getSkill(params.name);
  if (!skill) {
    return textResult(`未找到名为 "${params.name}" 的 skill`);
  }

  let result = skill.content;

  // 获取 references 配置
  const config = skillLoader.getSkillConfig(params.name);
  const configRefs = config?.references;
  const fmRefs = (skill.metadata as any)?.references;
  const refs = configRefs || fmRefs || null;

  const requiredRefs: string[] = refs?.required || [];
  const allFiles = skill.files || [];

  // 基础设施文件过滤
  const INFRA_FILES = new Set([
    'skill.config.yaml', 'requirements.txt', 'package.json',
    'package-lock.json', 'tsconfig.json', '.env',
  ]);
  const referenceFiles = allFiles.filter(f => !INFRA_FILES.has(f) && !requiredRefs.includes(f));

  if (requiredRefs.length > 0) {
    result += `\n\n<skill_required_references>`;
    result += `\n⚠️ 执行此 skill 的任务前，你必须先用 read_skill_file 读取以下文档：`;
    for (const ref of requiredRefs) {
      result += `\n- ${ref}`;
    }
    result += `\n未读取这些文档就执行操作会导致错误。`;
    result += `\n</skill_required_references>`;
  }

  if (referenceFiles.length > 0) {
    result += `\n\n<skill_optional_references>`;
    result += `\n以下文档可按需读取（用 read_skill_file 工具）：`;
    for (const ref of referenceFiles) {
      result += `\n- ${ref}`;
    }
    result += `\n</skill_optional_references>`;
  }

  return textResult(result);
},
```

- [ ] **Step 2: 验证编译通过**

Run: `cd packages/backend && npx tsc --noEmit`

- [ ] **Step 3: Commit**

```bash
git add packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts
git commit -m "feat(skill): enhance read_skill with required/optional references separation"
```

---

### Task 4: collectFiles 基础设施文件过滤

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`

- [ ] **Step 1: 在 collectFiles 方法中过滤基础设施文件**

在 `collectFiles` 方法的 `else if (entry.name !== 'SKILL.md')` 条件中，增加过滤：

```typescript
const INFRA_FILES = new Set([
  'skill.config.yaml', 'requirements.txt', 'package.json',
  'package-lock.json', 'tsconfig.json', '.env',
]);

// 在 else if 分支中
} else if (entry.name !== 'SKILL.md' && !INFRA_FILES.has(entry.name)) {
  const rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
  results.push(rel);
}
```

- [ ] **Step 2: Commit**

```bash
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
git commit -m "feat(skill): filter infrastructure files from skill file listing"
```

---

### Task 5: read_skill task 参数与 routes 匹配（Spec 10.10）

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts`

- [ ] **Step 1: 给 ReadSkillParams 新增可选 task 参数**

```typescript
const ReadSkillParams = Type.Object({
  name: Type.String({ description: '要读取的 skill 名称' }),
  task: Type.Optional(Type.String({ description: '当前任务描述，用于自动匹配需要读取的文档' })),
});
```

- [ ] **Step 2: 在 execute 方法中实现 routes 匹配**

在获取 `requiredRefs` 之后、构建返回结果之前，添加 routes 匹配逻辑：

```typescript
// routes 匹配：如果传入了 task 且 config 有 routes，尝试关键词匹配
let routeMatchedRefs: string[] = [];
if (params.task && refs?.routes) {
  const taskLower = params.task.toLowerCase();
  for (const route of refs.routes) {
    if (route.match.some(keyword => taskLower.includes(keyword.toLowerCase()))) {
      routeMatchedRefs.push(...route.required_refs);
    }
  }
  routeMatchedRefs = [...new Set(routeMatchedRefs)]; // 去重
}

// 合并 required + route matched
const allRequired = [...new Set([...requiredRefs, ...routeMatchedRefs])];
```

如果 `routeMatchedRefs` 命中了文档，尝试直接读取并拼接到返回结果中（减少 Agent 二次调用）：

```typescript
if (routeMatchedRefs.length > 0) {
  result += `\n\n<skill_route_matched_references>`;
  for (const ref of routeMatchedRefs) {
    const filePath = skillLoader.getSkillFilePath(params.name, ref);
    if (filePath) {
      try {
        const content = await fs.readFile(filePath, 'utf-8');
        result += `\n\n--- ${ref} ---\n${content}`;
      } catch { /* skip unreadable */ }
    }
  }
  result += `\n</skill_route_matched_references>`;
}
```

- [ ] **Step 3: Commit**

```bash
git add packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts
git commit -m "feat(skill): add task parameter and routes matching to read_skill tool"
```

---

### Task 6: Agent 分配 skill 时的类型校验（Spec 10.6）

**Files:**
- Modify: `packages/frontend/src/modules/agent/views/agent-edit.vue`
- Modify: `packages/backend/src/modules/netaclaw/controller/agent.ts`

- [ ] **Step 1: metas 端点返回 classification 字段**

在 `SkillLoaderService.getSkillMetas()` 中，返回对象添加（如果 P3 Task 5 尚未添加）：

```typescript
classification: this.skillConfig.classify(fs.name),
```

- [ ] **Step 2: 前端 skill 选择器显示分类标签**

在 `agent-edit.vue` 的可选 Skill 列表项中（约 line 38），在现有 `skillType` 标签旁新增分类标签：

```vue
<el-tag v-if="sk.classification" size="small"
  :type="sk.classification === 'compute-entry' ? 'success' : sk.classification === 'compute-toolkit' ? '' : 'info'">
  {{ sk.classification }}
</el-tag>
```

已选择列表中同样添加：

```vue
<span class="item-main">{{ getSkillLabel(name) }}</span>
<el-tag size="small" :type="getSkillClassification(name) === 'compute-entry' ? 'success' : 'info'">
  {{ getSkillClassification(name) }}
</el-tag>
```

新增辅助函数：

```typescript
function getSkillClassification(name: string): string {
  const meta = skillMetas.value.find((s) => s.name === name);
  return meta?.classification || 'prompt';
}
```

- [ ] **Step 3: 前端选择 compute skill 时显示工具权限 warning**

在 `addSkill` 函数中添加校验：

```typescript
function addSkill(name: string) {
  if (!form.value.skills.includes(name)) {
    form.value.skills.push(name);
    // 校验工具权限
    const classification = getSkillClassification(name);
    if (classification === 'compute-entry') {
      ElMessage.warning(`"${name}" 是 compute-entry 类型，请确保该 Agent 已启用 execute_skill 工具`);
    } else if (classification === 'compute-toolkit') {
      ElMessage.warning(`"${name}" 是 compute-toolkit 类型，请确保该 Agent 已启用 bash 工具`);
    }
  }
}
```

- [ ] **Step 4: 后端保存时返回 warnings**

在 `controller/agent.ts` 的 `update` 方法中，注入 `SkillLoaderService`，保存前校验：

```typescript
@Inject()
skillLoader: SkillLoaderService;

@Post('/update')
async update(@Body() body: any) {
  const warnings: string[] = [];
  if (body.skills?.length) {
    for (const skillName of body.skills) {
      const classification = this.skillLoader.getSkillClassification(skillName);
      if (classification === 'compute-entry') {
        warnings.push(`Skill "${skillName}" 需要 execute_skill 工具`);
      } else if (classification === 'compute-toolkit') {
        warnings.push(`Skill "${skillName}" 需要 bash 工具`);
      }
    }
  }
  await this.agentService.update(body);
  return { code: 1000, message: 'success', warnings };
}
```

- [ ] **Step 5: Commit**

```bash
git add packages/frontend/src/modules/agent/views/agent-edit.vue \
       packages/backend/src/modules/netaclaw/controller/agent.ts
git commit -m "feat(skill): add skill classification display and tool permission warnings in agent editor"
```

---

### Task 7: skill_context.ts 废弃标记（Spec 10.12）

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/service/skill_context.ts`

- [ ] **Step 1: 添加 @deprecated 注释**

在 `buildSkillContext` 函数上方添加：

```typescript
/**
 * @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。
 * 此函数不再被主链路调用，保留仅为兼容可能的外部引用。
 */
```

- [ ] **Step 2: Commit**

```bash
git add packages/backend/src/modules/netaclaw/service/skill_context.ts
git commit -m "chore(skill): mark buildSkillContext as deprecated"
```
