diff --git a/README.en.md b/README.en.md index 6d7c4305..20889e9c 100644 --- a/README.en.md +++ b/README.en.md @@ -35,6 +35,7 @@ A **locally runnable version** repaired from the leaked Claude Code source, with - **Computer Use desktop control** — [Guide](docs/features/computer-use.en.md) - **Memory System** (cross-session persistent memory) — [Usage Guide](docs/memory/01-usage-guide.md) - **Multi-Agent System** (agent orchestration, parallel tasks, Teams collaboration) — [Usage Guide](docs/agent/01-usage-guide.md) | [Implementation](docs/agent/02-implementation.md) +- **Skills System** (extensible capability plugins, custom workflows) — [Usage Guide](docs/skills/01-usage-guide.md) | [Implementation](docs/skills/02-implementation.md) - Fallback Recovery CLI mode (`CLAUDE_CODE_FORCE_RECOVERY_CLI=1 ./bin/claude-haha`) --- @@ -137,6 +138,7 @@ export PATH="$HOME/path/to/claude-code-haha/bin:$PATH" | [Computer Use](docs/features/computer-use.en.md) | Desktop control (screenshots, mouse, keyboard) | | [Memory System](docs/memory/01-usage-guide.md) | Cross-session persistent memory usage and implementation | | [Multi-Agent System](docs/agent/01-usage-guide.md) | Agent orchestration, parallel tasks and Teams collaboration | +| [Skills System](docs/skills/01-usage-guide.md) | Extensible capability plugins, custom workflows and conditional activation | | [Global Usage](docs/guide/global-usage.en.md) | Run claude-haha from any directory | | [FAQ](docs/guide/faq.en.md) | Common error troubleshooting | | [Source Fixes](docs/reference/fixes.en.md) | Fixes compared with the original leaked source | diff --git a/README.md b/README.md index 57e47d99..5b3407a8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ - **Computer Use 桌面控制** — [使用指南](docs/features/computer-use.md) - **记忆系统**(跨会话持久化记忆)— [使用指南](docs/memory/01-usage-guide.md) - **多 Agent 系统**(多代理编排、并行任务、Teams 协作)— [使用指南](docs/agent/01-usage-guide.md) | [实现原理](docs/agent/02-implementation.md) +- **Skills 系统**(可扩展能力插件、自定义工作流)— [使用指南](docs/skills/01-usage-guide.md) | [实现原理](docs/skills/02-implementation.md) - 降级 Recovery CLI 模式(`CLAUDE_CODE_FORCE_RECOVERY_CLI=1 ./bin/claude-haha`) --- @@ -137,6 +138,7 @@ export PATH="$HOME/path/to/claude-code-haha/bin:$PATH" | [Computer Use](docs/features/computer-use.md) | 桌面控制功能(截屏、鼠标、键盘) | | [记忆系统](docs/memory/01-usage-guide.md) | 跨会话持久化记忆的使用与实现 | | [多 Agent 系统](docs/agent/01-usage-guide.md) | 多代理编排、并行任务执行与 Teams 协作 | +| [Skills 系统](docs/skills/01-usage-guide.md) | 可扩展能力插件、自定义工作流与条件激活 | | [全局使用](docs/guide/global-usage.md) | 在任意目录启动 claude-haha | | [常见问题](docs/guide/faq.md) | 常见错误排查 | | [源码修复记录](docs/reference/fixes.md) | 相对于原始泄露源码的修复内容 | diff --git a/docs/skills/01-usage-guide.md b/docs/skills/01-usage-guide.md new file mode 100644 index 00000000..17e47dd2 --- /dev/null +++ b/docs/skills/01-usage-guide.md @@ -0,0 +1,393 @@ +# Claude Code Skills 系统 — 使用指南 + +> Skills 是 Claude Code 的扩展能力引擎,让你用 Markdown 文件定义专属的自动化工作流。 + +

+Skills 是什么 · 六种来源 · 定义格式 · 调用方式 · 执行上下文 · 条件激活 · 权限控制 · 快速参考 +

+ +![Skills 系统概览](./images/01-skills-overview.png) + +--- + +## 一、什么是 Skills? + +Skills 是 Claude Code 的**可扩展能力插件系统**。每个 Skill 是一个 Markdown 文件(含 YAML frontmatter),定义了一段专门的提示词和行为配置,让 Claude 在特定场景下执行专业化的工作流。 + +核心能力: + +| 能力 | 说明 | +|------|------| +| 专业化工作流 | 定义代码审查、TDD、调试等标准流程 | +| 工具权限控制 | 限制 Skill 只能使用指定的工具 | +| 模型切换 | 为不同 Skill 指定不同的模型 | +| 执行隔离 | Fork 模式在子代理中独立运行 | +| 条件激活 | 只在操作特定文件时才激活 | +| Hook 注入 | Skill 调用时自动注册生命周期钩子 | + +--- + +## 二、六种 Skill 来源 + +![Skill 来源类型](./images/02-skill-sources.png) + +Claude Code 从 6 个不同来源加载 Skills,按优先级从高到低: + +### 1. Bundled(内置 Skills) + +编译到 CLI 二进制中,所有用户可用。以 TypeScript 定义,通过 `registerBundledSkill()` 注册。 + +**当前内置 Skills:** + +| Skill | 说明 | 特殊条件 | +|-------|------|----------| +| `/verify` | 验证代码变更 | — | +| `/debug` | 调试助手 | — | +| `/simplify` | 代码简化审查 | — | +| `/remember` | 记忆管理 | 需启用 auto-memory | +| `/batch` | 批量处理 | — | +| `/stuck` | 卡住时求助 | — | +| `/skillify` | 创建新 Skill | — | +| `/keybindings` | 自定义快捷键 | — | +| `/loop` | 定时循环任务 | AGENT_TRIGGERS 特性门控 | +| `/schedule` | 远程代理调度 | AGENT_TRIGGERS_REMOTE 特性门控 | +| `/claude-api` | Claude API 集成 | BUILDING_CLAUDE_APPS 特性门控 | +| `/dream` | 自动记忆整理 | KAIROS 特性门控 | + +### 2. Managed(策略管理 Skills) + +由组织策略控制,存放在 `/.claude/skills/`,适用于企业部署。 + +### 3. User(用户 Skills) + +用户个人定义,存放在 `~/.claude/skills/`。 + +``` +~/.claude/skills/ +├── my-review/ +│ └── SKILL.md ← 主 Skill 文件 +├── deploy-check/ +│ └── SKILL.md +└── ... +``` + +### 4. Project(项目 Skills) + +项目级别定义,存放在 `.claude/skills/`,可提交到版本控制。 + +``` +your-project/ +└── .claude/ + └── skills/ + ├── lint-fix/ + │ └── SKILL.md + └── test-runner/ + └── SKILL.md +``` + +### 5. Plugin(插件 Skills) + +由已安装的插件提供。插件通过 manifest 的 `skillsPath` / `skillsPaths` 声明 Skills 目录。 + +命名格式:`{pluginName}:{skillName}` + +``` +例如:superpowers:code-reviewer + superpowers:brainstorming +``` + +### 6. MCP(MCP 服务器 Skills) + +由连接的 MCP 服务器提供,命名格式:`mcp__server-name__prompt-name`。 + +**安全限制**:MCP Skills 为远程不受信来源,**禁止执行** `!`...`` 内联 shell 命令。 + +--- + +## 三、Skill 定义格式 + +### 目录结构 + +每个 Skill 是一个目录,包含一个 `SKILL.md` 文件: + +``` +skill-name/ +└── SKILL.md ← 文件名必须是 SKILL.md(大小写不敏感) +``` + +### Frontmatter 完整字段 + +```yaml +--- +name: 我的技能 # 显示名称(可选,默认用目录名) +description: 这个技能做什么 # 描述(必填,缺少时自动从内容提取) +when_to_use: 什么时候该用这个技能 # 使用场景说明(可选) +version: 1.0.0 # 版本号(可选) + +# ── 调用控制 ── +user-invocable: true # 用户能否通过 /skill-name 调用(默认 true) +disable-model-invocation: false # 禁止模型通过 Skill tool 调用(可选) +argument-hint: "<文件路径>" # 参数提示(可选) + +# ── 执行配置 ── +context: inline # 执行上下文:inline(默认)或 fork(子代理) +agent: general-purpose # fork 时使用的代理类型(可选) +model: sonnet # 模型覆盖:haiku / sonnet / opus / inherit(可选) +effort: high # 思考力度:low / medium / high / max(可选) +allowed-tools: "Bash, Read" # 允许使用的工具(逗号分隔或 YAML 列表) +shell: bash # Shell 类型:bash(默认)或 powershell + +# ── 条件激活 ── +paths: "src/**/*.ts, test/**/*.ts" # Glob 模式,只在匹配文件被操作时激活 + +# ── 生命周期 Hook ── +hooks: + PreToolUse: + - matcher: "Bash" + hooks: + - command: "echo 'Before bash'" + once: true # 仅执行一次 +--- + +# Skill 正文内容 + +这里是 Markdown 格式的提示词,Claude 调用此 Skill 时会看到这些内容。 + +支持的特殊语法: +- `${CLAUDE_SKILL_DIR}` — 展开为 Skill 所在目录 +- `${CLAUDE_SESSION_ID}` — 展开为当前会话 ID +- `$ARGUMENTS` / `${ARG1}` — 参数替换 +- !`shell command` — 内联 Shell 命令执行 +``` + +### Frontmatter 字段速查表 + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `name` | string | 目录名 | 显示名称覆盖 | +| `description` | string | 自动提取 | Skill 简述 | +| `when_to_use` | string | — | 使用场景描述 | +| `user-invocable` | boolean | true | 用户能否输入 /name 调用 | +| `disable-model-invocation` | boolean | false | 禁止模型调用 | +| `context` | `inline` \| `fork` | inline | 执行上下文 | +| `agent` | string | general-purpose | fork 时代理类型 | +| `model` | string | 继承 | 模型覆盖(haiku/sonnet/opus) | +| `effort` | string \| int | — | 思考力度 | +| `allowed-tools` | string \| list | 全部 | 允许工具白名单 | +| `paths` | string \| list | — | 条件激活 Glob 模式 | +| `shell` | `bash` \| `powershell` | bash | Shell 命令类型 | +| `hooks` | object | — | 生命周期 Hook 配置 | +| `argument-hint` | string | — | 参数提示文本 | +| `version` | string | — | 版本号 | + +--- + +## 四、调用方式 + +![Skill 调用流程](./images/03-skill-invocation.png) + +### 方式一:用户斜杠命令 + +直接在终端输入 `/skill-name`: + +``` +> /commit +> /review-pr 123 +> /verify +``` + +**前提**:Skill 的 `user-invocable` 必须为 `true`。 + +### 方式二:模型自动调用 + +Claude 在对话中识别到合适的 Skill 时,通过 SkillTool 自动调用: + +``` +用户:帮我审查一下这段代码 +Claude:[通过 SkillTool 调用 superpowers:code-reviewer] +``` + +**前提**:Skill 的 `disable-model-invocation` 不能为 `true`。 + +### 方式三:嵌套调用 + +一个 Skill 执行过程中可以触发另一个 Skill: + +``` +/verify → 内部调用 → /simplify +``` + +遥测中通过 `invocation_trigger: 'nested-skill'` 追踪。 + +### 调用优先级 + +当同名 Skill 存在于多个来源时,按以下顺序解析(先匹配先用): + +``` +1. Bundled(内置) ← 最高优先级 +2. Built-in Plugin(内置插件) +3. Skill Dirs(用户/项目目录) +4. Workflow Commands +5. Plugin Commands(插件命令) +6. Plugin Skills(插件技能) +7. Built-in Commands(内建命令) ← 最低优先级 +``` + +--- + +## 五、执行上下文 + +### Inline 模式(默认) + +Skill 内容**展开到当前对话**中,Claude 直接看到提示词并在同一上下文中执行。 + +```yaml +context: inline # 默认值,可省略 +``` + +**特点:** +- 共享父对话的 token 预算 +- 可以访问对话历史上下文 +- `allowedTools` 限制当前轮次可用工具 +- `model` 覆盖当前轮次使用的模型 + +### Fork 模式(子代理) + +Skill 在**隔离的子代理**中运行,拥有独立的 token 预算和上下文。 + +```yaml +context: fork +agent: general-purpose # 可选,指定代理类型 +``` + +**特点:** +- 独立的 token 预算,不消耗父对话额度 +- 隔离的对话上下文 +- 可指定不同的 agent 类型(如 `Bash`、`general-purpose`) +- 执行完成后提取结果返回父对话 +- 支持进度汇报(`onProgress` 回调) + +### 两种模式对比 + +| 特性 | Inline | Fork | +|------|--------|------| +| Token 预算 | 共享父对话 | 独立预算 | +| 上下文访问 | 完整对话历史 | 仅 Skill 提示词 | +| 结果返回 | 直接在对话中 | 提取文本嵌入 tool_result | +| 适用场景 | 简短指导、扩展上下文 | 长任务、独立运算 | +| 工具限制 | contextModifier 修改 | modifiedGetAppState | + +--- + +## 六、条件激活 + +Skill 可以通过 `paths` frontmatter 实现**按需激活**,只在操作匹配文件时才对模型可见。 + +### 配置方式 + +```yaml +--- +name: TypeScript 修复 +description: 修复 TypeScript 类型错误 +paths: "src/**/*.ts, test/**/*.ts" +--- +``` + +### 工作原理 + +``` +1. 启动时加载所有 Skill +2. 带 paths 的 Skill 存入 conditionalSkills Map(不暴露给模型) +3. 当用户操作文件时(Read/Write/Edit) +4. activateConditionalSkillsForPaths() 用 ignore 库匹配 +5. 匹配成功 → 移入 dynamicSkills Map → 模型可见 +6. 一旦激活,在会话内持续有效 +``` + +### 动态发现 + +除了条件激活,Skills 还支持**运行时发现**: + +``` +1. 用户操作某个深层目录中的文件 +2. discoverSkillDirsForPaths() 从文件路径向上遍历 +3. 寻找 .claude/skills/ 目录(不超过 cwd) +4. 跳过 .gitignore 忽略的目录 +5. 发现新目录 → addSkillDirectories() → 加载并注册 +``` + +--- + +## 七、权限控制 + +### 自动允许 + +如果 Skill 只包含"安全属性"(无 `allowedTools`、无 `hooks`、无 `fork`),将**自动获准**执行,无需用户确认。 + +### 手动确认 + +包含工具限制、Hook 或 fork 执行的 Skill,首次调用时会提示用户: + +``` +Execute skill: my-custom-skill +Allow? (y)es / (n)o / (a)lways allow / (d)eny +``` + +### 权限规则 + +| 规则类型 | 格式 | 说明 | +|----------|------|------| +| 精确允许 | `Skill:commit` | 允许执行 commit Skill | +| 前缀允许 | `Skill:review:*` | 允许所有 review: 前缀的 Skill | +| 精确拒绝 | `Skill:dangerous` 设为 deny | 拒绝执行 | +| 前缀拒绝 | `Skill:untrusted:*` 设为 deny | 拒绝所有 untrusted: 前缀 | + +**处理顺序:** deny 规则 → allow 规则 → 安全属性检查 → 询问用户 + +--- + +## 八、快速参考 + +### 创建一个 Skill + +```bash +# 1. 创建目录 +mkdir -p ~/.claude/skills/my-skill + +# 2. 创建 SKILL.md +cat > ~/.claude/skills/my-skill/SKILL.md << 'EOF' +--- +name: 我的技能 +description: 一个示例 Skill +user-invocable: true +--- + +# 技能内容 + +你好,这是我的自定义 Skill。 +EOF +``` + +### 常用操作 + +| 操作 | 方法 | +|------|------| +| 创建 Skill | `~/.claude/skills//SKILL.md` | +| 项目级 Skill | `.claude/skills//SKILL.md` | +| 调用 Skill | 终端输入 `/skill-name` | +| 查看可用 Skills | 终端输入 `/skills` | +| 用 AI 创建 Skill | `/skillify` | +| 限制工具 | frontmatter 添加 `allowed-tools` | +| Fork 执行 | frontmatter 添加 `context: fork` | +| 条件激活 | frontmatter 添加 `paths: "src/**"` | + +### Skill 可用性矩阵 + +| 来源 | 用户可调用 | 模型可调用 | 支持 Fork | 支持 Hook | +|------|-----------|-----------|----------|----------| +| Bundled | 依定义 | 依定义 | 是 | 是 | +| Managed | 是 | 是 | 是 | 是 | +| User | 是(默认) | 是 | 是 | 是 | +| Project | 是(默认) | 是 | 是 | 是 | +| Plugin | 依配置 | 依配置 | 是 | 是 | +| MCP | 依配置 | 依配置 | 是 | 否(安全限制) | diff --git a/docs/skills/02-implementation.md b/docs/skills/02-implementation.md new file mode 100644 index 00000000..29ad9e85 --- /dev/null +++ b/docs/skills/02-implementation.md @@ -0,0 +1,897 @@ +# Claude Code Skills 系统 — 实现原理 + +> 深度剖析 Skills 如何被发现、加载、注入、执行和管理。 + +

+整体架构 · 发现与加载 · Frontmatter 解析 · 注入对话 · 执行引擎 · Fork 执行 · 条件激活 · Hook 集成 · 权限系统 · 完整生命周期 · 源码索引 +

+ +![Skills 架构概览](./images/04-skills-architecture.png) + +--- + +## 一、整体架构 + +Skills 系统由 5 个核心模块协同工作: + +``` +┌─────────────────────────────────────────────────────┐ +│ Skills 系统 │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ +│ │ Discovery │ │ Prompt │ │ SkillTool │ │ +│ │ 发现 & 加载 │→│ 注入 & 呈现 │→│ 执行引擎 │ │ +│ └─────────────┘ └──────────────┘ └────────────┘ │ +│ ↑ ↓ │ +│ ┌─────────────┐ ┌────────────┐ │ +│ │ Activation │ │ Context │ │ +│ │ 条件激活 │←────────────────────│ 上下文修改 │ │ +│ └─────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +### 模块职责 + +| 模块 | 核心文件 | 职责 | +|------|---------|------| +| Discovery | `loadSkillsDir.ts` | 从 6 种来源发现和加载 Skills | +| Prompt | `prompt.ts` + `attachments.ts` | 将 Skill 列表注入 system-reminder | +| SkillTool | `SkillTool.ts` | 验证、权限检查、执行 Skill | +| Activation | `loadSkillsDir.ts` (后半段) | 条件激活和动态发现 | +| Context | `forkedAgent.ts` | 上下文准备和修改 | + +--- + +## 二、Skill 发现与加载 + +### 加载入口 + +Skills 加载从 `commands.ts` 的 `getSkills()` 函数开始: + +```typescript +// src/commands.ts:351-396 +async function getSkills(cwd: string) { + const [skillDirCommands, pluginSkills] = await Promise.all([ + getSkillDirCommands(cwd), // 目录 Skills(managed/user/project) + getPluginSkills(), // 插件 Skills + ]) + const bundledSkills = getBundledSkills() // 内置 Skills + const builtinPluginSkills = getBuiltinPluginSkillCommands() // 内置插件 Skills + return { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills } +} +``` + +### 聚合与排序 + +所有来源的 Skills 汇总到 `loadAllCommands()`,按优先级排列: + +```typescript +// src/commands.ts:447-467 +const loadAllCommands = memoize(async (cwd: string): Promise => { + return [ + ...bundledSkills, // 1. 内置 Skills(最高优先级) + ...builtinPluginSkills, // 2. 内置插件 Skills + ...skillDirCommands, // 3. 目录 Skills(managed → user → project) + ...workflowCommands, // 4. Workflow 命令 + ...pluginCommands, // 5. 插件命令 + ...pluginSkills, // 6. 插件 Skills + ...COMMANDS(), // 7. 内建命令(最低优先级) + ] +}) +``` + +**关键特性:** 使用 `memoize` 缓存,避免重复磁盘 I/O。 + +### 目录 Skill 加载流程 + +![Skill 加载流程](./images/05-skill-loading.png) + +`getSkillDirCommands()` 是目录 Skills 的核心加载函数: + +``` +getSkillDirCommands(cwd) +├─ 确定加载路径 +│ ├─ managed: ${MANAGED_PATH}/.claude/skills/ +│ ├─ user: ~/.claude/skills/ +│ ├─ project: .claude/skills/ (向上遍历到 HOME) +│ └─ additional: --add-dir 指定的路径 +│ +├─ 并行加载(Promise.all) +│ ├─ loadSkillsFromSkillsDir(managedDir, 'policySettings') +│ ├─ loadSkillsFromSkillsDir(userDir, 'userSettings') +│ ├─ loadSkillsFromSkillsDir(projectDirs, 'projectSettings') +│ ├─ loadSkillsFromSkillsDir(additionalDirs, 'projectSettings') +│ └─ loadSkillsFromCommandsDir(cwd) ← 兼容遗留 /commands/ 格式 +│ +├─ 去重(按 realpath) +│ └─ getFileIdentity(filePath) → realpath 解析符号链接 +│ └─ seenFileIds Map,首次出现者胜出 +│ +└─ 分离条件 Skills + ├─ 无 paths → unconditionalSkills(立即可用) + └─ 有 paths → conditionalSkills Map(等待激活) +``` + +### 去重机制 + +```typescript +// src/skills/loadSkillsDir.ts:725-763 +const fileIds = await Promise.all( + allSkillsWithPaths.map(({ skill, filePath }) => + skill.type === 'prompt' + ? getFileIdentity(filePath) // realpath() 解析符号链接 + : Promise.resolve(null), + ), +) + +const seenFileIds = new Map() +for (const entry of allSkillsWithPaths) { + const fileId = fileIds[i] + const existingSource = seenFileIds.get(fileId) + if (existingSource !== undefined) continue // 跳过重复 + seenFileIds.set(fileId, skill.source) + deduplicatedSkills.push(skill) +} +``` + +### Bundled Skill 注册 + +内置 Skills 使用完全不同的注册路径: + +```typescript +// src/skills/bundledSkills.ts:53-100 +export function registerBundledSkill(definition: BundledSkillDefinition): void { + // 如果有 files,创建提取目录和延迟提取逻辑 + if (files && Object.keys(files).length > 0) { + skillRoot = getBundledSkillExtractDir(definition.name) + // 首次调用时提取文件到磁盘 + getPromptForCommand = async (args, ctx) => { + extractionPromise ??= extractBundledSkillFiles(name, files) + const extractedDir = await extractionPromise + const blocks = await inner(args, ctx) + return prependBaseDir(blocks, extractedDir) + } + } + + const command: Command = { + type: 'prompt', + source: 'bundled', + loadedFrom: 'bundled', + // ...其他字段 + } + bundledSkills.push(command) +} +``` + +**文件提取:** Bundled Skills 可以携带 `files: Record` 字段,首次调用时将这些文件提取到磁盘(`getBundledSkillExtractDir()`),让模型能通过 Read/Grep 访问。 + +### 启动注册 + +```typescript +// src/skills/bundled/index.ts:13-58 +export function initBundledSkills(): void { + require('./verify.js').registerVerifySkill() + require('./debug.js').registerDebugSkill() + require('./remember.js').registerRememberSkill() + // ... + if (feature('AGENT_TRIGGERS')) { + require('./loop.js').registerLoopSkill() // 特性门控 + } + if (feature('KAIROS')) { + require('./dream.js').registerDreamSkill() // 特性门控 + } +} +``` + +### Plugin Skill 加载 + +``` +插件系统 +├─ loadAllPluginsCacheOnly() +│ └─ 获取所有已启用插件 +│ +├─ 每个插件: +│ ├─ 读取 manifest.skillsPath → 默认 Skills 目录 +│ ├─ 读取 manifest.skillsPaths[] → 额外 Skills 目录 +│ └─ loadSkillsFromDirectory() 加载 SKILL.md +│ +├─ 命名空间: +│ └─ {pluginName}:{namespace}:{skillName} +│ 例如: superpowers:code-reviewer +│ +└─ 变量替换: + ├─ ${CLAUDE_PLUGIN_ROOT} → 插件根目录 + ├─ ${CLAUDE_PLUGIN_DATA} → 插件数据目录 + ├─ ${CLAUDE_SKILL_DIR} → 技能目录 + └─ ${user_config.X} → 用户配置值 +``` + +### MCP Skill 加载 + +```typescript +// src/services/mcp/client.ts:2030-2102 +// MCP prompts 转换为 Command 对象 +async function fetchCommandsForClient(client) { + const prompts = await client.listPrompts() + return prompts.map(prompt => ({ + type: 'prompt', + name: `mcp__${normalizeNameForMCP(serverName)}__${prompt.name}`, + source: 'mcp', + loadedFrom: 'mcp', + // getPromptForCommand 调用 MCP 服务器获取内容 + })) +} +``` + +**特性门控:** `feature('MCP_SKILLS')` 控制 MCP Skills 是否可用。 + +--- + +## 三、Frontmatter 解析 + +### 解析流程 + +``` +SKILL.md 文件 + ↓ +parseFrontmatter() ← frontmatterParser.ts + ├─ 分离 YAML frontmatter 和 Markdown 内容 + ├─ quoteProblematicValues() ← 处理特殊字符(glob 模式等) + └─ parseYaml() ← 解析 YAML + ↓ +parseSkillFrontmatterFields() ← loadSkillsDir.ts:185-265 + ├─ description 提取优先级: + │ 1. frontmatter.description 字段 + │ 2. Markdown 第一个 # 标题 + │ 3. 技能名称兜底 + ├─ parseUserSpecifiedModel() ← 模型别名解析 + ├─ parseEffortValue() ← 力度级别解析 + ├─ parseHooksFromFrontmatter() ← Hook 配置验证 + ├─ parseBooleanFrontmatter() ← boolean 字段解析 + └─ parseSlashCommandToolsFromFrontmatter() ← 工具列表解析 + ↓ +createSkillCommand() ← loadSkillsDir.ts:270-401 + └─ 生成 Command 对象(含 getPromptForCommand 闭包) +``` + +### FrontmatterData 类型定义 + +```typescript +// src/utils/frontmatterParser.ts:10-59 +export type FrontmatterData = { + 'allowed-tools'?: string | string[] | null + description?: string | null + 'argument-hint'?: string | null + when_to_use?: string | null + version?: string | null + model?: string | null // haiku, sonnet, opus, inherit + 'user-invocable'?: string | null + 'disable-model-invocation'?: string | null + hooks?: HooksSettings | null + effort?: string | null // low, medium, high, max, 或数字 + context?: 'inline' | 'fork' | null + agent?: string | null + paths?: string | string[] | null + shell?: string | null // bash, powershell + [key: string]: unknown +} +``` + +### getPromptForCommand 闭包 + +每个 Command 对象都包含一个 `getPromptForCommand` 闭包,在调用时执行: + +```typescript +// src/skills/loadSkillsDir.ts:344-399 +async getPromptForCommand(args, toolUseContext) { + // 1. 添加基目录头 + let finalContent = baseDir + ? `Base directory for this skill: ${baseDir}\n\n${markdownContent}` + : markdownContent + + // 2. 参数替换 + finalContent = substituteArguments(finalContent, args, true, argumentNames) + + // 3. 技能目录变量替换 + if (baseDir) { + finalContent = finalContent.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir) + } + + // 4. 会话 ID 替换 + finalContent = finalContent.replace(/\$\{CLAUDE_SESSION_ID\}/g, getSessionId()) + + // 5. 执行内联 Shell 命令(MCP Skills 跳过此步 — 安全限制) + if (loadedFrom !== 'mcp') { + finalContent = await executeShellCommandsInPrompt(finalContent, context) + } + + return [{ type: 'text', text: finalContent }] +} +``` + +--- + +## 四、Skill 注入到对话 + +### 注入流程 + +![Skill 列表注入](./images/06-skill-injection.png) + +Skills 通过 `system-reminder` 消息注入到对话中: + +``` +每轮对话开始 + ↓ +getSkillListingAttachments() ← attachments.ts:2600-2747 + ├─ getSkillToolCommands(cwd) ← 获取所有模型可调用 Skills + ├─ getMcpSkillCommands() ← 获取 MCP Skills + ├─ sentSkillNames Map 追踪 ← 避免重复发送(per-agent) + └─ formatCommandsWithinBudget() ← 按上下文预算截断 + ↓ +返回 Attachment: + { type: 'skill_listing', content, skillCount, isInitial } + ↓ +normalizeAttachmentForAPI() ← messages.ts:3732-3737 + ↓ +包装为 用户消息: + "The following skills are available for use with the Skill tool: + - commit: Create a git commit... + - review-pr: Review a pull request... + - superpowers:code-reviewer: Expert code review..." +``` + +### 上下文预算控制 + +```typescript +// src/tools/SkillTool/prompt.ts:21-29 +export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01 // 上下文窗口的 1% +export const CHARS_PER_TOKEN = 4 +export const DEFAULT_CHAR_BUDGET = 8_000 // 200k × 4 × 1% 兜底 +export const MAX_LISTING_DESC_CHARS = 250 // 每条描述上限 +``` + +**截断策略:** + +``` +formatCommandsWithinBudget(commands, contextWindowTokens) + ├─ 计算总预算 = contextWindowTokens × 4 × 1% + ├─ 尝试全量描述 + │ └─ 总字符 ≤ 预算 → 全部输出 + │ + ├─ 分区: Bundled(不截断) + 其余 + │ ├─ Bundled Skills 始终保留完整描述 + │ └─ 其余 Skills 平分剩余预算 + │ + ├─ 截断描述 → maxDescLen 字符 + │ └─ maxDescLen < 20 → 极端情况: 非 Bundled 只显示名称 + │ + └─ 输出格式: "- skill-name: description..." +``` + +### SkillTool Prompt + +模型看到的工具提示词定义: + +```typescript +// src/tools/SkillTool/prompt.ts:173-196 +export const getPrompt = memoize(async (_cwd: string): Promise => { + return `Execute a skill within the main conversation + +When users ask you to perform tasks, check if any of the available skills match. + +How to invoke: +- Use this tool with the skill name and optional arguments +- Examples: + - skill: "pdf" + - skill: "commit", args: "-m 'Fix bug'" + +Important: +- Available skills are listed in system-reminder messages +- When a skill matches, invoke BEFORE generating any other response +- NEVER mention a skill without calling this tool +- Do not invoke a skill that is already running +` +}) +``` + +--- + +## 五、SkillTool 执行引擎 + +### 工具定义 + +```typescript +// src/tools/SkillTool/SkillTool.ts:331-340 +export const SkillTool = buildTool({ + name: 'Skill', + inputSchema: z.object({ + skill: z.string(), // 技能名称 + args: z.string().optional(), // 可选参数 + }), + outputSchema: z.union([inlineOutputSchema, forkedOutputSchema]), +}) +``` + +### 执行流程 + +![SkillTool 执行流程](./images/07-skill-execution.png) + +``` +SkillTool.call({ skill, args }) + │ + ├─ 1. 标准化输入 + │ └─ 去除前导 / , trim 空白 + │ + ├─ 2. 远程 Skill 检查(实验性) + │ └─ _canonical_ 前缀 → executeRemoteSkill() + │ + ├─ 3. 查找 Command 对象 + │ └─ getAllCommands(context) → findCommand(name, commands) + │ + ├─ 4. 记录使用频率 + │ └─ recordSkillUsage(commandName) → 影响排序推荐 + │ + ├─ 5. 判断执行路径 + │ ├─ command.context === 'fork' + │ │ └─ → executeForkedSkill() [见第六节] + │ │ + │ └─ 默认 inline + │ ├─ processPromptSlashCommand() + │ │ └─ getMessagesForPromptSlashCommand() + │ │ ├─ command.getPromptForCommand(args, context) + │ │ ├─ registerSkillHooks() ← 注册 Hook + │ │ ├─ addInvokedSkill() ← 记录(压缩后恢复) + │ │ ├─ formatCommandLoadingMetadata() + │ │ │ └─ /skillName + │ │ └─ 提取附件 → 创建消息 + │ │ + │ ├─ 提取 metadata: allowedTools, model, effort + │ ├─ tagMessagesWithToolUseID() ← 关联 tool_use + │ └─ 返回 { newMessages, contextModifier } + │ + └─ 6. contextModifier() 闭包 + ├─ 更新 allowedTools + │ └─ appState.toolPermissionContext.alwaysAllowRules.command + ├─ 更新 model + │ └─ resolveSkillModelOverride() 保留 [1m] 后缀 + └─ 更新 effort + └─ appState.effortValue +``` + +### 验证逻辑 + +```typescript +// src/tools/SkillTool/SkillTool.ts:354-430 +async validateInput({ skill }, context) { + // 1. 格式检查 — 非空 + // 2. 标准化 — 去除前导 / + // 3. 远程 Skill 检查 — _canonical_ 前缀 + // 4. 查找 — findCommand() 在 getAllCommands() 中 + // 5. 禁用检查 — disableModelInvocation + // 6. 类型检查 — 必须是 'prompt' 类型 +} +``` + +**错误码定义:** + +| errorCode | 含义 | +|-----------|------| +| 1 | 格式无效(空技能名) | +| 2 | 未知技能 | +| 4 | 模型调用被禁用 | +| 5 | 非 prompt 类型 | +| 6 | 远程技能未发现 | + +### getAllCommands — MCP Skill 合并 + +```typescript +// src/tools/SkillTool/SkillTool.ts:81-94 +async function getAllCommands(context: ToolUseContext): Promise { + // 从 AppState 获取 MCP Skills(loadedFrom === 'mcp') + const mcpSkills = context.getAppState() + .mcp.commands.filter( + cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp', + ) + if (mcpSkills.length === 0) return getCommands(getProjectRoot()) + const localCommands = await getCommands(getProjectRoot()) + return uniqBy([...localCommands, ...mcpSkills], 'name') +} +``` + +--- + +## 六、Fork 子代理执行 + +### 执行流程 + +``` +executeForkedSkill(command, commandName, args, context, ...) + │ + ├─ 1. 创建子代理 ID + │ └─ agentId = createAgentId() + │ + ├─ 2. 遥测记录 + │ └─ logEvent('tengu_skill_tool_invocation', { execution_context: 'fork' }) + │ + ├─ 3. 准备 Fork 上下文 + │ └─ prepareForkedCommandContext(command, args, context) + │ ├─ command.getPromptForCommand(args, context) ← 获取技能内容 + │ ├─ parseToolListFromCLI(allowedTools) ← 解析工具白名单 + │ ├─ createGetAppStateWithAllowedTools() ← 修改 AppState + │ ├─ 选择 agent: command.agent ?? 'general-purpose' + │ └─ promptMessages = [createUserMessage(skillContent)] + │ + ├─ 4. 合并 effort + │ └─ command.effort → 注入 agentDefinition + │ + ├─ 5. 运行子代理 + │ └─ for await (message of runAgent({ + │ agentDefinition, + │ promptMessages, + │ toolUseContext: { ...context, getAppState: modifiedGetAppState }, + │ model: command.model, + │ override: { agentId }, + │ })) + │ └─ 收集消息 + 报告进度 (onProgress) + │ + ├─ 6. 提取结果 + │ └─ extractResultText(agentMessages) + │ └─ 获取最后一条 assistant 消息的文本 + │ + └─ 7. 清理 + └─ clearInvokedSkillsForAgent(agentId) +``` + +### prepareForkedCommandContext + +```typescript +// src/utils/forkedAgent.ts:191-232 +export async function prepareForkedCommandContext( + command: PromptCommand, + args: string, + context: ToolUseContext, +): Promise { + // 获取技能内容(含参数替换和 shell 执行) + const skillPrompt = await command.getPromptForCommand(args, context) + const skillContent = skillPrompt.map(b => b.type === 'text' ? b.text : '').join('\n') + + // 构建工具白名单 + const allowedTools = parseToolListFromCLI(command.allowedTools ?? []) + const modifiedGetAppState = createGetAppStateWithAllowedTools( + context.getAppState, allowedTools, + ) + + // 选择代理类型 + const agentTypeName = command.agent ?? 'general-purpose' + const baseAgent = agents.find(a => a.agentType === agentTypeName) + + // 构建提示消息 + const promptMessages = [createUserMessage({ content: skillContent })] + + return { skillContent, modifiedGetAppState, baseAgent, promptMessages } +} +``` + +### Inline vs Fork 返回差异 + +**Inline 返回:** +```typescript +{ + data: { + success: true, + commandName: 'commit', + allowedTools: ['Bash', 'Read'], + model: 'sonnet', + status: 'inline', + }, + newMessages: [...], // 注入到对话 + contextModifier: (ctx) => { ... }, // 修改上下文 +} +``` + +**Fork 返回:** +```typescript +{ + data: { + success: true, + commandName: 'verify', + status: 'forked', + agentId: 'agent_abc123', + result: '验证通过,所有测试已运行...', + }, + // 无 newMessages — 结果嵌入 tool_result block +} +``` + +--- + +## 七、条件激活与动态发现 + +### 条件 Skills + +![条件激活机制](./images/08-conditional-activation.png) + +带 `paths` frontmatter 的 Skills 不会立即暴露给模型: + +``` +启动时 +├─ 加载所有 Skills +├─ 有 paths 的 → conditionalSkills Map +└─ 无 paths 的 → 立即可用 + +运行时(文件操作触发) +├─ activateConditionalSkillsForPaths(filePaths, cwd) +│ ├─ 遍历 conditionalSkills Map +│ ├─ 用 ignore 库匹配 paths 模式 +│ │ └─ filePath 转为 cwd 相对路径后匹配 +│ ├─ 匹配成功: +│ │ ├─ 移入 dynamicSkills Map +│ │ ├─ 从 conditionalSkills 删除 +│ │ ├─ 加入 activatedConditionalSkillNames Set +│ │ └─ 记录遥测 tengu_dynamic_skills_changed +│ └─ 一旦激活,会话内持续有效 +└─ 通知缓存失效 → skillsLoaded.emit() +``` + +### 动态发现 + +当操作深层目录文件时,系统自动发现新的 Skills: + +```typescript +// src/skills/loadSkillsDir.ts:861-915 +export async function discoverSkillDirsForPaths( + filePaths: string[], + cwd: string, +): Promise { + for (const filePath of filePaths) { + let currentDir = dirname(filePath) + // 从文件所在目录向上遍历到 cwd(不含 cwd 本身) + while (currentDir.startsWith(resolvedCwd + pathSep)) { + const skillDir = join(currentDir, '.claude', 'skills') + if (!dynamicSkillDirs.has(skillDir)) { + dynamicSkillDirs.add(skillDir) + await fs.stat(skillDir) // 检查是否存在 + // 检查是否被 .gitignore 忽略 + if (await isPathGitignored(currentDir, resolvedCwd)) continue + newDirs.push(skillDir) + } + currentDir = dirname(currentDir) + } + } + // 按深度排序(最深优先),确保就近 Skill 优先级更高 + return newDirs.sort((a, b) => b.split(pathSep).length - a.split(pathSep).length) +} +``` + +### 缓存失效链 + +``` +动态 Skill 变化 + ↓ +skillsLoaded.emit() + ↓ +clearCommandMemoizationCaches() + ├─ loadAllCommands.cache.clear() + ├─ getSkillToolCommands.cache.clear() + ├─ getSlashCommandToolSkills.cache.clear() + └─ clearSkillIndexCache() + ↓ +下一轮对话会加载新的 Skill 列表 +``` + +--- + +## 八、Hook 集成 + +### Hook 注册 + +Skills 可以通过 frontmatter 声明 Hook,在调用时自动注册为会话级 Hook: + +```typescript +// src/utils/hooks/registerSkillHooks.ts:20-64 +export function registerSkillHooks( + setAppState, sessionId, hooks, skillName, skillRoot?, +): void { + for (const eventName of HOOK_EVENTS) { + const matchers = hooks[eventName] + if (!matchers) continue + for (const matcher of matchers) { + for (const hook of matcher.hooks) { + // once: true → 执行一次后自动移除 + const onHookSuccess = hook.once + ? () => removeSessionHook(setAppState, sessionId, eventName, hook) + : undefined + + addSessionHook( + setAppState, sessionId, eventName, + matcher.matcher || '', + hook, onHookSuccess, skillRoot, + ) + } + } + } +} +``` + +### Hook 生命周期 + +``` +Skill 调用 + ↓ +processPromptSlashCommand() + ├─ 检查 command.hooks + └─ registerSkillHooks(setAppState, sessionId, hooks, skillName, skillRoot) + ├─ 遍历 HOOK_EVENTS(PreToolUse, PostToolUse, Stop, ...) + ├─ 为每个 matcher 注册 addSessionHook() + ├─ skillRoot → CLAUDE_PLUGIN_ROOT 环境变量 + └─ once: true → 首次执行后 removeSessionHook() + ↓ +会话期间 + ├─ 工具调用触发 PreToolUse/PostToolUse + ├─ matcher 匹配 → 执行 Hook 命令 + └─ once: true 的 Hook 执行一次后自动移除 +``` + +--- + +## 九、权限系统 + +### 检查流程 + +``` +checkPermissions({ skill, args }, context) + │ + ├─ 1. Deny 规则检查(最高优先级) + │ └─ getRuleByContentsForTool(context, SkillTool, 'deny') + │ ├─ 精确匹配: "commit" === commandName + │ └─ 前缀匹配: "review:*" → commandName.startsWith("review") + │ + ├─ 2. 远程 Skill 自动允许 + │ └─ _canonical_ → 自动允许(Ant 专属实验性) + │ + ├─ 3. Allow 规则检查 + │ └─ getRuleByContentsForTool(context, SkillTool, 'allow') + │ + ├─ 4. 安全属性自动允许 + │ └─ skillHasOnlySafeProperties(command) + │ └─ SAFE_SKILL_PROPERTIES 白名单检查 + │ + └─ 5. 默认: 询问用户 + └─ 提供建议: 精确允许 + 前缀允许 +``` + +### 安全属性白名单 + +如果 Skill 只包含以下属性(无 hooks、无 allowedTools、无 fork),自动允许: + +``` +SAFE_SKILL_PROPERTIES = { + type, name, description, contentLength, source, + loadedFrom, progressMessage, userInvocable, + disableModelInvocation, hasUserSpecifiedDescription, + getPromptForCommand, userFacingName, ... +} +``` + +**核心逻辑:** 新增的 frontmatter 字段默认需要权限,除非显式加入白名单。 + +--- + +## 十、完整生命周期 + +### 数据流总览 + +![完整生命周期](./images/09-skill-lifecycle.png) + +``` +第一阶段: 发现与注册 +────────────────── +CLI 启动 + ├─ initBundledSkills() → bundledSkills[] + ├─ getPluginSkills() → pluginSkills[] + ├─ getSkillDirCommands(cwd) → skillDirCommands[] + │ └─ 条件 Skills → conditionalSkills Map + └─ loadAllCommands() → 聚合 & memoize + +第二阶段: 注入到对话 +────────────────── +每轮对话 + ├─ getSkillListingAttachments() + │ ├─ getSkillToolCommands() → 过滤模型可调用 Skills + │ ├─ getMcpSkillCommands() → MCP Skills + │ └─ formatCommandsWithinBudget() → 截断到预算 + └─ 包装为 消息注入 + +第三阶段: 调用与执行 +────────────────── +模型/用户触发 + ├─ SkillTool.validateInput() → 验证 + ├─ SkillTool.checkPermissions() → 权限 + └─ SkillTool.call() + ├─ Inline → processPromptSlashCommand() + │ ├─ getPromptForCommand() → 内容展开 + │ ├─ registerSkillHooks() → Hook 注册 + │ ├─ addInvokedSkill() → 记录(压缩恢复) + │ └─ contextModifier() → 更新 tools/model/effort + └─ Fork → executeForkedSkill() + ├─ prepareForkedCommandContext() + ├─ runAgent() → 子代理运行 + └─ extractResultText() → 提取结果 + +第四阶段: 运行时发现 +────────────────── +文件操作触发 + ├─ discoverSkillDirsForPaths() → 新 Skills 目录 + ├─ addSkillDirectories() → 加载 & 注册 + ├─ activateConditionalSkillsForPaths() → 条件激活 + └─ clearCommandMemoizationCaches() → 缓存失效 +``` + +### Skill 内容持久化 + +Inline Skills 的内容通过 `addInvokedSkill()` 记录到会话状态,确保在上下文压缩后仍可恢复: + +``` +addInvokedSkill(name, path, content, agentId) + ↓ +存储在 session state 中 + ↓ +压缩时 → buildPostCompactMessages() + ↓ +按 agentId 作用域恢复(防止跨代理泄漏) +``` + +--- + +## 十一、源码索引 + +### 核心文件 + +| 文件 | 职责 | +|------|------| +| `src/tools/SkillTool/SkillTool.ts` | SkillTool 定义、验证、权限、执行 | +| `src/tools/SkillTool/prompt.ts` | 工具提示词、Skill 列表格式化、预算控制 | +| `src/skills/loadSkillsDir.ts` | 目录 Skill 发现、加载、去重、条件激活 | +| `src/skills/bundledSkills.ts` | 内置 Skill 注册系统 | +| `src/skills/bundled/index.ts` | 内置 Skills 初始化入口 | +| `src/commands.ts` | 命令聚合、排序、过滤、缓存管理 | + +### 类型定义 + +| 文件 | 关键类型 | +|------|---------| +| `src/types/command.ts` | `PromptCommand`, `Command`, `LocalCommandResult` | +| `src/utils/frontmatterParser.ts` | `FrontmatterData`, `ParsedMarkdown` | +| `src/skills/bundledSkills.ts` | `BundledSkillDefinition` | + +### 辅助模块 + +| 文件 | 职责 | +|------|------| +| `src/utils/forkedAgent.ts` | Fork 上下文准备、结果提取 | +| `src/utils/hooks/registerSkillHooks.ts` | Skill Hook 注册 | +| `src/utils/argumentSubstitution.ts` | 参数替换($ARGUMENTS, ${ARG1}) | +| `src/utils/promptShellExecution.ts` | 内联 Shell 命令执行 | +| `src/utils/attachments.ts` | Skill 列表附件生成 | +| `src/utils/messages.ts` | system-reminder 包装 | +| `src/utils/plugins/loadPluginCommands.ts` | 插件 Skill 加载 | +| `src/services/mcp/client.ts` | MCP Skill 转换 | +| `src/skills/mcpSkillBuilders.ts` | MCP Skill 构建器注册 | + +### 关键常量 + +| 常量 | 值 | 位置 | +|------|----|----| +| `SKILL_BUDGET_CONTEXT_PERCENT` | 0.01 | prompt.ts:21 | +| `DEFAULT_CHAR_BUDGET` | 8,000 | prompt.ts:23 | +| `MAX_LISTING_DESC_CHARS` | 250 | prompt.ts:29 | +| `SKILL_TOOL_NAME` | `'Skill'` | constants.ts | + +### 遥测事件 + +| 事件 | 说明 | +|------|------| +| `tengu_skill_tool_invocation` | Skill 调用(含 execution_context, invocation_trigger) | +| `tengu_skill_tool_slash_prefix` | 模型使用了 / 前缀 | +| `tengu_dynamic_skills_changed` | 动态 Skills 变化(条件激活/目录发现) | +| `tengu_skill_descriptions_truncated` | Skill 描述被截断 | diff --git a/docs/skills/images/01-skills-overview.png b/docs/skills/images/01-skills-overview.png new file mode 100644 index 00000000..343c687f Binary files /dev/null and b/docs/skills/images/01-skills-overview.png differ diff --git a/docs/skills/images/02-skill-sources.png b/docs/skills/images/02-skill-sources.png new file mode 100644 index 00000000..65dc606f Binary files /dev/null and b/docs/skills/images/02-skill-sources.png differ diff --git a/docs/skills/images/03-skill-invocation.png b/docs/skills/images/03-skill-invocation.png new file mode 100644 index 00000000..53596187 Binary files /dev/null and b/docs/skills/images/03-skill-invocation.png differ diff --git a/docs/skills/images/04-skills-architecture.png b/docs/skills/images/04-skills-architecture.png new file mode 100644 index 00000000..a32b18df Binary files /dev/null and b/docs/skills/images/04-skills-architecture.png differ diff --git a/docs/skills/images/05-skill-loading.png b/docs/skills/images/05-skill-loading.png new file mode 100644 index 00000000..e2571fbb Binary files /dev/null and b/docs/skills/images/05-skill-loading.png differ diff --git a/docs/skills/images/06-skill-injection.png b/docs/skills/images/06-skill-injection.png new file mode 100644 index 00000000..34170451 Binary files /dev/null and b/docs/skills/images/06-skill-injection.png differ diff --git a/docs/skills/images/07-skill-execution.png b/docs/skills/images/07-skill-execution.png new file mode 100644 index 00000000..fb8f29fe Binary files /dev/null and b/docs/skills/images/07-skill-execution.png differ diff --git a/docs/skills/images/08-conditional-activation.png b/docs/skills/images/08-conditional-activation.png new file mode 100644 index 00000000..8e63ee3d Binary files /dev/null and b/docs/skills/images/08-conditional-activation.png differ diff --git a/docs/skills/images/09-skill-lifecycle.png b/docs/skills/images/09-skill-lifecycle.png new file mode 100644 index 00000000..a41e547b Binary files /dev/null and b/docs/skills/images/09-skill-lifecycle.png differ