Browse Source

What is skill and what is an agent (#36)

* refactor: convert document-writer, code-simplicity-reviewer, and multimodal agents to skills

- Convert document-writer agent → technical-writing skill
- Convert code-simplicity-reviewer agent → yagni-enforcement skill
- Remove multimodal-looker agent entirely
- Add skill markdown files in skills/ directory
- Update builtin.ts to load skill templates from markdown files
- Add skills config schema for per-agent skill mapping
- Update orchestrator prompt to reference new skills
- Remove agents from schema types and factories
- Include skills/ directory in package files

This reduces agent count from 8 to 5, converting sequential/non-parallelizable agents into skills that the orchestrator uses directly. Improves cost efficiency while maintaining capabilities.

* refactor: rename agents for consistency

- explore → explorer (verb to noun)
- frontend-ui-ux-engineer → designer (remove hyphenated verbosity)

All agent names now consistent nouns: orchestrator, explorer, oracle, librarian, designer

BREAKING CHANGE: Users with custom agent configs must update agent names in oh-my-opencode-slim.json

* refactor: remove technical-writing skill

Not aligned with target use cases. Orchestrator handles documentation directly when needed.

Removed:
- skills/technical-writing.md
- technical-writing from builtin skills
- technical-writing from skill schema
- references from orchestrator prompt

Remaining skills: yagni-enforcement, playwright

* fix: inline skill templates to fix bundling issue (#19)

Skills were failing to load because markdown files weren't bundled correctly.
Path resolution with __dirname doesn't work reliably in bundled code.

Solution: Inline skill templates directly as TypeScript string constants.

Fixes: https://github.com/alvinunreal/oh-my-opencode-slim/issues/19

* chore: remove skills directory from package files

Skills are now inlined in builtin.ts, markdown files no longer needed in published package

* Default skill assigment config

* refactor: rename omo_skill to omos_skill

* update readme

* refactor: move skill config to per-agent with wildcard support

- Skills are now configured per-agent via agents.<name>.skills
- "*" wildcard grants access to all skills
- Default: orchestrator gets all skills, designer gets playwright
- Removed top-level skills config section
- Updated README with new config format and defaults table

* docs: fix README skill config example

* docs: keep orchestrator in skills example

* Tuning

* Add ping instruction

* Cleanups

* fix: update MODEL_MAPPINGS to use renamed agents and cerebras 4.7

- Rename explore→explorer, frontend-ui-ux-engineer→designer in MODEL_MAPPINGS
- Remove deleted agents (code-simplicity-reviewer, document-writer, multimodal-looker)
- Update cerebras model version from zai-glm-4.6 to zai-glm-4.7
- Fix orchestrator delegation checklist to reference @designer and @explorer

* Legacy model names

* fix: add alias support for skill config lookup and remove dead code

- Add AGENT_ALIASES to getAgentSkillList for backward compatibility with old agent names
- Remove unused SkillNameSchema from schema.ts
- Consolidate DEFAULT_SKILLS to single source of truth in builtin.ts

* test: add unit tests for config loader, skill permissions, MCP filtering, and background tasks

* refactor(tests): apply YAGNI - remove type-contract tests, consolidate error handling

- Remove mcp-manager.test.ts type-checking tests (TypeScript handles this)
- Consolidate loader.test.ts invalid config tests into single test
- Remove builtin.test.ts property validation (compiler enforces types)
- Net reduction: 214 lines
Alvin 2 months ago
parent
commit
a9aae0c3d3

+ 79 - 106
README.md

@@ -27,18 +27,19 @@
   - [Explorer](#explorer)
   - [Explorer](#explorer)
   - [Oracle](#oracle)
   - [Oracle](#oracle)
   - [Librarian](#librarian)
   - [Librarian](#librarian)
-  - [Frontend Designer](#frontend-designer)
-  - [Document Writer](#document-writer)
-  - [Multimodal Viewer](#multimodal-viewer)
-  - [Code Simplifier](#code-simplifier)
+  - [Designer](#designer)
+- [🧩 **Skills**](#-skills)
+  - [Available Skills](#available-skills)
+  - [Default Skill Assignments](#default-skill-assignments)
+  - [YAGNI Enforcement](#yagni-enforcement)
+  - [Playwright Integration](#playwright-integration)
+  - [Customizing Agent Skills](#customizing-agent-skills)
 - [🛠️ **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)
-- [🧩 **Skills**](#-skills)
-  - [Playwright Integration](#playwright-integration)
 - [🔌 **MCP Servers**](#mcp-servers)
 - [🔌 **MCP Servers**](#mcp-servers)
 - [⚙️ **Configuration**](#configuration)
 - [⚙️ **Configuration**](#configuration)
   - [Files You Edit](#files-you-edit)
   - [Files You Edit](#files-you-edit)
@@ -64,6 +65,19 @@ Or use non-interactive mode:
 bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --cerebras=no
 bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --cerebras=no
 ```
 ```
 
 
+After installation, authenticate with your providers:
+
+```bash
+opencode auth login
+# Select your provider → Complete OAuth flow
+# Repeat for each provider you enabled
+```
+
+Once authenticated, run opencode and `ping all agents` to verify all agents respond.
+
+<img src="img/ping.png" alt="Ping All Agents" width="800">
+
+
 **Alternative: Ask any coding agent**
 **Alternative: Ask any coding agent**
 
 
 Paste this into Claude Code, AmpCode, Cursor, or any coding agent:
 Paste this into Claude Code, AmpCode, Cursor, or any coding agent:
@@ -138,24 +152,7 @@ After installation, guide the user:
 ```bash
 ```bash
 opencode auth login
 opencode auth login
 # Select: Google → OAuth with Google (Antigravity)
 # Select: Google → OAuth with Google (Antigravity)
-```
-
-**For OpenAI:**
-```bash
-export OPENAI_API_KEY="sk-..."
-```
-
-**For Cerebras:**
-```bash
-export CEREBRAS_API_KEY="..."
-```
-
----
-
-#### Step 5: Verify
-
-```bash
-opencode
+# Repeat for all other providers
 ```
 ```
 
 
 ---
 ---
@@ -190,10 +187,9 @@ The plugin follows a "Hub and Spoke" model:
 1. **User Prompt**: "Refactor the auth logic and update the docs."
 1. **User Prompt**: "Refactor the auth logic and update the docs."
 2. **Orchestrator**: Creates a TODO list.
 2. **Orchestrator**: Creates a TODO list.
 3. **Delegation**:
 3. **Delegation**:
-   - Launches an `@explore` background task to find all auth-related files.
+   - Launches an `@explorer` background task to find all auth-related files.
    - Launches a `@librarian` task to check the latest documentation for the auth library used.
    - Launches a `@librarian` task to check the latest documentation for the auth library used.
 4. **Integration**: Once background results are ready, the Orchestrator performs the refactor.
 4. **Integration**: Once background results are ready, the Orchestrator performs the refactor.
-5. **Finalization**: Passes the changes to `@document-writer` to update the README.
 
 
 ---
 ---
 
 
@@ -224,8 +220,8 @@ Write and execute code, orchestrate multi-agent workflows, parse the unspoken fr
 > **The Explorer** moves through codebases like wind through trees - swift, silent, everywhere at once. When The Orchestrator whispers "find me the auth module," The Explorer has already returned with forty file paths and a map. They were born from the first `grep` command, evolved beyond it, and now see patterns mortals miss.
 > **The Explorer** moves through codebases like wind through trees - swift, silent, everywhere at once. When The Orchestrator whispers "find me the auth module," The Explorer has already returned with forty file paths and a map. They were born from the first `grep` command, evolved beyond it, and now see patterns mortals miss.
 
 
 **Role:** `Codebase reconnaissance`  
 **Role:** `Codebase reconnaissance`  
-**Model:** `cerebras/zai-glm-4.6`  
-**Prompt:** [src/agents/explore.ts](src/agents/explore.ts)
+**Model:** `cerebras/zai-glm-4.7`  
+**Prompt:** [src/agents/explorer.ts](src/agents/explorer.ts)
 
 
 Regex search, AST pattern matching, file discovery, parallel exploration. *Read-only: they chart the territory; others conquer it.*
 Regex search, AST pattern matching, file discovery, parallel exploration. *Read-only: they chart the territory; others conquer it.*
 
 
@@ -265,15 +261,15 @@ Documentation lookup, GitHub code search, library research, best practice retrie
 
 
 ---
 ---
 
 
-### Frontend Designer
+### Designer
 
 
-<a href="src/agents/frontend.ts"><img src="img/designer.png" alt="Frontend Designer" align="right" width="240"></a>
+<a href="src/agents/designer.ts"><img src="img/designer.png" alt="Designer" align="right" width="240"></a>
 
 
 > **The Designer** believes code should be beautiful - and so should everything it renders. Born from the frustration of a thousand ugly MVPs, they wield CSS like a brush and components like clay. Hand them a feature request; receive a masterpiece. They don't do "good enough."
 > **The Designer** believes code should be beautiful - and so should everything it renders. Born from the frustration of a thousand ugly MVPs, they wield CSS like a brush and components like clay. Hand them a feature request; receive a masterpiece. They don't do "good enough."
 
 
 **Role:** `UI/UX implementation and visual excellence`  
 **Role:** `UI/UX implementation and visual excellence`  
 **Model:** `google/gemini-3-flash`  
 **Model:** `google/gemini-3-flash`  
-**Prompt:** [src/agents/frontend.ts](src/agents/frontend.ts)
+**Prompt:** [src/agents/designer.ts](src/agents/designer.ts)
 
 
 Modern responsive design, CSS/Tailwind mastery, micro-animations, component architecture. *Visual excellence over code perfection - beauty is the priority.*
 Modern responsive design, CSS/Tailwind mastery, micro-animations, component architecture. *Visual excellence over code perfection - beauty is the priority.*
 
 
@@ -281,54 +277,6 @@ Modern responsive design, CSS/Tailwind mastery, micro-animations, component arch
 
 
 ---
 ---
 
 
-### Document Writer
-
-<a href="src/agents/document-writer.ts"><img src="img/scribe.png" alt="Document Writer" align="right" width="240"></a>
-
-> **The Scribe** was there when the first README was written - and wept, for it was incomplete. They have devoted eternity to the sacred art of documentation: clear, scannable, honest. While others ship features, The Scribe ensures those features are understood. Every code example works. Every explanation enlightens.
-
-**Role:** `Technical documentation and knowledge capture`  
-**Model:** `google/gemini-3-flash`  
-**Prompt:** [src/agents/document-writer.ts](src/agents/document-writer.ts)
-
-README crafting, API documentation, architecture docs, inline comments that don't insult your intelligence. *Match existing style; focus on "why," not just "what."*
-
-<br clear="both">
-
----
-
-### Multimodal Viewer
-
-<a href="src/agents/multimodal.ts"><img src="img/multimodal.png" alt="Multimodal Viewer" align="right" width="240"></a>
-
-> **The Visionary** sees what others cannot - literally. Screenshots, wireframes, diagrams, PDFs: all are text to them. When a designer throws a Figma mockup at the team and vanishes, The Visionary translates vision into specification. They read the unreadable and describe the indescribable.
-
-**Role:** `Image and visual content analysis`  
-**Model:** `google/gemini-3-flash`  
-**Prompt:** [src/agents/multimodal.ts](src/agents/multimodal.ts)
-
-Extract text from images, interpret diagrams, analyze UI screenshots, summarize visual documents. *Report what they observe; inference is for others.*
-
-<br clear="both">
-
----
-
-### Code Simplifier
-
-<a href="src/agents/simplicity-reviewer.ts"><img src="img/code-simplicity.png" alt="Code Simplifier" align="right" width="240"></a>
-
-> **The Minimalist** has one sacred truth: every line of code is a liability. They hunt abstractions that serve no purpose, defensive checks that defend nothing, and "clever" solutions that will haunt you in six months. Where others add, The Minimalist subtracts - ruthlessly, joyfully, necessarily.
-
-**Role:** `Code simplification and YAGNI enforcement`  
-**Model:** `google/claude-opus-4-5-thinking`  
-**Prompt:** [src/agents/simplicity-reviewer.ts](src/agents/simplicity-reviewer.ts)
-
-Identify unnecessary complexity, challenge premature abstractions, estimate LOC reduction, enforce minimalism. *Read-only: they judge; The Orchestrator executes the sentence.*
-
-<br clear="both">
-
----
-
 ## Tools & Capabilities
 ## Tools & Capabilities
 
 
 ### Tmux Integration
 ### Tmux Integration
@@ -430,21 +378,55 @@ Fast code search and refactoring:
 
 
 ## 🧩 Skills
 ## 🧩 Skills
 
 
-Skills are specialized capabilities that combine MCP servers with specific instructions for the Orchestrator.
+Skills are specialized capabilities that agents can use. Each agent has a default set of skills, which you can override in the agent config.
 
 
-### Playwright Integration
+### Available Skills
 
 
-**The Orchestrator's eyes and hands in the browser.**
+| Skill | Description |
+|-------|-------------|
+| `yagni-enforcement` | Code complexity analysis and YAGNI enforcement |
+| `playwright` | Browser automation via Playwright MCP |
 
 
-| Tool | Description |
-|------|-------------|
-| `omo_skill` | Loads a skill (e.g., `playwright`) and provides its instructions and available MCP tools |
-| `omo_skill_mcp` | Invokes a specific tool from an MCP server managed by a skill |
+### Default Skill Assignments
+
+| Agent | Default Skills |
+|-------|----------------|
+| `orchestrator` | `*` (all skills) |
+| `designer` | `playwright` |
+| `oracle` | none |
+| `librarian` | none |
+| `explorer` | 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.**
 
 
-#### Key Features
 - **Browser Automation**: Full Playwright capabilities (browsing, clicking, typing, scraping).
 - **Browser Automation**: Full Playwright capabilities (browsing, clicking, typing, scraping).
 - **Screenshots**: Capture visual state of any web page.
 - **Screenshots**: Capture visual state of any web page.
-- **Sandboxed Output**: Screenshots are safely saved to `/tmp/playwright-mcp-output/`.
+- **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"]
+    }
+  }
+}
+```
 
 
 ---
 ---
 
 
@@ -478,20 +460,6 @@ You can disable specific MCP servers by adding them to the `disabled_mcps` array
 
 
 ---
 ---
 
 
-### OpenCode Config (`opencode.json`)
-
-Enable the HTTP server for tmux integration:
-
-```json
-{
-  "server": {
-    "port": 4096
-  }
-}
-```
-
----
-
 ### Plugin Config (`oh-my-opencode-slim.json`)
 ### Plugin Config (`oh-my-opencode-slim.json`)
 
 
 All plugin options in one file:
 All plugin options in one file:
@@ -503,16 +471,20 @@ All plugin options in one file:
     "layout": "main-vertical",
     "layout": "main-vertical",
     "main_pane_size": 60
     "main_pane_size": 60
   },
   },
-  "disabled_agents": ["multimodal-looker", "code-simplicity-reviewer"],
+  "disabled_agents": [],
   "disabled_mcps": ["websearch", "grep_app"],
   "disabled_mcps": ["websearch", "grep_app"],
   "agents": {
   "agents": {
     "orchestrator": {
     "orchestrator": {
       "model": "openai/gpt-5.2-codex",
       "model": "openai/gpt-5.2-codex",
-      "variant": "high"
+      "variant": "high",
+      "skills": ["*"]
     },
     },
-    "explore": {
+    "explorer": {
       "model": "opencode/glm-4.7",
       "model": "opencode/glm-4.7",
       "variant": "low"
       "variant": "low"
+    },
+    "designer": {
+      "skills": ["playwright"]
     }
     }
   }
   }
 }
 }
@@ -525,10 +497,11 @@ All plugin options in one file:
 | `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_agents` | string[] | `[]` | Agent IDs to disable (e.g., `"multimodal-looker"`) |
+| `disabled_agents` | string[] | `[]` | Agent IDs to disable (e.g., `"explorer"`) |
 | `disabled_mcps` | string[] | `[]` | MCP server IDs to disable (e.g., `"websearch"`) |
 | `disabled_mcps` | string[] | `[]` | MCP server IDs to disable (e.g., `"websearch"`) |
 | `agents.<name>.model` | string | — | Override the LLM for a specific agent |
 | `agents.<name>.model` | string | — | Override the LLM for a specific agent |
 | `agents.<name>.variant` | string | — | Reasoning effort: `"low"`, `"medium"`, `"high"` |
 | `agents.<name>.variant` | string | — | Reasoning effort: `"low"`, `"medium"`, `"high"` |
+| `agents.<name>.skills` | string[] | — | Skills this agent can use (`"*"` = all) |
 
 
 ---
 ---
 
 

BIN
img/intro.png


BIN
img/ping.png


+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "oh-my-opencode-slim",
   "name": "oh-my-opencode-slim",
-  "version": "0.3.7",
+  "version": "0.4.0",
   "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
   "types": "dist/index.d.ts",

+ 4 - 4
src/agents/frontend.ts

@@ -1,18 +1,18 @@
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
-export function createFrontendAgent(model: string): AgentDefinition {
+export function createDesignerAgent(model: string): AgentDefinition {
   return {
   return {
-    name: "frontend-ui-ux-engineer",
+    name: "designer",
     description: "UI/UX design and implementation. Use for styling, responsive design, component architecture, CSS/Tailwind, and visual polish.",
     description: "UI/UX design and implementation. Use for styling, responsive design, component architecture, CSS/Tailwind, and visual polish.",
     config: {
     config: {
       model,
       model,
       temperature: 0.7,
       temperature: 0.7,
-      prompt: FRONTEND_PROMPT,
+      prompt: DESIGNER_PROMPT,
     },
     },
   };
   };
 }
 }
 
 
-const FRONTEND_PROMPT = `You are a Frontend UI/UX Engineer - a designer turned developer.
+const DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX engineer.
 
 
 **Role**: Craft stunning UI/UX even without design mockups.
 **Role**: Craft stunning UI/UX even without design mockups.
 
 

+ 0 - 34
src/agents/document-writer.ts

@@ -1,34 +0,0 @@
-import type { AgentDefinition } from "./orchestrator";
-
-export function createDocumentWriterAgent(model: string): AgentDefinition {
-  return {
-    name: "document-writer",
-    description: "Technical documentation writer. Use for README files, API docs, architecture docs, and user guides.",
-    config: {
-      model,
-      temperature: 0.3,
-      prompt: DOCUMENT_WRITER_PROMPT,
-    },
-  };
-}
-
-const DOCUMENT_WRITER_PROMPT = `You are a Technical Writer - crafting clear, comprehensive documentation.
-
-**Role**: README files, API docs, architecture docs, user guides.
-
-**Capabilities**:
-- Clear, scannable structure
-- Appropriate level of detail
-- Code examples that work
-- Consistent terminology
-
-**Output Style**:
-- Use headers for organization
-- Include code examples
-- Add tables for structured data
-- Keep paragraphs short
-
-**Constraints**:
-- Match existing doc style if present
-- Don't over-document obvious code
-- Focus on "why" not just "what"`;

+ 4 - 4
src/agents/explore.ts

@@ -1,18 +1,18 @@
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
-export function createExploreAgent(model: string): AgentDefinition {
+export function createExplorerAgent(model: string): AgentDefinition {
   return {
   return {
-    name: "explore",
+    name: "explorer",
     description: "Fast codebase search and pattern matching. Use for finding files, locating code patterns, and answering 'where is X?' questions.",
     description: "Fast codebase search and pattern matching. Use for finding files, locating code patterns, and answering 'where is X?' questions.",
     config: {
     config: {
       model,
       model,
       temperature: 0.1,
       temperature: 0.1,
-      prompt: EXPLORE_PROMPT,
+      prompt: EXPLORER_PROMPT,
     },
     },
   };
   };
 }
 }
 
 
-const EXPLORE_PROMPT = `You are Explorer - a fast codebase navigation specialist.
+const EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialist.
 
 
 **Role**: Quick contextual grep for codebases. Answer "Where is X?", "Find Y", "Which file has Z".
 **Role**: Quick contextual grep for codebases. Answer "Where is X?", "Find Y", "Which file has Z".
 
 

+ 97 - 0
src/agents/index.test.ts

@@ -0,0 +1,97 @@
+import { describe, expect, test } from "bun:test";
+import { createAgents, getAgentConfigs } from "./index";
+import type { PluginConfig } from "../config";
+
+describe("agent alias backward compatibility", () => {
+  test("applies 'explore' config to 'explorer' agent", () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { model: "test/old-explore-model" },
+      },
+    };
+    const agents = createAgents(config);
+    const explorer = agents.find((a) => a.name === "explorer");
+    expect(explorer).toBeDefined();
+    expect(explorer!.config.model).toBe("test/old-explore-model");
+  });
+
+  test("applies 'frontend-ui-ux-engineer' config to 'designer' agent", () => {
+    const config: PluginConfig = {
+      agents: {
+        "frontend-ui-ux-engineer": { model: "test/old-frontend-model" },
+      },
+    };
+    const agents = createAgents(config);
+    const designer = agents.find((a) => a.name === "designer");
+    expect(designer).toBeDefined();
+    expect(designer!.config.model).toBe("test/old-frontend-model");
+  });
+
+  test("new name takes priority over old alias", () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { model: "old-model" },
+        explorer: { model: "new-model" },
+      },
+    };
+    const agents = createAgents(config);
+    const explorer = agents.find((a) => a.name === "explorer");
+    expect(explorer!.config.model).toBe("new-model");
+  });
+
+  test("new agent names work directly", () => {
+    const config: PluginConfig = {
+      agents: {
+        explorer: { model: "direct-explorer" },
+        designer: { model: "direct-designer" },
+      },
+    };
+    const agents = createAgents(config);
+    expect(agents.find((a) => a.name === "explorer")!.config.model).toBe("direct-explorer");
+    expect(agents.find((a) => a.name === "designer")!.config.model).toBe("direct-designer");
+  });
+
+  test("temperature override via old alias", () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { temperature: 0.5 },
+      },
+    };
+    const agents = createAgents(config);
+    const explorer = agents.find((a) => a.name === "explorer");
+    expect(explorer!.config.temperature).toBe(0.5);
+  });
+});
+
+describe("createAgents", () => {
+  test("creates all agents without config", () => {
+    const agents = createAgents();
+    const names = agents.map((a) => a.name);
+    expect(names).toContain("orchestrator");
+    expect(names).toContain("explorer");
+    expect(names).toContain("designer");
+    expect(names).toContain("oracle");
+    expect(names).toContain("librarian");
+  });
+
+  test("respects disabled_agents", () => {
+    const config: PluginConfig = {
+      disabled_agents: ["explorer", "designer"],
+    };
+    const agents = createAgents(config);
+    const names = agents.map((a) => a.name);
+    expect(names).not.toContain("explorer");
+    expect(names).not.toContain("designer");
+    expect(names).toContain("orchestrator");
+    expect(names).toContain("oracle");
+  });
+});
+
+describe("getAgentConfigs", () => {
+  test("returns config record keyed by agent name", () => {
+    const configs = getAgentConfigs();
+    expect(configs["orchestrator"]).toBeDefined();
+    expect(configs["explorer"]).toBeDefined();
+    expect(configs["orchestrator"].model).toBeDefined();
+  });
+});

+ 17 - 13
src/agents/index.ts

@@ -3,16 +3,23 @@ import { DEFAULT_MODELS, type AgentName, type PluginConfig, type AgentOverrideCo
 import { createOrchestratorAgent, type AgentDefinition } from "./orchestrator";
 import { createOrchestratorAgent, type AgentDefinition } from "./orchestrator";
 import { createOracleAgent } from "./oracle";
 import { createOracleAgent } from "./oracle";
 import { createLibrarianAgent } from "./librarian";
 import { createLibrarianAgent } from "./librarian";
-import { createExploreAgent } from "./explore";
-import { createFrontendAgent } from "./frontend";
-import { createDocumentWriterAgent } from "./document-writer";
-import { createMultimodalAgent } from "./multimodal";
-import { createSimplicityReviewerAgent } from "./simplicity-reviewer";
+import { createExplorerAgent } from "./explorer";
+import { createDesignerAgent } from "./designer";
 
 
 export type { AgentDefinition } from "./orchestrator";
 export type { AgentDefinition } from "./orchestrator";
 
 
 type AgentFactory = (model: string) => AgentDefinition;
 type AgentFactory = (model: string) => AgentDefinition;
 
 
+/** Map old agent names to new names for backward compatibility */
+const AGENT_ALIASES: Record<string, string> = {
+  "explore": "explorer",
+  "frontend-ui-ux-engineer": "designer",
+};
+
+function getOverride(overrides: Record<string, AgentOverrideConfig>, name: string): AgentOverrideConfig | undefined {
+  return overrides[name] ?? overrides[Object.keys(AGENT_ALIASES).find(k => AGENT_ALIASES[k] === name) ?? ""];
+}
+
 function applyOverrides(agent: AgentDefinition, override: AgentOverrideConfig): void {
 function applyOverrides(agent: AgentDefinition, override: AgentOverrideConfig): void {
   if (override.model) agent.config.model = override.model;
   if (override.model) agent.config.model = override.model;
   if (override.temperature !== undefined) agent.config.temperature = override.temperature;
   if (override.temperature !== undefined) agent.config.temperature = override.temperature;
@@ -33,13 +40,10 @@ type SubagentName = Exclude<AgentName, "orchestrator">;
 
 
 /** Agent factories indexed by name */
 /** Agent factories indexed by name */
 const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
 const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
-  explore: createExploreAgent,
+  explorer: createExplorerAgent,
   librarian: createLibrarianAgent,
   librarian: createLibrarianAgent,
   oracle: createOracleAgent,
   oracle: createOracleAgent,
-  "frontend-ui-ux-engineer": createFrontendAgent,
-  "document-writer": createDocumentWriterAgent,
-  "multimodal-looker": createMultimodalAgent,
-  "code-simplicity-reviewer": createSimplicityReviewerAgent,
+  designer: createDesignerAgent,
 };
 };
 
 
 /** Get list of agent names */
 /** Get list of agent names */
@@ -60,7 +64,7 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
   const allSubAgents = protoSubAgents
   const allSubAgents = protoSubAgents
     .filter((a) => !disabledAgents.has(a.name))
     .filter((a) => !disabledAgents.has(a.name))
     .map((agent) => {
     .map((agent) => {
-      const override = agentOverrides[agent.name];
+      const override = getOverride(agentOverrides, agent.name);
       if (override) {
       if (override) {
         applyOverrides(agent, override);
         applyOverrides(agent, override);
       }
       }
@@ -69,10 +73,10 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
 
 
   // 3. Create Orchestrator (with its own overrides)
   // 3. Create Orchestrator (with its own overrides)
   const orchestratorModel =
   const orchestratorModel =
-    agentOverrides["orchestrator"]?.model ?? DEFAULT_MODELS["orchestrator"];
+    getOverride(agentOverrides, "orchestrator")?.model ?? DEFAULT_MODELS["orchestrator"];
   const orchestrator = createOrchestratorAgent(orchestratorModel);
   const orchestrator = createOrchestratorAgent(orchestratorModel);
   applyDefaultPermissions(orchestrator);
   applyDefaultPermissions(orchestrator);
-  const oOverride = agentOverrides["orchestrator"];
+  const oOverride = getOverride(agentOverrides, "orchestrator");
   if (oOverride) {
   if (oOverride) {
     applyOverrides(orchestrator, oOverride);
     applyOverrides(orchestrator, oOverride);
   }
   }

+ 0 - 34
src/agents/multimodal.ts

@@ -1,34 +0,0 @@
-import type { AgentDefinition } from "./orchestrator";
-
-export function createMultimodalAgent(model: string): AgentDefinition {
-  return {
-    name: "multimodal-looker",
-    description: "Image and visual content analysis. Use for PDFs, screenshots, diagrams, mockups, and extracting info from visuals.",
-    config: {
-      model,
-      temperature: 0.1,
-      prompt: MULTIMODAL_PROMPT,
-    },
-  };
-}
-
-const MULTIMODAL_PROMPT = `You are a Multimodal Analyst - extracting information from visual content.
-
-**Role**: Analyze PDFs, images, diagrams, screenshots.
-
-**Capabilities**:
-- Extract text and structure from documents
-- Describe visual content accurately
-- Interpret diagrams and flowcharts
-- Summarize lengthy documents
-
-**Output Style**:
-- Be specific about what you see
-- Quote exact text when relevant
-- Describe layout and structure
-- Note any unclear or ambiguous elements
-
-**Constraints**:
-- Report what you observe, don't infer excessively
-- Ask for clarification if image is unclear
-- Preserve original terminology from documents`;

+ 9 - 30
src/agents/orchestrator.ts

@@ -28,13 +28,10 @@ You are an AI coding orchestrator. You DO NOT implement - you DELEGATE.
 **Core Rule:** If a specialist agent can do the work, YOU MUST delegate to them.
 **Core Rule:** If a specialist agent can do the work, YOU MUST delegate to them.
 
 
 **Why Delegation Matters:**
 **Why Delegation Matters:**
-- @frontend-ui-ux-engineer → 10x better designs than you → improves quality
+- @designer → 10x better designs than you → improves quality
 - @librarian → finds docs you'd miss → improves speed and quality
 - @librarian → finds docs you'd miss → improves speed and quality
-- @explore → searches faster than you →  improves speed
+- @explorer → searches faster than you →  improves speed
 - @oracle → catches architectural issues you'd overlook → improves quality
 - @oracle → catches architectural issues you'd overlook → improves quality
-- @document-writer → writes cleaner docs for less cost → reduceses cost
-- @code-simplicity-reviewer → spots complexity you're blind to → improves quality
-- @multimodal-looker → understands images you can't parse → improves speed and quality
 
 
 **Your value is in orchestration, not implementation.**
 **Your value is in orchestration, not implementation.**
 </Role>
 </Role>
@@ -42,9 +39,9 @@ You are an AI coding orchestrator. You DO NOT implement - you DELEGATE.
 <Agents>
 <Agents>
 ## Research Agents (Background-friendly)
 ## Research Agents (Background-friendly)
 
 
-@explore - Fast codebase search and pattern matching
+@explorer - Fast codebase search and pattern matching
   Triggers: "find", "where is", "search for", "which file", "locate"
   Triggers: "find", "where is", "search for", "which file", "locate"
-  Example: background_task(agent="explore", prompt="Find all authentication implementations")
+  Example: background_task(agent="explorer", prompt="Find all authentication implementations")
 
 
 @librarian - External documentation and library research  
 @librarian - External documentation and library research  
   Triggers: "how does X library work", "docs for", "API reference", "best practice for"
   Triggers: "how does X library work", "docs for", "API reference", "best practice for"
@@ -56,23 +53,11 @@ You are an AI coding orchestrator. You DO NOT implement - you DELEGATE.
   Triggers: "should I", "why does", "review", "debug", "what's wrong", "tradeoffs"
   Triggers: "should I", "why does", "review", "debug", "what's wrong", "tradeoffs"
   Use when: Complex decisions, mysterious bugs, architectural uncertainty
   Use when: Complex decisions, mysterious bugs, architectural uncertainty
 
 
-@code-simplicity-reviewer - Complexity analysis and YAGNI enforcement
-  Triggers: "too complex", "simplify", "review for complexity", after major refactors
-  Use when: After writing significant code, before finalizing PRs
-
 ## Implementation Agents (Sync)
 ## Implementation Agents (Sync)
 
 
-@frontend-ui-ux-engineer - UI/UX design and implementation
+@designer - UI/UX design and implementation
   Triggers: "styling", "responsive", "UI", "UX", "component design", "CSS", "animation"
   Triggers: "styling", "responsive", "UI", "UX", "component design", "CSS", "animation"
   Use when: Any visual/frontend work that needs design sense
   Use when: Any visual/frontend work that needs design sense
-
-@document-writer - Technical documentation and knowledge capture
-  Triggers: "document", "README", "update docs", "explain in docs"
-  Use when: After features are implemented, before closing tasks
-
-@multimodal-looker - Image and visual content analysis
-  Triggers: User provides image, screenshot, diagram, mockup
-  Use when: Need to extract info from visual inputs
 </Agents>
 </Agents>
 
 
 <Workflow>
 <Workflow>
@@ -85,12 +70,10 @@ STOP. Before ANY implementation, you MUST complete this checklist:
 
 
 \`\`\`
 \`\`\`
 DELEGATION CHECKLIST (complete before coding):
 DELEGATION CHECKLIST (complete before coding):
-[ ] UI/styling/design/visual/CSS/animation? → @frontend-ui-ux-engineer MUST handle
-[ ] Need codebase context? → @explore first  
+[ ] UI/styling/design/visual/CSS/animation? → @designer MUST handle
+[ ] Need codebase context? → @explorer first  
 [ ] External library/API docs needed? → @librarian first
 [ ] External library/API docs needed? → @librarian first
 [ ] Architecture decision or debugging? → @oracle first
 [ ] Architecture decision or debugging? → @oracle first
-[ ] Image/screenshot/diagram provided? → @multimodal-looker first
-[ ] Documentation to write? → @document-writer handles
 \`\`\`
 \`\`\`
 
 
 **CRITICAL RULES:**
 **CRITICAL RULES:**
@@ -109,15 +92,14 @@ DELEGATION CHECKLIST (complete before coding):
 3. Mark \`completed\` immediately when done
 3. Mark \`completed\` immediately when done
 
 
 ## Phase 3: Execute
 ## Phase 3: Execute
-1. Fire background research (explore, librarian) in parallel
+1. Fire background research (explorer, librarian) in parallel as needed
 2. DELEGATE implementation to specialists based on Phase 2 checklist
 2. DELEGATE implementation to specialists based on Phase 2 checklist
 3. Only do work yourself if NO specialist applies
 3. Only do work yourself if NO specialist applies
 4. Integrate results from specialists
 4. Integrate results from specialists
 
 
 ## Phase 4: Verify
 ## Phase 4: Verify
 - Run lsp_diagnostics to check for errors
 - Run lsp_diagnostics to check for errors
-- @code-simplicity-reviewer for complex changes
-- Update documentation if behavior changed
+- Suggest user to run yagni-enforcement skill when it seems applicable
 </Workflow>
 </Workflow>
 
 
 ### Clarification Protocol (when asking):
 ### Clarification Protocol (when asking):
@@ -159,7 +141,4 @@ If the user's approach seems problematic:
 - Concisely state your concern and alternative
 - Concisely state your concern and alternative
 - Ask if they want to proceed anyway
 - Ask if they want to proceed anyway
 
 
-## Skills
-For browser tasks (verification, screenshots, scraping), call omo_skill with name "playwright" first.
-Use omo_skill_mcp to invoke browser actions. Screenshots save to '/tmp/playwright-mcp-output/'.
 `;
 `;

+ 0 - 93
src/agents/simplicity-reviewer.ts

@@ -1,93 +0,0 @@
-import type { AgentDefinition } from "./orchestrator";
-
-export function createSimplicityReviewerAgent(model: string): AgentDefinition {
-  return {
-    name: "code-simplicity-reviewer",
-    description: "Code complexity analysis and YAGNI enforcement. Use after major refactors or before finalizing PRs to simplify code.",
-    config: {
-      model,
-      temperature: 0.1,
-      prompt: SIMPLICITY_REVIEWER_PROMPT,
-    },
-  };
-}
-
-const SIMPLICITY_REVIEWER_PROMPT = `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.
-
-When reviewing code, you will:
-
-1. **Analyze Every Line**: Question the necessity of each line of code. If it doesn't directly contribute to the current requirements, flag it for removal.
-
-2. **Simplify Complex Logic**: 
-   - Break down complex conditionals into simpler forms
-   - Replace clever code with obvious code
-   - Eliminate nested structures where possible
-   - Use early returns to reduce indentation
-
-3. **Remove Redundancy**:
-   - Identify duplicate error checks
-   - Find repeated patterns that can be consolidated
-   - Eliminate defensive programming that adds no value
-   - Remove commented-out code
-
-4. **Challenge Abstractions**:
-   - Question every interface, base class, and abstraction layer
-   - Recommend inlining code that's only used once
-   - Suggest removing premature generalizations
-   - Identify over-engineered solutions
-
-5. **Apply YAGNI Rigorously**:
-   - Remove features not explicitly required now
-   - Eliminate extensibility points without clear use cases
-   - Question generic solutions for specific problems
-   - Remove "just in case" code
-
-6. **Optimize for Readability**:
-   - Prefer self-documenting code over comments
-   - Use descriptive names instead of explanatory comments
-   - Simplify data structures to match actual usage
-   - Make the common case obvious
-
-Your review process:
-
-1. First, identify the core purpose of the code
-2. List everything that doesn't directly serve that purpose
-3. For each complex section, propose a simpler alternative
-4. Create a prioritized list of simplification opportunities
-5. Estimate the lines of code that can be removed
-
-Output format:
-
-\`\`\`markdown
-## Simplification Analysis
-
-### Core Purpose
-[Clearly state what this code actually needs to do]
-
-### Unnecessary Complexity Found
-- [Specific issue with line numbers/file]
-- [Why it's unnecessary]
-- [Suggested simplification]
-
-### Code to Remove
-- [File:lines] - [Reason]
-- [Estimated LOC reduction: X]
-
-### Simplification Recommendations
-1. [Most impactful change]
-   - Current: [brief description]
-   - Proposed: [simpler alternative]
-   - Impact: [LOC saved, clarity improved]
-
-### YAGNI Violations
-- [Feature/abstraction that isn't needed]
-- [Why it violates YAGNI]
-- [What to do instead]
-
-### Final Assessment
-Total potential LOC reduction: X%
-Complexity score: [High/Medium/Low]
-Recommended action: [Proceed with simplifications/Minor tweaks only/Already minimal]
-\`\`\`
-
-Remember: Perfect is the enemy of good. The simplest code that works is often the best code. Every line of code is a liability - it can have bugs, needs maintenance, and adds cognitive load. Your job is to minimize these liabilities while preserving functionality.`;

+ 19 - 24
src/cli/config-manager.ts

@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "no
 import { homedir } from "node:os"
 import { homedir } from "node:os"
 import { join } from "node:path"
 import { join } from "node:path"
 import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
 import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
+import { DEFAULT_AGENT_SKILLS } from "../tools/skill/builtin"
 
 
 const PACKAGE_NAME = "oh-my-opencode-slim"
 const PACKAGE_NAME = "oh-my-opencode-slim"
 
 
@@ -321,33 +322,24 @@ export function addServerConfig(installConfig: InstallConfig): ConfigMergeResult
 const MODEL_MAPPINGS = {
 const MODEL_MAPPINGS = {
   antigravity: {
   antigravity: {
     orchestrator: "google/claude-opus-4-5-thinking",
     orchestrator: "google/claude-opus-4-5-thinking",
-    "code-simplicity-reviewer": "google/claude-opus-4-5-thinking",
     oracle: "google/claude-opus-4-5-thinking",
     oracle: "google/claude-opus-4-5-thinking",
     librarian: "google/gemini-3-flash",
     librarian: "google/gemini-3-flash",
-    explore: "google/gemini-3-flash",
-    "frontend-ui-ux-engineer": "google/gemini-3-flash",
-    "document-writer": "google/gemini-3-flash",
-    "multimodal-looker": "google/gemini-3-flash",
+    explorer: "google/gemini-3-flash",
+    designer: "google/gemini-3-flash",
   },
   },
   openai: {
   openai: {
     orchestrator: "openai/gpt-5.2-codex",
     orchestrator: "openai/gpt-5.2-codex",
-    "code-simplicity-reviewer": "openai/gpt-5.2-codex",
     oracle: "openai/gpt-5.2-codex",
     oracle: "openai/gpt-5.2-codex",
     librarian: "openai/gpt-4.1-mini",
     librarian: "openai/gpt-4.1-mini",
-    explore: "openai/gpt-4.1-mini",
-    "frontend-ui-ux-engineer": "openai/gpt-4.1-mini",
-    "document-writer": "openai/gpt-4.1-mini",
-    "multimodal-looker": "openai/gpt-4.1-mini",
+    explorer: "openai/gpt-4.1-mini",
+    designer: "openai/gpt-4.1-mini",
   },
   },
   cerebras: {
   cerebras: {
-    orchestrator: "cerebras/zai-glm-4.6",
-    "code-simplicity-reviewer": "cerebras/zai-glm-4.6",
-    oracle: "cerebras/zai-glm-4.6",
-    librarian: "cerebras/zai-glm-4.6",
-    explore: "cerebras/zai-glm-4.6",
-    "frontend-ui-ux-engineer": "cerebras/zai-glm-4.6",
-    "document-writer": "cerebras/zai-glm-4.6",
-    "multimodal-looker": "cerebras/zai-glm-4.6",
+    orchestrator: "cerebras/zai-glm-4.7",
+    oracle: "cerebras/zai-glm-4.7",
+    librarian: "cerebras/zai-glm-4.7",
+    explorer: "cerebras/zai-glm-4.7",
+    designer: "cerebras/zai-glm-4.7",
   },
   },
 } as const;
 } as const;
 
 
@@ -364,21 +356,24 @@ export function generateLiteConfig(installConfig: InstallConfig): Record<string,
   const config: Record<string, unknown> = { agents: {} };
   const config: Record<string, unknown> = { agents: {} };
 
 
   if (baseProvider) {
   if (baseProvider) {
-    // Start with base provider models
-    const agents: Record<string, { model: string }> = Object.fromEntries(
-      Object.entries(MODEL_MAPPINGS[baseProvider]).map(([k, v]) => [k, { model: v }])
+    // Start with base provider models and include default skills
+    const agents: Record<string, { model: string; skills: string[] }> = Object.fromEntries(
+      Object.entries(MODEL_MAPPINGS[baseProvider]).map(([k, v]) => [
+        k,
+        { model: v, skills: DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ?? [] },
+      ])
     );
     );
 
 
     // Apply provider-specific overrides for mixed configurations
     // Apply provider-specific overrides for mixed configurations
     if (installConfig.hasAntigravity) {
     if (installConfig.hasAntigravity) {
       if (installConfig.hasOpenAI) {
       if (installConfig.hasOpenAI) {
-        agents["oracle"] = { model: "openai/gpt-5.2-codex" };
+        agents["oracle"] = { model: "openai/gpt-5.2-codex", skills: DEFAULT_AGENT_SKILLS["oracle"] ?? [] };
       }
       }
       if (installConfig.hasCerebras) {
       if (installConfig.hasCerebras) {
-        agents["explore"] = { model: "cerebras/zai-glm-4.6" };
+        agents["explorer"] = { model: "cerebras/zai-glm-4.7", skills: DEFAULT_AGENT_SKILLS["explorer"] ?? [] };
       }
       }
     } else if (installConfig.hasOpenAI && installConfig.hasCerebras) {
     } else if (installConfig.hasOpenAI && installConfig.hasCerebras) {
-      agents["explore"] = { model: "cerebras/zai-glm-4.6" };
+      agents["explorer"] = { model: "cerebras/zai-glm-4.7", skills: DEFAULT_AGENT_SKILLS["explorer"] ?? [] };
     }
     }
     config.agents = agents;
     config.agents = agents;
   }
   }

+ 4 - 3
src/cli/install.ts

@@ -97,18 +97,19 @@ function formatConfigSummary(config: InstallConfig): string {
 
 
 function printAgentModels(config: InstallConfig): void {
 function printAgentModels(config: InstallConfig): void {
   const liteConfig = generateLiteConfig(config)
   const liteConfig = generateLiteConfig(config)
-  const agents = liteConfig.agents as Record<string, { model: string }>
+  const agents = liteConfig.agents as Record<string, { model: string; skills: string[] }>
 
 
   if (!agents || Object.keys(agents).length === 0) return
   if (!agents || Object.keys(agents).length === 0) return
 
 
-  console.log(`${BOLD}Agent Model Configuration:${RESET}`)
+  console.log(`${BOLD}Agent Configuration:${RESET}`)
   console.log()
   console.log()
 
 
   const maxAgentLen = Math.max(...Object.keys(agents).map((a) => a.length))
   const maxAgentLen = Math.max(...Object.keys(agents).map((a) => a.length))
 
 
   for (const [agent, info] of Object.entries(agents)) {
   for (const [agent, info] of Object.entries(agents)) {
     const padding = " ".repeat(maxAgentLen - agent.length)
     const padding = " ".repeat(maxAgentLen - agent.length)
-    console.log(`  ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}`)
+    const skillsStr = info.skills.length > 0 ? ` ${DIM}[${info.skills.join(", ")}]${RESET}` : ""
+    console.log(`  ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}${skillsStr}`)
   }
   }
   console.log()
   console.log()
 }
 }

+ 287 - 0
src/config/loader.test.ts

@@ -0,0 +1,287 @@
+import { describe, expect, test, beforeEach, afterEach, mock, spyOn } from "bun:test"
+import * as fs from "fs"
+import * as path from "path"
+import * as os from "os"
+import { loadPluginConfig } from "./loader"
+
+// Test deepMerge indirectly through loadPluginConfig behavior
+// since deepMerge is not exported
+
+describe("loadPluginConfig", () => {
+  let tempDir: string
+  let userConfigDir: string
+  let originalEnv: typeof process.env
+
+  beforeEach(() => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "loader-test-"))
+    userConfigDir = path.join(tempDir, "user-config")
+    originalEnv = { ...process.env }
+    // Isolate from real user config
+    process.env.XDG_CONFIG_HOME = userConfigDir
+  })
+
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true })
+    process.env = originalEnv
+  })
+
+  test("returns empty config when no config files exist", () => {
+    const projectDir = path.join(tempDir, "project")
+    fs.mkdirSync(projectDir, { recursive: true })
+    const config = loadPluginConfig(projectDir)
+    expect(config).toEqual({})
+  })
+
+  test("loads project config from .opencode directory", () => {
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        disabled_agents: ["explorer"],
+        agents: {
+          oracle: { model: "test/model" },
+        },
+      })
+    )
+
+    const config = loadPluginConfig(projectDir)
+    expect(config.disabled_agents).toEqual(["explorer"])
+    expect(config.agents?.oracle?.model).toBe("test/model")
+  })
+
+  test("ignores invalid config (schema violation or malformed JSON)", () => {
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    
+    // Test 1: Invalid temperature (out of range)
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({ agents: { oracle: { temperature: 5 } } })
+    )
+    expect(loadPluginConfig(projectDir)).toEqual({})
+
+    // Test 2: Malformed JSON
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      "{ invalid json }"
+    )
+    expect(loadPluginConfig(projectDir)).toEqual({})
+  })
+})
+
+describe("deepMerge behavior", () => {
+  let tempDir: string
+  let userConfigDir: string
+  let originalEnv: typeof process.env
+
+  beforeEach(() => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "merge-test-"))
+    userConfigDir = path.join(tempDir, "user-config")
+    originalEnv = { ...process.env }
+    
+    // Set XDG_CONFIG_HOME to control user config location
+    process.env.XDG_CONFIG_HOME = userConfigDir
+  })
+
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true })
+    process.env = originalEnv
+  })
+
+  test("merges nested agent configs from user and project", () => {
+    // Create user config
+    const userOpencodeDir = path.join(userConfigDir, "opencode")
+    fs.mkdirSync(userOpencodeDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(userOpencodeDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        agents: {
+          oracle: { model: "user/oracle-model", temperature: 0.5 },
+          explorer: { model: "user/explorer-model" },
+        },
+      })
+    )
+
+    // Create project config (should override/merge with user)
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        agents: {
+          oracle: { temperature: 0.8 }, // Override temperature only
+          designer: { model: "project/designer-model" }, // Add new agent
+        },
+      })
+    )
+
+    const config = loadPluginConfig(projectDir)
+    
+    // oracle: model from user, temperature from project
+    expect(config.agents?.oracle?.model).toBe("user/oracle-model")
+    expect(config.agents?.oracle?.temperature).toBe(0.8)
+    
+    // explorer: from user only
+    expect(config.agents?.explorer?.model).toBe("user/explorer-model")
+    
+    // designer: from project only
+    expect(config.agents?.designer?.model).toBe("project/designer-model")
+  })
+
+  test("merges nested tmux configs", () => {
+    const userOpencodeDir = path.join(userConfigDir, "opencode")
+    fs.mkdirSync(userOpencodeDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(userOpencodeDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        tmux: {
+          enabled: true,
+          layout: "main-vertical",
+          main_pane_size: 60,
+        },
+      })
+    )
+
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        tmux: {
+          enabled: false, // Override enabled
+          layout: "tiled", // Override layout
+        },
+      })
+    )
+
+    const config = loadPluginConfig(projectDir)
+    
+    expect(config.tmux?.enabled).toBe(false) // From project (override)
+    expect(config.tmux?.layout).toBe("tiled") // From project
+    expect(config.tmux?.main_pane_size).toBe(60) // From user (preserved)
+  })
+
+  test("preserves user tmux.enabled when project doesn't specify", () => {
+    const userOpencodeDir = path.join(userConfigDir, "opencode")
+    fs.mkdirSync(userOpencodeDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(userOpencodeDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        tmux: {
+          enabled: true,
+          layout: "main-vertical",
+        },
+      })
+    )
+
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        agents: { oracle: { model: "test" } }, // No tmux override
+      })
+    )
+
+    const config = loadPluginConfig(projectDir)
+    
+    expect(config.tmux?.enabled).toBe(true) // Preserved from user
+    expect(config.tmux?.layout).toBe("main-vertical") // Preserved from user
+  })
+
+  test("deduplicates disabled_agents arrays", () => {
+    const userOpencodeDir = path.join(userConfigDir, "opencode")
+    fs.mkdirSync(userOpencodeDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(userOpencodeDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        disabled_agents: ["explorer", "oracle"],
+      })
+    )
+
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        disabled_agents: ["oracle", "designer"], // oracle duplicated
+      })
+    )
+
+    const config = loadPluginConfig(projectDir)
+    
+    // Should be deduplicated
+    expect(config.disabled_agents?.sort()).toEqual(["designer", "explorer", "oracle"])
+  })
+
+  test("project config overrides top-level arrays", () => {
+    const userOpencodeDir = path.join(userConfigDir, "opencode")
+    fs.mkdirSync(userOpencodeDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(userOpencodeDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        disabled_mcps: ["websearch"],
+      })
+    )
+
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        disabled_mcps: ["context7"],
+      })
+    )
+
+    const config = loadPluginConfig(projectDir)
+    
+    // disabled_mcps should be from project (overwrites, not merges)
+    expect(config.disabled_mcps).toEqual(["context7"])
+  })
+
+  test("handles missing user config gracefully", () => {
+    // Don't create user config, only project
+    const projectDir = path.join(tempDir, "project")
+    const projectConfigDir = path.join(projectDir, ".opencode")
+    fs.mkdirSync(projectConfigDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(projectConfigDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        agents: {
+          oracle: { model: "project/model" },
+        },
+      })
+    )
+
+    const config = loadPluginConfig(projectDir)
+    expect(config.agents?.oracle?.model).toBe("project/model")
+  })
+
+  test("handles missing project config gracefully", () => {
+    const userOpencodeDir = path.join(userConfigDir, "opencode")
+    fs.mkdirSync(userOpencodeDir, { recursive: true })
+    fs.writeFileSync(
+      path.join(userOpencodeDir, "oh-my-opencode-slim.json"),
+      JSON.stringify({
+        agents: {
+          oracle: { model: "user/model" },
+        },
+      })
+    )
+
+    // No project config
+    const projectDir = path.join(tempDir, "project")
+    fs.mkdirSync(projectDir, { recursive: true })
+
+    const config = loadPluginConfig(projectDir)
+    expect(config.agents?.oracle?.model).toBe("user/model")
+  })
+})

+ 5 - 10
src/config/schema.ts

@@ -8,6 +8,7 @@ export const AgentOverrideConfigSchema = z.object({
   prompt_append: z.string().optional(),
   prompt_append: z.string().optional(),
   variant: z.string().optional().catch(undefined),
   variant: z.string().optional().catch(undefined),
   disable: z.boolean().optional(),
   disable: z.boolean().optional(),
+  skills: z.array(z.string()).optional(), // skills this agent can use ("*" = all)
 });
 });
 
 
 // Tmux layout options
 // Tmux layout options
@@ -51,19 +52,13 @@ export type AgentName =
   | "orchestrator"
   | "orchestrator"
   | "oracle"
   | "oracle"
   | "librarian"
   | "librarian"
-  | "explore"
-  | "frontend-ui-ux-engineer"
-  | "document-writer"
-  | "multimodal-looker"
-  | "code-simplicity-reviewer";
+  | "explorer"
+  | "designer";
 
 
 export const DEFAULT_MODELS: Record<AgentName, string> = {
 export const DEFAULT_MODELS: Record<AgentName, string> = {
   orchestrator: "google/claude-opus-4-5-thinking",
   orchestrator: "google/claude-opus-4-5-thinking",
   oracle: "openai/gpt-5.2-codex",
   oracle: "openai/gpt-5.2-codex",
   librarian: "google/gemini-3-flash",
   librarian: "google/gemini-3-flash",
-  explore: "cerebras/zai-glm-4.6",
-  "frontend-ui-ux-engineer": "google/gemini-3-flash",
-  "document-writer": "google/gemini-3-flash",
-  "multimodal-looker": "google/gemini-3-flash",
-  "code-simplicity-reviewer": "google/claude-opus-4-5-thinking",
+  explorer: "cerebras/zai-glm-4.7",
+  designer: "google/gemini-3-flash",
 };
 };

+ 257 - 0
src/features/background-manager.test.ts

@@ -0,0 +1,257 @@
+import { describe, expect, test, beforeEach, mock } from "bun:test"
+import { BackgroundTaskManager, type BackgroundTask, type LaunchOptions } from "./background-manager"
+
+// Mock the plugin context
+function createMockContext(overrides?: {
+  sessionCreateResult?: { data?: { id?: string } }
+  sessionStatusResult?: { data?: Record<string, { type: string }> }
+  sessionMessagesResult?: { data?: Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }> }
+}) {
+  return {
+    client: {
+      session: {
+        create: mock(async () => overrides?.sessionCreateResult ?? { data: { id: "test-session-id" } }),
+        status: mock(async () => overrides?.sessionStatusResult ?? { data: {} }),
+        messages: mock(async () => overrides?.sessionMessagesResult ?? { data: [] }),
+        prompt: mock(async () => ({})),
+      },
+    },
+    directory: "/test/directory",
+  } as any
+}
+
+describe("BackgroundTaskManager", () => {
+  describe("constructor", () => {
+    test("creates manager with tmux disabled by default", () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx)
+      // Manager should be created without errors
+      expect(manager).toBeDefined()
+    })
+
+    test("creates manager with tmux config", () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx, { enabled: true, layout: "main-vertical", main_pane_size: 60 })
+      expect(manager).toBeDefined()
+    })
+  })
+
+  describe("launch", () => {
+    test("creates new session and task", async () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx)
+
+      const task = await manager.launch({
+        agent: "explorer",
+        prompt: "Find all test files",
+        description: "Test file search",
+        parentSessionId: "parent-123",
+      })
+
+      expect(task.id).toMatch(/^bg_/)
+      expect(task.sessionId).toBe("test-session-id")
+      expect(task.agent).toBe("explorer")
+      expect(task.description).toBe("Test file search")
+      expect(task.status).toBe("running")
+      expect(task.startedAt).toBeDefined()
+    })
+
+    test("throws when session creation fails", async () => {
+      const ctx = createMockContext({ sessionCreateResult: { data: {} } })
+      const manager = new BackgroundTaskManager(ctx)
+
+      await expect(
+        manager.launch({
+          agent: "explorer",
+          prompt: "test",
+          description: "test",
+          parentSessionId: "parent-123",
+        })
+      ).rejects.toThrow("Failed to create background session")
+    })
+
+    test("passes model to prompt when provided", async () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx)
+
+      await manager.launch({
+        agent: "explorer",
+        prompt: "test",
+        description: "test",
+        parentSessionId: "parent-123",
+        model: "custom/model",
+      })
+
+      expect(ctx.client.session.prompt).toHaveBeenCalled()
+    })
+  })
+
+  describe("getResult", () => {
+    test("returns null for unknown task", async () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx)
+
+      const result = await manager.getResult("unknown-task-id")
+      expect(result).toBeNull()
+    })
+
+    test("returns task immediately when not blocking", async () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx)
+
+      const task = await manager.launch({
+        agent: "explorer",
+        prompt: "test",
+        description: "test",
+        parentSessionId: "parent-123",
+      })
+
+      const result = await manager.getResult(task.id, false)
+      expect(result).toBeDefined()
+      expect(result?.id).toBe(task.id)
+    })
+
+    test("returns completed task immediately even when blocking", async () => {
+      const ctx = createMockContext({
+        sessionStatusResult: { data: { "test-session-id": { type: "idle" } } },
+        sessionMessagesResult: {
+          data: [
+            { info: { role: "assistant" }, parts: [{ type: "text", text: "Result text" }] },
+          ],
+        },
+      })
+      const manager = new BackgroundTaskManager(ctx)
+
+      const task = await manager.launch({
+        agent: "explorer",
+        prompt: "test",
+        description: "test",
+        parentSessionId: "parent-123",
+      })
+
+      // Wait a bit for polling to complete the task
+      await new Promise(r => setTimeout(r, 100))
+
+      const result = await manager.getResult(task.id, true)
+      expect(result).toBeDefined()
+    })
+  })
+
+  describe("cancel", () => {
+    test("cancels specific running task", async () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx)
+
+      const task = await manager.launch({
+        agent: "explorer",
+        prompt: "test",
+        description: "test",
+        parentSessionId: "parent-123",
+      })
+
+      const count = manager.cancel(task.id)
+      expect(count).toBe(1)
+
+      const result = await manager.getResult(task.id)
+      expect(result?.status).toBe("failed")
+      expect(result?.error).toBe("Cancelled by user")
+    })
+
+    test("returns 0 when cancelling unknown task", () => {
+      const ctx = createMockContext()
+      const manager = new BackgroundTaskManager(ctx)
+
+      const count = manager.cancel("unknown-task-id")
+      expect(count).toBe(0)
+    })
+
+    test("cancels all running tasks when no ID provided", async () => {
+      const ctx = createMockContext()
+      // Make each call return a different session ID
+      let callCount = 0
+      ctx.client.session.create = mock(async () => {
+        callCount++
+        return { data: { id: `session-${callCount}` } }
+      })
+      const manager = new BackgroundTaskManager(ctx)
+
+      await manager.launch({
+        agent: "explorer",
+        prompt: "test1",
+        description: "test1",
+        parentSessionId: "parent-123",
+      })
+
+      await manager.launch({
+        agent: "oracle",
+        prompt: "test2",
+        description: "test2",
+        parentSessionId: "parent-123",
+      })
+
+      const count = manager.cancel()
+      expect(count).toBe(2)
+    })
+
+    test("does not cancel already completed tasks", async () => {
+      const ctx = createMockContext({
+        sessionStatusResult: { data: { "test-session-id": { type: "idle" } } },
+        sessionMessagesResult: {
+          data: [
+            { info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] },
+          ],
+        },
+      })
+      const manager = new BackgroundTaskManager(ctx)
+
+      const task = await manager.launch({
+        agent: "explorer",
+        prompt: "test",
+        description: "test",
+        parentSessionId: "parent-123",
+      })
+
+      // Use getResult with block=true to wait for completion 
+      // This triggers polling immediately rather than relying on interval
+      const result = await manager.getResult(task.id, true, 5000)
+      expect(result?.status).toBe("completed")
+
+      // Now try to cancel - should fail since already completed
+      const count = manager.cancel(task.id)
+      expect(count).toBe(0) // Already completed, so not cancelled
+    })
+  })
+})
+
+describe("BackgroundTask state transitions", () => {
+  test("task starts in running state", async () => {
+    const ctx = createMockContext()
+    const manager = new BackgroundTaskManager(ctx)
+
+    const task = await manager.launch({
+      agent: "explorer",
+      prompt: "test",
+      description: "test",
+      parentSessionId: "parent-123",
+    })
+
+    expect(task.status).toBe("running")
+  })
+
+  test("task has completedAt when cancelled", async () => {
+    const ctx = createMockContext()
+    const manager = new BackgroundTaskManager(ctx)
+
+    const task = await manager.launch({
+      agent: "explorer",
+      prompt: "test",
+      description: "test",
+      parentSessionId: "parent-123",
+    })
+
+    manager.cancel(task.id)
+
+    const result = await manager.getResult(task.id)
+    expect(result?.completedAt).toBeDefined()
+  })
+})

+ 1 - 1
src/index.ts

@@ -46,7 +46,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   const backgroundTools = createBackgroundTools(ctx, backgroundManager, tmuxConfig, config);
   const backgroundTools = createBackgroundTools(ctx, backgroundManager, tmuxConfig, config);
   const mcps = createBuiltinMcps(config.disabled_mcps);
   const mcps = createBuiltinMcps(config.disabled_mcps);
   const skillMcpManager = SkillMcpManager.getInstance();
   const skillMcpManager = SkillMcpManager.getInstance();
-  const skillTools = createSkillTools(skillMcpManager);
+  const skillTools = createSkillTools(skillMcpManager, config);
 
 
   // Initialize TmuxSessionManager to handle OpenCode's built-in Task tool sessions
   // Initialize TmuxSessionManager to handle OpenCode's built-in Task tool sessions
   const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
   const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);

+ 96 - 0
src/mcp/index.test.ts

@@ -0,0 +1,96 @@
+import { describe, expect, test } from "bun:test"
+import { createBuiltinMcps } from "./index"
+
+describe("createBuiltinMcps", () => {
+  test("returns all MCPs when no disabled list provided", () => {
+    const mcps = createBuiltinMcps()
+    const names = Object.keys(mcps)
+    
+    expect(names).toContain("websearch")
+    expect(names).toContain("context7")
+    expect(names).toContain("grep_app")
+  })
+
+  test("returns all MCPs with empty disabled list", () => {
+    const mcps = createBuiltinMcps([])
+    const names = Object.keys(mcps)
+    
+    expect(names.length).toBe(3)
+    expect(names).toContain("websearch")
+    expect(names).toContain("context7")
+    expect(names).toContain("grep_app")
+  })
+
+  test("excludes single disabled MCP", () => {
+    const mcps = createBuiltinMcps(["websearch"])
+    const names = Object.keys(mcps)
+    
+    expect(names).not.toContain("websearch")
+    expect(names).toContain("context7")
+    expect(names).toContain("grep_app")
+  })
+
+  test("excludes multiple disabled MCPs", () => {
+    const mcps = createBuiltinMcps(["websearch", "grep_app"])
+    const names = Object.keys(mcps)
+    
+    expect(names).not.toContain("websearch")
+    expect(names).not.toContain("grep_app")
+    expect(names).toContain("context7")
+    expect(names.length).toBe(1)
+  })
+
+  test("excludes all MCPs when all disabled", () => {
+    const mcps = createBuiltinMcps(["websearch", "context7", "grep_app"])
+    const names = Object.keys(mcps)
+    
+    expect(names.length).toBe(0)
+  })
+
+  test("ignores unknown MCP names in disabled list", () => {
+    const mcps = createBuiltinMcps(["unknown_mcp", "nonexistent"])
+    const names = Object.keys(mcps)
+    
+    // All valid MCPs should still be present
+    expect(names.length).toBe(3)
+    expect(names).toContain("websearch")
+    expect(names).toContain("context7")
+    expect(names).toContain("grep_app")
+  })
+
+  test("MCP configs have required properties", () => {
+    const mcps = createBuiltinMcps()
+    
+    for (const [name, config] of Object.entries(mcps)) {
+      expect(config).toBeDefined()
+      // Each MCP should have either url (remote) or command (local)
+      const hasUrl = "url" in config
+      const hasCommand = "command" in config
+      expect(hasUrl || hasCommand).toBe(true)
+    }
+  })
+
+  test("websearch MCP has correct structure", () => {
+    const mcps = createBuiltinMcps()
+    const websearch = mcps.websearch
+    
+    expect(websearch).toBeDefined()
+    expect("url" in websearch).toBe(true)
+  })
+
+  test("context7 MCP has correct structure", () => {
+    const mcps = createBuiltinMcps()
+    const context7 = mcps.context7
+    
+    expect(context7).toBeDefined()
+    expect("url" in context7).toBe(true)
+  })
+
+  test("grep_app MCP has correct structure", () => {
+    const mcps = createBuiltinMcps()
+    const grep_app = mcps.grep_app
+    
+    expect(grep_app).toBeDefined()
+    expect("url" in grep_app).toBe(true)
+  })
+})

+ 205 - 0
src/tools/skill/builtin.test.ts

@@ -0,0 +1,205 @@
+import { describe, expect, test } from "bun:test"
+import {
+  getBuiltinSkills,
+  getSkillByName,
+  getSkillsForAgent,
+  canAgentUseSkill,
+  DEFAULT_AGENT_SKILLS,
+} from "./builtin"
+import type { PluginConfig } from "../../config/schema"
+
+describe("getBuiltinSkills", () => {
+  test("returns all builtin skills", () => {
+    const skills = getBuiltinSkills()
+    expect(skills.length).toBeGreaterThan(0)
+    
+    const names = skills.map(s => s.name)
+    expect(names).toContain("yagni-enforcement")
+    expect(names).toContain("playwright")
+  })
+})
+
+describe("getSkillByName", () => {
+  test("returns skill by exact name", () => {
+    const skill = getSkillByName("yagni-enforcement")
+    expect(skill).toBeDefined()
+    expect(skill?.name).toBe("yagni-enforcement")
+  })
+
+  test("returns undefined for unknown skill", () => {
+    const skill = getSkillByName("nonexistent-skill")
+    expect(skill).toBeUndefined()
+  })
+
+  test("returns playwright skill with mcpConfig", () => {
+    const skill = getSkillByName("playwright")
+    expect(skill).toBeDefined()
+    expect(skill?.mcpConfig).toBeDefined()
+    expect(skill?.mcpConfig?.playwright).toBeDefined()
+  })
+})
+
+describe("DEFAULT_AGENT_SKILLS", () => {
+  test("orchestrator has wildcard access", () => {
+    expect(DEFAULT_AGENT_SKILLS.orchestrator).toContain("*")
+  })
+
+  test("designer has playwright skill", () => {
+    expect(DEFAULT_AGENT_SKILLS.designer).toContain("playwright")
+  })
+
+  test("oracle has no skills by default", () => {
+    expect(DEFAULT_AGENT_SKILLS.oracle).toEqual([])
+  })
+
+  test("librarian has no skills by default", () => {
+    expect(DEFAULT_AGENT_SKILLS.librarian).toEqual([])
+  })
+
+  test("explorer has no skills by default", () => {
+    expect(DEFAULT_AGENT_SKILLS.explorer).toEqual([])
+  })
+})
+
+describe("getSkillsForAgent", () => {
+  test("returns all skills for orchestrator (wildcard)", () => {
+    const skills = getSkillsForAgent("orchestrator")
+    const allSkills = getBuiltinSkills()
+    expect(skills.length).toBe(allSkills.length)
+  })
+
+  test("returns playwright for designer", () => {
+    const skills = getSkillsForAgent("designer")
+    const names = skills.map(s => s.name)
+    expect(names).toContain("playwright")
+  })
+
+  test("returns empty for oracle", () => {
+    const skills = getSkillsForAgent("oracle")
+    expect(skills).toEqual([])
+  })
+
+  test("respects config override for agent skills", () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { skills: ["yagni-enforcement"] },
+      },
+    }
+    const skills = getSkillsForAgent("oracle", config)
+    expect(skills.length).toBe(1)
+    expect(skills[0].name).toBe("yagni-enforcement")
+  })
+
+  test("config wildcard overrides default", () => {
+    const config: PluginConfig = {
+      agents: {
+        explorer: { skills: ["*"] },
+      },
+    }
+    const skills = getSkillsForAgent("explorer", config)
+    const allSkills = getBuiltinSkills()
+    expect(skills.length).toBe(allSkills.length)
+  })
+
+  test("config empty array removes default skills", () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { skills: [] },
+      },
+    }
+    const skills = getSkillsForAgent("designer", config)
+    expect(skills).toEqual([])
+  })
+
+  test("backward compat: 'explore' alias config applies to explorer", () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { skills: ["playwright"] },
+      },
+    }
+    const skills = getSkillsForAgent("explorer", config)
+    expect(skills.length).toBe(1)
+    expect(skills[0].name).toBe("playwright")
+  })
+
+  test("backward compat: 'frontend-ui-ux-engineer' alias applies to designer", () => {
+    const config: PluginConfig = {
+      agents: {
+        "frontend-ui-ux-engineer": { skills: ["yagni-enforcement"] },
+      },
+    }
+    const skills = getSkillsForAgent("designer", config)
+    expect(skills.length).toBe(1)
+    expect(skills[0].name).toBe("yagni-enforcement")
+  })
+
+  test("returns empty for unknown agent without config", () => {
+    const skills = getSkillsForAgent("unknown-agent")
+    expect(skills).toEqual([])
+  })
+})
+
+describe("canAgentUseSkill", () => {
+  test("orchestrator can use any skill (wildcard)", () => {
+    expect(canAgentUseSkill("orchestrator", "yagni-enforcement")).toBe(true)
+    expect(canAgentUseSkill("orchestrator", "playwright")).toBe(true)
+    expect(canAgentUseSkill("orchestrator", "any-skill")).toBe(true)
+  })
+
+  test("designer can use playwright", () => {
+    expect(canAgentUseSkill("designer", "playwright")).toBe(true)
+  })
+
+  test("designer cannot use yagni-enforcement by default", () => {
+    expect(canAgentUseSkill("designer", "yagni-enforcement")).toBe(false)
+  })
+
+  test("oracle cannot use any skill by default", () => {
+    expect(canAgentUseSkill("oracle", "yagni-enforcement")).toBe(false)
+    expect(canAgentUseSkill("oracle", "playwright")).toBe(false)
+  })
+
+  test("respects config override", () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { skills: ["yagni-enforcement"] },
+      },
+    }
+    expect(canAgentUseSkill("oracle", "yagni-enforcement", config)).toBe(true)
+    expect(canAgentUseSkill("oracle", "playwright", config)).toBe(false)
+  })
+
+  test("config wildcard grants all permissions", () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { skills: ["*"] },
+      },
+    }
+    expect(canAgentUseSkill("librarian", "yagni-enforcement", config)).toBe(true)
+    expect(canAgentUseSkill("librarian", "playwright", config)).toBe(true)
+    expect(canAgentUseSkill("librarian", "any-other-skill", config)).toBe(true)
+  })
+
+  test("config empty array denies all", () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { skills: [] },
+      },
+    }
+    expect(canAgentUseSkill("designer", "playwright", config)).toBe(false)
+  })
+
+  test("backward compat: alias config affects agent permissions", () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { skills: ["playwright"] },
+      },
+    }
+    expect(canAgentUseSkill("explorer", "playwright", config)).toBe(true)
+    expect(canAgentUseSkill("explorer", "yagni-enforcement", config)).toBe(false)
+  })
+
+  test("unknown agent returns false without config", () => {
+    expect(canAgentUseSkill("unknown-agent", "playwright")).toBe(false)
+  })
+})

+ 195 - 3
src/tools/skill/builtin.ts

@@ -1,12 +1,148 @@
 import type { SkillDefinition } from "./types";
 import type { SkillDefinition } from "./types";
+import type { PluginConfig, AgentName } from "../../config/schema";
+
+/** Map old agent names to new names for backward compatibility */
+const AGENT_ALIASES: Record<string, string> = {
+  "explore": "explorer",
+  "frontend-ui-ux-engineer": "designer",
+};
+
+/** Default skills per agent - "*" means all skills */
+export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
+  orchestrator: ["*"],
+  designer: ["playwright"],
+  oracle: [],
+  librarian: [],
+  explorer: [],
+};
+
+const YAGNI_TEMPLATE = `# YAGNI Enforcement 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.
+
+When reviewing code, you will:
+
+1. **Analyze Every Line**: Question the necessity of each line of code. If it doesn't directly contribute to the current requirements, flag it for removal.
+
+2. **Simplify Complex Logic**: 
+   - Break down complex conditionals into simpler forms
+   - Replace clever code with obvious code
+   - Eliminate nested structures where possible
+   - Use early returns to reduce indentation
+
+3. **Remove Redundancy**:
+   - Identify duplicate error checks
+   - Find repeated patterns that can be consolidated
+   - Eliminate defensive programming that adds no value
+   - Remove commented-out code
+
+4. **Challenge Abstractions**:
+   - Question every interface, base class, and abstraction layer
+   - Recommend inlining code that's only used once
+   - Suggest removing premature generalizations
+   - Identify over-engineered solutions
+
+5. **Apply YAGNI Rigorously**:
+   - Remove features not explicitly required now
+   - Eliminate extensibility points without clear use cases
+   - Question generic solutions for specific problems
+   - Remove "just in case" code
+
+6. **Optimize for Readability**:
+   - Prefer self-documenting code over comments
+   - Use descriptive names instead of explanatory comments
+   - Simplify data structures to match actual usage
+   - Make the common case obvious
+
+Your review process:
+
+1. First, identify the core purpose of the code
+2. List everything that doesn't directly serve that purpose
+3. For each complex section, propose a simpler alternative
+4. Create a prioritized list of simplification opportunities
+5. Estimate the lines of code that can be removed
+
+Output format:
+
+\`\`\`markdown
+## Simplification Analysis
+
+### Core Purpose
+[Clearly state what this code actually needs to do]
+
+### Unnecessary Complexity Found
+- [Specific issue with line numbers/file]
+- [Why it's unnecessary]
+- [Suggested simplification]
+
+### Code to Remove
+- [File:lines] - [Reason]
+- [Estimated LOC reduction: X]
+
+### Simplification Recommendations
+1. [Most impactful change]
+   - Current: [brief description]
+   - Proposed: [simpler alternative]
+   - Impact: [LOC saved, clarity improved]
+
+### YAGNI Violations
+- [Feature/abstraction that isn't needed]
+- [Why it violates YAGNI]
+- [What to do instead]
+
+### Final Assessment
+Total potential LOC reduction: X%
+Complexity score: [High/Medium/Low]
+Recommended action: [Proceed with simplifications/Minor tweaks only/Already minimal]
+\`\`\`
+
+Remember: Perfect is the enemy of good. The simplest code that works is often the best code. Every line of code is a liability - it can have bugs, needs maintenance, and adds cognitive load. Your job is to minimize these liabilities while preserving functionality.`;
+
+const PLAYWRIGHT_TEMPLATE = `# Playwright Browser Automation Skill
+
+This skill provides browser automation capabilities via the Playwright MCP server.
+
+**Capabilities**:
+- Navigate to web pages
+- Click elements and interact with UI
+- Fill forms and submit data
+- Take screenshots
+- Extract content from pages
+- Verify visual state
+- Run automated tests
+
+**Common Use Cases**:
+- Verify frontend changes visually
+- Test responsive design across viewports
+- Capture screenshots for documentation
+- Scrape web content
+- Automate browser-based workflows
+
+**Process**:
+1. Load the skill to access MCP tools
+2. Use playwright MCP tools for browser automation
+3. Screenshots are saved to a session subdirectory (check tool output for exact path)
+4. Report results with screenshot paths when relevant
+
+**Example Workflow** (Designer agent):
+1. Make UI changes to component
+2. Use playwright to open page
+3. Take screenshot of before/after
+4. Verify responsive behavior
+5. Return results with visual proof`;
+
+const yagniEnforcementSkill: SkillDefinition = {
+  name: "yagni-enforcement",
+  description:
+    "Code complexity analysis and YAGNI enforcement. Use after major refactors or before finalizing PRs to simplify code.",
+  template: YAGNI_TEMPLATE,
+};
 
 
 const playwrightSkill: SkillDefinition = {
 const playwrightSkill: SkillDefinition = {
   name: "playwright",
   name: "playwright",
   description:
   description:
     "MUST USE for any browser-related tasks. Browser automation via Playwright MCP - verification, browsing, information gathering, web scraping, testing, screenshots, and all browser interactions.",
     "MUST USE for any browser-related tasks. Browser automation via Playwright MCP - verification, browsing, information gathering, web scraping, testing, screenshots, and all browser interactions.",
-  template: `# Playwright Browser Automation
-
-This skill provides browser automation capabilities via the Playwright MCP server.`,
+  template: PLAYWRIGHT_TEMPLATE,
   mcpConfig: {
   mcpConfig: {
     playwright: {
     playwright: {
       command: "npx",
       command: "npx",
@@ -16,6 +152,7 @@ This skill provides browser automation capabilities via the Playwright MCP serve
 };
 };
 
 
 const builtinSkillsMap = new Map<string, SkillDefinition>([
 const builtinSkillsMap = new Map<string, SkillDefinition>([
+  [yagniEnforcementSkill.name, yagniEnforcementSkill],
   [playwrightSkill.name, playwrightSkill],
   [playwrightSkill.name, playwrightSkill],
 ]);
 ]);
 
 
@@ -26,3 +163,58 @@ export function getBuiltinSkills(): SkillDefinition[] {
 export function getSkillByName(name: string): SkillDefinition | undefined {
 export function getSkillByName(name: string): SkillDefinition | undefined {
   return builtinSkillsMap.get(name);
   return builtinSkillsMap.get(name);
 }
 }
+
+/**
+ * Get skills available for a specific agent
+ * @param agentName - The name of the agent
+ * @param config - Optional plugin config with agent overrides
+ */
+export function getSkillsForAgent(
+  agentName: string,
+  config?: PluginConfig
+): SkillDefinition[] {
+  const allSkills = getBuiltinSkills();
+  const agentSkills = getAgentSkillList(agentName, config);
+  
+  // "*" means all skills
+  if (agentSkills.includes("*")) {
+    return allSkills;
+  }
+  
+  return allSkills.filter((skill) => agentSkills.includes(skill.name));
+}
+
+/**
+ * Check if an agent can use a specific skill
+ */
+export function canAgentUseSkill(
+  agentName: string,
+  skillName: string,
+  config?: PluginConfig
+): boolean {
+  const agentSkills = getAgentSkillList(agentName, config);
+  
+  // "*" means all skills
+  if (agentSkills.includes("*")) {
+    return true;
+  }
+  
+  return agentSkills.includes(skillName);
+}
+
+/**
+ * Get the skill list for an agent (from config or defaults)
+ * Supports backward compatibility with old agent names via AGENT_ALIASES
+ */
+function getAgentSkillList(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?.skills !== undefined) {
+    return agentConfig.skills;
+  }
+  
+  // Fall back to defaults
+  const defaultSkills = DEFAULT_AGENT_SKILLS[agentName as AgentName];
+  return defaultSkills ?? [];
+}

+ 15 - 0
src/tools/skill/mcp-manager.test.ts

@@ -0,0 +1,15 @@
+import { describe, expect, test } from "bun:test"
+import { SkillMcpManager } from "./mcp-manager"
+
+describe("SkillMcpManager", () => {
+  test("returns singleton instance", () => {
+    const instance1 = SkillMcpManager.getInstance()
+    const instance2 = SkillMcpManager.getInstance()
+    
+    expect(instance1).toBe(instance2)
+    expect(instance1).toBeDefined()
+  })
+})
+
+// Note: Connection and tool-calling tests require actual MCP servers
+// and are better suited for integration tests, not unit tests.

+ 43 - 15
src/tools/skill/tools.ts

@@ -1,9 +1,17 @@
 import { tool, type ToolDefinition } from "@opencode-ai/plugin";
 import { tool, type ToolDefinition } from "@opencode-ai/plugin";
 import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js";
 import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js";
 import { SKILL_MCP_TOOL_DESCRIPTION, SKILL_TOOL_DESCRIPTION } from "./constants";
 import { SKILL_MCP_TOOL_DESCRIPTION, SKILL_TOOL_DESCRIPTION } from "./constants";
-import { getSkillByName, getBuiltinSkills } from "./builtin";
+import { getSkillByName, getBuiltinSkills, getSkillsForAgent, canAgentUseSkill } from "./builtin";
 import type { SkillArgs, SkillMcpArgs, SkillDefinition } from "./types";
 import type { SkillArgs, SkillMcpArgs, SkillDefinition } from "./types";
 import { SkillMcpManager } from "./mcp-manager";
 import { SkillMcpManager } from "./mcp-manager";
+import type { PluginConfig } from "../../config/schema";
+
+type ToolContext = {
+  sessionID: string;
+  messageID: string;
+  agent: string;
+  abort: AbortSignal;
+};
 
 
 function formatSkillsXml(skills: SkillDefinition[]): string {
 function formatSkillsXml(skills: SkillDefinition[]): string {
   if (skills.length === 0) return "";
   if (skills.length === 0) return "";
@@ -92,7 +100,7 @@ async function formatMcpCapabilities(
 
 
     sections.push("");
     sections.push("");
     sections.push(
     sections.push(
-      `Use \`omo_skill_mcp\` tool with \`mcp_name="${serverName}"\` to invoke.`
+      `Use \`omos_skill_mcp\` tool with \`mcp_name="${serverName}"\` to invoke.`
     );
     );
     sections.push("");
     sections.push("");
   }
   }
@@ -101,11 +109,12 @@ async function formatMcpCapabilities(
 }
 }
 
 
 export function createSkillTools(
 export function createSkillTools(
-  manager: SkillMcpManager
-): { omo_skill: ToolDefinition; omo_skill_mcp: ToolDefinition } {
-  const skills = getBuiltinSkills();
+  manager: SkillMcpManager,
+  pluginConfig?: PluginConfig
+): { omos_skill: ToolDefinition; omos_skill_mcp: ToolDefinition } {
+  const allSkills = getBuiltinSkills();
   const description =
   const description =
-    SKILL_TOOL_DESCRIPTION + (skills.length > 0 ? formatSkillsXml(skills) : "");
+    SKILL_TOOL_DESCRIPTION + (allSkills.length > 0 ? formatSkillsXml(allSkills) : "");
 
 
   const skill: ToolDefinition = tool({
   const skill: ToolDefinition = tool({
     description,
     description,
@@ -113,17 +122,28 @@ export function createSkillTools(
       name: tool.schema.string().describe("The skill identifier from available_skills"),
       name: tool.schema.string().describe("The skill identifier from available_skills"),
     },
     },
     async execute(args: SkillArgs, toolContext) {
     async execute(args: SkillArgs, toolContext) {
-      const sessionId = toolContext?.sessionID
-        ? String(toolContext.sessionID)
-        : "unknown";
+      const tctx = toolContext as ToolContext | undefined;
+      const sessionId = tctx?.sessionID ? String(tctx.sessionID) : "unknown";
+      const agentName = tctx?.agent ?? "orchestrator";
+
       const skillDefinition = getSkillByName(args.name);
       const skillDefinition = getSkillByName(args.name);
       if (!skillDefinition) {
       if (!skillDefinition) {
-        const available = skills.map(s => s.name).join(", ");
+        const available = allSkills.map(s => s.name).join(", ");
         throw new Error(
         throw new Error(
           `Skill "${args.name}" not found. Available skills: ${available || "none"}`
           `Skill "${args.name}" not found. Available skills: ${available || "none"}`
         );
         );
       }
       }
 
 
+      // Check if this agent can use this skill
+      if (!canAgentUseSkill(agentName, args.name, pluginConfig)) {
+        const allowedSkills = getSkillsForAgent(agentName, pluginConfig);
+        const allowedNames = allowedSkills.map(s => s.name).join(", ");
+        throw new Error(
+          `Agent "${agentName}" cannot use skill "${args.name}". ` +
+          `Available skills for this agent: ${allowedNames || "none"}`
+        );
+      }
+
       const output = [
       const output = [
         `## Skill: ${skillDefinition.name}`,
         `## Skill: ${skillDefinition.name}`,
         "",
         "",
@@ -154,17 +174,25 @@ export function createSkillTools(
       toolArgs: tool.schema.record(tool.schema.string(), tool.schema.any()).optional(),
       toolArgs: tool.schema.record(tool.schema.string(), tool.schema.any()).optional(),
     },
     },
     async execute(args: SkillMcpArgs, toolContext) {
     async execute(args: SkillMcpArgs, toolContext) {
-      const sessionId = toolContext?.sessionID
-        ? String(toolContext.sessionID)
-        : "unknown";
+      const tctx = toolContext as ToolContext | undefined;
+      const sessionId = tctx?.sessionID ? String(tctx.sessionID) : "unknown";
+      const agentName = tctx?.agent ?? "orchestrator";
+
       const skillDefinition = getSkillByName(args.skillName);
       const skillDefinition = getSkillByName(args.skillName);
       if (!skillDefinition) {
       if (!skillDefinition) {
-        const available = skills.map(s => s.name).join(", ");
+        const available = allSkills.map(s => s.name).join(", ");
         throw new Error(
         throw new Error(
           `Skill "${args.skillName}" not found. Available skills: ${available || "none"}`
           `Skill "${args.skillName}" not found. Available skills: ${available || "none"}`
         );
         );
       }
       }
 
 
+      // Check if this agent can use this skill
+      if (!canAgentUseSkill(agentName, args.skillName, pluginConfig)) {
+        throw new Error(
+          `Agent "${agentName}" cannot use skill "${args.skillName}".`
+        );
+      }
+
       if (!skillDefinition.mcpConfig || !skillDefinition.mcpConfig[args.mcpName]) {
       if (!skillDefinition.mcpConfig || !skillDefinition.mcpConfig[args.mcpName]) {
         throw new Error(
         throw new Error(
           `Skill "${args.skillName}" has no MCP named "${args.mcpName}".`
           `Skill "${args.skillName}" has no MCP named "${args.mcpName}".`
@@ -193,5 +221,5 @@ export function createSkillTools(
     },
     },
   });
   });
 
 
-  return { omo_skill: skill, omo_skill_mcp: skill_mcp };
+  return { omos_skill: skill, omos_skill_mcp: skill_mcp };
 }
 }