---
url: /wave-agent/specs/ecosystem/agent-skills.md
description: '`SKILL.md` 技能包的发现与解析、`/` 调用、`allowed-tools` 限制与派生到子代理'
---

# 功能规格说明：Agent 技能支持

**创建日期**：2024-12-19

## 用户场景与测试 *（必填）*

### 用户故事：创建个人 Agent 技能（优先级：P1）

开发者希望为个人工作流创建可跨所有 Wave 项目使用的可复用技能。他们需要将自己的专业知识打包为可发现的能力，以便 Wave 在相关时自主调用。

**为什么是这个优先级**：这是核心价值主张——使用户能够用自己的专业知识扩展 Wave 的能力。

**独立测试**：可以通过创建包含 SKILL.md 文件的个人技能目录结构并验证 Wave 可以跨多个项目发现和使用它来完整测试。

**验收场景**：

1. **假设**我想创建个人技能，**当**我在 `~/.wave/skills/my-skill-name/` 创建带有有效 SKILL.md 文件的目录时，**则**Wave 应该识别并使此技能在我所有项目中可用
2. **假设**我已创建了带有正确 YAML frontmatter 的个人技能，**当**我在任何项目中与 Wave 交互时，**则**Wave 应该基于我的请求自主决定何时调用此技能
3. **假设**我有包含辅助文件的个人技能，**当**Wave 调用该技能时，**则**它应该可以访问所有引用的文件（脚本、模板、文档）

***

### 用户故事：技能里的目录占位符与 frontmatter 解析（优先级：P1）

技能作者需要用 `${WAVE_SKILL_DIR}` 之类的占位符在正文里引用技能目录内的脚本与模板（如 `node ${WAVE_SKILL_DIR}/tool.mjs`），也需要用 YAML 的多行写法描述「何时该用这个技能」。前者必须替换成可直接执行的绝对路径，后者的多行文本必须按 YAML 语义解析成真实描述。同一组占位符还会出现在 frontmatter 的 `allowed-tools` 里——技能放行自己那条命令的写法就是 `Bash(node ${WAVE_SKILL_DIR}/scripts/x.mjs*)`——这类规则在注册进权限系统前同样必须换成绝对路径，否则它永远匹配不上任何一条命令。

**为什么是这个优先级**：三者都直接决定技能能不能用——占位符不替换，正文里的命令在模型手上就是一条跑不通的路径；`allowed-tools` 里的占位符不替换更安静：不报错、不失败，规则只是永远匹配不上，作者以为放行了、实际每次都要用户确认；描述被解析成 `>-` 这类语法符号，「何时该用我」的信息整段丢失，技能就不会被主动调用（比"锦上添花"重要得多）。

**独立测试**：写一个正文含 `${WAVE_SKILL_DIR}/tool.mjs`、frontmatter 用 `description: >-` 加多行描述的技能，确认调用后模型看到的首行是 `Base directory for this skill: <绝对路径>`、正文里没有字面量占位符，且可用技能列表与调用结果里展示的是折叠后的完整描述；再写一个 `allowed-tools` 含 `${WAVE_SKILL_DIR}` 的技能，确认替换后声明的命令直接执行、且带残留占位符的规则会给出可见提示。

**验收场景**：

1. **假设**技能正文含 `${WAVE_SKILL_DIR}` 或 `${CLAUDE_SKILL_DIR}`，**当**技能被调用（`Skill` 工具或 `/skill-name`）时，**则**交给模型的正文首行是 `Base directory for this skill: <该技能目录的绝对路径>`，且正文中这些占位符已全部替换为同一路径；正文含 `${WAVE_PLUGIN_ROOT}`／`${CLAUDE_PLUGIN_ROOT}` 时替换为该技能所属插件的根目录。
2. **假设**技能的 frontmatter 用 YAML 块标量写多行值（`>`／`>-`／`|`／`|-` 等），**当**技能被加载时，**则**`description` 等字段必须是折叠／保留后的真实文本（不含 `>`、`|`、`-` 这类指示符），续行**不得**被解析成独立的 frontmatter 键；可用技能列表里展示的描述与技能管理界面用的描述都是这样解析后的结果。
3. **假设**技能的 frontmatter 只有单行值（`description: 一句话`），**当**技能被加载时，**则**解析结果与块标量路径完全一致（单行不能被块标量处理改坏，含引号包裹的值仍去掉引号）。
4. **假设**技能的 frontmatter 在 `allowed-tools` 里写了含 `${WAVE_SKILL_DIR}`／`${CLAUDE_SKILL_DIR}`（插件技能还可用 `${WAVE_PLUGIN_ROOT}`／`${CLAUDE_PLUGIN_ROOT}`）的规则，**当**技能被调用、这些规则被注册为临时放行规则时，**则**注册的是替换成绝对路径后的规则：声明的那条命令直接执行、不提示确认，且技能正文里与之匹配的注入命令同样直接执行；技能派生出的子代理拿到的也是替换后的规则。
5. **假设**技能的 `allowed-tools` 里有一个没有可替换来源的占位符（如非插件技能写了 `${WAVE_PLUGIN_ROOT}`），**当**技能被加载时，**则**该规则不被当作有效放行规则，并以一条可见提示说明它不会被替换——不允许把一个永不匹配的规则静默留在权限系统里。

***

### 用户故事：创建项目特定 Agent 技能（优先级：P2）

开发团队希望创建共享技能，封装项目特定知识、团队约定和工作流。这些技能应进行版本控制并自动对所有团队成员可用。

**为什么是这个优先级**：通过技能实现团队协作和知识共享，建立在个人技能的基础之上。

**独立测试**：可以通过在 `.wave/skills/` 目录中创建项目技能、提交到 git 并验证团队成员自动获得访问权来测试。

**验收场景**：

1. **假设**我想创建项目技能，**当**我在 `.wave/skills/my-skill-name/` 创建带有有效 SKILL.md 文件的目录时，**则**该技能应对所有参与此项目的团队成员可用
2. **假设**我将项目技能提交到 git，**当**团队成员克隆或拉取仓库时，**则**他们应该无需额外设置即可自动访问这些技能
3. **假设**存在项目技能，**当**任何团队成员在此项目中与 Wave 交互时，**则**Wave 应该将项目特定技能与个人技能一起考虑

***

### 用户故事：技能发现与调用（优先级：P1）

Wave 需要根据用户请求和技能描述自主发现何时使用可用技能，无需用户显式命令。

**为什么是这个优先级**：这是使技能有用的核心机制——基于上下文的自动智能调用。

**独立测试**：可以通过创建具有特定描述的技能并验证 Wave 对匹配请求适当调用它们来测试。

**验收场景**：

1. **假设**我有带有描述性 YAML frontmatter 的技能，**当**我发出匹配技能用途的请求时，**则**Wave 应该自主决定调用该技能
2. **假设**多个技能可能适用于一个请求，**当**Wave 评估可用技能时，**则**它应该基于描述和上下文选择最相关的技能
3. **假设**技能有辅助文件，**当**Wave 调用该技能时，**则**它应该渐进式地仅加载特定请求所需的文件

***

### 用户故事：技能管理与验证（优先级：P3）

用户需要验证其技能定义、了解技能结构要求并有效管理其技能集合。

**为什么是这个优先级**：虽然对可用性很重要，但这建立在核心功能之上，可以在基本技能支持工作后实现。

**独立测试**：可以通过创建无效技能并验证适当的错误消息以及管理技能集合来测试。

**验收场景**：

1. **假设**我创建了带有无效 YAML frontmatter 的 SKILL.md 文件，**当**Wave 尝试加载该技能时，**则**它应该提供关于需要修复内容的清晰错误消息
2. **假设**我有多个可用技能，**当**我请求有关技能的信息时，**则**Wave 应该能够列出并描述可用的个人和项目技能
3. **假设**我修改了技能的 SKILL.md 文件，**当**我与 Wave 交互时，**则**它应该自动重新加载更新后的技能定义

***

### 用户故事：用户可调用的带参数技能（优先级：P2）

用户希望使用斜杠命令语法显式调用技能并提供参数替换到技能内容中。他们还希望技能能够执行 bash 命令以提供动态信息。

**为什么是这个优先级**：增强技能的灵活性和能力，使其像自定义斜杠命令一样运行，同时保持其可发现性。

**独立测试**：可以通过使用 `/skill-name arg1 arg2` 调用技能并验证 `$1` 和 `$ARGUMENTS` 被正确替换，以及 bash 命令如 !`pwd` 和 `! pwd ` 被执行且输出上限为 30K 字符来测试。

**验收场景**：

1. **假设**我有带参数占位符（如 `$1`、`$ARGUMENTS`）的技能，**当**我使用 `/skill-name arg1 arg2` 调用时，**则**Wave 应该在处理前用提供的参数替换占位符
2. **假设**我有带 bash 命令占位符（如 !`pwd` 或 `! pwd `）的技能，**当**技能被调用时，**则**Wave 应该执行 bash 命令并用其原始 stdout 输出替换占位符（不带任何额外格式或代码块）。超过 30,000 字符的输出将被截断并附带预览和临时文件路径
3. **假设**技能已注册，**当**我在聊天中输入 `/` 时，**则**该技能应出现在斜杠命令建议中
4. **假设**我有不含参数占位符的技能，**当**我使用 `/skill-name arg1 arg2` 调用时，**则**Wave 应该自动将参数追加到技能内容末尾

***

### 用户故事：注入命令执行前的权限校验（优先级：P1）

作为用户，我希望技能与自定义命令正文里的内联注入命令（``!`cmd` ``）与代码块形式的注入命令在执行前经过**与 Bash 工具同一套**权限规则判定，未被允许的命令不执行，以便第三方分发来的技能不会借"正文注入"这条路径拿到超出我已授权范围的执行权。

**为什么是这个优先级**：这是一条**任意命令执行**通道，且是唯一一条不经过权限层的通道——它绕过了用户对 Bash 的规则、白名单与确认提示。技能与命令可以来自插件市场（他人分发），因此这条通道的暴露面不只是用户自己写的内容。

**独立测试**：配置一条拒绝规则（如 deny `Bash(curl*)`）并加载正文含 ``!`curl http://x` `` 的技能；断言该命令未被执行、调用被中止、错误文本含 `Shell command permission check failed for pattern`；再在技能 frontmatter 的 `allowed-tools` 中声明 `Bash(curl*)`，断言重新调用时命令被执行。

**验收场景**：

1. **假设**技能或自定义命令正文含注入命令（内联形式 ``!`cmd` `` 或代码块形式），**当**它被调用时，**则**每条命令在执行前按 Bash 工具同一套权限规则判定（同一套规则来源与同一套匹配语义，不另立一套）
2. **假设**某条注入命令命中了拒绝规则，**当**执行到它时，**则**整次调用被中止：该命令不执行、其后命令不执行、技能正文不交给模型，并向用户给出错误文本 `Shell command permission check failed for pattern "<匹配到的命令原文>": <原因>`
3. **假设**某条注入命令命中了 `allowed-tools` 中声明的模式（如 frontmatter 写了 `allowed-tools: Bash(gh pr view*)`），**当**执行到它时，**则**直接执行、不提示确认
4. **假设**某条注入命令未命中任何允许或拒绝规则，**当**当前环境无法向用户发起确认时，**则**按拒绝处理（中止整次调用），不静默执行
5. **假设**某条注入命令被授权执行但自身以非零退出码退出或超时，**当**它结束时，**则**不中止调用：该命令的错误输出以 `<error>Command failed (exit code N): …</error>` 形式留在正文里供模型看到，后续命令继续执行（与权限拒绝的中止行为区分开）
6. **假设**同一技能有多条注入命令，其中第一条被拒绝，**当**调用发生时，**则**后续命令一条都不执行（不做"跳过被拒的那条、继续跑其余"的降级）
7. **假设**正文里的 `!` 后紧跟反引号、但 `!` 之前是词字符（如 `` `#REF!`: 无效引用 ``），**当**正文被解析时，**则**不视为注入命令、不执行也不做权限判定——内联形式的识别要求 `!` 处于行首或空白之后（与 Claude Code 的 `INLINE_PATTERN` 相同）。否则正文里提到错误码（`#REF!` / `#DIV/0!`）的技能会因为解析出的"命令"未被授权而被整次拒绝

### 用户故事：限制技能的工具访问（优先级：P2）

用户希望在调用技能时限制 AI 可用的工具，无论是通过斜杠命令手动调用还是 AI 自主调用。这确保 AI 仅使用特定任务的预期工具，提高安全性和可靠性。

**为什么是这个优先级**：通过将 AI 的工具集限制为技能所需的工具来增强安全性和可靠性。

**独立测试**：可以通过在 frontmatter 中创建带有 `allowed-tools` 的技能并验证 AI 在技能调用时被限制为仅使用这些工具来测试。

**验收场景**：

1. **假设**我有在 frontmatter 中指定了 `allowed-tools` 的技能，**当**我使用斜杠命令调用时，**则**Wave 应该将 AI 限制为仅在该轮次中使用指定的工具
2. **假设**带有 `allowed-tools` 的技能被 AI 通过 `Skill` 工具自主调用，**当**技能执行时，**则**Wave 应该在 AI 该轮次的剩余时间内执行工具限制
3. **假设**技能将 `allowed-tools` 指定为 YAML 列表或逗号分隔的字符串，**当**技能被解析时，**则**Wave 应该正确识别并执行所有指定的工具

***

### 用户故事：将技能派生到子代理（优先级：P2）

用户希望在独立的子代理中执行复杂技能，以提供全新上下文或使用专门的代理类型。这有助于隔离技能的执行并利用特定的代理专业知识。

**为什么是这个优先级**：通过允许技能在专门上下文中运行来增强技能的能力，提高复杂任务结果的质量。

**独立测试**：可以通过在 frontmatter 中创建带有 `context: fork` 和可选 `agent` 字段的技能并验证 Wave 在指定类型的子代理中执行技能来测试。

**验收场景**：

1. **假设**我有在 frontmatter 中带有 `context: fork` 的技能，**当**技能被调用时（通过斜杠命令手动或 AI 自主），**则**Wave 应该在新子代理实例中执行技能内容
2. **假设**我有带有 `context: fork` 和 `agent: general-purpose` 的技能，**当**技能被调用时，**则**Wave 应该使用 `general-purpose` 子代理执行技能
3. **假设**技能被派生到子代理，**当**子代理运行时，**则**Wave 应该在工具的 short result（AI 调用）或用户消息中的 ToolBlock（手动调用）中提供子代理进度的实时更新（使用的工具、消耗的 token）
4. **假设**我有带有 `model: gpt-4o` 的技能，**当**技能被调用时（手动或自主），**则**Wave 应该使用指定模型执行
5. **假设**派生技能被手动调用，**当**子代理完成时，**则**其最终结果应作为用户消息中的 ToolBlock 展平到主对话中，且主代理不应被自动触发

***

### 用户故事：控制技能调用和可见性（优先级：P2）

用户希望控制技能的调用方式以及它们是否在斜杠命令菜单中可见。这允许仅由 AI 使用的"内部"技能或 AI 不应自动触发的"手动"技能。

**为什么是这个优先级**：为复杂的技能生态系统提供必要的控制，其中某些技能可能太强大或太具体而不适合 AI 自主调用，或某些技能仅作为 AI 的构建块。

**独立测试**：可以通过设置 `disable-model-invocation: true` 和 `user-invocable: false` 的各种组合并验证 `/` 菜单和 AI 工具提示中的可见性来测试。

**验收场景**：

1. **假设**技能在 frontmatter 中带有 `disable-model-invocation: true`，**当**Wave 为 AI 生成工具提示时，**则**此技能应从可用技能列表中排除
2. **假设**技能带有 `disable-model-invocation: true`，**当**AI 尝试通过 `Skill` 工具调用时，**则**Wave 应该阻止执行并向 AI 返回错误
3. **假设**技能在 frontmatter 中带有 `user-invocable: false`，**当**我在聊天中输入 `/` 时，**则**该技能不应出现在斜杠命令建议中
4. **假设**技能带有 `user-invocable: false`，**当**我尝试通过 `/skill-name` 执行时，**则**Wave 不应将其识别为有效命令

***

### 边界情况

* 当技能的 SKILL.md 文件有格式错误的 YAML frontmatter 时会发生什么？
* Wave 如何处理同名的个人技能和项目技能之间的冲突？
* 当引用的辅助文件（脚本、模板）缺失或不可访问时会发生什么？
* Wave 如何处理辅助文件中有循环引用的技能？
* 当技能名称超过 64 个字符或包含无效字符时会发生什么？
* **注入命令的权限判定用哪套规则？** 与 Bash 工具完全同一套：同一份规则来源（用户级 / 项目级 settings、`allowed-tools`、拒绝规则）、同一套模式匹配语义。不新增一套"技能专用"白名单，否则两份规则必然漂移，且用户在一个地方放行的命令会在另一个地方被悄悄执行。
* **注入命令被拒绝为什么要中止整次调用，而不是跳过该条？** 因为正文是**一个整体**：被拒命令的输出本该参与后文（例如 ``!`git status` `` 的结果会被后续指令引用），跳过它等于交给模型一段带空洞的指令，它仍可能凭残缺信息行动。中止让"没被授权"这件事显式可见，与 Claude Code 的行为一致（`Shell command permission check failed` 直接终止该次调用）。
* **注入命令与模型自己调用 Bash 的权限判定是同一个吗？** 是同一个判定入口，但**时机不同**：注入命令发生在技能/命令加载时（模型尚未介入），因此无法就地弹交互确认；模型调用 Bash 发生在模型回合内，可以弹确认。这个差异只影响"如何取得授权"，不影响"用什么规则判定"。
* **为什么参数要另开一条文件通道，而不是继续把 `$ARGUMENTS` 拼进命令行？** 因为那条命令行会被**真 shell** 解析（与 Claude Code 一致），而用户参数里出现 `>`、`&&`、反引号、引号、多行都是正常内容。转义解决不了：作者写的是模板，拼进来的字符串决定了 shell 的解析结构，任何「先转义再拼」都要作者自己对着每种上下文写对一遍。文件通道把「内容」与「命令结构」彻底分开——命令里只出现一个路径，参数怎么长都不影响 shell 怎么解析，权限判定与不放参数时逐字相同。不选 stdin 是因为一条正文可以有多条注入命令，stdin 只能给其中一条；不选环境变量是因为长 prompt 会撞上环境块大小上限（Windows 约 32KB），而参数恰恰可能是长文本。
