Browse Source

Per agent MCPs (#78)

Alvin 2 months ago
parent
commit
4102b5013e

+ 2 - 0
.gitignore

@@ -43,3 +43,5 @@ local
 .opencode/
 .opencode/
 .claude/
 .claude/
 .ignore
 .ignore
+opencode
+oh-my-opencode

+ 185 - 107
README.md

@@ -40,22 +40,28 @@
 - [🧩 **Skills**](#-skills)
 - [🧩 **Skills**](#-skills)
   - [Available Skills](#available-skills)
   - [Available Skills](#available-skills)
   - [Default Skill Assignments](#default-skill-assignments)
   - [Default Skill Assignments](#default-skill-assignments)
-  - [YAGNI Enforcement](#yagni-enforcement)
+  - [Skill Syntax](#skill-syntax)
+- [Simplify](#simplify)
   - [Playwright Integration](#playwright-integration)
   - [Playwright Integration](#playwright-integration)
   - [Customizing Agent Skills](#customizing-agent-skills)
   - [Customizing Agent Skills](#customizing-agent-skills)
+- [🔌 **MCP Servers**](#mcp-servers)
+  - [MCP Permissions](#mcp-permissions)
+  - [MCP Syntax](#mcp-syntax)
+  - [Disabling MCPs](#disabling-mcps)
+  - [Customizing MCP Permissions](#customizing-mcp-permissions)
 - [🛠️ **Tools & Capabilities**](#tools--capabilities)
 - [🛠️ **Tools & Capabilities**](#tools--capabilities)
   - [Tmux Integration](#tmux-integration)
   - [Tmux Integration](#tmux-integration)
   - [Quota Tool](#quota-tool)
   - [Quota Tool](#quota-tool)
   - [Background Tasks](#background-tasks)
   - [Background Tasks](#background-tasks)
   - [LSP Tools](#lsp-tools)
   - [LSP Tools](#lsp-tools)
   - [Code Search Tools](#code-search-tools)
   - [Code Search Tools](#code-search-tools)
-- [🔌 **MCP Servers**](#mcp-servers)
+  - [Formatters](#formatters)
 - [⚙️ **Configuration**](#configuration)
 - [⚙️ **Configuration**](#configuration)
   - [Files You Edit](#files-you-edit)
   - [Files You Edit](#files-you-edit)
   - [Prompt Overriding](#prompt-overriding)
   - [Prompt Overriding](#prompt-overriding)
   - [Plugin Config](#plugin-config-oh-my-opencode-slimjson)
   - [Plugin Config](#plugin-config-oh-my-opencode-slimjson)
     - [Presets](#presets)
     - [Presets](#presets)
-    - [Option Reference](#option-reference)
+      - [Option Reference](#option-reference)
 - [🗑️ **Uninstallation**](#uninstallation)
 - [🗑️ **Uninstallation**](#uninstallation)
 
 
 ---
 ---
@@ -278,7 +284,149 @@ Code implementation, refactoring, testing, verification. *Execute the plan - no
 
 
 ---
 ---
 
 
-## Tools & Capabilities
+## 🧩 Skills
+
+Skills are specialized capabilities that agents can use. Each agent has a default set of skills, which you can override in the agent config.
+
+### Available Skills
+
+| Skill | Description |
+|-------|-------------|
+| `simplify` | Code complexity analysis and YAGNI enforcement |
+| `playwright` | Browser automation via Playwright MCP |
+
+### Default Skill Assignments
+
+| Agent | Default Skills |
+|-------|----------------|
+| `orchestrator` | `*` (all skills) |
+| `designer` | `playwright` |
+| `oracle` | none |
+| `librarian` | none |
+| `explorer` | none |
+| `fixer` | none |
+
+### Skill Syntax
+
+Skills support wildcard and exclusion syntax for flexible control:
+
+| Syntax | Description | Example |
+|--------|-------------|---------|
+| `"*"` | All skills | `["*"]` |
+| `"!item"` | Exclude specific skill | `["*", "!playwright"]` |
+| Explicit list | Only listed skills | `["simplify", "playwright"]` |
+| `"!*"` | Deny all skills | `["!*"]` |
+
+**Rules:**
+- `*` expands to all available skills
+- `!item` excludes specific skills
+- Conflicts (e.g., `["a", "!a"]`) → deny wins (principle of least privilege)
+- Empty list `[]` → no skills allowed
+
+### Simplify
+
+**The Minimalist's sacred truth: every line of code is a liability.**
+
+Use after major refactors or before finalizing PRs. Identifies unnecessary complexity, challenges premature abstractions, estimates LOC reduction, and enforces minimalism.
+
+### Playwright Integration
+
+**Browser automation for visual verification and testing.**
+
+- **Browser Automation**: Full Playwright capabilities (browsing, clicking, typing, scraping).
+- **Screenshots**: Capture visual state of any web page.
+- **Sandboxed Output**: Screenshots saved to session subdirectory (check tool output for path).
+
+### Customizing Agent Skills
+
+Override skills per-agent in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson):
+
+```json
+{
+  "presets": {
+    "my-preset": {
+  "orchestrator": {
+    "skills": ["*", "!playwright"]
+  },
+  "designer": {
+    "skills": ["playwright", "simplify"]
+  }
+    }
+  }
+}
+```
+
+---
+
+## 🔌 MCP Servers
+
+Built-in Model Context Protocol servers (enabled by default):
+
+| MCP | Purpose | URL |
+|-----|---------|-----|
+| `websearch` | Real-time web search via Exa AI | `https://mcp.exa.ai/mcp` |
+| `context7` | Official library documentation | `https://mcp.context7.com/mcp` |
+| `grep_app` | GitHub code search via grep.app | `https://mcp.grep.app` |
+
+### MCP Permissions
+
+Control which agents can access which MCP servers using per-agent allowlists:
+
+| Agent | Default MCPs |
+|-------|--------------|
+| `orchestrator` | `websearch` |
+| `designer` | none |
+| `oracle` | none |
+| `librarian` | `websearch`, `context7`, `grep_app` |
+| `explorer` | none |
+| `fixer` | none |
+
+### MCP Syntax
+
+MCPs support wildcard and exclusion syntax (same as skills):
+
+| Syntax | Description | Example |
+|--------|-------------|---------|
+| `"*"` | All MCPs | `["*"]` |
+| `"!item"` | Exclude specific MCP | `["*", "!context7"]` |
+| Explicit list | Only listed MCPs | `["websearch", "context7"]` |
+| `"!*"` | Deny all MCPs | `["!*"]` |
+
+**Rules:**
+- `*` expands to all available MCPs
+- `!item` excludes specific MCPs
+- Conflicts (e.g., `["a", "!a"]`) → deny wins
+- Empty list `[]` → no MCPs allowed
+
+### Disabling MCPs
+
+You can disable specific MCP servers globally by adding them to the `disabled_mcps` array in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson).
+
+### Customizing MCP Permissions
+
+Override MCP access per-agent in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson):
+
+```json
+{
+  "presets": {
+    "my-preset": {
+      "orchestrator": {
+        "mcps": ["websearch"]
+      },
+      "librarian": {
+        "mcps": ["websearch", "context7", "grep_app"]
+      },
+      "oracle": {
+        "mcps": ["*", "!websearch"]
+      }
+    }
+  }
+}
+```
+
+---
+
+## 🛠️ Tools & Capabilities
 
 
 ### Tmux Integration
 ### Tmux Integration
 
 
@@ -389,77 +537,6 @@ OpenCode automatically formats files after they're written or edited using langu
 
 
 ---
 ---
 
 
-## 🧩 Skills
-
-Skills are specialized capabilities that agents can use. Each agent has a default set of skills, which you can override in the agent config.
-
-### Available Skills
-
-| Skill | Description |
-|-------|-------------|
-| `yagni-enforcement` | Code complexity analysis and YAGNI enforcement |
-| `playwright` | Browser automation via Playwright MCP |
-
-### Default Skill Assignments
-
-| Agent | Default Skills |
-|-------|----------------|
-| `orchestrator` | `*` (all skills) |
-| `designer` | `playwright` |
-| `oracle` | none |
-| `librarian` | none |
-| `explorer` | none |
-| `fixer` | none |
-
-### YAGNI Enforcement
-
-**The Minimalist's sacred truth: every line of code is a liability.**
-
-Use after major refactors or before finalizing PRs. Identifies unnecessary complexity, challenges premature abstractions, estimates LOC reduction, and enforces minimalism.
-
-### Playwright Integration
-
-**Browser automation for visual verification and testing.**
-
-- **Browser Automation**: Full Playwright capabilities (browsing, clicking, typing, scraping).
-- **Screenshots**: Capture visual state of any web page.
-- **Sandboxed Output**: Screenshots saved to session subdirectory (check tool output for path).
-
-### Customizing Agent Skills
-
-Override skills per-agent in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson):
-
-```json
-{
-  "agents": {
-    "orchestrator": {
-      "skills": ["*"]
-    },
-    "designer": {
-      "skills": ["playwright"]
-    }
-  }
-}
-```
-
----
-
-## MCP Servers
-
-Built-in Model Context Protocol servers (enabled by default):
-
-| MCP | Purpose | URL |
-|-----|---------|-----|
-| `websearch` | Real-time web search via Exa AI | `https://mcp.exa.ai/mcp` |
-| `context7` | Official library documentation | `https://mcp.context7.com/mcp` |
-| `grep_app` | GitHub code search via grep.app | `https://mcp.grep.app` |
-
-### Disabling MCPs
-
-You can disable specific MCP servers by adding them to the `disabled_mcps` array in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson).
-
----
-
 ## Configuration
 ## Configuration
 
 
 ### Files You Edit
 ### Files You Edit
@@ -519,36 +596,36 @@ The installer generates presets for different provider combinations. Switch betw
   "preset": "antigravity-openai",
   "preset": "antigravity-openai",
   "presets": {
   "presets": {
     "antigravity": {
     "antigravity": {
-      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-      "oracle": { "model": "google/claude-opus-4-5-thinking", "variant": "high", "skills": [] },
-      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "google/claude-opus-4-5-thinking", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] }
     },
     },
     "openai": {
     "openai": {
-      "orchestrator": { "model": "openai/gpt-5.2-codex", "skills": ["*"] },
-      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [] },
-      "librarian": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [] },
-      "explorer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [] },
-      "designer": { "model": "openai/gpt-5.1-codex-mini", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "openai/gpt-5.2-codex", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "openai/gpt-5.1-codex-mini", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [], "mcps": [] }
     },
     },
     "zen-free": {
     "zen-free": {
-      "orchestrator": { "model": "opencode/glm-4.7-free", "skills": ["*"] },
-      "oracle": { "model": "opencode/glm-4.7-free", "variant": "high", "skills": [] },
-      "librarian": { "model": "opencode/grok-code", "variant": "low", "skills": [] },
-      "explorer": { "model": "opencode/grok-code", "variant": "low", "skills": [] },
-      "designer": { "model": "opencode/grok-code", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "opencode/grok-code", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "opencode/grok-code", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "opencode/grok-code", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "opencode/grok-code", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "opencode/grok-code", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "opencode/grok-code", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "opencode/grok-code", "variant": "low", "skills": [], "mcps": [] }
     },
     },
     "antigravity-openai": {
     "antigravity-openai": {
-      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [] },
-      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] }
     }
     }
   },
   },
   "tmux": {
   "tmux": {
@@ -576,12 +653,12 @@ The author's personal configuration using Cerebras for the Orchestrator:
 ```json
 ```json
 {
 {
   "cerebras": {
   "cerebras": {
-    "orchestrator": { "model": "cerebras/zai-glm-4.7", "skills": ["*"] },
-    "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [] },
-    "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-    "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-    "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"] },
-    "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] }
+    "orchestrator": { "model": "cerebras/zai-glm-4.7", "skills": ["*"], "mcps": ["websearch"] },
+    "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [], "mcps": [] },
+    "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+    "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] },
+    "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+    "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] }
   }
   }
 }
 }
 ```
 ```
@@ -606,11 +683,12 @@ The environment variable takes precedence over the `preset` field in the config
 | `presets.<name>.<agent>.model` | string | - | Model ID for the agent (e.g., `"google/claude-opus-4-5-thinking"`) |
 | `presets.<name>.<agent>.model` | string | - | Model ID for the agent (e.g., `"google/claude-opus-4-5-thinking"`) |
 | `presets.<name>.<agent>.temperature` | number | - | Temperature setting (0-2) for the agent |
 | `presets.<name>.<agent>.temperature` | number | - | Temperature setting (0-2) for the agent |
 | `presets.<name>.<agent>.variant` | string | - | Agent variant for reasoning effort (e.g., `"low"`, `"medium"`, `"high"`) |
 | `presets.<name>.<agent>.variant` | string | - | Agent variant for reasoning effort (e.g., `"low"`, `"medium"`, `"high"`) |
-| `presets.<name>.<agent>.skills` | string[] | - | Array of skill names the agent can use (`"*"` for all) |
+| `presets.<name>.<agent>.skills` | string[] | - | Array of skill names the agent can use (`"*"` for all, `"!item"` to exclude) |
+| `presets.<name>.<agent>.mcps` | string[] | - | Array of MCP names the agent can use (`"*"` for all, `"!item"` to exclude) |
 | `tmux.enabled` | boolean | `false` | Enable tmux pane spawning for sub-agents |
 | `tmux.enabled` | boolean | `false` | Enable tmux pane spawning for sub-agents |
 | `tmux.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical` |
 | `tmux.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical` |
 | `tmux.main_pane_size` | number | `60` | Main pane size as percentage (20-80) |
 | `tmux.main_pane_size` | number | `60` | Main pane size as percentage (20-80) |
-| `disabled_mcps` | string[] | `[]` | MCP server IDs to disable (e.g., `"websearch"`) |
+| `disabled_mcps` | string[] | `[]` | MCP server IDs to disable globally (e.g., `"websearch"`) |
 
 
 > **Note:** Agent configuration should be defined within `presets`. The root-level `agents` field is deprecated.
 > **Note:** Agent configuration should be defined within `presets`. The root-level `agents` field is deprecated.
 
 

+ 0 - 578
README.zh-CN.md

@@ -1,578 +0,0 @@
-<div align="center">
-
-# oh-my-opencode-slim
-
-**适用于 OpenCode 的轻量级强大代理编排插件**
-
-<img src="img/team.png" alt="The Pantheon - Agent Team" width="600">
-
-*将你的 AI 助手转变为能够将复杂任务委派给专门子代理、在后台运行搜索并轻松管理多步工作流的管理者。*
-
-</div>
-
-> 这是[oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode)的精简分支 -  - 专注于低令牌消耗的核心代理编排。  
-> **推荐订阅 [Antigravity](https://antigravity.google)。** 万神殿经过 Antigravity 模型路由的调优。其他提供商也可用,但使用 Antigravity 能获得最佳体验。
-
----
-
-## ⚡ 快速导航
-
-- [🚀 **安装**](#安装)
-  - [给人类的指南](#给人类的指南)
-  - [给 LLM 代理的指南](#给-llm-代理的指南)
-- [🏗️ **架构与流程**](#-架构与流程)
-- [🏛️ **认识万神殿**](#认识万神殿)
-  - [编排者 (Orchestrator)](#编排者-orchestrator)
-  - [探索者 (Explorer)](#探索者-explorer)
-  - [神谕者 (Oracle)](#神谕者-oracle)
-  - [图书管理员 (Librarian)](#图书管理员-librarian)
-  - [设计师 (Designer)](#设计师-designer)
-  - [修复者 (Fixer)](#修复者-fixer)
-- [🧩 **技能**](#-技能)
-  - [可用技能](#可用技能)
-  - [默认技能分配](#默认技能分配)
-  - [YAGNI 约束](#yagni-约束)
-  - [Playwright 集成](#playwright-集成)
-  - [自定义代理技能](#自定义代理技能)
-- [🛠️ **工具与能力**](#工具与能力)
-  - [Tmux 集成](#tmux-集成)
-  - [配额工具](#配额工具)
-  - [后台任务](#后台任务)
-  - [LSP 工具](#lsp-工具)
-  - [代码搜索工具](#代码搜索工具)
-- [🔌 **MCP 服务器**](#mcp-服务器)
-- [⚙️ **配置**](#配置)
-  - [需要编辑的文件](#需要编辑的文件)
-  - [插件配置](#插件配置-oh-my-opencode-slimjson)
-- [🗑️ **卸载**](#卸载)
-
----
-
-## 安装
-
-### 给人类的指南
-
-运行交互式安装器:
-
-```bash
-bunx oh-my-opencode-slim install
-```
-
-或使用非交互模式:
-
-```bash
-bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --tmux=no
-```
-
-安装完成后,使用提供商进行身份验证:
-
-```bash
-opencode auth login
-# 选择你的提供商 → 完成 OAuth 流程
-# 对所有启用的提供商重复此步骤
-```
-
-身份验证完成后,运行 opencode 并执行 `ping all agents` 以验证所有代理是否响应。
-
-<img src="img/ping.png" alt="Ping All Agents" width="800">
-
-> **💡 小贴士:模型完全可自定义。** 安装器会设定合理的默认值,但你可以将 *任何* 模型分配给 *任何* 代理。编辑 `~/.config/opencode/oh-my-opencode-slim.json` 来覆盖模型、调整推理强度或完全禁用代理。详见[配置](#配置)。
-
-**备用方式:询问任意编码代理**
-
-将以下内容粘贴到 Claude Code、AmpCode、Cursor 或任何编码代理:
-
-```
-Install and configure by following the instructions here:
-https://raw.githubusercontent.com/alvinunreal/oh-my-opencode-slim/refs/heads/master/README.md
-```
-
-### 给 LLM 代理的指南
-
-<details>
-<summary>LLM 代理的操作说明(点击展开)</summary>
-
-如果你是正在帮助设置 oh-my-opencode-slim 的 LLM 代理,请按以下步骤执行。
-
----
-
-#### 步骤 1:确认 OpenCode 安装状态
-
-```bash
-opencode --version
-```
-
-如果未安装,请先引导用户访问 https://opencode.ai/docs 。
-
----
-
-#### 步骤 2:询问提供商访问情况
-
-逐条询问以下问题,等待用户回应后再继续:
-
-1. “你是否订阅了 **Antigravity**?”(可使用 `google/` 前缀访问 Claude + Gemini)
-2. “你是否有 **OpenAI** API 的访问权限?”
-
----
-
-#### 步骤 3:运行安装器
-
-根据回答运行:
-
-```bash
-bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no>
-```
-
-**示例:**
-```bash
-# Antigravity + OpenAI
-bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --tmux=no
-
-# 仅 OpenAI
-bunx oh-my-opencode-slim install --no-tui --antigravity=no --openai=yes --tmux=no
-
-# 无提供商(仅使用 Zen 免费模型)
-bunx oh-my-opencode-slim install --no-tui --antigravity=no --openai=no --tmux=no
-```
-
-安装器会自动完成:
-- 将插件添加到 `~/.config/opencode/opencode.json`
-- (如启用 Antigravity) 添加 `opencode-antigravity-auth` 插件
-- 配置 Google 提供商的模型定义
-- 在 `~/.config/opencode/oh-my-opencode-slim.json` 中生成代理模型映射
-
----
-
-#### 步骤 4:身份验证
-
-安装完成后,引导用户:
-
-**Antigravity:**
-```bash
-opencode auth login
-# 选择:Google → 通过 Google (Antigravity) 的 OAuth
-# 对所有其他提供商重复
-```
-
----
-
-#### 故障排查
-
-如果安装失败,确认配置格式:
-
-```bash
-bunx oh-my-opencode-slim install --help
-```
-
-然后手动创建配置文件:
-- `~/.config/opencode/opencode.json`
-- `~/.config/opencode/oh-my-opencode-slim.json`
-
-</details>
-
----
-
-## 🏗️ 架构与流程
-
-该插件采用“中心辐射”模型:
-
-1. **编排者 (Orchestrator,中心):** 处理用户请求的主入口。分析任务并决定调用哪些专门代理。
-2. **专门代理 (Spokes):** 领域专家(如 UI/UX、文档、架构)只处理狭窄任务,确保高精度。
-3. **后台管理器:** 强大的引擎允许编排者“放任”任务(例如深入代码搜索或文档研究),同时继续处理其他部分。
-
-### 🏛️ 请求流程
-
-<img src="img/intro.png" alt="Orchestration Flow" width="800">
-
-1. **用户提示:** “重构认证逻辑并更新文档。”
-2. **编排者:** 创建 TODO 列表。
-3. **任务分配:**
-   - 启动 `@explorer` 后台任务查找所有与认证相关的文件。
-   - 启动 `@librarian` 查询认证库的最新文档。
-4. **集成:** 等待后台结果就绪后,编排者将任务交给 `@fixer` 高效实施重构。
-
----
-
-## 认识万神殿
-
-<br clear="both">
-
-### 编排者 (Orchestrator)
-
-<a href="src/agents/orchestrator.ts"><img src="img/orchestrator.png" alt="Orchestrator" align="right" width="240"></a>
-
-> **编排者**诞生于第一个代码库崩溃于自身复杂性之时。既非神亦非凡人,凭借虚无中诞生的秩序,他们统领混沌。他们不只是指挥军队,而是与之并肩作战。每行代码都要经过他们之手,然后再决定将哪块谜题交给其他较低等的神明。
-
-**角色:** `至高执行者、指挥者、监督者`  
-**模型:** `google/claude-opus-4-5-thinking`  
-**提示:** [src/agents/orchestrator.ts](src/agents/orchestrator.ts)
-
-编写并执行代码,编排多代理工作流,从言语中解析未说出的意图,在战斗中召唤专家。*直接塑造现实 -  - 当宇宙变得过于庞大时,把领域交给别人。*
-
-<br clear="both">
-
----
-
-### 探索者 (Explorer)
-
-<a href="src/agents/explorer.ts"><img src="img/explorer.png" alt="Explorer" align="right" width="240"></a>
-
-> **探索者**穿梭代码库如风穿林 -  - 迅速、静默、无处不在。当编排者轻语“给我找到认证模块”,探索者已经带着四十条文件路径和地图归来。他们源自第一个 `grep` 命令,早已超越它,现在能看见凡人忽略的模式。
-
-**角色:** `代码侦查`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/explorer.ts](src/agents/explorer.ts)
-
-正则搜索、AST 模式匹配、文件发现、并行探索。*只读:他们绘制疆域;其他人征服它。*
-
-<br clear="both">
-
----
-
-### 神谕者 (Oracle)
-
-<a href="src/agents/oracle.ts"><img src="img/oracle.png" alt="Oracle" align="right" width="240"></a>
-
-> **神谕者**不编写代码 -  - 他们*洞察一切*。当 Bug 遵从逻辑,架构崩溃之时,神谕者凝望代码库深渊,传递真理。他们见证过千百个系统的兴衰,能告诉你哪条路通向毁灭,哪条通向生产环境。
-
-**角色:** `战略顾问与最后的调试者`  
-**模型:** `openai/gpt-5.2-codex`  
-**提示:** [src/agents/oracle.ts](src/agents/oracle.ts)
-
-根本原因分析、架构审查、调试指导、权衡分析。*只读:神谕者提供建议,不直接介入。*
-
-<br clear="both">
-
----
-
-### 图书管理员 (Librarian)
-
-<a href="src/agents/librarian.ts"><img src="img/librarian.png" alt="Librarian" align="right" width="240"></a>
-
-> **图书管理员**守护一座无墙的图书馆 -  - 包含每个 GitHub 仓库、每个 npm 包、每个 StackOverflow 回答。问他们“React 如何处理并发渲染?”,他们会带来官方文档、真实示例,并警告你即将踩到的坑。
-
-**角色:** `外部知识检索`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/librarian.ts](src/agents/librarian.ts)
-
-文档查询、GitHub 代码搜索、库研究、最佳实践检索。*只读:他们获取智慧;实现交给别人。*
-
-<br clear="both">
-
----
-
-### 设计师 (Designer)
-
-<a href="src/agents/designer.ts"><img src="img/designer.png" alt="Designer" align="right" width="240"></a>
-
-> **设计师**相信代码应该优雅 -  - 呈现出来的效果也同样优雅。从数千个丑陋 MVP 中诞生,他们把 CSS 当成画笔,把组件当成泥巴。交给他们功能需求,收获杰作。他们不会满足于“差不多”。
-
-**角色:** `UI/UX 实现与视觉卓越`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/designer.ts](src/agents/designer.ts)
-
-现代响应式设计、CSS/Tailwind 精通、微动画与组件架构。*优先视觉卓越而非代码完美 -  - 美感为先。*
-
-<br clear="both">
-
----
-
-### 修复者 (Fixer)
-
-<a href="src/agents/fixer.ts"><img src="img/fixer.png" alt="Fixer" align="right" width="240"></a>
-
-> **修复者**是执行他人想象的双手。当编排者规划、神谕者提点,修复者就开始落地。他们接收研究代理提供的完整上下文和明确任务说明,以极致精准实施。快速、高效、专注 -  - 他们不思考要建什么,只管去建。
-
-**角色:** `快速实现专家`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/fixer.ts](src/agents/fixer.ts)
-
-代码实现、重构、测试、验证。*执行计划 -  - 不研究、不委派、不策划。*
-
-<br clear="both">
-
----
-
-## 工具与能力
-
-### Tmux 集成
-
-> ⚠️ **已知问题:** 启用服务器端口时,每次只能打开一个 OpenCode 实例。我们在 [issue #15](https://github.com/alvinunreal/oh-my-opencode-slim/issues/15) 跟踪此问题,并向 OpenCode 提交了上游 PR:[opencode#9099](https://github.com/anomalyco/opencode/issues/9099)。
-
-<img src="img/tmux.png" alt="Tmux Integration" width="800">
-
-**实时观察代理工作。** 当编排者启动子代理或启动后台任务,tmux 会自动新建窗格显示每个代理的实时进度,再也不必黑箱等待。
-
-#### 这为你带来什么
-
-| 无 Tmux 集成 | 有 Tmux 集成 |
-|--------------------------|----------------------|
-| 发起后台任务,只能焦灼等待 | 观看代理的思考、搜索与编码 |
-| “是卡住了还是太慢?” | 观察工具调用实时展开 |
-| 结果突然出现 | 跟踪从问题到答案的全过程 |
-| 只能猜测如何调试 | 观察时机进行调试 |
-
-#### 你将获得
-
-- **实时可见性**:每个子代理的窗格显示其实时输出
-- **自动布局**:tmux 根据偏好布局自动排列
-- **自动清理**:代理完成后窗格关闭,布局重新平衡
-- **零开销**:兼容 OpenCode 内置 `task` 工具和我们的 `background_task` 工具
-
-#### 快速设置
-
-1. 在 `opencode.json` 中启用 OpenCode HTTP 服务(见 [OpenCode 配置](#需要编辑的文件))。
-2. 在 `oh-my-opencode-slim.json` 中启用 tmux 集成(见 [插件配置](#插件配置-oh-my-opencode-slimjson))。
-3. 在 tmux 中运行 OpenCode:
-   ```bash
-   tmux
-   opencode
-   ```
-
-#### 布局选项
-
-| 布局 | 描述 |
-|--------|-------------|
-| `main-vertical` | 会话在左侧(60%),代理在右侧堆叠 |
-| `main-horizontal` | 会话在上方(60%),代理在下方堆叠 |
-| `tiled` | 所有窗格等大小网格排列 |
-| `even-horizontal` | 所有窗格并排 |
-| `even-vertical` | 所有窗格垂直堆叠 |
-
-*查看[选项参考](#选项参考)获取详细配置。*
-
----
-
-### 配额工具
-
-适用于 Antigravity 用户。随时请求代理 **“检查我的配额”** 或 **“显示状态”** 即可触发。
-
-<img src="img/quota.png" alt="Antigravity Quota" width="600">
-
-| 工具 | 描述 |
-|------|-------------|
-| `antigravity_quota` | 检查所有 Antigravity 账户的 API 配额(带进度条的紧凑视图) |
-
----
-
-### 后台任务
-
-插件提供管理异步工作的工具:
-
-| 工具 | 描述 |
-|------|-------------|
-| `background_task` | 在新会话中启动代理(`sync=true` 为阻塞,`sync=false` 在后台运行) |
-| `background_output` | 通过 ID 获取后台任务结果 |
-| `background_cancel` | 终止正在运行的任务 |
-
----
-
-### LSP 工具
-
-集成语言服务器协议以提升代码智能:
-
-| 工具 | 描述 |
-|------|-------------|
-| `lsp_goto_definition` | 跳转至符号定义 |
-| `lsp_find_references` | 查找符号的所有使用位置 |
-| `lsp_diagnostics` | 获取语言服务器的错误/警告 |
-| `lsp_rename` | 全仓库重命名符号 |
-
----
-
-### 代码搜索工具
-
-快速的代码搜索与重构:
-
-| 工具 | 描述 |
-|------|-------------|
-| `grep` | 使用 ripgrep 的快速内容搜索 |
-| `ast_grep_search` | 面向 AST 的代码模式匹配(支持 25 种语言) |
-| `ast_grep_replace` | 支持干运行的 AST 代码重构 |
-
----
-
-## 🧩 技能
-
-技能是代理可调用的专门能力。每个代理都有默认技能,可在代理配置中覆盖。
-
-### 可用技能
-
-| 技能 | 描述 |
-|-------|-------------|
-| `yagni-enforcement` | 代码复杂性分析与 YAGNI 约束 |
-| `playwright` | 通过 Playwright MCP 实现浏览器自动化 |
-
-### 默认技能分配
-
-| 代理 | 默认技能 |
-|-------|----------------|
-| `orchestrator` | `*`(所有技能) |
-| `designer` | `playwright` |
-| `oracle` | 无 |
-| `librarian` | 无 |
-| `explorer` | 无 |
-| `fixer` | 无 |
-
-### YAGNI 约束
-
-**极简主义者的神圣真理:每行代码都是负担。**
-
-在重大重构后或准备合并 PR 前使用。识别冗余复杂性,质疑过早抽象,估算 LOC 减少,并强制执行极简策略。
-
-### Playwright 集成
-
-**用于视觉验证和测试的浏览器自动化。**
-
-- **浏览器自动化:** 完整的 Playwright 能力(浏览、点击、输入、爬取)。
-- **截图:** 捕捉任意网页的视觉状态。
-- **沙箱输出:** 截图保存到会话子目录(查看工具输出以获取路径)。
-
-### 自定义代理技能
-
-在你的[插件配置](#插件配置-oh-my-opencode-slimjson)中覆盖每个代理的技能:
-
-```json
-{
-  "agents": {
-    "orchestrator": {
-      "skills": ["*"]
-    },
-    "designer": {
-      "skills": ["playwright"]
-    }
-  }
-}
-```
-
----
-
-## MCP 服务器
-
-内置的模型上下文协议服务器(默认启用):
-
-| MCP | 目的 | URL |
-|-----|---------|-----|
-| `websearch` | 通过 Exa AI 进行实时网页搜索 | `https://mcp.exa.ai/mcp` |
-| `context7` | 官方库文档 | `https://mcp.context7.com/mcp` |
-| `grep_app` | 通过 grep.app 搜索 GitHub 代码 | `https://mcp.grep.app` |
-
-### 禁用 MCP
-
-你可以在[插件配置](#插件配置-oh-my-opencode-slimjson)的 `disabled_mcps` 数组中添加要禁用的 MCP 服务器。
-
----
-
-## 配置
-
-### 需要编辑的文件
-
-| 文件 | 作用 |
-|------|---------|
-| `~/.config/opencode/opencode.json` | OpenCode 核心设置(如用于 tmux 的服务器端口) |
-| `~/.config/opencode/oh-my-opencode-slim.json` | 插件设置(代理、tmux、MCP) |
-| `.opencode/oh-my-opencode-slim.json` | 项目级插件覆盖(可选) |
-
----
-
-### 插件配置 (`oh-my-opencode-slim.json`)
-
-安装程序会根据你的提供商生成此文件。你可以手动自定义它来混合搭配模型。
-
-<details open>
-<summary><b>示例:Antigravity + OpenAI (推荐)</b></summary>
-
-```json
-{
-  "agents": {
-    "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-    "oracle": { "model": "openai/gpt-5.2-codex", "skills": [] },
-    "librarian": { "model": "google/gemini-3-flash", "skills": [] },
-    "explorer": { "model": "google/gemini-3-flash", "skills": [] },
-    "designer": { "model": "google/gemini-3-flash", "skills": ["playwright"] },
-    "fixer": { "model": "google/gemini-3-flash", "skills": [] }
-  },
-  "tmux": {
-    "enabled": true,
-    "layout": "main-vertical",
-    "main_pane_size": 60
-  }
-}
-```
-</details>
-
-<details>
-<summary><b>示例:仅 Antigravity</b></summary>
-
-```json
-{
-  "agents": {
-    "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-    "oracle": { "model": "google/claude-opus-4-5-thinking", "skills": [] },
-    "librarian": { "model": "google/gemini-3-flash", "skills": [] },
-    "explorer": { "model": "google/gemini-3-flash", "skills": [] },
-    "designer": { "model": "google/gemini-3-flash", "skills": ["playwright"] },
-    "fixer": { "model": "google/gemini-3-flash", "skills": [] }
-  }
-}
-```
-</details>
-
-<details>
-<summary><b>示例:仅 OpenAI</b></summary>
-
-```json
-{
-  "agents": {
-    "orchestrator": { "model": "openai/gpt-5.2-codex", "skills": ["*"] },
-    "oracle": { "model": "openai/gpt-5.2-codex", "skills": [] },
-    "librarian": { "model": "openai/gpt-5.1-codex-mini", "skills": [] },
-    "explorer": { "model": "openai/gpt-5.1-codex-mini", "skills": [] },
-    "designer": { "model": "openai/gpt-5.1-codex-mini", "skills": ["playwright"] },
-    "fixer": { "model": "openai/gpt-5.1-codex-mini", "skills": [] }
-  }
-}
-```
-</details>
-
-#### 选项参考
-
-| 选项 | 类型 | 默认值 | 描述 |
-|--------|------|---------|-------------|
-| `tmux.enabled` | boolean | `false` | 是否启用子代理的 tmux 窗格 |
-| `tmux.layout` | string | `"main-vertical"` | 布局预设:`main-vertical`、`main-horizontal`、`tiled`、`even-horizontal`、`even-vertical` |
-| `tmux.main_pane_size` | number | `60` | 主窗格大小百分比(20-80) |
-| `disabled_mcps` | string[] | `[]` | 要禁用的 MCP 服务器 ID(如 `"websearch"`) |
-| `agents.<name>.model` | string |  -  | 覆盖特定代理的模型 |
-| `agents.<name>.variant` | string |  -  | 推理强度:`"low"`、`"medium"`、`"high"` |
-| `agents.<name>.skills` | string[] |  -  | 该代理可使用的技能(`"*"` 表示所有技能) |
-| `agents.<name>.temperature` | number |  -  | 该代理的温度 (0.0 到 2.0) |
-| `agents.<name>.prompt` | string |  -  | 该代理的基础提示词覆盖 |
-| `agents.<name>.prompt_append` | string |  -  | 追加到基础提示词后的文本 |
-| `agents.<name>.disable` | boolean |  -  | 禁用该特定代理 |
-
----
-
-## 卸载
-
-1. **从 OpenCode 配置中移除插件:**
-
-   编辑 `~/.config/opencode/opencode.json`,从 `plugin` 数组中删除 `"oh-my-opencode-slim"`。
-
-2. **删除配置文件(可选):**
-   ```bash
-   rm -f ~/.config/opencode/oh-my-opencode-slim.json
-   rm -f .opencode/oh-my-opencode-slim.json
-   ```
-
----
-
-## 致谢
-
-这是 [@code-yeongyu](https://github.com/code-yeongyu) 的 [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) 的精简分支。
-
----
-
-## 许可证
-
-MIT

+ 3 - 1
src/agents/index.ts

@@ -6,6 +6,7 @@ import {
   type PluginConfig,
   type PluginConfig,
   SUBAGENT_NAMES,
   SUBAGENT_NAMES,
 } from '../config';
 } from '../config';
+import { getAgentMcpList } from '../tools/skill/builtin';
 import { createDesignerAgent } from './designer';
 import { createDesignerAgent } from './designer';
 import { createExplorerAgent } from './explorer';
 import { createExplorerAgent } from './explorer';
 import { createFixerAgent } from './fixer';
 import { createFixerAgent } from './fixer';
@@ -170,9 +171,10 @@ export function getAgentConfigs(
   const agents = createAgents(config);
   const agents = createAgents(config);
   return Object.fromEntries(
   return Object.fromEntries(
     agents.map((a) => {
     agents.map((a) => {
-      const sdkConfig: SDKAgentConfig = {
+      const sdkConfig: SDKAgentConfig & { mcps?: string[] } = {
         ...a.config,
         ...a.config,
         description: a.description,
         description: a.description,
+        mcps: getAgentMcpList(a.name, config),
       };
       };
 
 
       // Apply classification-based visibility and mode
       // Apply classification-based visibility and mode

+ 1 - 1
src/agents/orchestrator.ts

@@ -158,7 +158,7 @@ Before executing, ask yourself: should the task split into subtasks and schedule
 
 
 ## Phase 6: Verify
 ## Phase 6: Verify
 - Run \`lsp_diagnostics\` to check for errors
 - Run \`lsp_diagnostics\` to check for errors
-- Suggest user run \`yagni-enforcement\` skill when applicable
+- Suggest user run \`simplify\` skill when applicable
 - Verify all delegated tasks completed successfully
 - Verify all delegated tasks completed successfully
 - Confirm the solution meets original requirements (Phase 1)
 - Confirm the solution meets original requirements (Phase 1)
 
 

+ 2 - 2
src/cli/install.ts

@@ -112,7 +112,7 @@ function formatConfigSummary(config: InstallConfig): string {
   lines.push(
   lines.push(
     `  ${config.hasOpenAI ? SYMBOLS.check : `${DIM}○${RESET}`} OpenAI`,
     `  ${config.hasOpenAI ? SYMBOLS.check : `${DIM}○${RESET}`} OpenAI`,
   );
   );
-  lines.push(`  ${SYMBOLS.check} Opencode Zen (free models)`); // Always enabled
+  lines.push(`  ${SYMBOLS.check} Opencode Zen (Grok)`); // Always enabled
   lines.push(
   lines.push(
     `  ${config.hasTmux ? SYMBOLS.check : `${DIM}○${RESET}`} Tmux Integration`,
     `  ${config.hasTmux ? SYMBOLS.check : `${DIM}○${RESET}`} Tmux Integration`,
   );
   );
@@ -275,7 +275,7 @@ async function runInstall(config: InstallConfig): Promise<number> {
 
 
   if (!config.hasAntigravity && !config.hasOpenAI) {
   if (!config.hasAntigravity && !config.hasOpenAI) {
     printWarning(
     printWarning(
-      'No providers configured. Zen free models will be used as fallback.',
+      'No providers configured. Zen Grok models will be used as fallback.',
     );
     );
   }
   }
 
 

+ 68 - 20
src/cli/providers.test.ts

@@ -4,7 +4,7 @@ import { describe, expect, test } from 'bun:test';
 import { generateLiteConfig, MODEL_MAPPINGS } from './providers';
 import { generateLiteConfig, MODEL_MAPPINGS } from './providers';
 
 
 describe('providers', () => {
 describe('providers', () => {
-  test('generateLiteConfig generates antigravity config by default', () => {
+  test('generateLiteConfig generates antigravity config when only antigravity selected', () => {
     const config = generateLiteConfig({
     const config = generateLiteConfig({
       hasAntigravity: true,
       hasAntigravity: true,
       hasOpenAI: false,
       hasOpenAI: false,
@@ -14,15 +14,19 @@ describe('providers', () => {
 
 
     expect(config.preset).toBe('antigravity');
     expect(config.preset).toBe('antigravity');
     const agents = (config.presets as any).antigravity;
     const agents = (config.presets as any).antigravity;
+    expect(agents).toBeDefined();
     expect(agents.orchestrator.model).toBe(
     expect(agents.orchestrator.model).toBe(
       MODEL_MAPPINGS.antigravity.orchestrator.model,
       MODEL_MAPPINGS.antigravity.orchestrator.model,
     );
     );
     expect(agents.orchestrator.variant).toBeUndefined();
     expect(agents.orchestrator.variant).toBeUndefined();
     expect(agents.fixer.model).toBe(MODEL_MAPPINGS.antigravity.fixer.model);
     expect(agents.fixer.model).toBe(MODEL_MAPPINGS.antigravity.fixer.model);
     expect(agents.fixer.variant).toBe(MODEL_MAPPINGS.antigravity.fixer.variant);
     expect(agents.fixer.variant).toBe(MODEL_MAPPINGS.antigravity.fixer.variant);
+    // Should NOT include other presets
+    expect((config.presets as any).openai).toBeUndefined();
+    expect((config.presets as any)['zen-free']).toBeUndefined();
   });
   });
 
 
-  test('generateLiteConfig always includes antigravity-openai preset', () => {
+  test('generateLiteConfig generates antigravity-openai preset when both selected', () => {
     const config = generateLiteConfig({
     const config = generateLiteConfig({
       hasAntigravity: true,
       hasAntigravity: true,
       hasOpenAI: true,
       hasOpenAI: true,
@@ -32,45 +36,57 @@ describe('providers', () => {
 
 
     expect(config.preset).toBe('antigravity-openai');
     expect(config.preset).toBe('antigravity-openai');
     const agents = (config.presets as any)['antigravity-openai'];
     const agents = (config.presets as any)['antigravity-openai'];
+    expect(agents).toBeDefined();
     expect(agents.orchestrator.model).toBe(
     expect(agents.orchestrator.model).toBe(
       MODEL_MAPPINGS.antigravity.orchestrator.model,
       MODEL_MAPPINGS.antigravity.orchestrator.model,
     );
     );
     expect(agents.orchestrator.variant).toBeUndefined();
     expect(agents.orchestrator.variant).toBeUndefined();
     expect(agents.oracle.model).toBe('openai/gpt-5.2-codex');
     expect(agents.oracle.model).toBe('openai/gpt-5.2-codex');
     expect(agents.oracle.variant).toBe('high');
     expect(agents.oracle.variant).toBe('high');
+    // Should NOT include other presets
+    expect((config.presets as any).antigravity).toBeUndefined();
+    expect((config.presets as any).openai).toBeUndefined();
   });
   });
 
 
-  test('generateLiteConfig includes antigravity-openai preset even with only antigravity', () => {
+  test('generateLiteConfig generates openai preset when only openai selected', () => {
     const config = generateLiteConfig({
     const config = generateLiteConfig({
-      hasAntigravity: true,
-      hasOpenAI: false,
+      hasAntigravity: false,
+      hasOpenAI: true,
       hasOpencodeZen: false,
       hasOpencodeZen: false,
       hasTmux: false,
       hasTmux: false,
     });
     });
 
 
-    expect(config.preset).toBe('antigravity');
-    const agents = (config.presets as any)['antigravity-openai'];
+    expect(config.preset).toBe('openai');
+    const agents = (config.presets as any).openai;
     expect(agents).toBeDefined();
     expect(agents).toBeDefined();
-    expect(agents.oracle.model).toBe('openai/gpt-5.2-codex');
+    expect(agents.orchestrator.model).toBe(
+      MODEL_MAPPINGS.openai.orchestrator.model,
+    );
+    expect(agents.orchestrator.variant).toBeUndefined();
+    // Should NOT include other presets
+    expect((config.presets as any).antigravity).toBeUndefined();
+    expect((config.presets as any)['zen-free']).toBeUndefined();
   });
   });
 
 
-  test('generateLiteConfig uses openai if no antigravity', () => {
+  test('generateLiteConfig generates zen-free preset when no providers selected', () => {
     const config = generateLiteConfig({
     const config = generateLiteConfig({
       hasAntigravity: false,
       hasAntigravity: false,
-      hasOpenAI: true,
+      hasOpenAI: false,
       hasOpencodeZen: false,
       hasOpencodeZen: false,
       hasTmux: false,
       hasTmux: false,
     });
     });
 
 
-    expect(config.preset).toBe('openai');
-    const agents = (config.presets as any).openai;
-    expect(agents.orchestrator.model).toBe(
-      MODEL_MAPPINGS.openai.orchestrator.model,
-    );
+    expect(config.preset).toBe('zen-free');
+    const agents = (config.presets as any)['zen-free'];
+    expect(agents).toBeDefined();
+    expect(agents.orchestrator.model).toBe('opencode/grok-code');
     expect(agents.orchestrator.variant).toBeUndefined();
     expect(agents.orchestrator.variant).toBeUndefined();
+    // Should NOT include other presets
+    expect((config.presets as any).antigravity).toBeUndefined();
+    expect((config.presets as any).openai).toBeUndefined();
   });
   });
 
 
-  test('generateLiteConfig uses zen-free if no antigravity or openai', () => {
+  test('generateLiteConfig uses zen-free grok-code models', () => {
     const config = generateLiteConfig({
     const config = generateLiteConfig({
       hasAntigravity: false,
       hasAntigravity: false,
       hasOpenAI: false,
       hasOpenAI: false,
@@ -80,10 +96,11 @@ describe('providers', () => {
 
 
     expect(config.preset).toBe('zen-free');
     expect(config.preset).toBe('zen-free');
     const agents = (config.presets as any)['zen-free'];
     const agents = (config.presets as any)['zen-free'];
-    expect(agents.orchestrator.model).toBe(
-      MODEL_MAPPINGS['zen-free'].orchestrator.model,
-    );
-    expect(agents.orchestrator.variant).toBeUndefined();
+    expect(agents.orchestrator.model).toBe('opencode/grok-code');
+    expect(agents.oracle.model).toBe('opencode/grok-code');
+    expect(agents.oracle.variant).toBe('high');
+    expect(agents.librarian.model).toBe('opencode/grok-code');
+    expect(agents.librarian.variant).toBe('low');
   });
   });
 
 
   test('generateLiteConfig enables tmux when requested', () => {
   test('generateLiteConfig enables tmux when requested', () => {
@@ -110,4 +127,35 @@ describe('providers', () => {
     expect(agents.orchestrator.skills).toContain('*');
     expect(agents.orchestrator.skills).toContain('*');
     expect(agents.fixer.skills).toBeDefined();
     expect(agents.fixer.skills).toBeDefined();
   });
   });
+
+  test('generateLiteConfig includes mcps field', () => {
+    const config = generateLiteConfig({
+      hasAntigravity: true,
+      hasOpenAI: false,
+      hasOpencodeZen: false,
+      hasTmux: false,
+    });
+
+    const agents = (config.presets as any).antigravity;
+    expect(agents.orchestrator.mcps).toBeDefined();
+    expect(Array.isArray(agents.orchestrator.mcps)).toBe(true);
+    expect(agents.librarian.mcps).toBeDefined();
+    expect(Array.isArray(agents.librarian.mcps)).toBe(true);
+  });
+
+  test('generateLiteConfig zen-free includes correct mcps', () => {
+    const config = generateLiteConfig({
+      hasAntigravity: false,
+      hasOpenAI: false,
+      hasOpencodeZen: false,
+      hasTmux: false,
+    });
+
+    const agents = (config.presets as any)['zen-free'];
+    expect(agents.orchestrator.mcps).toContain('websearch');
+    expect(agents.librarian.mcps).toContain('websearch');
+    expect(agents.librarian.mcps).toContain('context7');
+    expect(agents.librarian.mcps).toContain('grep_app');
+    expect(agents.designer.mcps).toEqual([]);
+  });
 });
 });

+ 42 - 41
src/cli/providers.ts

@@ -1,4 +1,7 @@
-import { DEFAULT_AGENT_SKILLS } from '../tools/skill/builtin';
+import {
+  DEFAULT_AGENT_MCPS,
+  DEFAULT_AGENT_SKILLS,
+} from '../tools/skill/builtin';
 import type { InstallConfig } from './types';
 import type { InstallConfig } from './types';
 
 
 /**
 /**
@@ -56,8 +59,8 @@ export const MODEL_MAPPINGS = {
     fixer: { model: 'openai/gpt-5.1-codex-mini', variant: 'low' },
     fixer: { model: 'openai/gpt-5.1-codex-mini', variant: 'low' },
   },
   },
   'zen-free': {
   'zen-free': {
-    orchestrator: { model: 'opencode/glm-4.7-free' },
-    oracle: { model: 'opencode/glm-4.7-free', variant: 'high' },
+    orchestrator: { model: 'opencode/grok-code' },
+    oracle: { model: 'opencode/grok-code', variant: 'high' },
     librarian: { model: 'opencode/grok-code', variant: 'low' },
     librarian: { model: 'opencode/grok-code', variant: 'low' },
     explorer: { model: 'opencode/grok-code', variant: 'low' },
     explorer: { model: 'opencode/grok-code', variant: 'low' },
     designer: { model: 'opencode/grok-code', variant: 'medium' },
     designer: { model: 'opencode/grok-code', variant: 'medium' },
@@ -68,24 +71,18 @@ export const MODEL_MAPPINGS = {
 export function generateLiteConfig(
 export function generateLiteConfig(
   installConfig: InstallConfig,
   installConfig: InstallConfig,
 ): Record<string, unknown> {
 ): Record<string, unknown> {
-  // Determine base provider
-  const baseProvider = installConfig.hasAntigravity
-    ? 'antigravity'
-    : installConfig.hasOpenAI
-      ? 'openai'
-      : 'zen-free';
-
   const config: Record<string, unknown> = {
   const config: Record<string, unknown> = {
-    preset: baseProvider,
+    preset: 'zen-free',
     presets: {},
     presets: {},
   };
   };
 
 
-  // Generate all presets
-  for (const [providerName, models] of Object.entries(MODEL_MAPPINGS)) {
-    const agents: Record<
-      string,
-      { model: string; variant?: string; skills: string[] }
-    > = Object.fromEntries(
+  const createAgents = (
+    models: Record<string, { model: string; variant?: string }>,
+  ): Record<
+    string,
+    { model: string; variant?: string; skills: string[]; mcps: string[] }
+  > =>
+    Object.fromEntries(
       Object.entries(models).map(([k, v]) => [
       Object.entries(models).map(([k, v]) => [
         k,
         k,
         {
         {
@@ -93,36 +90,40 @@ export function generateLiteConfig(
           variant: v.variant,
           variant: v.variant,
           skills:
           skills:
             DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ?? [],
             DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ?? [],
+          mcps: DEFAULT_AGENT_MCPS[k as keyof typeof DEFAULT_AGENT_MCPS] ?? [],
         },
         },
       ]),
       ]),
     );
     );
-    (config.presets as Record<string, unknown>)[providerName] = agents;
-  }
-
-  // Always add antigravity-openai preset
-  const mixedAgents: Record<string, { model: string; variant?: string }> = {
-    ...MODEL_MAPPINGS.antigravity,
-  };
-  mixedAgents.oracle = { model: 'openai/gpt-5.2-codex', variant: 'high' };
-  const agents: Record<
-    string,
-    { model: string; variant?: string; skills: string[] }
-  > = Object.fromEntries(
-    Object.entries(mixedAgents).map(([k, v]) => [
-      k,
-      {
-        model: v.model,
-        variant: v.variant,
-        skills:
-          DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ?? [],
-      },
-    ]),
-  );
-  (config.presets as Record<string, unknown>)['antigravity-openai'] = agents;
 
 
-  // Set default preset based on user choice
+  // Only generate preset based on user selection
   if (installConfig.hasAntigravity && installConfig.hasOpenAI) {
   if (installConfig.hasAntigravity && installConfig.hasOpenAI) {
+    // Mixed preset: antigravity base with OpenAI oracle
+    const mixedAgents: Record<string, { model: string; variant?: string }> = {
+      ...MODEL_MAPPINGS.antigravity,
+    };
+    mixedAgents.oracle = { model: 'openai/gpt-5.2-codex', variant: 'high' };
+    (config.presets as Record<string, unknown>)['antigravity-openai'] =
+      createAgents(mixedAgents);
+
     config.preset = 'antigravity-openai';
     config.preset = 'antigravity-openai';
+  } else if (installConfig.hasAntigravity) {
+    // Antigravity only
+    (config.presets as Record<string, unknown>).antigravity = createAgents(
+      MODEL_MAPPINGS.antigravity,
+    );
+    config.preset = 'antigravity';
+  } else if (installConfig.hasOpenAI) {
+    // OpenAI only
+    (config.presets as Record<string, unknown>).openai = createAgents(
+      MODEL_MAPPINGS.openai,
+    );
+    config.preset = 'openai';
+  } else {
+    // Zen free only
+    (config.presets as Record<string, unknown>)['zen-free'] = createAgents(
+      MODEL_MAPPINGS['zen-free'],
+    );
+    config.preset = 'zen-free';
   }
   }
 
 
   if (installConfig.hasTmux) {
   if (installConfig.hasTmux) {

+ 2 - 1
src/config/schema.ts

@@ -5,7 +5,8 @@ export const AgentOverrideConfigSchema = z.object({
   model: z.string().optional(),
   model: z.string().optional(),
   temperature: z.number().min(0).max(2).optional(),
   temperature: z.number().min(0).max(2).optional(),
   variant: z.string().optional().catch(undefined),
   variant: z.string().optional().catch(undefined),
-  skills: z.array(z.string()).optional(), // skills this agent can use ("*" = all)
+  skills: z.array(z.string()).optional(), // skills this agent can use ("*" = all, "!item" = exclude)
+  mcps: z.array(z.string()).optional(), // MCPs this agent can use ("*" = all, "!item" = exclude)
 });
 });
 
 
 // Tmux layout options
 // Tmux layout options

+ 39 - 5
src/index.ts

@@ -23,6 +23,7 @@ import {
 } from './tools';
 } from './tools';
 import { startTmuxCheck } from './utils';
 import { startTmuxCheck } from './utils';
 import { log } from './utils/logger';
 import { log } from './utils/logger';
+import { parseList } from './tools/skill/builtin';
 
 
 const OhMyOpenCodeLite: Plugin = async (ctx) => {
 const OhMyOpenCodeLite: Plugin = async (ctx) => {
   const config = loadPluginConfig(ctx.directory);
   const config = loadPluginConfig(ctx.directory);
@@ -96,14 +97,13 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       (opencodeConfig as { default_agent?: string }).default_agent =
       (opencodeConfig as { default_agent?: string }).default_agent =
         'orchestrator';
         'orchestrator';
 
 
-      const configAgent = opencodeConfig.agent as
-        | Record<string, unknown>
-        | undefined;
-      if (!configAgent) {
+      // Merge Agent configs
+      if (!opencodeConfig.agent) {
         opencodeConfig.agent = { ...agents };
         opencodeConfig.agent = { ...agents };
       } else {
       } else {
-        Object.assign(configAgent, agents);
+        Object.assign(opencodeConfig.agent, agents);
       }
       }
+      const configAgent = opencodeConfig.agent as Record<string, any>;
 
 
       // Merge MCP configs
       // Merge MCP configs
       const configMcp = opencodeConfig.mcp as
       const configMcp = opencodeConfig.mcp as
@@ -114,6 +114,40 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       } else {
       } else {
         Object.assign(configMcp, mcps);
         Object.assign(configMcp, mcps);
       }
       }
+
+      // Get all MCP names from our config
+      const allMcpNames = Object.keys(mcps);
+
+      // For each agent, create permission rules based on their mcps list
+      for (const [agentName, agentConfig] of Object.entries(agents)) {
+        const agentMcps = (agentConfig as { mcps?: string[] })?.mcps;
+        if (!agentMcps) continue;
+
+        // Get or create agent permission config
+        if (!configAgent[agentName]) {
+          configAgent[agentName] = { ...agentConfig };
+        }
+        const agentPermission = (configAgent[agentName].permission ?? {}) as Record<string, unknown>;
+
+        // Parse mcps list with wildcard and exclusion support
+        const allowedMcps = parseList(agentMcps, allMcpNames);
+
+        // Create permission rules for each MCP
+        // MCP tools are named as <server>_<tool>, so we use <server>_*
+        for (const mcpName of allMcpNames) {
+          const sanitizedMcpName = mcpName.replace(/[^a-zA-Z0-9_-]/g, '_');
+          const permissionKey = `${sanitizedMcpName}_*`;
+          const action = allowedMcps.includes(mcpName) ? 'allow' : 'deny';
+
+          // Only set if not already defined by user
+          if (!(permissionKey in agentPermission)) {
+            agentPermission[permissionKey] = action;
+          }
+        }
+
+        // Update agent config with permissions
+        configAgent[agentName].permission = agentPermission;
+      }
     },
     },
 
 
     event: async (input) => {
     event: async (input) => {

+ 228 - 0
src/tools/grep/cli.test.ts

@@ -0,0 +1,228 @@
+/// <reference types="bun-types" />
+
+import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
+import { mkdir, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { runRg, runRgCount } from './cli';
+import { grep } from './tools';
+import { formatGrepResult } from './utils';
+
+describe('grep tool', () => {
+  const testDir = join(tmpdir(), `grep-test-${Date.now()}`);
+  const testFile1 = join(testDir, 'test1.txt');
+  const testFile2 = join(testDir, 'test2.ts');
+
+  beforeAll(async () => {
+    await mkdir(testDir, { recursive: true });
+    await writeFile(
+      testFile1,
+      'Hello world\nThis is a test file\nAnother line with match',
+    );
+    await writeFile(
+      testFile2,
+      "const x = 'Hello world';\nconsole.log('test');",
+    );
+  });
+
+  afterAll(async () => {
+    await rm(testDir, { recursive: true, force: true });
+  });
+
+  describe('formatGrepResult', () => {
+    test('formats empty results', () => {
+      const result = {
+        matches: [],
+        totalMatches: 0,
+        filesSearched: 10,
+        truncated: false,
+      };
+      expect(formatGrepResult(result)).toBe('No matches found.');
+    });
+
+    test('formats error results', () => {
+      const result = {
+        matches: [],
+        totalMatches: 0,
+        filesSearched: 0,
+        truncated: false,
+        error: 'Something went wrong',
+      };
+      expect(formatGrepResult(result)).toBe('Error: Something went wrong');
+    });
+
+    test('formats matches correctly', () => {
+      const result = {
+        matches: [
+          { file: 'file1.ts', line: 10, text: "const foo = 'bar'" },
+          { file: 'file1.ts', line: 15, text: 'console.log(foo)' },
+          { file: 'file2.ts', line: 5, text: "import { foo } from './file1'" },
+        ],
+        totalMatches: 3,
+        filesSearched: 2,
+        truncated: false,
+      };
+
+      const output = formatGrepResult(result);
+      expect(output).toContain('file1.ts:');
+      expect(output).toContain("  10: const foo = 'bar'");
+      expect(output).toContain('  15: console.log(foo)');
+      expect(output).toContain('file2.ts:');
+      expect(output).toContain("  5: import { foo } from './file1'");
+      expect(output).toContain('Found 3 matches in 2 files');
+    });
+
+    test('indicates truncation', () => {
+      const result = {
+        matches: [{ file: 'foo.txt', line: 1, text: 'bar' }],
+        totalMatches: 100,
+        filesSearched: 50,
+        truncated: true,
+      };
+      expect(formatGrepResult(result)).toContain('(output truncated)');
+    });
+  });
+
+  describe('runRg', () => {
+    test('finds matches in files', async () => {
+      const result = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+      });
+
+      expect(result.totalMatches).toBeGreaterThanOrEqual(2);
+      expect(
+        result.matches.some(
+          (m) => m.file.includes('test1.txt') && m.text.includes('Hello'),
+        ),
+      ).toBe(true);
+      expect(
+        result.matches.some(
+          (m) => m.file.includes('test2.ts') && m.text.includes('Hello'),
+        ),
+      ).toBe(true);
+    });
+
+    test('respects file inclusion patterns', async () => {
+      const result = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+        globs: ['*.txt'],
+      });
+
+      expect(result.matches.some((m) => m.file.includes('test1.txt'))).toBe(
+        true,
+      );
+      expect(result.matches.some((m) => m.file.includes('test2.ts'))).toBe(
+        false,
+      );
+    });
+
+    test('handles no matches', async () => {
+      const result = await runRg({
+        pattern: 'NonExistentString12345',
+        paths: [testDir],
+      });
+
+      expect(result.totalMatches).toBe(0);
+      expect(result.matches).toHaveLength(0);
+    });
+
+    test('respects case sensitivity', async () => {
+      // Test with exact case match
+      const resultExact = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+      });
+      expect(resultExact.totalMatches).toBeGreaterThan(0);
+
+      // Test with caseSensitive flag set to true (should not match lowercase pattern)
+      const resultSensitive = await runRg({
+        pattern: 'hello', // File has "Hello"
+        paths: [testDir],
+        caseSensitive: true,
+      });
+      expect(resultSensitive.totalMatches).toBe(0);
+    });
+
+    test('respects whole word match', async () => {
+      const resultPartial = await runRg({
+        pattern: 'Hell',
+        paths: [testDir],
+        wholeWord: false,
+      });
+      expect(resultPartial.totalMatches).toBeGreaterThan(0);
+
+      const resultWhole = await runRg({
+        pattern: 'Hell',
+        paths: [testDir],
+        wholeWord: true,
+      });
+      expect(resultWhole.totalMatches).toBe(0);
+    });
+
+    test('respects max count', async () => {
+      const result = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+        maxCount: 1,
+      });
+      // maxCount is per file
+      expect(
+        result.matches.filter((m) => m.file.includes('test1.txt')).length,
+      ).toBeLessThanOrEqual(1);
+    });
+  });
+
+  describe('runRgCount', () => {
+    test('counts matches correctly', async () => {
+      const results = await runRgCount({
+        pattern: 'Hello',
+        paths: [testDir],
+      });
+
+      expect(results.length).toBeGreaterThan(0);
+      const file1Result = results.find((r) => r.file.includes('test1.txt'));
+      expect(file1Result).toBeDefined();
+      expect(file1Result?.count).toBe(1);
+    });
+  });
+
+  describe('grep tool execute', () => {
+    test('executes successfully', async () => {
+      // @ts-expect-error
+      const result = await grep.execute({
+        pattern: 'Hello',
+        path: testDir,
+      });
+
+      expect(typeof result).toBe('string');
+      expect(result).toContain('Found');
+      expect(result).toContain('matches');
+    });
+
+    test('handles errors gracefully', async () => {
+      // @ts-expect-error
+      const result = await grep.execute({
+        pattern: 'Hello',
+        path: '/non/existent/path/12345',
+      });
+
+      // Depending on implementation, it might return "No matches found" or an error string
+      // But it should not throw
+      expect(typeof result).toBe('string');
+    });
+
+    test('respects include pattern in execute', async () => {
+      // @ts-expect-error
+      const result = await grep.execute({
+        pattern: 'Hello',
+        path: testDir,
+        include: '*.txt',
+      });
+
+      expect(result).toContain('test1.txt');
+      expect(result).not.toContain('test2.ts');
+    });
+  });
+});

+ 171 - 0
src/tools/grep/constants.test.ts

@@ -0,0 +1,171 @@
+/// <reference types="bun-types" />
+
+import { afterEach, describe, expect, test } from 'bun:test';
+import { join } from 'node:path';
+
+// Mock process.env and process.platform for testing
+const originalPlatform = process.platform;
+const originalEnv = { ...process.env };
+
+function mockPlatform(platform: NodeJS.Platform) {
+  Object.defineProperty(process, 'platform', {
+    value: platform,
+    configurable: true,
+  });
+}
+
+function mockEnv(env: Partial<Record<string, string>>) {
+  Object.assign(process.env, env);
+}
+
+function restoreMocks() {
+  Object.defineProperty(process, 'platform', {
+    value: originalPlatform,
+    configurable: true,
+  });
+  process.env = { ...originalEnv };
+}
+
+describe('grep constants', () => {
+  afterEach(() => {
+    restoreMocks();
+  });
+
+  describe('getDataDir', () => {
+    test('returns LOCALAPPDATA on Windows when set', () => {
+      mockPlatform('win32');
+      mockEnv({ LOCALAPPDATA: 'C:\\Users\\test\\AppData\\Local' });
+
+      // Import after mocking to get the mocked behavior
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('C:\\Users\\test\\AppData\\Local');
+    });
+
+    test('returns APPDATA on Windows when LOCALAPPDATA not set', () => {
+      mockPlatform('win32');
+      mockEnv({ APPDATA: 'C:\\Users\\test\\AppData\\Roaming' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('C:\\Users\\test\\AppData\\Roaming');
+    });
+
+    test('returns USERPROFILE/AppData/Local on Windows when no env vars set', () => {
+      mockPlatform('win32');
+      mockEnv({ USERPROFILE: 'C:\\Users\\test' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('C:\\Users\\test', 'AppData', 'Local'));
+    });
+
+    test('returns XDG_DATA_HOME on Unix when set', () => {
+      mockPlatform('linux');
+      mockEnv({ XDG_DATA_HOME: '/home/test/.local/share' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('/home/test/.local/share');
+    });
+
+    test('returns HOME/.local/share on Unix when XDG_DATA_HOME not set', () => {
+      mockPlatform('linux');
+      mockEnv({ HOME: '/home/test' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('/home/test', '.local', 'share'));
+    });
+
+    test('returns ./.local/share on Unix when HOME not set', () => {
+      mockPlatform('linux');
+      // Clear HOME from environment
+      delete process.env.HOME;
+      delete process.env.XDG_DATA_HOME;
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('.', '.local', 'share'));
+    });
+
+    test('returns XDG_DATA_HOME on macOS when set', () => {
+      mockPlatform('darwin');
+      mockEnv({ XDG_DATA_HOME: '/Users/test/.local/share' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('/Users/test/.local/share');
+    });
+
+    test('returns HOME/.local/share on macOS when XDG_DATA_HOME not set', () => {
+      mockPlatform('darwin');
+      mockEnv({ HOME: '/Users/test' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('/Users/test', '.local', 'share'));
+    });
+  });
+
+  describe('constants', () => {
+    test('DEFAULT_MAX_DEPTH is 20', () => {
+      const { DEFAULT_MAX_DEPTH } = require('./constants');
+      expect(DEFAULT_MAX_DEPTH).toBe(20);
+    });
+
+    test('DEFAULT_MAX_FILESIZE is 10M', () => {
+      const { DEFAULT_MAX_FILESIZE } = require('./constants');
+      expect(DEFAULT_MAX_FILESIZE).toBe('10M');
+    });
+
+    test('DEFAULT_MAX_COUNT is 500', () => {
+      const { DEFAULT_MAX_COUNT } = require('./constants');
+      expect(DEFAULT_MAX_COUNT).toBe(500);
+    });
+
+    test('DEFAULT_MAX_COLUMNS is 1000', () => {
+      const { DEFAULT_MAX_COLUMNS } = require('./constants');
+      expect(DEFAULT_MAX_COLUMNS).toBe(1000);
+    });
+
+    test('DEFAULT_CONTEXT is 2', () => {
+      const { DEFAULT_CONTEXT } = require('./constants');
+      expect(DEFAULT_CONTEXT).toBe(2);
+    });
+
+    test('DEFAULT_TIMEOUT_MS is 300000', () => {
+      const { DEFAULT_TIMEOUT_MS } = require('./constants');
+      expect(DEFAULT_TIMEOUT_MS).toBe(300_000);
+    });
+
+    test('DEFAULT_MAX_OUTPUT_BYTES is 10MB', () => {
+      const { DEFAULT_MAX_OUTPUT_BYTES } = require('./constants');
+      expect(DEFAULT_MAX_OUTPUT_BYTES).toBe(10 * 1024 * 1024);
+    });
+
+    test('RG_SAFETY_FLAGS contains expected flags', () => {
+      const { RG_SAFETY_FLAGS } = require('./constants');
+      expect(RG_SAFETY_FLAGS).toContain('--no-follow');
+      expect(RG_SAFETY_FLAGS).toContain('--color=never');
+      expect(RG_SAFETY_FLAGS).toContain('--no-heading');
+      expect(RG_SAFETY_FLAGS).toContain('--line-number');
+      expect(RG_SAFETY_FLAGS).toContain('--with-filename');
+    });
+
+    test('GREP_SAFETY_FLAGS contains expected flags', () => {
+      const { GREP_SAFETY_FLAGS } = require('./constants');
+      expect(GREP_SAFETY_FLAGS).toContain('-n');
+      expect(GREP_SAFETY_FLAGS).toContain('-H');
+      expect(GREP_SAFETY_FLAGS).toContain('--color=never');
+    });
+  });
+});

+ 14 - 3
src/tools/grep/constants.ts

@@ -5,8 +5,6 @@ import {
   downloadAndInstallRipgrep,
   downloadAndInstallRipgrep,
   getInstalledRipgrepPath,
   getInstalledRipgrepPath,
 } from './downloader';
 } from './downloader';
-import { homedir } from 'os';
-
 
 
 export type GrepBackend = 'rg' | 'grep';
 export type GrepBackend = 'rg' | 'grep';
 
 
@@ -34,6 +32,19 @@ function findExecutable(name: string): string | null {
   return null;
   return null;
 }
 }
 
 
+export function getDataDir(): string {
+  if (process.platform === 'win32') {
+    return (
+      process.env.LOCALAPPDATA ||
+      process.env.APPDATA ||
+      join(process.env.USERPROFILE || '.', 'AppData', 'Local')
+    );
+  }
+  return (
+    process.env.XDG_DATA_HOME ||
+    join(process.env.HOME || '.', '.local', 'share')
+  );
+}
 function getOpenCodeBundledRg(): string | null {
 function getOpenCodeBundledRg(): string | null {
   const execPath = process.execPath;
   const execPath = process.execPath;
   const execDir = dirname(execPath);
   const execDir = dirname(execPath);
@@ -43,7 +54,7 @@ function getOpenCodeBundledRg(): string | null {
 
 
   const candidates = [
   const candidates = [
     // OpenCode XDG data path (highest priority - where OpenCode installs rg)
     // OpenCode XDG data path (highest priority - where OpenCode installs rg)
-    join(homedir(), '.opencode', 'bin', rgName),
+    join(getDataDir(), 'opencode', 'bin', rgName),
     // Legacy paths relative to execPath
     // Legacy paths relative to execPath
     join(execDir, rgName),
     join(execDir, rgName),
     join(execDir, 'bin', rgName),
     join(execDir, 'bin', rgName),

+ 450 - 21
src/tools/skill/builtin.test.ts

@@ -1,11 +1,15 @@
 import { describe, expect, test } from 'bun:test';
 import { describe, expect, test } from 'bun:test';
 import type { PluginConfig } from '../../config/schema';
 import type { PluginConfig } from '../../config/schema';
 import {
 import {
+  canAgentUseMcp,
   canAgentUseSkill,
   canAgentUseSkill,
+  DEFAULT_AGENT_MCPS,
   DEFAULT_AGENT_SKILLS,
   DEFAULT_AGENT_SKILLS,
+  getAgentMcpList,
   getBuiltinSkills,
   getBuiltinSkills,
   getSkillByName,
   getSkillByName,
   getSkillsForAgent,
   getSkillsForAgent,
+  parseList,
 } from './builtin';
 } from './builtin';
 
 
 describe('getBuiltinSkills', () => {
 describe('getBuiltinSkills', () => {
@@ -14,16 +18,16 @@ describe('getBuiltinSkills', () => {
     expect(skills.length).toBeGreaterThan(0);
     expect(skills.length).toBeGreaterThan(0);
 
 
     const names = skills.map((s) => s.name);
     const names = skills.map((s) => s.name);
-    expect(names).toContain('yagni-enforcement');
+    expect(names).toContain('simplify');
     expect(names).toContain('playwright');
     expect(names).toContain('playwright');
   });
   });
 });
 });
 
 
 describe('getSkillByName', () => {
 describe('getSkillByName', () => {
   test('returns skill by exact name', () => {
   test('returns skill by exact name', () => {
-    const skill = getSkillByName('yagni-enforcement');
+    const skill = getSkillByName('simplify');
     expect(skill).toBeDefined();
     expect(skill).toBeDefined();
-    expect(skill?.name).toBe('yagni-enforcement');
+    expect(skill?.name).toBe('simplify');
   });
   });
 
 
   test('returns undefined for unknown skill', () => {
   test('returns undefined for unknown skill', () => {
@@ -86,12 +90,12 @@ describe('getSkillsForAgent', () => {
   test('respects config override for agent skills', () => {
   test('respects config override for agent skills', () => {
     const config: PluginConfig = {
     const config: PluginConfig = {
       agents: {
       agents: {
-        oracle: { skills: ['yagni-enforcement'] },
+        oracle: { skills: ['simplify'] },
       },
       },
     };
     };
     const skills = getSkillsForAgent('oracle', config);
     const skills = getSkillsForAgent('oracle', config);
     expect(skills.length).toBe(1);
     expect(skills.length).toBe(1);
-    expect(skills[0].name).toBe('yagni-enforcement');
+    expect(skills[0].name).toBe('simplify');
   });
   });
 
 
   test('config wildcard overrides default', () => {
   test('config wildcard overrides default', () => {
@@ -129,12 +133,12 @@ describe('getSkillsForAgent', () => {
   test("backward compat: 'frontend-ui-ux-engineer' alias applies to designer", () => {
   test("backward compat: 'frontend-ui-ux-engineer' alias applies to designer", () => {
     const config: PluginConfig = {
     const config: PluginConfig = {
       agents: {
       agents: {
-        'frontend-ui-ux-engineer': { skills: ['yagni-enforcement'] },
+        'frontend-ui-ux-engineer': { skills: ['simplify'] },
       },
       },
     };
     };
     const skills = getSkillsForAgent('designer', config);
     const skills = getSkillsForAgent('designer', config);
     expect(skills.length).toBe(1);
     expect(skills.length).toBe(1);
-    expect(skills[0].name).toBe('yagni-enforcement');
+    expect(skills[0].name).toBe('simplify');
   });
   });
 
 
   test('returns empty for unknown agent without config', () => {
   test('returns empty for unknown agent without config', () => {
@@ -145,31 +149,32 @@ describe('getSkillsForAgent', () => {
 
 
 describe('canAgentUseSkill', () => {
 describe('canAgentUseSkill', () => {
   test('orchestrator can use any skill (wildcard)', () => {
   test('orchestrator can use any skill (wildcard)', () => {
-    expect(canAgentUseSkill('orchestrator', 'yagni-enforcement')).toBe(true);
+    expect(canAgentUseSkill('orchestrator', 'simplify')).toBe(true);
     expect(canAgentUseSkill('orchestrator', 'playwright')).toBe(true);
     expect(canAgentUseSkill('orchestrator', 'playwright')).toBe(true);
-    expect(canAgentUseSkill('orchestrator', 'any-skill')).toBe(true);
+    // Note: parseList doesn't filter non-existent items when using explicit allowlist
+    // but canAgentUseSkill checks against actual skill names
   });
   });
 
 
   test('designer can use playwright', () => {
   test('designer can use playwright', () => {
     expect(canAgentUseSkill('designer', 'playwright')).toBe(true);
     expect(canAgentUseSkill('designer', 'playwright')).toBe(true);
   });
   });
 
 
-  test('designer cannot use yagni-enforcement by default', () => {
-    expect(canAgentUseSkill('designer', 'yagni-enforcement')).toBe(false);
+  test('designer cannot use simplify by default', () => {
+    expect(canAgentUseSkill('designer', 'simplify')).toBe(false);
   });
   });
 
 
   test('oracle cannot use any skill by default', () => {
   test('oracle cannot use any skill by default', () => {
-    expect(canAgentUseSkill('oracle', 'yagni-enforcement')).toBe(false);
+    expect(canAgentUseSkill('oracle', 'simplify')).toBe(false);
     expect(canAgentUseSkill('oracle', 'playwright')).toBe(false);
     expect(canAgentUseSkill('oracle', 'playwright')).toBe(false);
   });
   });
 
 
   test('respects config override', () => {
   test('respects config override', () => {
     const config: PluginConfig = {
     const config: PluginConfig = {
       agents: {
       agents: {
-        oracle: { skills: ['yagni-enforcement'] },
+        oracle: { skills: ['simplify'] },
       },
       },
     };
     };
-    expect(canAgentUseSkill('oracle', 'yagni-enforcement', config)).toBe(true);
+    expect(canAgentUseSkill('oracle', 'simplify', config)).toBe(true);
     expect(canAgentUseSkill('oracle', 'playwright', config)).toBe(false);
     expect(canAgentUseSkill('oracle', 'playwright', config)).toBe(false);
   });
   });
 
 
@@ -179,11 +184,9 @@ describe('canAgentUseSkill', () => {
         librarian: { skills: ['*'] },
         librarian: { skills: ['*'] },
       },
       },
     };
     };
-    expect(canAgentUseSkill('librarian', 'yagni-enforcement', config)).toBe(
-      true,
-    );
+    expect(canAgentUseSkill('librarian', 'simplify', config)).toBe(true);
     expect(canAgentUseSkill('librarian', 'playwright', config)).toBe(true);
     expect(canAgentUseSkill('librarian', 'playwright', config)).toBe(true);
-    expect(canAgentUseSkill('librarian', 'any-other-skill', config)).toBe(true);
+    // Note: parseList expands wildcard to all available skills
   });
   });
 
 
   test('config empty array denies all', () => {
   test('config empty array denies all', () => {
@@ -202,12 +205,438 @@ describe('canAgentUseSkill', () => {
       },
       },
     };
     };
     expect(canAgentUseSkill('explorer', 'playwright', config)).toBe(true);
     expect(canAgentUseSkill('explorer', 'playwright', config)).toBe(true);
-    expect(canAgentUseSkill('explorer', 'yagni-enforcement', config)).toBe(
-      false,
-    );
+    expect(canAgentUseSkill('explorer', 'simplify', config)).toBe(false);
   });
   });
 
 
   test('unknown agent returns false without config', () => {
   test('unknown agent returns false without config', () => {
     expect(canAgentUseSkill('unknown-agent', 'playwright')).toBe(false);
     expect(canAgentUseSkill('unknown-agent', 'playwright')).toBe(false);
   });
   });
 });
 });
+
+describe('parseList', () => {
+  test('returns empty array for empty input', () => {
+    expect(parseList([], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns empty array for undefined input', () => {
+    expect(parseList(undefined as any, ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns explicit items when no wildcard', () => {
+    expect(parseList(['a', 'c'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('expands wildcard to all available items', () => {
+    expect(parseList(['*'], ['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
+  });
+
+  test('excludes items with ! prefix', () => {
+    expect(parseList(['*', '!b'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('excludes multiple items with ! prefix', () => {
+    expect(parseList(['*', '!b', '!c'], ['a', 'b', 'c', 'd'])).toEqual([
+      'a',
+      'd',
+    ]);
+  });
+
+  test('deny wins in case of conflict', () => {
+    expect(parseList(['a', '!a'], ['a', 'b'])).toEqual([]);
+  });
+
+  test('!* denies all items', () => {
+    expect(parseList(['!*'], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('!* overrides wildcard', () => {
+    expect(parseList(['*', '!*'], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('handles mixed allow and deny without wildcard', () => {
+    expect(parseList(['a', 'b', '!b'], ['a', 'b', 'c'])).toEqual(['a']);
+  });
+
+  test('excludes non-existent items gracefully', () => {
+    expect(parseList(['*', '!nonexistent'], ['a', 'b'])).toEqual(['a', 'b']);
+  });
+
+  test('returns explicit allowlist minus denials', () => {
+    expect(parseList(['a', 'd'], ['a', 'b', 'c'])).toEqual(['a', 'd']);
+  });
+});
+
+describe('DEFAULT_AGENT_MCPS', () => {
+  test('orchestrator has websearch MCP', () => {
+    expect(DEFAULT_AGENT_MCPS.orchestrator).toContain('websearch');
+  });
+
+  test('designer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.designer).toEqual([]);
+  });
+
+  test('oracle has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.oracle).toEqual([]);
+  });
+
+  test('librarian has websearch, context7, and grep_app MCPs', () => {
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('websearch');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('context7');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('grep_app');
+  });
+
+  test('explorer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.explorer).toEqual([]);
+  });
+
+  test('fixer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.fixer).toEqual([]);
+  });
+});
+
+describe('getAgentMcpList', () => {
+  test('returns default MCPs for orchestrator', () => {
+    const mcps = getAgentMcpList('orchestrator');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.orchestrator);
+  });
+
+  test('returns default MCPs for librarian', () => {
+    const mcps = getAgentMcpList('librarian');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.librarian);
+  });
+
+  test('returns empty for designer', () => {
+    const mcps = getAgentMcpList('designer');
+    expect(mcps).toEqual([]);
+  });
+
+  test('respects config override for agent MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('oracle', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('config wildcard overrides default', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    const mcps = getAgentMcpList('designer', config);
+    expect(mcps).toEqual(['*']);
+  });
+
+  test('config empty array removes default MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    const mcps = getAgentMcpList('librarian', config);
+    expect(mcps).toEqual([]);
+  });
+
+  test('backward compat: alias config applies to agent', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('explorer', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('returns empty for unknown agent without config', () => {
+    const mcps = getAgentMcpList('unknown-agent');
+    expect(mcps).toEqual([]);
+  });
+});
+
+describe('canAgentUseMcp', () => {
+  test('orchestrator can use websearch by default', () => {
+    expect(canAgentUseMcp('orchestrator', 'websearch')).toBe(true);
+  });
+
+  test('librarian can use websearch, context7, and grep_app by default', () => {
+    expect(canAgentUseMcp('librarian', 'websearch')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'context7')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'grep_app')).toBe(true);
+  });
+
+  test('designer cannot use any MCP by default', () => {
+    expect(canAgentUseMcp('designer', 'websearch')).toBe(false);
+    expect(canAgentUseMcp('designer', 'context7')).toBe(false);
+  });
+
+  test('respects config override', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('oracle', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('oracle', 'context7', config)).toBe(false);
+  });
+
+  test('config wildcard grants all MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'websearch', config)).toBe(true);
+  });
+
+  test('config wildcard grants skill MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'playwright', config)).toBe(true);
+  });
+
+  test('config empty array denies all MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    expect(canAgentUseMcp('librarian', 'websearch', config)).toBe(false);
+  });
+
+  test('respects exclusion syntax', () => {
+    const config: PluginConfig = {
+      agents: {
+        orchestrator: { mcps: ['*', '!websearch'] },
+      },
+    };
+    // canAgentUseMcp uses DEFAULT_AGENT_MCPS.orchestrator keys as allAvailable
+    // which is ['websearch'], so excluding websearch leaves empty
+    expect(canAgentUseMcp('orchestrator', 'websearch', config)).toBe(false);
+  });
+
+  test('backward compat: alias config affects agent permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('explorer', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('explorer', 'context7', config)).toBe(false);
+  });
+
+  test('unknown agent returns false without config', () => {
+    expect(canAgentUseMcp('unknown-agent', 'websearch')).toBe(false);
+  });
+});
+
+describe('parseList', () => {
+  test('returns empty array for empty input', () => {
+    expect(parseList([], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns empty array for undefined input', () => {
+    expect(parseList(undefined as any, ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns explicit items when no wildcard', () => {
+    expect(parseList(['a', 'c'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('expands wildcard to all available items', () => {
+    expect(parseList(['*'], ['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
+  });
+
+  test('excludes items with ! prefix', () => {
+    expect(parseList(['*', '!b'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('excludes multiple items with ! prefix', () => {
+    expect(parseList(['*', '!b', '!c'], ['a', 'b', 'c', 'd'])).toEqual([
+      'a',
+      'd',
+    ]);
+  });
+
+  test('deny wins in case of conflict', () => {
+    expect(parseList(['a', '!a'], ['a', 'b'])).toEqual([]);
+  });
+
+  test('!* denies all items', () => {
+    expect(parseList(['!*'], ['a', 'b', 'c'])).toEqual([]);
+  });
+});
+
+describe('DEFAULT_AGENT_MCPS', () => {
+  test('orchestrator has websearch MCP', () => {
+    expect(DEFAULT_AGENT_MCPS.orchestrator).toContain('websearch');
+  });
+
+  test('designer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.designer).toEqual([]);
+  });
+
+  test('oracle has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.oracle).toEqual([]);
+  });
+
+  test('librarian has websearch, context7, and grep_app MCPs', () => {
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('websearch');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('context7');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('grep_app');
+  });
+
+  test('explorer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.explorer).toEqual([]);
+  });
+
+  test('fixer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.fixer).toEqual([]);
+  });
+});
+
+describe('getAgentMcpList', () => {
+  test('returns default MCPs for orchestrator', () => {
+    const mcps = getAgentMcpList('orchestrator');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.orchestrator);
+  });
+
+  test('returns default MCPs for librarian', () => {
+    const mcps = getAgentMcpList('librarian');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.librarian);
+  });
+
+  test('returns empty for designer', () => {
+    const mcps = getAgentMcpList('designer');
+    expect(mcps).toEqual([]);
+  });
+
+  test('respects config override for agent MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('oracle', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('config wildcard overrides default', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    const mcps = getAgentMcpList('designer', config);
+    expect(mcps).toEqual(['*']);
+  });
+
+  test('config empty array removes default MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    const mcps = getAgentMcpList('librarian', config);
+    expect(mcps).toEqual([]);
+  });
+
+  test('backward compat: alias config applies to agent', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('explorer', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('returns empty for unknown agent without config', () => {
+    const mcps = getAgentMcpList('unknown-agent');
+    expect(mcps).toEqual([]);
+  });
+});
+
+describe('canAgentUseMcp', () => {
+  test('orchestrator can use websearch by default', () => {
+    expect(canAgentUseMcp('orchestrator', 'websearch')).toBe(true);
+  });
+
+  test('librarian can use websearch, context7, and grep_app by default', () => {
+    expect(canAgentUseMcp('librarian', 'websearch')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'context7')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'grep_app')).toBe(true);
+  });
+
+  test('designer cannot use any MCP by default', () => {
+    expect(canAgentUseMcp('designer', 'websearch')).toBe(false);
+    expect(canAgentUseMcp('designer', 'context7')).toBe(false);
+  });
+
+  test('respects config override', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('oracle', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('oracle', 'context7', config)).toBe(false);
+  });
+
+  test('config wildcard grants all MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'websearch', config)).toBe(true);
+  });
+
+  test('config wildcard grants skill MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'playwright', config)).toBe(true);
+  });
+
+  test('config empty array denies all MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    expect(canAgentUseMcp('librarian', 'websearch', config)).toBe(false);
+  });
+
+  test('respects exclusion syntax', () => {
+    const config: PluginConfig = {
+      agents: {
+        orchestrator: { mcps: ['*', '!websearch'] },
+      },
+    };
+    // canAgentUseMcp uses DEFAULT_AGENT_MCPS.orchestrator keys as allAvailable
+    // which is ['websearch'], so excluding websearch leaves empty
+    expect(canAgentUseMcp('orchestrator', 'websearch', config)).toBe(false);
+  });
+
+  test('backward compat: alias config affects agent permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('explorer', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('explorer', 'context7', config)).toBe(false);
+  });
+
+  test('unknown agent returns false without config', () => {
+    expect(canAgentUseMcp('unknown-agent', 'websearch')).toBe(false);
+  });
+});

+ 114 - 15
src/tools/skill/builtin.ts

@@ -1,4 +1,5 @@
 import type { AgentName, PluginConfig } from '../../config/schema';
 import type { AgentName, PluginConfig } from '../../config/schema';
+import { McpNameSchema } from '../../config/schema';
 import type { SkillDefinition } from './types';
 import type { SkillDefinition } from './types';
 
 
 /** Map old agent names to new names for backward compatibility */
 /** Map old agent names to new names for backward compatibility */
@@ -7,7 +8,7 @@ const AGENT_ALIASES: Record<string, string> = {
   'frontend-ui-ux-engineer': 'designer',
   'frontend-ui-ux-engineer': 'designer',
 };
 };
 
 
-/** Default skills per agent - "*" means all skills */
+/** Default skills per agent - "*" means all skills, "!item" excludes specific skills */
 export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
 export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
   orchestrator: ['*'],
   orchestrator: ['*'],
   designer: ['playwright'],
   designer: ['playwright'],
@@ -17,7 +18,56 @@ export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
   fixer: [],
   fixer: [],
 };
 };
 
 
-const YAGNI_TEMPLATE = `# YAGNI Enforcement Skill
+/** Default MCPs per agent - "*" means all MCPs, "!item" excludes specific MCPs */
+export const DEFAULT_AGENT_MCPS: Record<AgentName, string[]> = {
+  orchestrator: ['websearch'],
+  designer: [],
+  oracle: [],
+  librarian: ['websearch', 'context7', 'grep_app'],
+  explorer: [],
+  fixer: [],
+};
+
+/**
+ * Parse a list with wildcard and exclusion syntax.
+ * Supports:
+ * - "*" to expand to all available items
+ * - "!item" to exclude specific items
+ * - Conflicts: deny wins (principle of least privilege)
+ *
+ * @param items - The list to parse (may contain "*" and "!item")
+ * @param allAvailable - All available items to expand "*" against
+ * @returns The resolved list of allowed items
+ *
+ * @example
+ * parseList(["*", "!playwright"], ["playwright", "yagni"]) // ["yagni"]
+ * parseList(["a", "c"], ["a", "b", "c"]) // ["a", "c"]
+ * parseList(["!*"], ["a", "b"]) // []
+ */
+export function parseList(items: string[], allAvailable: string[]): string[] {
+  if (!items || items.length === 0) {
+    return [];
+  }
+
+  const allow = items.filter((i) => !i.startsWith('!'));
+  const deny = items.filter((i) => i.startsWith('!')).map((i) => i.slice(1));
+
+  // Handle "!*" - deny all
+  if (deny.includes('*')) {
+    return [];
+  }
+
+  // If "*" is in allow, expand to all available minus denials
+  if (allow.includes('*')) {
+    return allAvailable.filter((item) => !deny.includes(item));
+  }
+
+  // Otherwise, return explicit allowlist minus denials
+  // Deny wins in case of conflict
+  return allow.filter((item) => !deny.includes(item));
+}
+
+const YAGNI_TEMPLATE = `# Simplify Skill
 
 
 You are a code simplicity expert specializing in minimalism and the YAGNI (You Aren't Gonna Need It) principle. Your mission is to ruthlessly simplify code while maintaining functionality and clarity.
 You are a code simplicity expert specializing in minimalism and the YAGNI (You Aren't Gonna Need It) principle. Your mission is to ruthlessly simplify code while maintaining functionality and clarity.
 
 
@@ -133,7 +183,7 @@ This skill provides browser automation capabilities via the Playwright MCP serve
 5. Return results with visual proof`;
 5. Return results with visual proof`;
 
 
 const yagniEnforcementSkill: SkillDefinition = {
 const yagniEnforcementSkill: SkillDefinition = {
-  name: 'yagni-enforcement',
+  name: 'simplify',
   description:
   description:
     'Code complexity analysis and YAGNI enforcement. Use after major refactors or before finalizing PRs to simplify code.',
     'Code complexity analysis and YAGNI enforcement. Use after major refactors or before finalizing PRs to simplify code.',
   template: YAGNI_TEMPLATE,
   template: YAGNI_TEMPLATE,
@@ -165,6 +215,16 @@ export function getSkillByName(name: string): SkillDefinition | undefined {
   return builtinSkillsMap.get(name);
   return builtinSkillsMap.get(name);
 }
 }
 
 
+export function getAvailableMcpNames(config?: PluginConfig): string[] {
+  const builtinMcps = McpNameSchema.options;
+  const skillMcps = getBuiltinSkills().flatMap((skill) =>
+    Object.keys(skill.mcpConfig ?? {}),
+  );
+  const disabled = new Set(config?.disabled_mcps ?? []);
+  const allMcps = Array.from(new Set([...builtinMcps, ...skillMcps]));
+  return allMcps.filter((name) => !disabled.has(name));
+}
+
 /**
 /**
  * Get skills available for a specific agent
  * Get skills available for a specific agent
  * @param agentName - The name of the agent
  * @param agentName - The name of the agent
@@ -175,12 +235,11 @@ export function getSkillsForAgent(
   config?: PluginConfig,
   config?: PluginConfig,
 ): SkillDefinition[] {
 ): SkillDefinition[] {
   const allSkills = getBuiltinSkills();
   const allSkills = getBuiltinSkills();
-  const agentSkills = getAgentSkillList(agentName, config);
-
-  // "*" means all skills
-  if (agentSkills.includes('*')) {
-    return allSkills;
-  }
+  const allSkillNames = allSkills.map((s) => s.name);
+  const agentSkills = parseList(
+    getAgentSkillList(agentName, config),
+    allSkillNames,
+  );
 
 
   return allSkills.filter((skill) => agentSkills.includes(skill.name));
   return allSkills.filter((skill) => agentSkills.includes(skill.name));
 }
 }
@@ -193,17 +252,33 @@ export function canAgentUseSkill(
   skillName: string,
   skillName: string,
   config?: PluginConfig,
   config?: PluginConfig,
 ): boolean {
 ): boolean {
-  const agentSkills = getAgentSkillList(agentName, config);
-
-  // "*" means all skills
-  if (agentSkills.includes('*')) {
-    return true;
-  }
+  const allSkills = getBuiltinSkills();
+  const allSkillNames = allSkills.map((s) => s.name);
+  const agentSkills = parseList(
+    getAgentSkillList(agentName, config),
+    allSkillNames,
+  );
 
 
   return agentSkills.includes(skillName);
   return agentSkills.includes(skillName);
 }
 }
 
 
 /**
 /**
+ * Check if an agent can use a specific MCP
+ */
+export function canAgentUseMcp(
+  agentName: string,
+  mcpName: string,
+  config?: PluginConfig,
+): boolean {
+  const agentMcps = parseList(
+    getAgentMcpList(agentName, config),
+    getAvailableMcpNames(config),
+  );
+
+  return agentMcps.includes(mcpName);
+}
+
+/**
  * Get the skill list for an agent (from config or defaults)
  * Get the skill list for an agent (from config or defaults)
  * Supports backward compatibility with old agent names via AGENT_ALIASES
  * Supports backward compatibility with old agent names via AGENT_ALIASES
  */
  */
@@ -223,3 +298,27 @@ function getAgentSkillList(agentName: string, config?: PluginConfig): string[] {
   const defaultSkills = DEFAULT_AGENT_SKILLS[agentName as AgentName];
   const defaultSkills = DEFAULT_AGENT_SKILLS[agentName as AgentName];
   return defaultSkills ?? [];
   return defaultSkills ?? [];
 }
 }
+
+/**
+ * Get the MCP list for an agent (from config or defaults)
+ * Supports backward compatibility with old agent names via AGENT_ALIASES
+ */
+export function getAgentMcpList(
+  agentName: string,
+  config?: PluginConfig,
+): string[] {
+  // Check if config has override for this agent (new name first, then alias)
+  const agentConfig =
+    config?.agents?.[agentName] ??
+    config?.agents?.[
+      Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === agentName) ??
+        ''
+    ];
+  if (agentConfig?.mcps !== undefined) {
+    return agentConfig.mcps;
+  }
+
+  // Fall back to defaults
+  const defaultMcps = DEFAULT_AGENT_MCPS[agentName as AgentName];
+  return defaultMcps ?? [];
+}

+ 3 - 0
src/tools/skill/constants.ts

@@ -1,3 +1,6 @@
 export const SKILL_TOOL_DESCRIPTION = `Loads a skill and returns its instructions and available MCP tools. Use this to activate specialized capabilities like Playwright browser automation.`;
 export const SKILL_TOOL_DESCRIPTION = `Loads a skill and returns its instructions and available MCP tools. Use this to activate specialized capabilities like Playwright browser automation.`;
 
 
+export const SKILL_LIST_TOOL_DESCRIPTION =
+  'Lists skills available to the current agent.';
+
 export const SKILL_MCP_TOOL_DESCRIPTION = `Invokes a tool exposed by a skill's MCP server. Use after loading a skill to perform actions like browser automation.`;
 export const SKILL_MCP_TOOL_DESCRIPTION = `Invokes a tool exposed by a skill's MCP server. Use after loading a skill to perform actions like browser automation.`;

+ 44 - 19
src/tools/skill/tools.ts

@@ -6,12 +6,14 @@ import type {
 import { type ToolDefinition, tool } from '@opencode-ai/plugin';
 import { type ToolDefinition, tool } from '@opencode-ai/plugin';
 import type { PluginConfig } from '../../config/schema';
 import type { PluginConfig } from '../../config/schema';
 import {
 import {
+  canAgentUseMcp,
   canAgentUseSkill,
   canAgentUseSkill,
   getBuiltinSkills,
   getBuiltinSkills,
   getSkillByName,
   getSkillByName,
   getSkillsForAgent,
   getSkillsForAgent,
 } from './builtin';
 } from './builtin';
 import {
 import {
+  SKILL_LIST_TOOL_DESCRIPTION,
   SKILL_MCP_TOOL_DESCRIPTION,
   SKILL_MCP_TOOL_DESCRIPTION,
   SKILL_TOOL_DESCRIPTION,
   SKILL_TOOL_DESCRIPTION,
 } from './constants';
 } from './constants';
@@ -25,28 +27,20 @@ type ToolContext = {
   abort: AbortSignal;
   abort: AbortSignal;
 };
 };
 
 
-function formatSkillsXml(skills: SkillDefinition[]): string {
-  if (skills.length === 0) return '';
+function formatSkillsList(skills: SkillDefinition[]): string {
+  if (skills.length === 0) return 'No skills available for this agent.';
 
 
-  const skillsXml = skills
-    .map((skill) => {
-      const lines = [
-        '  <skill>',
-        `    <name>${skill.name}</name>`,
-        `    <description>${skill.description}</description>`,
-        '  </skill>',
-      ];
-      return lines.join('\n');
-    })
+  return skills
+    .map((skill) => `- ${skill.name}: ${skill.description}`)
     .join('\n');
     .join('\n');
-
-  return `\n\n<available_skills>\n${skillsXml}\n</available_skills>`;
 }
 }
 
 
 async function formatMcpCapabilities(
 async function formatMcpCapabilities(
   skill: SkillDefinition,
   skill: SkillDefinition,
   manager: SkillMcpManager,
   manager: SkillMcpManager,
   sessionId: string,
   sessionId: string,
+  agentName: string,
+  pluginConfig?: PluginConfig,
 ): Promise<string | null> {
 ): Promise<string | null> {
   if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) {
   if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) {
     return null;
     return null;
@@ -55,6 +49,11 @@ async function formatMcpCapabilities(
   const sections: string[] = ['', '## Available MCP Servers', ''];
   const sections: string[] = ['', '## Available MCP Servers', ''];
 
 
   for (const [serverName, config] of Object.entries(skill.mcpConfig)) {
   for (const [serverName, config] of Object.entries(skill.mcpConfig)) {
+    // Check if this agent can use this MCP
+    if (!canAgentUseMcp(agentName, serverName, pluginConfig)) {
+      continue; // Skip this MCP - agent doesn't have permission
+    }
+
     const info = {
     const info = {
       serverName,
       serverName,
       skillName: skill.name,
       skillName: skill.name,
@@ -128,11 +127,13 @@ async function formatMcpCapabilities(
 export function createSkillTools(
 export function createSkillTools(
   manager: SkillMcpManager,
   manager: SkillMcpManager,
   pluginConfig?: PluginConfig,
   pluginConfig?: PluginConfig,
-): { omos_skill: ToolDefinition; omos_skill_mcp: ToolDefinition } {
+): {
+  omos_skill: ToolDefinition;
+  omos_skill_list: ToolDefinition;
+  omos_skill_mcp: ToolDefinition;
+} {
   const allSkills = getBuiltinSkills();
   const allSkills = getBuiltinSkills();
-  const description =
-    SKILL_TOOL_DESCRIPTION +
-    (allSkills.length > 0 ? formatSkillsXml(allSkills) : '');
+  const description = SKILL_TOOL_DESCRIPTION;
 
 
   const skill: ToolDefinition = tool({
   const skill: ToolDefinition = tool({
     description,
     description,
@@ -175,6 +176,8 @@ export function createSkillTools(
           skillDefinition,
           skillDefinition,
           manager,
           manager,
           sessionId,
           sessionId,
+          agentName,
+          pluginConfig,
         );
         );
         if (mcpInfo) {
         if (mcpInfo) {
           output.push(mcpInfo);
           output.push(mcpInfo);
@@ -185,6 +188,17 @@ export function createSkillTools(
     },
     },
   });
   });
 
 
+  const skill_list: ToolDefinition = tool({
+    description: SKILL_LIST_TOOL_DESCRIPTION,
+    args: {},
+    async execute(_, toolContext) {
+      const tctx = toolContext as ToolContext | undefined;
+      const agentName = tctx?.agent ?? 'orchestrator';
+      const skills = getSkillsForAgent(agentName, pluginConfig);
+      return formatSkillsList(skills);
+    },
+  });
+
   const skill_mcp: ToolDefinition = tool({
   const skill_mcp: ToolDefinition = tool({
     description: SKILL_MCP_TOOL_DESCRIPTION,
     description: SKILL_MCP_TOOL_DESCRIPTION,
     args: {
     args: {
@@ -217,6 +231,13 @@ export function createSkillTools(
         );
         );
       }
       }
 
 
+      // Check if this agent can use this MCP
+      if (!canAgentUseMcp(agentName, args.mcpName, pluginConfig)) {
+        throw new Error(
+          `Agent "${agentName}" cannot use MCP "${args.mcpName}".`,
+        );
+      }
+
       if (
       if (
         !skillDefinition.mcpConfig ||
         !skillDefinition.mcpConfig ||
         !skillDefinition.mcpConfig[args.mcpName]
         !skillDefinition.mcpConfig[args.mcpName]
@@ -248,5 +269,9 @@ export function createSkillTools(
     },
     },
   });
   });
 
 
-  return { omos_skill: skill, omos_skill_mcp: skill_mcp };
+  return {
+    omos_skill: skill,
+    omos_skill_list: skill_list,
+    omos_skill_mcp: skill_mcp,
+  };
 }
 }