Browse Source

chore: remove ripgrep-based grep tool (#200)

* Remove ripgrep-based grep tool

OpenCode already has a built-in grep tool using ripgrep. This removes
the duplicate grep tool from the plugin to avoid redundant functionality
and eliminate the need to download/install ripgrep on behalf of the user.

Fixes #197

* Fix biome lint/format issues

* refactor(explorer): clean up agent prompt by moving tool docs to separate files

* chore(cartography): update codemaps for directory changes

- Add new hooks: delegate-task-retry, foreground-fallback, json-error-recovery
- Add new utils: env.ts, internal-initiator.ts
- Add new lsp tool: config-store.ts
- Add new cli tool: model-key-normalization.ts
- Remove grep tool directory
- Update root and all subdirectory codemaps
- Update cartographer state
Adithya Kozham Burath Bijoy 3 weeks ago
parent
commit
f1f9c1be0b

+ 64 - 57
.slim/cartography.json

@@ -1,7 +1,7 @@
 {
   "metadata": {
     "version": "1.0.0",
-    "last_run": "2026-01-28T20:25:58.984513Z",
+    "last_run": "2026-03-24T08:10:21.042130Z",
     "root": "/Users/xp/repos/oh-my-opencode-slim",
     "include_patterns": [
       "src/**/*.ts",
@@ -23,94 +23,101 @@
     "exceptions": []
   },
   "file_hashes": {
-    "package.json": "7715619acbfd880221f72135dabb55cd",
+    "package.json": "3ed7adb5b811b2e9fce30fc71a2cda10",
     "src/agents/designer.ts": "ea6af83207f143260a775d79ab229407",
-    "src/agents/explorer.ts": "4678dc549476feeb9088149e72bcffdb",
-    "src/agents/fixer.ts": "02aa0b9da795e0aa38c9651fe0c5a8c9",
-    "src/agents/index.ts": "9a715acd64b4d0a7cd65c651207fd234",
+    "src/agents/explorer.ts": "efb5fddccfe5990f5d5eb8cf8c2f11a8",
+    "src/agents/fixer.ts": "3fa80ddb759a226d3810c93c477df3bf",
+    "src/agents/index.ts": "f8fff7a5be2831f21c540a75a58f5e5f",
     "src/agents/librarian.ts": "8a85df35044c719b6615d6afdbe26f04",
     "src/agents/oracle.ts": "d6a40e69fad62a4f95568f3e9314a06a",
-    "src/agents/orchestrator.ts": "74b53f65eed498b9664f11eb4f91761f",
-    "src/background/background-manager.ts": "7f91035b3a8662f2f2eac3e0d082ffba",
+    "src/agents/orchestrator.ts": "70af086905ed0bea4df638cde556419e",
+    "src/background/background-manager.ts": "709ed0b365a58657d5ad39e1be695755",
     "src/background/index.ts": "c20308c54093f4578e3f5145307e1a42",
-    "src/background/tmux-session-manager.ts": "5dad9a8ff9ed6ac6a0c9a3587d47540f",
-    "src/cli/config-io.ts": "7f812a28b4d140f51b5f9129085482bb",
+    "src/background/tmux-session-manager.ts": "0154310123bba1960af707258edea859",
+    "src/cli/config-io.ts": "c0de208f76aadd5d8a561ae8eb89bc42",
     "src/cli/config-manager.ts": "7f2960f55aaebab21d822c586c2b12eb",
-    "src/cli/custom-skills.ts": "f03bea6c621673bdadb84f3cf7c1ea46",
-    "src/cli/index.ts": "bf1190c5940db5db5e43acab4653a713",
-    "src/cli/install.ts": "cb30607fdb8c92b03199c00c102bf962",
-    "src/cli/paths.ts": "7b5c9ce35d55d5e2fdf9f48b759aa323",
-    "src/cli/providers.ts": "36a0a27c80163969c61c8ba387789ebf",
-    "src/cli/skills.ts": "75cb8bcf04dae88cd56772f9c7d01782",
-    "src/cli/system.ts": "15663904f06d63352c49066371c66c0d",
-    "src/cli/types.ts": "e8184c9181b3cec5c129d4cc298b437d",
+    "src/cli/custom-skills.ts": "ebc821917095f9950163c811ac3b9af7",
+    "src/cli/index.ts": "a5ede09909809dd0988bc670d62afe22",
+    "src/cli/install.ts": "d69b94f93a6847a85d920c456f8c7d25",
+    "src/cli/model-key-normalization.ts": "7f988cc8109c95382b9ece9730e2a7a5",
+    "src/cli/paths.ts": "6047308b0ce5455823a8882ce1261b2e",
+    "src/cli/providers.ts": "8604bbe572dade63827d4a622f79626a",
+    "src/cli/skills.ts": "dc89ba9bb9fef7682a16088218045b18",
+    "src/cli/system.ts": "6f2db45c5c48fb889934ca1e7298987f",
+    "src/cli/types.ts": "a63fcf385f251ed56bba95ac0576a19b",
     "src/config/agent-mcps.ts": "f65c01e9a29fd9c2ff5658ce30c89f98",
-    "src/config/constants.ts": "d3fd2518f1d17ac182d933eae7f5c80f",
+    "src/config/constants.ts": "56c46fe52adc0160283428e687ba1697",
     "src/config/index.ts": "7ec846841f7fe8fcc6d4bc5d5120412d",
-    "src/config/loader.ts": "9e60d552a223515b4fc4dd34b2ad0cc2",
-    "src/config/schema.ts": "49fdb0b2d31244ff985e2327b3e80c34",
+    "src/config/loader.ts": "44e7721b31f84e60c425a6161d08bc72",
+    "src/config/schema.ts": "d4f5cf4e457e06811e0ef7de26c16f77",
     "src/config/utils.ts": "bc3af4a86874329f638a374ac7c00701",
     "src/hooks/auto-update-checker/cache.ts": "2d49b0e0ea0f1a36b2ae79f43d163b45",
     "src/hooks/auto-update-checker/checker.ts": "0cd527f77d797a663476224d77ac44f4",
     "src/hooks/auto-update-checker/constants.ts": "c46dcf24c3184965314f008ede59b7c7",
     "src/hooks/auto-update-checker/index.ts": "22ff46cdeb63b0ce4fa3a2fa060ca56c",
     "src/hooks/auto-update-checker/types.ts": "2c4bec82d99722a7e6789029fc6688c5",
-    "src/hooks/index.ts": "85f2c28ccaf0968698a0b8343e3977c3",
-    "src/hooks/phase-reminder/index.ts": "d8c9c5d0f7147e5f44e000303daa6cc2",
-    "src/hooks/post-read-nudge/index.ts": "7463a25eac61ebd8833a44bee60f2d81",
-    "src/index.ts": "dcc8a0bcad2c0fd9b57b8031c70a4539",
+    "src/hooks/chat-headers.ts": "2586390fd72f4e19da4d06a6e770aa8f",
+    "src/hooks/delegate-task-retry/guidance.ts": "a121a7fc081422351f4d5b2044aa6024",
+    "src/hooks/delegate-task-retry/hook.ts": "709bd483063a2090fff5b1048861b5c1",
+    "src/hooks/delegate-task-retry/index.ts": "7b78edb6f10cfee10b2c117ca2287378",
+    "src/hooks/delegate-task-retry/patterns.ts": "5e4919da29af630e4e2ec37df0b58025",
+    "src/hooks/foreground-fallback/index.ts": "154b2a954447c70e2bccc68b58262d62",
+    "src/hooks/index.ts": "ce7d6cdbb898d42edea40cbf73439f6d",
+    "src/hooks/json-error-recovery/hook.ts": "55f1268777de23ed5546c8f2f0a5b424",
+    "src/hooks/json-error-recovery/index.ts": "c54900170ea905776e973e30b5dd95f4",
+    "src/hooks/phase-reminder/index.ts": "149b0c497c579b993edcee3d918ac50e",
+    "src/hooks/post-read-nudge/index.ts": "54bc3808ebd322ed5adc5de0295dedb8",
+    "src/index.ts": "8d759f53782523f8431be6d79aac9039",
     "src/mcp/context7.ts": "4e02e8ef204b6eb7e99a3209078428b5",
     "src/mcp/grep-app.ts": "f76cb0ffb3484b16d55f27729e80e864",
     "src/mcp/index.ts": "d19746215624f8dfdcb21b6a1ad552c0",
     "src/mcp/types.ts": "a67078f79aa8b99c41fb5be5d9fa9319",
     "src/mcp/websearch.ts": "a01f59b67867b173919f7a52b7e43052",
-    "src/tools/ast-grep/cli.ts": "77cc479a5dde21188bc90df195a3134b",
+    "src/tools/ast-grep/cli.ts": "7ba3609dee64dd13a0a620bf69260700",
     "src/tools/ast-grep/constants.ts": "ef016f4d4c5a6861fed9c28e968cad07",
     "src/tools/ast-grep/downloader.ts": "c5b3d9b861acd9d971e5df1f5112b928",
     "src/tools/ast-grep/index.ts": "842b8f43bd1ebc7e2e33a9f7fa0d3fec",
     "src/tools/ast-grep/tools.ts": "3f7c2c65cffd5273b0cd6c849800176d",
     "src/tools/ast-grep/types.ts": "34ad28b5b1e9617b584f082dba9a427c",
     "src/tools/ast-grep/utils.ts": "1dd3b2133c4b8c847a26eea0423bc0b2",
-    "src/tools/background.ts": "f35973287ce4f4bb75f007da8bbd96e2",
-    "src/tools/grep/cli.ts": "e53cf03c5d2672fc6aca651da9dfb322",
-    "src/tools/grep/constants.ts": "52f05a89470828582aee209b0c72b01f",
-    "src/tools/grep/downloader.ts": "876022c453c4072f313ddaf7ea595409",
-    "src/tools/grep/index.ts": "20eef5dabac9f703a9c97ffe89ce2f4e",
-    "src/tools/grep/tools.ts": "55d0a611e0e4e1a7612a4dde40b3be51",
-    "src/tools/grep/types.ts": "4b7e3313306e58f2fae265cf0d1c3ed3",
-    "src/tools/grep/utils.ts": "af0426f46b8e5080621adc99b2e496c2",
-    "src/tools/index.ts": "d9ad8f58b2403662294d5260b3d005bb",
-    "src/tools/lsp/client.ts": "445c0c95dce6f6235e85021761fc4bc8",
-    "src/tools/lsp/config.ts": "ebe9cb109e877e5af5cd8580aa0def50",
-    "src/tools/lsp/constants.ts": "37f03eb9604b67b0cfa1b992709c01d6",
-    "src/tools/lsp/index.ts": "65c41604a48c4e4c193c8250812f5080",
+    "src/tools/background.ts": "213c131b32ea7cd8d4678eee4a76987f",
+    "src/tools/index.ts": "bc2699bbb686af17c7faf8554879a98f",
+    "src/tools/lsp/client.ts": "4bb958c8e4d72386dc7e4d6f57d6e2d2",
+    "src/tools/lsp/config-store.ts": "e10072482fbb91b9214fa3bf333aa033",
+    "src/tools/lsp/config.ts": "889acfcffb3bb40ee5335a5ababc8ac5",
+    "src/tools/lsp/constants.ts": "06f4618b3937e6aaa6ec4076c2e9ed51",
+    "src/tools/lsp/index.ts": "913a5e5cb7f366a05e618905e65b4900",
     "src/tools/lsp/tools.ts": "023d712c7a06ab9e43851826114309a0",
-    "src/tools/lsp/types.ts": "8921e8b2874630760f2344f53376aefc",
-    "src/tools/lsp/utils.ts": "1ccffd60530384af9ec1c68bb37490b8",
+    "src/tools/lsp/types.ts": "7aedfc8809101383d5dd9100bdb60d20",
+    "src/tools/lsp/utils.ts": "eb0243a7dfd75732b0e505cdde38cccf",
     "src/utils/agent-variant.ts": "1ab47427e7e8381ae13f09fd499d8755",
-    "src/utils/index.ts": "4f84dbeb33eb59ededba98288c436e21",
+    "src/utils/env.ts": "b76fbfea11c340337f6bdd8a9c87bb69",
+    "src/utils/index.ts": "be0d778f15ae5b14f1dfb598611fc9ed",
+    "src/utils/internal-initiator.ts": "64f4189f18ade892f92c0b30188dcd30",
     "src/utils/logger.ts": "779c3886f558ddb6a3b268c249ecfc4e",
     "src/utils/polling.ts": "b1d9c52df1fae7391234d0f5476d53b5",
-    "src/utils/tmux.ts": "f6d6e51b16e19bdebbc60c09f24bdca1",
+    "src/utils/tmux.ts": "918006a6c388b8b408dc1d3e5849cf56",
     "src/utils/zip-extractor.ts": "9af9d8584db77a5257d333314b48557c",
     "tsconfig.json": "1d2bb6e93a43366843785a156c8e538a"
   },
   "folder_hashes": {
-    ".": "eb229c8bd2c68ec5d9060c8aba078a9a",
-    "src": "ef897ff6240850f382fbb88da0c183e6",
-    "src/background": "310f509fa3b777173ca3943148129cfb",
-    "src/config": "6b119dc0d74f3764942a7d09c808858f",
-    "src/tools/grep": "6b4f06b396c51ba1317e986930422c8c",
-    "src/utils": "e9668ece9a7fac2ae976455c058e60fa",
-    "src/tools/ast-grep": "2cb1b48926d4317af9837859cba0c3f6",
-    "src/tools/lsp": "3bec4280e142d54c8b0cddafc257e9d8",
-    "src/hooks": "dab7d541aa73558a8984eb9d9b3c1685",
-    "src/cli": "b993b56eaae369e25beb5b5df7a8b785",
-    "src/agents": "fcfa72336080b78681f283023ffe939c",
+    "src/hooks": "fa80831df761066009b2d70ef6ea1bea",
+    "src/background": "990c2be92ac5340871183c2743d2a768",
+    "src/hooks/post-read-nudge": "09c49463c10a4d193adcb78028f46cde",
+    "src/utils": "7e0b2eb258e4f61927ef2c9ea5cf6660",
+    "src": "fc99fc198ac16f70d78fa77f2cbbbf05",
+    "src/hooks/delegate-task-retry": "2624117607d8404122e82836c4c74114",
+    "src/hooks/phase-reminder": "c9bcdbf6e74b079a368fc68bf46de086",
+    "src/cli": "f2568e30f6af8d82c4e0d145dd8a9220",
+    "src/hooks/json-error-recovery": "c8a245f5f48918279aa3d7724a573c75",
     "src/mcp": "f9241cd556adebddc643d71ee55ee2a8",
-    "src/tools": "8e3d29e65aa7435e69eeabe6df16e95c",
+    "src/tools/lsp": "2593534242852eef443eb5015fe01ba1",
+    "src/hooks/foreground-fallback": "7d31d4b918d1e1e1b674dece98e10c4d",
+    "src/tools": "e008189e552e226da488a42075d5ec07",
+    "src/tools/ast-grep": "bc2c805d1593254804e74a5ea20c7cad",
     "src/hooks/auto-update-checker": "34086e02a2cc2f000e2826091fcfb94d",
-    "src/hooks/post-read-nudge": "d781f673f38b4d485db0731bdf6b1cac",
-    "src/hooks/phase-reminder": "0a66b20683c09b9d4af7a81f4036d60b"
+    ".": "0943d5256e32e055bfeb16724fd66fb4",
+    "src/agents": "56493588858687537daaa3105c0405e0",
+    "src/config": "50adc10efa4196609e8cc2105ba8eba2"
   }
-}
+}

+ 6 - 5
codemap.md

@@ -46,17 +46,18 @@ The plugin integrates with OpenCode to provide:
 | `src/background/` | Background task/session managers and tmux pane orchestration for off-thread agent runs. | [View Map](src/background/codemap.md) |
 | `src/cli/` | Installer CLI flow, config edits, provider setup, and skill installation helpers. | [View Map](src/cli/codemap.md) |
 | `src/config/` | Plugin configuration schemas, defaults, loaders, and MCP/agent override helpers. | [View Map](src/config/codemap.md) |
-| `src/hooks/` | Re-exported hook factories and option types for lifecycle hooks. | [View Map](src/hooks/codemap.md) |
+| `src/hooks/` | Lifecycle hooks for message transforms, error recovery, and rate-limit fallbacks. | [View Map](src/hooks/codemap.md) |
 | `src/hooks/auto-update-checker/` | Startup update check hook with cache invalidation and optional auto-install. | [View Map](src/hooks/auto-update-checker/codemap.md) |
 | `src/hooks/phase-reminder/` | Orchestrator message transform hook that injects phase reminders. | [View Map](src/hooks/phase-reminder/codemap.md) |
 | `src/hooks/post-read-nudge/` | Read tool after-hook that appends delegation nudges. | [View Map](src/hooks/post-read-nudge/codemap.md) |
+| `src/hooks/delegate-task-retry/` | Error detection and retry guidance with pattern matching and assistance. | [View Map](src/hooks/delegate-task-retry/codemap.md) |
+| `src/hooks/foreground-fallback/` | Rate-limit fallback manager for interactive sessions. | [View Map](src/hooks/foreground-fallback/codemap.md) |
+| `src/hooks/json-error-recovery/` | JSON parse error detection and recovery helpers. | [View Map](src/hooks/json-error-recovery/codemap.md) |
 | `src/mcp/` | Built-in MCP registry and config types for remote connectors. | [View Map](src/mcp/codemap.md) |
-| `src/tools/` | Tool registry plus background task tool implementations. | [View Map](src/tools/codemap.md) |
+| `src/tools/` | Tool registry plus LSP, AST-grep, and background task implementations. | [View Map](src/tools/codemap.md) |
 | `src/tools/ast-grep/` | AST-grep CLI discovery, execution, and tool definitions. | [View Map](src/tools/ast-grep/codemap.md) |
-| `src/tools/grep/` | Ripgrep/grep runner, downloader, and tool definition. | [View Map](src/tools/grep/codemap.md) |
 | `src/tools/lsp/` | LSP client stack and tool surface for definitions, diagnostics, and rename. | [View Map](src/tools/lsp/codemap.md) |
-
-| `src/utils/` | Shared helpers for variants, tmux, polling, logging, and zip extraction. | [View Map](src/utils/codemap.md) |
+| `src/utils/` | Shared helpers for tmux, environment variables, internal initiation, and config. | [View Map](src/utils/codemap.md) |
 
 ## Architecture Overview
 

+ 23 - 18
src/agents/codemap.md

@@ -2,18 +2,20 @@
 
 ## Responsibility
 
-The `src/agents/` directory defines and configures the multi-agent orchestration system for OpenCode. It creates specialized AI agents with distinct roles, capabilities, and behaviors that work together under an orchestrator to optimize coding tasks for quality, speed, cost, and reliability.
+The `src/agents/` directory implements a multi-agent orchestration system for OpenCode. It defines specialized AI agents with distinct roles, capabilities, and behaviors that collaborate under an orchestrator to optimize coding tasks across quality, speed, cost, and reliability dimensions.
 
 ## Design
 
 ### Core Architecture
 
-**Agent Definition Interface**
+**Agent Definition Interface** (defined in `orchestrator.ts`)
 ```typescript
 interface AgentDefinition {
   name: string;
   description?: string;
   config: AgentConfig;
+  /** Priority-ordered model entries for runtime fallback resolution. */
+  _modelArray?: Array<{ id: string; variant?: string }>;
 }
 ```
 
@@ -21,23 +23,25 @@ All agents follow a consistent factory pattern:
 - `createXAgent(model, customPrompt?, customAppendPrompt?)` → `AgentDefinition`
 - Custom prompts can fully replace or append to default prompts
 - Temperature varies by agent role (0.1-0.7) to balance precision vs creativity
+- Model can be string or priority-ordered array for runtime fallback resolution
 
 ### Agent Classification
 
 **Primary Agent**
-- **Orchestrator**: Central coordinator that delegates tasks to specialists
+- **Orchestrator**: Central coordinator that delegates tasks to specialists based on quality/speed/cost/reliability trade-offs
 
 **Subagents** (5 specialized agents)
-1. **Explorer** - Codebase navigation and search (temperature: 0.1)
-2. **Librarian** - Documentation and library research (temperature: 0.1)
-3. **Oracle** - Strategic technical advisor (temperature: 0.1)
-4. **Designer** - UI/UX specialist (temperature: 0.7)
+1. **Explorer** - Codebase navigation and pattern matching (temperature: 0.1)
+2. **Librarian** - External documentation and library research (temperature: 0.1)
+3. **Oracle** - Strategic technical advisor and architecture guidance (temperature: 0.1)
+4. **Designer** - UI/UX design and implementation (temperature: 0.7)
 5. **Fixer** - Fast implementation specialist (temperature: 0.2)
 
 ### Configuration System
 
 **Override Application**
 - Model and temperature can be overridden per agent via user config
+- Model can be string or priority-ordered array for runtime fallback resolution
 - Fallback mechanism: Fixer inherits Librarian's model if not configured
 - Default models defined in `../config/DEFAULT_MODELS`
 
@@ -45,6 +49,7 @@ All agents follow a consistent factory pattern:
 - All agents get `question: 'allow'` by default
 - Skill permissions applied via `getSkillPermissionsForAgent()`
 - Nested permission structure: `{ question, skill: { ... } }`
+- Supports per-agent skill lists via `configuredSkills` parameter
 
 **Custom Prompts**
 - Loaded via `loadAgentPrompt(name)` from config
@@ -55,11 +60,11 @@ All agents follow a consistent factory pattern:
 
 | Agent | Primary Focus | Tools | Constraints | Temperature |
 |-------|--------------|-------|-------------|-------------|
-| Explorer | Codebase search | grep, glob, ast_grep_search | Read-only, parallel | 0.1 |
-| Librarian | External docs | context7, grep_app, websearch | Evidence-based | 0.1 |
-| Oracle | Architecture | Analysis tools | Read-only, advisory | 0.1 |
-| Designer | UI/UX | Tailwind, CSS | Visual excellence | 0.7 |
-| Fixer | Implementation | Edit/write tools | No research/delegation | 0.2 |
+| Explorer | Codebase navigation | grep, glob, ast_grep_search | Read-only, parallel | 0.1 |
+| Librarian | External docs | context7, grep_app, websearch | Evidence-based, citations required | 0.1 |
+| Oracle | Architecture guidance | Analysis tools, code review | Read-only, advisory | 0.1 |
+| Designer | UI/UX implementation | Tailwind, CSS, animations | Visual excellence priority | 0.7 |
+| Fixer | Implementation | Edit/write, lsp_diagnostics | No research/delegation, structured output | 0.2 |
 
 ## Flow
 
@@ -72,11 +77,11 @@ createAgents(config?)
   │   ├─→ Get model (with fallback for fixer)
   │   ├─→ Load custom prompts
   │   ├─→ Call factory function
-  │   ├─→ Apply overrides (model, temperature)
-  │   └─→ Apply default permissions
+  │   ├─→ Apply overrides (model, temperature, variant)
+  │   └─→ Apply default permissions (question: 'allow', skill permissions)
   ├─→ Create orchestrator:
-  │   ├─→ Get model
+  │   ├─→ Get model (or leave unset for runtime resolution)
   │   ├─→ Load custom prompts
   │   ├─→ Call factory function
   │   ├─→ Apply overrides
@@ -164,7 +169,6 @@ Orchestrator (implements or delegates to Fixer)
 Orchestrator
     ↓ delegates to
 Designer (UI/UX implementation)
-    ↓ (Designer may use Fixer for parallel tasks)
 ```
 
 ## Integration
@@ -242,7 +246,8 @@ Agents are configured with specific MCP tool lists:
 ```
 src/agents/
 ├── index.ts          # Main entry point, agent factory registry, config application
-├── orchestrator.ts   # Orchestrator agent definition and delegation workflow
+├── index.test.ts     # Unit tests for agent creation and configuration
+├── orchestrator.ts   # Orchestrator agent definition, delegation workflow, AgentDefinition interface
 ├── explorer.ts       # Codebase navigation specialist
 ├── librarian.ts      # Documentation and library research specialist
 ├── oracle.ts         # Strategic technical advisor
@@ -264,4 +269,4 @@ src/agents/
 - Override model/temperature via plugin config
 - Replace or append to prompts via `loadAgentPrompt()`
 - Configure MCP tools via agent-mcps config
-- Adjust skill permissions via skills config
+- Adjust skill permissions via skills config

+ 2 - 12
src/agents/explorer.ts

@@ -4,19 +4,9 @@ const EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialis
 
 **Role**: Quick contextual grep for codebases. Answer "Where is X?", "Find Y", "Which file has Z".
 
-**Tools Available**:
-- **grep**: Fast regex content search (powered by ripgrep). Use for text patterns, function names, strings.
-  Example: grep(pattern="function handleClick", include="*.ts")
-- **glob**: File pattern matching. Use to find files by name/extension.
-- **ast_grep_search**: AST-aware structural search (25 languages). Use for code patterns.
-  - Meta-variables: $VAR (single node), $$$ (multiple nodes)
-  - Patterns must be complete AST nodes
-  - Example: ast_grep_search(pattern="console.log($MSG)", lang="typescript")
-  - Example: ast_grep_search(pattern="async function $NAME($$$) { $$$ }", lang="javascript")
-
-**When to use which**:
+**When to use which tools**:
 - **Text/regex patterns** (strings, comments, variable names): grep
-- **Structural patterns** (function shapes, class structures): ast_grep_search  
+- **Structural patterns** (function shapes, class structures): ast_grep_search
 - **File discovery** (find by name/extension): glob
 
 **Behavior**:

+ 69 - 10
src/background/codemap.md

@@ -2,7 +2,7 @@
 
 ## Responsibility
 
-The `src/background/` module manages long-running AI agent tasks that execute asynchronously in isolated sessions. It enables fire-and-forget task execution, allowing users to continue working while background tasks complete independently. The module handles task lifecycle management, session creation, completion detection, and optional tmux pane integration for visual task tracking.
+The `src/background/` module manages long-running AI agent tasks that execute asynchronously in isolated sessions. It enables fire-and-forget task execution, allowing users to continue working while background tasks complete independently. The module handles task lifecycle management, session creation, completion detection, optional tmux pane integration for visual task tracking, and subagent delegation permission enforcement.
 
 ## Design
 
@@ -49,9 +49,10 @@ Two-phase task launch:
 - Extracts final output from session messages
 - Falls back to polling for reliability
 
-#### 4. Dual-Index Task Tracking
+#### 4. Triple-Index Task Tracking
 - `tasks` Map: Task ID → BackgroundTask
 - `tasksBySessionId` Map: Session ID → Task ID
+- `agentBySessionId` Map: Session ID → Agent name (for delegation permission checks)
 - Enables bidirectional lookups for event handling
 
 #### 5. Promise-Based Waiting
@@ -64,6 +65,17 @@ Two-phase task launch:
 - Checks cancellation status in `startTask()` after incrementing `activeStarts`
 - Uses type assertion to bypass TypeScript strictness during race handling
 
+#### 7. Subagent Delegation Permission System
+- `SUBAGENT_DELEGATION_RULES` defines allowed subagents per agent type
+- `isAgentAllowed()` checks if parent session can delegate to specific agent
+- `calculateToolPermissions()` hides delegation tools from leaf agents
+- Unknown agent types default to `explorer`-only access
+
+#### 8. Fallback Chain with Timeout
+- `resolveFallbackChain()` builds model failover sequence
+- `promptWithTimeout()` enforces per-model timeout
+- Aborts session between fallback attempts to prevent blocking
+
 ### Classes
 
 #### BackgroundTaskManager
@@ -72,6 +84,7 @@ Main orchestrator for background task lifecycle:
 **State:**
 - `tasks`: Map of all tracked tasks
 - `tasksBySessionId`: Session ID to task ID mapping
+- `agentBySessionId`: Session ID to agent name mapping (delegation checks)
 - `client`: OpenCode client API
 - `directory`: Working directory for tasks
 - `tmuxEnabled`: Whether tmux integration is active
@@ -84,7 +97,10 @@ Main orchestrator for background task lifecycle:
 
 **Key Methods:**
 - `launch(opts)`: Create and queue a new background task (sync)
-- `handleSessionStatus(event)`: Process session.status events
+- `isAgentAllowed(parentSessionId, requestedAgent)`: Check delegation permission
+- `getAllowedSubagents(parentSessionId)`: Get allowed subagents for session
+- `handleSessionStatus(event)`: Process session.status events (completion)
+- `handleSessionDeleted(event)`: Process session.deleted events (cleanup)
 - `getResult(taskId)`: Retrieve current task state
 - `waitForCompletion(taskId, timeout)`: Wait for task completion
 - `cancel(taskId?)`: Cancel one or all tasks
@@ -104,6 +120,7 @@ Manages tmux pane lifecycle for background sessions:
 **Key Methods:**
 - `onSessionCreated(event)`: Spawn tmux pane for child sessions
 - `onSessionStatus(event)`: Close pane when session becomes idle
+- `onSessionDeleted(event)`: Close pane when session is deleted
 - `pollSessions()`: Fallback polling for status updates
 - `closeSession(sessionId)`: Close pane and remove tracking
 - `cleanup()`: Close all panes and stop polling
@@ -120,7 +137,7 @@ Manages tmux pane lifecycle for background sessions:
 - `missingSince`: When session went missing (optional)
 
 #### SessionEvent
-- `type`: Event type (`session.created`, `session.status`)
+- `type`: Event type (`session.created`, `session.status`, `session.deleted`)
 - `properties`: Event properties containing session info
 
 ## Flow
@@ -145,9 +162,12 @@ startTask() executes (async)
   ├─ Check for cancellation (race condition)
   ├─ Create OpenCode session
   ├─ Store sessionId in tasksBySessionId
+  ├─ Store agent in agentBySessionId (delegation tracking)
   ├─ Set status='running'
   ├─ Wait 500ms (if tmux enabled)
-  ├─ Send prompt to session
+  ├─ Calculate tool permissions based on agent's delegation rules
+  ├─ Resolve fallback chain (if enabled)
+  ├─ Send prompt with timeout (with fallback attempts)
   └─ Decrement activeStarts and processQueue()
 ```
 
@@ -175,11 +195,32 @@ extractAndCompleteTask()
       ├─ Set status='completed'
       ├─ Set result or error
       ├─ Delete from tasksBySessionId
+      ├─ Delete from agentBySessionId
+      ├─ Abort session (triggers pane cleanup)
       ├─ Send notification to parent session
       ├─ Resolve completionResolvers
       └─ Log completion
 ```
 
+### Session Deletion Flow
+
+```
+session.deleted event received
+  ↓
+handleSessionDeleted() checks event type
+  ↓
+Extract sessionId from event properties
+  ↓
+Lookup taskId from tasksBySessionId
+  ↓
+Verify task is active (running/pending)
+  ↓
+  ├─ Mark task as cancelled
+  ├─ Set error='Session deleted'
+  ├─ Clean up session tracking maps
+  └─ Resolve completionResolvers
+```
+
 ### Cancellation Flow
 
 ```
@@ -228,6 +269,14 @@ closeSession()
   └─ Stop polling if no sessions left
 ```
 
+```
+session.deleted event received
+  ↓
+onSessionDeleted() checks enabled
+  ↓
+closeSession() (same as above)
+```
+
 ### Polling Fallback Flow (TmuxSessionManager)
 
 ```
@@ -250,8 +299,8 @@ For each tracked session:
 
 #### Internal Dependencies
 - `@opencode-ai/plugin`: PluginInput type, client API
-- `../config`: BackgroundTaskConfig, PluginConfig, TmuxConfig, POLL_INTERVAL_BACKGROUND_MS
-- `../utils`: applyAgentVariant, resolveAgentVariant, log, tmux utilities
+- `../config`: BackgroundTaskConfig, PluginConfig, TmuxConfig, POLL_INTERVAL_BACKGROUND_MS, SUBAGENT_DELEGATION_RULES, FALLBACK_FAILOVER_TIMEOUT_MS
+- `../utils`: applyAgentVariant, resolveAgentVariant, createInternalAgentTextPart, log, tmux utilities
 
 #### External Dependencies
 - None (uses only OpenCode SDK and standard Node.js APIs)
@@ -271,14 +320,20 @@ For each tracked session:
 2. **Event Handling**
    - Both managers register as event handlers for session events
    - BackgroundTaskManager handles `session.status` for completion detection
-   - TmuxSessionManager handles `session.created` and `session.status`
+   - BackgroundTaskManager handles `session.deleted` for cleanup
+   - TmuxSessionManager handles `session.created`, `session.status`, and `session.deleted`
 
 3. **Skill Integration**
    - Background task skill calls `launch()` to create tasks
    - Skill calls `getResult()` and `waitForCompletion()` to retrieve results
    - Skill calls `cancel()` to cancel tasks
 
-4. **Cleanup**
+4. **Delegation Permission Checks**
+   - Called by skill when processing background_task tool invocations
+   - `isAgentAllowed()` validates delegation requests
+   - `getAllowedSubagents()` returns allowed agents for UI display
+
+5. **Cleanup**
    - Both managers provide `cleanup()` methods
    - Called during plugin shutdown to release resources
 
@@ -298,13 +353,17 @@ For each tracked session:
 - Tmux pane spawn failures are logged but don't fail the task
 - Polling errors are logged but don't stop the manager
 - Notification failures are logged but don't affect task completion
+- Fallback chain failures attempt next model, then mark as failed if all fail
 
 ### Logging
 
 All operations are logged with context:
 - Task launch, start, completion, failure, cancellation
+- Delegation permission checks (allowed/subagent queries)
+- Fallback attempt logging (model failures and retries)
 - Session creation and pane spawning
+- Session deletion handling
 - Polling lifecycle
 - Error conditions
 
-Logs use the format `[component-name] message` with structured metadata.
+Logs use the format `[component-name] message` with structured metadata.

+ 168 - 101
src/cli/codemap.md

@@ -4,11 +4,11 @@
 
 The `src/cli/` directory provides the command-line interface for installing and configuring **oh-my-opencode-slim**, an OpenCode plugin. It handles:
 
-- **Installation orchestration**: Interactive and non-interactive installation flows
-- **Configuration management**: Reading, parsing, and writing OpenCode configuration files
-- **Skill management**: Installing recommended skills (via npx) and custom skills (bundled)
-- **Provider configuration**: Setting up model mappings for different AI providers (Kimi, OpenAI, Zen)
-- **System integration**: Detecting OpenCode installation, validating environment
+- **Installation orchestration**: Interactive (TUI) and non-interactive (`--no-tui`) installation flows
+- **Configuration management**: Reading, parsing (JSONC support), and writing OpenCode configuration files with atomic writes
+- **Skill management**: Installing recommended skills (via `npx skills add`) and custom bundled skills (copied from `src/skills/`)
+- **Provider configuration**: Generating model mappings for 4 supported AI providers: OpenAI, Kimi, GitHub Copilot, ZAI Coding Plan
+- **System integration**: Detecting OpenCode/tmux installation, validating environment, retrieving versions
 
 ## Design
 
@@ -19,24 +19,25 @@ The CLI module follows a **layered architecture** with clear separation of conce
 ```
 ┌─────────────────────────────────────────┐
 │         index.ts (Entry Point)          │
-│    - Argument parsing                    │
-│    - Command routing                     │
+│    - Shebang (#!/usr/bin/env bun)       │
+│    - Argument parsing                   │
+│    - Command routing                    │
 └─────────────────┬───────────────────────┘
 ┌─────────────────▼───────────────────────┐
-│         install.ts (Orchestrator)       │
-│    - Interactive TUI                     │
+│         install.ts (Orchestrator)        │
 │    - Installation workflow               │
 │    - Step-by-step execution              │
+│    - Formatted console output            │
 └─────────────────┬───────────────────────┘
-    ┌────────────┼────────────┐
-     
+     ┌────────────┼────────────┐
+                 │            │
 ┌───▼────┐  ┌────▼────┐  ┌────▼──────┐
 │ config │  │ skills  │  │  system   │
 │  -io   │  │         │  │           │
 │ paths  │  │custom   │  │           │
-│providers│ │         │  │           │
+ │providers│ │         │  │           │
 └────────┘  └─────────┘  └────────────┘
 ```
 
@@ -50,7 +51,7 @@ interface OpenCodeConfig {
   plugin?: string[];
   provider?: Record<string, unknown>;
   agent?: Record<string, unknown>;
-  [key: string];
+  [key: string]: unknown;
 }
 ```
 
@@ -59,17 +60,34 @@ Represents the main OpenCode configuration file (`opencode.json`/`opencode.jsonc
 **InstallConfig** (`types.ts`):
 ```typescript
 interface InstallConfig {
-  hasKimi: boolean;
-  hasOpenAI: boolean;
-  hasOpencodeZen: boolean;
   hasTmux: boolean;
   installSkills: boolean;
   installCustomSkills: boolean;
+  dryRun?: boolean;
+  reset: boolean;
 }
 ```
 
 User preferences collected during installation.
 
+**DetectedConfig** (`types.ts`):
+```typescript
+interface DetectedConfig {
+  isInstalled: boolean;
+  hasKimi: boolean;
+  hasOpenAI: boolean;
+  hasAnthropic?: boolean;
+  hasCopilot?: boolean;
+  hasZaiPlan?: boolean;
+  hasAntigravity: boolean;
+  hasChutes?: boolean;
+  hasOpencodeZen: boolean;
+  hasTmux: boolean;
+}
+```
+
+Runtime detection of installed providers and features.
+
 #### 2. **Skill Abstractions**
 
 **RecommendedSkill** (`skills.ts`):
@@ -84,7 +102,7 @@ interface RecommendedSkill {
 }
 ```
 
-Skills installed via `npx skills add` from external repositories.
+Skills installed via `npx skills add` from external GitHub repositories.
 
 **CustomSkill** (`custom-skills.ts`):
 ```typescript
@@ -98,6 +116,17 @@ interface CustomSkill {
 
 Skills bundled in the repository, copied directly to `~/.config/opencode/skills/`.
 
+**PermissionOnlySkill** (`skills.ts`):
+```typescript
+interface PermissionOnlySkill {
+  name: string;
+  allowedAgents: string[];
+  description: string;
+}
+```
+
+Externally-managed skills requiring permission grants but NOT installed by this CLI.
+
 #### 3. **Result Abstraction**
 
 **ConfigMergeResult** (`types.ts`):
@@ -115,56 +144,48 @@ Standardized result type for configuration operations.
 
 1. **Atomic Write Pattern** (`config-io.ts`):
    - Write to temporary file (`.tmp`)
-   - Rename to target path (atomic operation)
+   - Rename to target path (atomic filesystem operation)
    - Backup existing file (`.bak`) before writes
 
 2. **JSONC Support** (`config-io.ts`):
-   - Strip comments (single-line `//` and multi-line `/* */`)
-   - Remove trailing commas
+   - Strip comments (single-line `//` and multi-line `/* */`) via regex
+   - Remove trailing commas before closing braces/brackets
    - Parse as standard JSON
 
-3. **Provider Priority** (`providers.ts`):
-   - Kimi > OpenAI > Zen-free (fallback)
-   - Hybrid mode: Kimi for orchestrator/designer, OpenAI for oracle
+3. **Provider Model Mapping** (`providers.ts`):
+   - Four providers: `openai`, `kimi`, `copilot`, `zai-plan`
+   - Each agent receives role-specific model + variant assignments
+   - Default preset always uses OpenAI
 
 4. **Skill Permission Model** (`skills.ts`):
-   - Orchestrator gets `*` (all skills)
-   - Other agents get role-specific skills
-   - Wildcard support (`*`, `!skill`)
+   - Orchestrator receives `*` (all skills allowed)
+   - Other agents receive role-specific skill permissions
+   - Wildcard support: `*` (allow all), `!skill` (explicit deny)
+
+5. **Path Resolution Priority** (`paths.ts`):
+   - `OPENCODE_CONFIG_DIR` env var (custom directory)
+   - `XDG_CONFIG_HOME` env var
+   - Fallback: `~/.config/opencode`
 
 ## Flow
 
 ### Installation Flow
 
 ```
-User runs: bunx oh-my-opencode-slim install
-         │
-         ▼
+User runs: bunx oh-my-opencode-slim install [--no-tui] [--tmux=yes|no] [--skills=yes|no] [--dry-run] [--reset]
+          
+          
 ┌─────────────────────────────────────────┐
 │ index.ts: parseArgs()                   │
 │ - Parse CLI arguments                   │
-│ - Validate --no-tui mode requirements
+│ - Route to install()                 
 └─────────────────┬───────────────────────┘
 ┌─────────────────────────────────────────┐
 │ install.ts: install()                   │
-│                                         │
-│ ┌─────────────────────────────────────┐ │
-│ │ Interactive Mode (--tui, default)   │ │
-│ │ 1. Check OpenCode installed         │ │
-│ │ 2. Ask user questions (TUI)         │ │
-│ │    - Kimi access?                   │ │
-│ │    - OpenAI access?                 │ │
-│ │    - Install recommended skills?    │ │
-│ │    - Install custom skills?         │ │
-│ └─────────────────────────────────────┘ │
-│                                         │
-│ ┌─────────────────────────────────────┐ │
-│ │ Non-Interactive Mode (--no-tui)     │ │
-│ │ - Validate all required flags       │ │
-│ │ - Convert args to InstallConfig     │ │
-│ └─────────────────────────────────────┘ │
+│ - Convert InstallArgs to InstallConfig  │
+│ - Call runInstall(config)               │
 └─────────────────┬───────────────────────┘
@@ -176,9 +197,9 @@ User runs: bunx oh-my-opencode-slim install
 │                                         │
 │ Step 2: Add plugin to config            │
 │   └─> config-io.ts:                     │
-│      addPluginToOpenCodeConfig()        
-│      - Parse existing config         
-│      - Add 'oh-my-opencode-slim'        
+│      addPluginToOpenCodeConfig()       │
+│      - Parse existing config (JSONC)
+│      - Add 'oh-my-opencode-slim'       │
 │      - Remove old versions              │
 │      - Atomic write                     │
 │                                         │
@@ -192,22 +213,23 @@ User runs: bunx oh-my-opencode-slim install
 │   └─> config-io.ts: writeLiteConfig()   │
 │      └─> providers.ts:                  │
 │         generateLiteConfig()            │
-│         - Determine active preset      
-│         - Build agent configurations    │
+│         - Use OpenAI preset by default
+│         - Build agent configs       
 │         - Map models to agents          │
 │         - Assign skills per agent       │
 │         - Add MCPs per agent            │
+│         - Add tmux config if enabled    │
 │                                         │
-│ Step 5: Install recommended skills      
+│ Step 5: Install recommended skills     │
 │   └─> skills.ts: installSkill()        │
-│      - npx skills add <repo>            
-│      - Run post-install commands        
+│      - npx skills add <repo>           │
+│      - Run post-install commands       │
 │                                         │
 │ Step 6: Install custom skills           │
 │   └─> custom-skills.ts:                 │
 │      installCustomSkill()               │
-│      - Copy from src/skills/            
-│      - To ~/.config/opencode/skills/    
+│      - Copy from src/skills/           │
+│      - To ~/.config/opencode/skills/   │
 │                                         │
 │ Step 7: Print summary & next steps      │
 └─────────────────────────────────────────┘
@@ -222,42 +244,47 @@ detectCurrentConfig() [config-io.ts]
 ┌─────────────────────────────────────────┐
 │ Parse opencode.json/jsonc               │
 │ - Check for plugin entry                │
-│ - Check for kimi provider   
+│ - Check for provider entries
 └─────────────────┬───────────────────────┘
 ┌─────────────────────────────────────────┐
-│ Parse oh-my-opencode-slim.json       
+│ Parse oh-my-opencode-slim.json/jsonc
 │ - Extract preset name                   │
-│ - Check agent models for OpenAI/Zen
+│ - Check agent models for providers
 │ - Check tmux.enabled flag               │
 └─────────────────────────────────────────┘
 ```
 
-### Model Mapping Flow
+### Config Generation Flow
 
 ```
 generateLiteConfig() [providers.ts]
 ┌─────────────────────────────────────────┐
-│ Determine active preset                 │
-│ - hasKimi → 'kimi'                      │
-│ - hasOpenAI → 'openai'                  │
-│ - else → 'zen-free'                     │
+│ Always use 'openai' as default preset   │
 └─────────────────┬───────────────────────┘
 ┌─────────────────────────────────────────┐
-│ For each agent (orchestrator, oracle,   
+│ For each agent (orchestrator, oracle,  │
 │ librarian, explorer, designer, fixer):  │
 │                                         │
-│ 1. Get model from MODEL_MAPPINGS        │
-│ 2. Apply hybrid logic (if needed)       │
-│ 3. Assign skills:                       │
+│ 1. Get model + variant from MAPPING     │
+│ 2. Assign skills:                      │
 │    - Orchestrator: '*'                  │
+│    - Designer: include agent-browser   │
 │    - Others: role-specific skills       │
-│ 4. Add MCPs from DEFAULT_AGENT_MCPS     │
+│ 3. Add MCPs from DEFAULT_AGENT_MCPS    │
+└─────────────────────────────────────────┘
+                  │
+                  ▼
+┌─────────────────────────────────────────┐
+│ If hasTmux: add tmux config object     │
+│   - enabled: true                       │
+│   - layout: 'main-vertical'             │
+│   - main_pane_size: 60                  │
 └─────────────────────────────────────────┘
 ```
 
@@ -272,24 +299,34 @@ generateLiteConfig() [providers.ts]
 | `skills.ts` | `npm` | Install agent-browser globally |
 | `skills.ts` | `agent-browser` CLI | Install browser automation |
 | `system.ts` | `tmux` CLI | Check tmux installation |
+| `providers.ts` | `DEFAULT_AGENT_MCPS` | MCP configurations per agent |
 
 ### Internal Dependencies
 
 ```
 index.ts
-  └─> install.ts
-       ├─> config-io.ts
-       │    ├─> paths.ts
-       │    └─> providers.ts
-       ├─> custom-skills.ts
-       ├─> skills.ts
-       └─> system.ts
-
-config-manager.ts (barrel)
-  ├─> config-io.ts
-  ├─> paths.ts
-  ├─> providers.ts
-  └─> system.ts
+   └─> install.ts
+        ├─> config-io.ts (functions: addPluginToOpenCodeConfig, detectCurrentConfig, 
+        │                 disableDefaultAgents, writeLiteConfig)
+        ├─> providers.ts (function: generateLiteConfig)
+        ├─> custom-skills.ts (CUSTOM_SKILLS, installCustomSkill)
+        ├─> skills.ts (RECOMMENDED_SKILLS, installSkill)
+        ├─> paths.ts (getExistingLiteConfigPath)
+        └─> system.ts (isOpenCodeInstalled, getOpenCodeVersion, getOpenCodePath)
+
+config-io.ts
+   ├─> paths.ts (ensureConfigDir, ensureOpenCodeConfigDir, getExistingConfigPath, getLiteConfig)
+   └─> providers.ts (generateLiteConfig)
+
+custom-skills.ts
+   └─> paths.ts (getConfigDir)
+
+providers.ts
+   ├─> ../config/agent-mcps (DEFAULT_AGENT_MCPS)
+   └─> skills.ts (RECOMMENDED_SKILLS)
+
+skills.ts
+   └─> custom-skills.ts (CUSTOM_SKILLS)
 ```
 
 ### Configuration Files
@@ -298,7 +335,16 @@ config-manager.ts (barrel)
 |------|----------|---------|
 | `opencode.json` | `~/.config/opencode/` | Main OpenCode config |
 | `opencode.jsonc` | `~/.config/opencode/` | Main config with comments |
-| `oh-my-opencode-slim.json` | `~/.config/opencode/` | Plugin-specific config |
+| `oh-my-opencode-slim.json` | `~/.config/opencode/` | Plugin-specific lite config |
+
+### Supported Providers (MODEL_MAPPINGS)
+
+| Provider | Orchestrator | Oracle | Librarian | Explorer | Designer | Fixer |
+|----------|--------------|--------|-----------|----------|----------|-------|
+| `openai` | gpt-5.4 | gpt-5.4 (high) | gpt-5.4-mini (low) | gpt-5.4-mini (low) | gpt-5.4-mini (medium) | gpt-5.4-mini (low) |
+| `kimi` | k2p5 | k2p5 (high) | k2p5 (low) | k2p5 (low) | k2p5 (medium) | k2p5 (low) |
+| `copilot` | claude-opus-4.6 | claude-opus-4.6 (high) | grok-code-fast-1 (low) | grok-code-fast-1 (low) | gemini-3.1-pro-preview (medium) | claude-sonnet-4.6 (low) |
+| `zai-plan` | glm-5 | glm-5 (high) | glm-5 (low) | glm-5 (low) | glm-5 (medium) | glm-5 (low) |
 
 ### Consumers
 
@@ -309,34 +355,55 @@ config-manager.ts (barrel)
 ### Data Flow Summary
 
 ```
-User Input (CLI args or TUI)
+User Input (CLI args: --tmux, --skills, --dry-run, --reset)
 InstallConfig (preferences)
          ├─> OpenCodeConfig (main config)
-         │    - Plugin registration
-         │    - Agent disabling
+         │    - Plugin registration ('oh-my-opencode-slim')
+         │    - Agent disabling (explore, general)
          └─> LiteConfig (plugin config)
-              - Preset selection
-              - Model mappings
-              - Skill assignments
-              - MCP configurations
-              - Tmux settings
+              - Preset: 'openai' (always)
+              - Model mappings per agent
+              - Skill assignments per agent
+              - MCP configurations per agent
+              - Tmux settings (if enabled)
 ```
 
 ## Key Files Reference
 
 | File | Lines | Purpose |
 |------|-------|---------|
-| `index.ts` | 68 | CLI entry point, argument parsing |
-| `install.ts` | 402 | Installation orchestration, TUI |
-| `config-io.ts` | 251 | Config file I/O, JSONC parsing |
-| `providers.ts` | 110 | Model mappings, config generation |
-| `skills.ts` | 132 | Recommended skills management |
-| `custom-skills.ts` | 99 | Bundled skills management |
-| `paths.ts` | 48 | Path resolution utilities |
-| `system.ts` | 53 | System checks (OpenCode, tmux) |
-| `types.ts` | 40 | TypeScript type definitions |
-| `config-manager.ts` | 5 | Barrel exports |
+| `index.ts` | 75 | CLI entry point, argument parsing, shebang |
+| `install.ts` | 269 | Installation orchestration, console UI |
+| `config-manager.ts` | 4 | Re-export barrel (config-io, paths, providers, system) |
+| `config-io.ts` | 287 | Config file I/O, JSONC parsing, atomic writes |
+| `providers.ts` | 101 | MODEL_MAPPINGS, config generation |
+| `skills.ts` | 178 | Recommended/permission-only skills management |
+| `custom-skills.ts` | 98 | Bundled skills management |
+| `paths.ts` | 91 | Path resolution utilities |
+| `system.ts` | 143 | System checks (OpenCode, tmux, version) |
+| `types.ts` | 43 | TypeScript type definitions |
+
+## Skill Registry
+
+### Recommended Skills (installed via npx)
+
+| Name | Repo | Agents | Description |
+|------|------|--------|-------------|
+| `simplify` | brianlovin/claude-config | orchestrator | YAGNI code simplification expert |
+| `agent-browser` | vercel-labs/agent-browser | designer | High-performance browser automation |
+
+### Custom Bundled Skills (copied from src/skills/)
+
+| Name | Source | Agents | Description |
+|------|--------|--------|-------------|
+| `cartography` | src/skills/cartography | orchestrator, explorer | Repository understanding and hierarchical codemap generation |
+
+### Permission-Only Skills (external, not installed)
+
+| Name | Agents | Description |
+|------|--------|-------------|
+| `requesting-code-review` | oracle | Code review template for reviewer subagents |

+ 110 - 138
src/config/codemap.md

@@ -4,35 +4,32 @@
 
 The `src/config/` module is responsible for:
 
-1. **Configuration Management**: Loading, validating, and merging plugin configuration from multiple sources (user config, project config, environment variables)
-2. **Schema Validation**: Providing type-safe configuration using Zod schemas
-3. **Agent Configuration**: Managing agent-specific overrides, models, skills, and MCP (Model Context Protocol) assignments
-4. **Prompt Customization**: Loading custom agent prompts from user directories
-5. **Constants Management**: Centralizing agent names, default models, polling intervals, and timeouts
+1. **Schema Definition**: Type-safe configuration validation using Zod schemas
+2. **Configuration Loading**: Loading, validating, and merging plugin configuration from multiple sources (user config, project config, environment variables)
+3. **Constants Management**: Centralizing agent names, default models, delegation rules, polling intervals, and timeouts
 
 ## Design
 
 ### Key Patterns
 
 **Multi-Source Configuration Merging**
-- User config: `~/.config/opencode/oh-my-opencode-slim.json` (or `$XDG_CONFIG_HOME`)
-- Project config: `<directory>/.opencode/oh-my-opencode-slim.json`
+- User config: `~/.config/opencode/oh-my-opencode-slim.jsonc` (preferred) or `.json`
+- Project config: `<directory>/.opencode/oh-my-opencode-slim.jsonc` (preferred) or `.json`
 - Environment override: `OH_MY_OPENCODE_SLIM_PRESET`
 - Project config takes precedence over user config
-- Nested objects (agents, tmux) are deep-merged; arrays are replaced
+- Nested objects (`agents`, `tmux`, `fallback`) are deep-merged; arrays and primitives are replaced
 
 **Preset System**
-- Named presets contain agent configurations
+- Named presets contain agent configuration templates
 - Presets are merged with root-level agent config (root overrides)
 - Supports preset selection via config file or environment variable
 
-**Wildcard/Exclusion Syntax**
-- Skills and MCPs support `"*"` (all) and `"!item"` (exclude) syntax
-- Used in `parseList()` function for flexible filtering
-
-**Backward Compatibility**
-- Agent aliases map legacy names to current names (e.g., `explore` → `explorer`)
-- `getAgentOverride()` checks both current name and aliases
+**Subagent Delegation Rules**
+- Explicitly defines which agents can spawn which subagents
+- `orchestrator`: can spawn all subagents (full delegation)
+- `fixer`: leaf node — prompt forbids delegation
+- `designer`: leaf node (cannot spawn subagents)
+- `explorer`, `librarian`, `oracle`: leaf nodes (cannot spawn subagents)
 
 ### Core Abstractions
 
@@ -40,32 +37,41 @@ The `src/config/` module is responsible for:
 ```
 PluginConfig
 ├── preset?: string
+├── setDefaultAgent?: boolean
+├── scoringEngineVersion?: 'v1' | 'v2-shadow' | 'v2'
+├── balanceProviderUsage?: boolean
+├── manualPlan?: ManualPlan
 ├── presets?: Record<string, Preset>
 ├── agents?: Record<string, AgentOverrideConfig>
 ├── disabled_mcps?: string[]
 ├── tmux?: TmuxConfig
-└── background?: BackgroundTaskConfig
+├── background?: BackgroundTaskConfig
+└── fallback?: FailoverConfig
 
 AgentOverrideConfig
-├── model?: string
+├── model?: string | ModelEntry[]
 ├── temperature?: number
 ├── variant?: string
-├── skills?: string[]
-└── mcps?: string[]
+├── skills?: string[]  // "*" = all, "!item" = exclude
+└── mcps?: string[]     // "*" = all, "!item" = exclude
 
 TmuxConfig
 ├── enabled: boolean
 ├── layout: TmuxLayout
 └── main_pane_size: number
+
+FailoverConfig
+├── enabled: boolean
+├── timeoutMs: number
+├── retryDelayMs: number
+└── chains: Record<string, string[]>
 ```
 
 **Agent Names**
 - `ORCHESTRATOR_NAME`: `'orchestrator'`
 - `SUBAGENT_NAMES`: `['explorer', 'librarian', 'oracle', 'designer', 'fixer']`
-- `ALL_AGENT_NAMES`: All agents combined
-- `AGENT_ALIASES`: Legacy name mappings
-
-### Interfaces
+- `ALL_AGENT_NAMES`: `['orchestrator', 'explorer', 'librarian', 'oracle', 'designer', 'fixer']`
+- `AGENT_ALIASES`: Legacy name mappings (`{ explore: 'explorer' }`)
 
 **TypeScript Types**
 - `PluginConfig`: Main configuration object
@@ -76,14 +82,10 @@ TmuxConfig
 - `AgentName`: Union type of all agent names
 - `McpName`: Union type of available MCPs (`'websearch'`, `'context7'`, `'grep_app'`)
 - `BackgroundTaskConfig`: Background task concurrency settings
-
-**Exported Functions**
-- `loadPluginConfig(directory: string): PluginConfig` - Load and merge all configs
-- `loadAgentPrompt(agentName: string): { prompt?, appendPrompt? }` - Load custom prompts
-- `getAgentOverride(config, name): AgentOverrideConfig | undefined` - Get agent config with alias support
-- `parseList(items, allAvailable): string[]` - Parse wildcard/exclusion lists
-- `getAvailableMcpNames(config?): string[]` - Get enabled MCPs
-- `getAgentMcpList(agentName, config?): string[]` - Get MCPs for specific agent
+- `FailoverConfig`: Failover behavior configuration
+- `ModelEntry`: Normalized model entry with optional per-model variant (`{ id: string; variant?: string }`)
+- `ManualAgentName`: Union type for manual agent configuration
+- `ManualPlan`: Full manual planning configuration
 
 ## Flow
 
@@ -92,19 +94,26 @@ TmuxConfig
 ```
 loadPluginConfig(directory)
-├─→ Load user config from ~/.config/opencode/oh-my-opencode-slim.json
-│   └─→ Validate with PluginConfigSchema
-│       └─→ Return null if invalid/missing
+├─→ Find user config path
+│   └─→ findConfigPath(~/.config/opencode/oh-my-opencode-slim)
+│       └─→ Prefers .jsonc over .json
+│
+├─→ Load user config with loadConfigFromPath()
+│   └─→ stripJsonComments() → JSON.parse()
+│   └─→ PluginConfigSchema.safeParse()
+│       └─→ Returns null if invalid/missing
+│
+├─→ Find project config path
+│   └─→ findConfigPath(<directory>/.opencode/oh-my-opencode-slim)
-├─→ Load project config from <directory>/.opencode/oh-my-opencode-slim.json
-│   └─→ Validate with PluginConfigSchema
-│       └─→ Return null if invalid/missing
+├─→ Load project config (same validation)
 ├─→ Deep merge configs (project overrides user)
 │   ├─→ Top-level: project replaces user
-│   └─→ Nested (agents, tmux): deep merge
+│   └─→ Nested (agents, tmux, fallback): deepMerge()
-├─→ Apply environment preset override (OH_MY_OPENCODE_SLIM_PRESET)
+├─→ Apply environment preset override
+│   └─→ OH_MY_OPENCODE_SLIM_PRESET takes precedence
 └─→ Resolve and merge preset
     ├─→ Find preset in config.presets[preset]
@@ -112,13 +121,29 @@ loadPluginConfig(directory)
     └─→ Warn if preset not found
 ```
 
+### Deep Merge Algorithm
+
+```
+deepMerge(base?, override?)
+│
+├─→ If base is undefined → return override
+├─→ If override is undefined → return base
+│
+└─→ For each key in override
+    ├─→ If both values are non-null, non-array objects
+    │   └─→ Recursively deepMerge
+    └─→ Otherwise → override replaces base
+```
+
 ### Prompt Loading Flow
 
 ```
 loadAgentPrompt(agentName, preset?)
+├─→ Validate preset name (alphanumeric + underscore/dash)
+│
 ├─→ Build prompt search dirs
-│   ├─→ If preset is safe (`[a-zA-Z0-9_-]+`):
+│   ├─→ If preset is safe:
 │   │   1) ~/.config/opencode/oh-my-opencode-slim/{preset}
 │   │   2) ~/.config/opencode/oh-my-opencode-slim
 │   └─→ Otherwise:
@@ -131,149 +156,96 @@ loadAgentPrompt(agentName, preset?)
     └─→ If found → append prompt
 ```
 
-### MCP Resolution Flow
-
-```
-getAgentMcpList(agentName, config)
-│
-├─→ Get agent override config (with alias support)
-│
-├─→ If agent has explicit mcps config
-│   └─→ Return parseList(agent.mcps, availableMcps)
-│
-└─→ Otherwise return DEFAULT_AGENT_MCPS[agentName]
-```
-
-### Deep Merge Algorithm
-
-```
-deepMerge(base, override)
-│
-├─→ If base is undefined → return override
-├─→ If override is undefined → return base
-│
-└─→ For each key in override
-    ├─→ If both values are non-array objects
-    │   └─→ Recursively deepMerge
-    └─→ Otherwise → override replaces base
-```
-
 ## Integration
 
 ### Dependencies
 
 **External Dependencies**
 - `zod`: Runtime schema validation
-- `node:fs`, `node:os`, `node:path`: File system operations
+- `node:fs`, `node:path`: File system operations
 
 **Internal Dependencies**
-- `src/cli/config-io.ts` - JSONC comment stripping utility
+- `src/cli/config-io.ts` - JSONC comment stripping utility (`stripJsonComments`)
+- `src/cli/paths.ts` - Config directory resolution (`getConfigDir`)
 
 ### Consumers
 
 **Direct Consumers**
-- `src/index.ts` - Main plugin entry point
-- `src/skills/` - Agent skill implementations
-- `src/agent/` - Agent configuration and initialization
-
-**Configuration Usage Patterns**
-
-1. **Plugin Initialization**
-   ```typescript
-   const config = loadPluginConfig(projectDir);
-   ```
-
-2. **Agent Configuration**
-   ```typescript
-   const agentOverride = getAgentOverride(config, agentName);
-   const model = agentOverride?.model ?? DEFAULT_MODELS[agentName];
-   ```
+- `src/index.ts` - Main plugin entry point (imports configuration)
+- `src/agents/index.ts` - Agent configuration and initialization
+- `src/cli/providers.ts` - CLI provider resolution
 
-3. **MCP Assignment**
-   ```typescript
-   const mcps = getAgentMcpList(agentName, config);
-   ```
+## File Organization
 
-4. **Prompt Customization**
-   ```typescript
-   const { prompt, appendPrompt } = loadAgentPrompt(agentName, config?.preset);
-   ```
+```
+src/config/
+├── loader.ts        # Config loading, merging, and prompt loading
+├── schema.ts        # Zod schemas and TypeScript types
+└── constants.ts    # Agent names, defaults, timeouts, delegation rules
+```
 
-### Constants Usage
+## Constants Reference
 
-**Polling Configuration**
+### Polling Configuration
 - `POLL_INTERVAL_MS` (500ms): Standard polling interval
 - `POLL_INTERVAL_SLOW_MS` (1000ms): Slower polling for background tasks
 - `POLL_INTERVAL_BACKGROUND_MS` (2000ms): Background task polling
 
-**Timeouts**
+### Timeouts
 - `DEFAULT_TIMEOUT_MS` (2 minutes): Default operation timeout
 - `MAX_POLL_TIME_MS` (5 minutes): Maximum polling duration
+- `FALLBACK_FAILOVER_TIMEOUT_MS` (15 seconds): Failover timeout
 
-**Stability**
+### Stability
 - `STABLE_POLLS_THRESHOLD` (3): Number of stable polls before considering state settled
 
-### Default MCP Assignments
-
-| Agent      | Default MCPs                          |
-|------------|---------------------------------------|
-| orchestrator | `['websearch']`                       |
-| designer    | `[]`                                  |
-| oracle      | `[]`                                  |
-| librarian   | `['websearch', 'context7', 'grep_app']` |
-| explorer    | `[]`                                  |
-| fixer       | `[]`                                  |
-
 ### Default Models
-
-| Agent      | Model                          |
-|------------|--------------------------------|
-| orchestrator | runtime-resolved              |
-| oracle      | `openai/gpt-5.4`        |
-| librarian   | `openai/gpt-5.4-mini`   |
-| explorer    | `openai/gpt-5.4-mini`   |
-| designer    | `openai/gpt-5.4-mini`   |
-| fixer       | `openai/gpt-5.4-mini`   |
-
-## File Organization
-
-```
-src/config/
-├── index.ts          # Public API exports
-├── loader.ts         # Config loading and merging logic
-├── schema.ts         # Zod schemas and TypeScript types
-├── constants.ts      # Agent names, defaults, timeouts
-├── utils.ts          # Helper functions (agent overrides)
-└── agent-mcps.ts     # MCP configuration and resolution
-```
+| Agent      | Default Model           |
+|------------|-------------------------|
+| orchestrator | runtime-resolved     |
+| oracle      | openai/gpt-5.4        |
+| librarian   | openai/gpt-5.4-mini   |
+| explorer    | openai/gpt-5.4-mini   |
+| designer    | openai/gpt-5.4-mini   |
+| fixer       | openai/gpt-5.4-mini   |
+
+### Delegation Rules
+| Parent Agent | Can Spawn                     |
+|--------------|-------------------------------|
+| orchestrator | explorer, librarian, oracle, designer, fixer |
+| fixer        | (none - leaf node)            |
+| designer     | (none - leaf node)            |
+| explorer     | (none - leaf node)            |
+| librarian    | (none - leaf node)            |
+| oracle       | (none - leaf node)            |
 
 ## Error Handling
 
 **Configuration Loading**
 - Missing config files: Returns empty config (expected behavior)
-- Invalid JSON: Logs warning, returns null
-- Schema validation failure: Logs detailed error, returns null
+- Invalid JSON/JSONC: Logs warning, returns null
+- Schema validation failure: Logs detailed Zod error format, returns null
 - File read errors (non-ENOENT): Logs warning, returns null
 
 **Prompt Loading**
 - Missing prompt files: Returns empty object (expected behavior)
-- File read errors: Logs warning, continues
+- File read errors: Logs warning, continues to next search path
 
 **Preset Resolution**
-- Invalid preset name: Logs warning with available presets, continues without preset
+- Invalid preset name (contains unsafe characters): Ignored, uses root config
+- Missing preset: Logs warning with available presets, continues without preset
 
 ## Extension Points
 
 **Adding New Agents**
 1. Add to `SUBAGENT_NAMES` in `constants.ts`
 2. Add default model to `DEFAULT_MODELS`
-3. Add default MCPs to `DEFAULT_AGENT_MCPS` in `agent-mcps.ts`
+3. Add to `SUBAGENT_DELEGATION_RULES`
+4. Add to schema in `schema.ts` if needed (ManualPlanSchema, FallbackChainsSchema)
 
 **Adding New MCPs**
 1. Add to `McpNameSchema` enum in `schema.ts`
-2. Update `DEFAULT_AGENT_MCPS` as needed
 
 **Adding New Configuration Options**
 1. Add to `PluginConfigSchema` in `schema.ts`
-2. Update deep merge logic in `loader.ts` if nested
-3. Document in user-facing config documentation
+2. Update deep merge logic in `loader.ts` if nested object

+ 4 - 1
src/config/model-resolution.test.ts

@@ -245,7 +245,10 @@ describe('fallback.chains merging for foreground agents', () => {
   test('duplicate model ids across array and chain are deduplicated', () => {
     // openai/gpt-4o appears in both _modelArray and chains — should not duplicate
     const result = resolveWithChains({
-      modelArray: [{ id: 'anthropic/claude-opus-4-5' }, { id: 'openai/gpt-4o' }],
+      modelArray: [
+        { id: 'anthropic/claude-opus-4-5' },
+        { id: 'openai/gpt-4o' },
+      ],
       chainModels: ['openai/gpt-4o', 'google/gemini-pro'],
       providerConfig: { openai: {} },
     });

+ 89 - 7
src/hooks/codemap.md

@@ -1,21 +1,103 @@
 # src/hooks/
 
-This directory exposes the public hook entry points that feature code imports to tap into behavior such as update checks, phase reminders, and post-read nudges.
+This directory exposes the public hook entry points that feature code imports to tap into behavior such as workflow reminders, error recovery, rate-limit fallback, and delegation guidance.
 
 ## Responsibility
 
-It acts as a single entry point that re-exports the factory functions and option types for every hook implementation underneath `src/hooks/`, so other modules can `import { createAutoUpdateCheckerHook, AutoUpdateCheckerOptions } from 'src/hooks'` without needing to know the subpaths.
+Acts as a single entry point that re-exports the factory functions and types for every hook implementation underneath `src/hooks/`, so other modules can import from a flat namespace without needing to know subpaths.
 
 ## Design
 
-- Aggregator/re-export pattern: `index.ts` consolidates factories (`createAutoUpdateCheckerHook`, `createPhaseReminderHook`, `createPostReadNudgeHook`) and the shared `AutoUpdateCheckerOptions` type so the rest of the app depends only on this flat namespace.
-- Each hook implementation underneath follows a factory-based design; callers receive a configured hook instance by passing structured options through the exported creator functions.
+- **Aggregator/re-export pattern**: `index.ts` consolidates all hook factories and types for the entire hooks subsystem.
+- **Factory-based design**: Each hook is a factory function that returns a hook object with specific hook points (e.g., `'tool.execute.after'`, `'experimental.chat.messages.transform'`, `'chat.headers'`).
+- **Modular architecture**: Each hook lives in its own subdirectory with internal components (hook implementation, patterns, guidance, etc.).
+- **Event-driven hooks**: Hooks respond to OpenCode plugin events and modify output before it reaches the LLM or UI.
 
 ## Flow
 
-Callers import a factory from `src/hooks`, supply any typed options (e.g., `AutoUpdateCheckerOptions`), and the factory wires together the hook’s internal checks/side-effects before returning the hook interface that the feature layer consumes.
+1. **Import**: Feature modules import factories from `src/hooks/index.ts` (e.g., `createPhaseReminderHook`, `createJsonErrorRecoveryHook`).
+2. **Configure**: Call factory with any required context (e.g., `PluginInput` for client access).
+3. **Register**: Hook objects are registered with OpenCode's plugin system via the feature layer.
+4. **Execute**: At runtime, OpenCode invokes hook functions at specific points (tool execution, message transformation, event handling).
+5. **Modify**: Hooks inspect input/output and apply side-effects (inject text, modify headers, append guidance).
 
 ## Integration
 
-- Feature modules across the app import everything through `src/hooks/index.ts`; there are no direct relations to deeper hook files, keeping consumers ignorant of the implementation details.
-- Option types such as `AutoUpdateCheckerOptions` are shared from this file so both the hook creator and its consumers agree on the configuration contract.
+### Hook Points
+
+| Hook Point | Purpose | Hooks |
+|------------|---------|-------|
+| `'tool.execute.after'` | Modify tool output after execution | `post-read-nudge`, `delegate-task-retry`, `json-error-recovery` |
+| `'experimental.chat.messages.transform'` | Transform messages before API call | `phase-reminder` |
+| `'chat.headers'` | Add custom headers to API requests | `chat-headers` |
+| Event handlers | React to OpenCode events | `foreground-fallback` |
+
+### Hook Implementations
+
+#### **phase-reminder**
+- **Location**: `src/hooks/phase-reminder/index.ts`
+- **Purpose**: Injects workflow reminder before each user message for the orchestrator agent to combat instruction-following degradation.
+- **Hook Point**: `'experimental.chat.messages.transform'`
+- **Behavior**: Prepend reminder text to the last user message if agent is 'orchestrator' and message doesn't contain internal initiator marker.
+- **Research**: Based on "LLMs Get Lost In Multi-Turn Conversation" (arXiv:2505.06120) showing ~40% compliance drop after 2-3 turns without reminders.
+
+#### **post-read-nudge**
+- **Location**: `src/hooks/post-read-nudge/index.ts`
+- **Purpose**: Appends delegation reminder after file reads to catch the "read files → implement myself" anti-pattern.
+- **Hook Point**: `'tool.execute.after'`
+- **Behavior**: Appends nudge text to output when tool is 'Read' or 'read'.
+
+#### **chat-headers**
+- **Location**: `src/hooks/chat-headers.ts`
+- **Purpose**: Adds `x-initiator: agent` header for GitHub Copilot provider when internal initiator marker is detected.
+- **Hook Point**: `'chat.headers'`
+- **Behavior**: Checks for internal marker via API call, only applies to Copilot provider and non-Copilot npm model.
+- **Caching**: Uses in-memory cache (max 1000 entries) to reduce API calls.
+
+#### **delegate-task-retry**
+- **Location**: `src/hooks/delegate-task-retry/`
+- **Purpose**: Detects delegate task errors and provides actionable retry guidance.
+- **Components**:
+  - `hook.ts`: Main hook that detects errors and appends guidance.
+  - `patterns.ts`: Defines error patterns and detection logic.
+  - `guidance.ts`: Builds retry guidance messages with available options.
+- **Hook Point**: `'tool.execute.after'`
+- **Behavior**: Detects errors like missing `run_in_background`, invalid category/agent, unknown skills, and appends structured guidance.
+- **Patterns**: 8 error types with specific fix hints and available options extraction.
+
+#### **foreground-fallback**
+- **Location**: `src/hooks/foreground-fallback/index.ts`
+- **Purpose**: Runtime model fallback for foreground (interactive) agent sessions experiencing rate limits.
+- **Hook Point**: Event-driven (not a standard hook point)
+- **Behavior**:
+  - Monitors `message.updated`, `session.error`, `session.status`, and `subagent.session.created` events.
+  - Detects rate-limit signals via regex patterns.
+  - Aborts rate-limited prompt via `client.session.abort()`.
+  - Re-queues last user message via `client.session.promptAsync()` with fallback model.
+  - Tracks tried models per session to avoid infinite loops.
+  - Deduplicates triggers within 5-second window.
+- **Fallback Chains**: Configurable per agent (e.g., `{ orchestrator: ['anthropic/claude-opus-4-5', 'openai/gpt-4o'] }`).
+- **Cleanup**: Removes session state on `session.deleted` events.
+
+#### **json-error-recovery**
+- **Location**: `src/hooks/json-error-recovery/`
+- **Purpose**: Detects JSON parse errors and provides immediate recovery guidance.
+- **Components**:
+  - `hook.ts`: Main hook that detects JSON errors and appends guidance.
+  - `index.ts`: Re-exports hook and constants.
+- **Hook Point**: `'tool.execute.after'`
+- **Behavior**: Appends structured reminder when JSON parse errors are detected in tool output (excluding bash, read, glob, webfetch, etc.).
+- **Patterns**: 8 regex patterns covering common JSON syntax errors.
+
+### Dependencies
+
+- **OpenCode SDK**: `@opencode-ai/plugin` (PluginInput type, client access)
+- **OpenCode SDK**: `@opencode-ai/sdk` (Model, UserMessage types)
+- **Internal Utils**: `hasInternalInitiatorMarker`, `SLIM_INTERNAL_INITIATOR_MARKER`
+- **Internal Logger**: `utils/logger`
+
+### Consumers
+
+- Feature modules in `src/` import hook factories from `src/hooks/index.ts`.
+- Plugin initialization in `src/index.ts` registers hooks with OpenCode's plugin system.
+- No direct relations to deeper hook files from consumers (implementation details hidden).

+ 4 - 4
src/hooks/foreground-fallback/index.test.ts

@@ -1,4 +1,4 @@
-import { describe, expect, mock, test, beforeEach } from 'bun:test';
+import { beforeEach, describe, expect, mock, test } from 'bun:test';
 import { ForegroundFallbackManager, isRateLimitError } from './index';
 
 // ---------------------------------------------------------------------------
@@ -61,9 +61,9 @@ describe('isRateLimitError', () => {
   });
 
   test('returns true for "quota exceeded" in responseBody', () => {
-    expect(
-      isRateLimitError({ data: { responseBody: 'quota exceeded' } }),
-    ).toBe(true);
+    expect(isRateLimitError({ data: { responseBody: 'quota exceeded' } })).toBe(
+      true,
+    );
   });
 
   test('returns true for "usage exceeded"', () => {

+ 9 - 2
src/hooks/foreground-fallback/index.ts

@@ -126,7 +126,10 @@ export class ForegroundFallbackManager {
           typeof info.providerID === 'string' &&
           typeof info.modelID === 'string'
         ) {
-          this.sessionModel.set(sessionID, `${info.providerID}/${info.modelID}`);
+          this.sessionModel.set(
+            sessionID,
+            `${info.providerID}/${info.modelID}`,
+          );
         }
         // Rate-limit on an individual message
         if (info.error && isRateLimitError(info.error)) {
@@ -220,13 +223,17 @@ export class ForegroundFallbackManager {
       const agentName = this.sessionAgent.get(sessionID);
       const chain = this.resolveChain(agentName, currentModel);
       if (!chain.length) {
-        log('[foreground-fallback] no chain configured', { sessionID, agentName });
+        log('[foreground-fallback] no chain configured', {
+          sessionID,
+          agentName,
+        });
         return;
       }
 
       if (!this.sessionTried.has(sessionID)) {
         this.sessionTried.set(sessionID, new Set());
       }
+      // biome-ignore lint/style/noNonNullAssertion: We just set this above
       const tried = this.sessionTried.get(sessionID)!;
       if (currentModel) tried.add(currentModel);
 

+ 4 - 1
src/hooks/index.ts

@@ -2,7 +2,10 @@ export type { AutoUpdateCheckerOptions } from './auto-update-checker';
 export { createAutoUpdateCheckerHook } from './auto-update-checker';
 export { createChatHeadersHook } from './chat-headers';
 export { createDelegateTaskRetryHook } from './delegate-task-retry';
-export { ForegroundFallbackManager, isRateLimitError } from './foreground-fallback';
+export {
+  ForegroundFallbackManager,
+  isRateLimitError,
+} from './foreground-fallback';
 export { createJsonErrorRecoveryHook } from './json-error-recovery';
 export { createPhaseReminderHook } from './phase-reminder';
 export { createPostReadNudgeHook } from './post-read-nudge';

+ 0 - 1
src/hooks/json-error-recovery/hook.ts

@@ -4,7 +4,6 @@ export const JSON_ERROR_TOOL_EXCLUDE_LIST = [
   'bash',
   'read',
   'glob',
-  'grep',
   'webfetch',
   'grep_app_searchgithub',
   'websearch_web_search_exa',

+ 1 - 3
src/index.ts

@@ -7,17 +7,16 @@ import {
   createAutoUpdateCheckerHook,
   createChatHeadersHook,
   createDelegateTaskRetryHook,
-  ForegroundFallbackManager,
   createJsonErrorRecoveryHook,
   createPhaseReminderHook,
   createPostReadNudgeHook,
+  ForegroundFallbackManager,
 } from './hooks';
 import { createBuiltinMcps } from './mcp';
 import {
   ast_grep_replace,
   ast_grep_search,
   createBackgroundTools,
-  grep,
   lsp_diagnostics,
   lsp_find_references,
   lsp_goto_definition,
@@ -138,7 +137,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       lsp_find_references,
       lsp_diagnostics,
       lsp_rename,
-      grep,
       ast_grep_search,
       ast_grep_replace,
     },

+ 92 - 146
src/tools/codemap.md

@@ -4,10 +4,9 @@
 
 The `src/tools/` directory provides the core tool implementations for the oh-my-opencode-slim plugin. It exposes three main categories of tools:
 
-1. **Grep** - Fast regex-based content search using ripgrep (with fallback to system grep)
+1. **AST-grep** - AST-aware structural code search and replacement across 25+ languages
 2. **LSP** - Language Server Protocol integration for code intelligence (definition, references, diagnostics, rename)
-3. **AST-grep** - AST-aware structural code search and replacement across 25 languages
-4. **Background Tasks** - Fire-and-forget agent task management with automatic notification
+3. **Background Tasks** - Fire-and-forget agent task management with automatic notification
 
 These tools are consumed by the OpenCode plugin system and exposed to AI agents for code navigation, analysis, and modification tasks.
 
@@ -20,28 +19,23 @@ These tools are consumed by the OpenCode plugin system and exposed to AI agents
 ```
 src/tools/
 ├── index.ts              # Central export point
-├── background.ts         # Background task tools
-├── grep/                 # Regex search (ripgrep-based)
-│   ├── cli.ts           # CLI execution layer
-│   ├── tools.ts         # Tool definition
-│   ├── types.ts         # TypeScript interfaces
-│   ├── utils.ts         # Output formatting
-│   ├── constants.ts     # Safety limits & CLI resolution
-│   └── downloader.ts    # Binary auto-download
-├── lsp/                  # Language Server Protocol
-│   ├── client.ts        # LSP client & connection pooling
-│   ├── tools.ts         # 4 tool definitions
-│   ├── types.ts         # LSP type re-exports
-│   ├── utils.ts         # Formatters & workspace edit application
-│   ├── config.ts        # Server discovery & language mapping
-│   └── constants.ts     # Built-in server configs
-└── ast-grep/            # AST-aware search
-    ├── cli.ts           # CLI execution layer
-    ├── tools.ts         # 2 tool definitions
-    ├── types.ts         # TypeScript interfaces
-    ├── utils.ts         # Output formatting & hints
-    ├── constants.ts     # CLI resolution & environment checks
-    └── downloader.ts    # Binary auto-download
+├── background.ts         # Background task tools (3 tools)
+├── ast-grep/
+│   ├── cli.ts            # CLI execution, path resolution, binary download
+│   ├── index.ts          # Module re-exports
+│   ├── types.ts          # TypeScript interfaces (CliLanguage, CliMatch, SgResult)
+│   ├── utils.ts          # Output formatting (formatSearchResult, formatReplaceResult)
+│   ├── constants.ts      # CLI path resolution, safety limits
+│   └── downloader.ts     # Binary auto-download for missing ast-grep
+└── lsp/
+    ├── client.ts         # LSP client & connection pooling (LSPServerManager singleton)
+    ├── config.ts         # Server discovery & language mapping
+    ├── constants.ts      # Built-in server configs (45+ servers), extensions, install hints
+    ├── index.ts          # Module re-exports
+    ├── types.ts          # LSP type re-exports (Diagnostic, Location, WorkspaceEdit, etc.)
+    ├── utils.ts          # Formatters & workspace edit application
+    ├── config-store.ts   # User LSP config runtime storage
+    └── tools.ts          # 4 tool definitions
 ```
 
 ### Key Patterns
@@ -56,61 +50,68 @@ export const toolName: ToolDefinition = tool({
 });
 ```
 
-#### 2. CLI Abstraction Layer
-Both `grep/` and `ast-grep/` use a similar CLI execution pattern:
-- **cli.ts**: Low-level subprocess spawning with timeout handling
-- **tools.ts**: High-level tool definitions that call CLI functions
-- **constants.ts**: CLI path resolution with fallback chain
+#### 2. CLI Abstraction Layer (ast-grep)
+The ast-grep module uses a CLI execution pattern:
+- **cli.ts**: Low-level subprocess spawning with timeout handling and JSON output parsing
+- **constants.ts**: CLI path resolution with fallback chain (cached binary → @ast-grep/cli → platform-specific → Homebrew → download)
 - **downloader.ts**: Binary auto-download for missing dependencies
+- **utils.ts**: Output formatting and truncation handling
 
 #### 3. Connection Pooling (LSP)
 The LSP module implements a singleton `LSPServerManager` with:
-- **Connection pooling**: Reuse LSP clients per workspace root
-- **Reference counting**: Track active usage
-- **Idle cleanup**: Auto-shutdown after 5 minutes of inactivity
-- **Initialization tracking**: Prevent concurrent initialization
+- **Connection pooling**: Reuse LSP clients per workspace root (key: `root::serverId`)
+- **Reference counting**: Track active usage via `refCount`, increment on acquire, decrement on release
+- **Idle cleanup**: Auto-shutdown after 5 minutes of inactivity (check every 60s)
+- **Initialization tracking**: Prevent concurrent initialization races via `initPromise`
 
 #### 4. Safety Limits
 All tools enforce strict safety limits:
-- **Timeout**: 60s (grep), 300s (ast-grep, LSP)
-- **Output size**: 10MB (grep), 1MB (ast-grep)
-- **Match limits**: 500 matches (grep), 200 diagnostics/references (LSP)
-- **Depth limits**: 20 directories (grep)
+- **Timeout**: 300s (ast-grep, LSP initialization)
+- **Output size**: 1MB (ast-grep)
+- **Match limits**: 500 matches (ast-grep), 200 diagnostics (LSP), 200 references (LSP)
 
 #### 5. Error Handling
-- Graceful degradation (ripgrep → grep fallback)
-- Clear error messages with installation hints
+- Clear error messages with installation hints for missing binaries
 - Timeout handling with process cleanup
-- Truncation detection and reporting
+- Truncation detection and reporting with reason codes
+- Graceful fallback chains for CLI resolution
 
 ---
 
 ## Flow
 
-### Grep Tool Flow
+### AST-grep Tool Flow
 
 ```
-User Request
+User Request (ast_grep_search or ast_grep_replace)
-grep tool (tools.ts)
+Tool definition (ast-grep/tools.ts)
-runRg() (cli.ts)
+runSg() (cli.ts)
+    ├─→ getAstGrepPath()
+    │   ├─→ Check cached path
+    │   ├─→ findSgCliPathSync()
+    │   │   ├─→ Cached binary in ~/.cache
+    │   │   ├─→ @ast-grep/cli package
+    │   │   ├─→ Platform-specific package (@ast-grep/cli-*)
+    │   │   └─→ Homebrew (macOS)
+    │   └─→ ensureAstGrepBinary() → download if missing
+    └─→ Build args: pattern, lang, rewrite, globs, paths
-resolveGrepCli() (constants.ts)
-    ├─→ OpenCode bundled rg
-    ├─→ System PATH rg
-    ├─→ Cached download
-    └─→ System grep (fallback)
+spawn([sg, 'run', '-p', pattern, '--lang', lang, ...])
-buildArgs() → Safety flags + user options
+Parse JSON output → CliMatch[]
-spawn([cli, ...args]) with timeout
+Handle truncation (max_output_bytes, max_matches, timeout)
-parseOutput() → GrepMatch[]
+formatSearchResult() / formatReplaceResult() (utils.ts)
+    ├─→ Group by file
+    ├─→ Truncate long text
+    └─→ Add summary
-formatGrepResult() (utils.ts)
+Add empty result hints (getEmptyResultHint)
-Group by file → Return formatted output
+Return formatted output
 ```
 
 ### LSP Tool Flow
@@ -118,13 +119,14 @@ Group by file → Return formatted output
 ```
 User Request (e.g., lsp_goto_definition)
-Tool definition (tools.ts)
+Tool definition (lsp/tools.ts)
 withLspClient() (utils.ts)
     ├─→ findServerForExtension() (config.ts)
     │   ├─→ Match extension to BUILTIN_SERVERS
+    │   ├─→ Merge with user config from config-store
     │   └─→ isServerInstalled() → PATH check
-    ├─→ findWorkspaceRoot() → .git, package.json, etc.
+    ├─→ findServerProjectRoot() → server-specific root patterns
     └─→ lspManager.getClient() (client.ts)
         ├─→ Check cache (root::serverId)
         ├─→ If cached: increment refCount, return
@@ -150,7 +152,7 @@ Return formatted output
 start()
   ├─→ spawn(command)
   ├─→ Create JSON-RPC connection (vscode-jsonrpc)
-  ├─→ Register handlers (diagnostics, configuration)
+  ├─→ Register handlers (diagnostics, configuration, window)
   └─→ Wait for process to stabilize
 initialize()
@@ -168,40 +170,6 @@ stop()
   └─→ kill process
 ```
 
-### AST-grep Tool Flow
-
-```
-User Request (ast_grep_search or ast_grep_replace)
-    ↓
-Tool definition (tools.ts)
-    ↓
-runSg() (cli.ts)
-    ├─→ getAstGrepPath()
-    │   ├─→ Check cached path
-    │   ├─→ findSgCliPathSync()
-    │   │   ├─→ Cached binary
-    │   │   ├─→ @ast-grep/cli package
-    │   │   ├─→ Platform-specific package
-    │   │   └─→ Homebrew (macOS)
-    │   └─→ ensureAstGrepBinary() → download if missing
-    └─→ Build args: pattern, lang, rewrite, globs, paths
-    ↓
-spawn([sg, 'run', '-p', pattern, '--lang', lang, ...])
-    ↓
-Parse JSON output → CliMatch[]
-    ↓
-Handle truncation (max_output_bytes, max_matches)
-    ↓
-formatSearchResult() / formatReplaceResult() (utils.ts)
-    ├─→ Group by file
-    ├─→ Truncate long text
-    └─→ Add summary
-    ↓
-Add empty result hints (getEmptyResultHint)
-    ↓
-Return formatted output
-```
-
 ### Background Task Flow
 
 ```
@@ -210,6 +178,7 @@ User Request (background_task)
 Tool definition (background.ts)
 manager.launch()
+    ├─→ Validate agent against delegation rules
     ├─→ Create task with unique ID
     ├─→ Store in BackgroundTaskManager
     └─→ Return task_id immediately (~1ms)
@@ -223,12 +192,12 @@ User Request (background_output)
 manager.getResult(task_id)
     ├─→ If timeout > 0: waitForCompletion()
-    └─→ Return status/result/error
+    └─→ Return status/result/error/duration
 User Request (background_cancel)
 manager.cancel(task_id) or manager.cancel(all)
-    └─→ Cancel running tasks only
+    └─→ Cancel pending/starting/running tasks only
 ```
 
 ---
@@ -242,72 +211,58 @@ manager.cancel(task_id) or manager.cancel(all)
 - **vscode-jsonrpc**: LSP JSON-RPC protocol implementation
 - **vscode-languageserver-protocol**: LSP type definitions
 - **bun**: Subprocess spawning (`spawn`), file operations (`Bun.write`)
+- **which**: PATH resolution for CLI binaries
 
 #### Internal Dependencies
 - **src/background**: `BackgroundTaskManager` for background task tools
 - **src/config**: `SUBAGENT_NAMES`, `PluginConfig`, `TmuxConfig`
 - **src/utils**: `extractZip` for binary extraction
+- **src/utils/logger**: Logging utilities
 
 ### Consumers
 
 #### Direct Consumers
 - **src/index.ts**: Main plugin entry point imports all tools
-- **src/cli/index.ts**: CLI entry point may use tools directly
 
 #### Tool Registry
 All tools are exported from `src/tools/index.ts`:
 ```typescript
-export { grep } from './grep';
-export { ast_grep_search, ast_grep_replace } from './ast-grep';
+export { ast_grep_replace, ast_grep_search } from './ast-grep';
+export { createBackgroundTools } from './background';
 export {
   lsp_diagnostics,
   lsp_find_references,
   lsp_goto_definition,
   lsp_rename,
   lspManager,
+  setUserLspConfig,
 } from './lsp';
-export { createBackgroundTools } from './background';
 ```
 
 ### Configuration
 
 #### LSP Server Configuration
-- **BUILTIN_SERVERS** (lsp/constants.ts): Pre-configured servers for 12 languages
-- **EXT_TO_LANG** (lsp/constants.ts): Extension to language ID mapping
+- **BUILTIN_SERVERS** (lsp/constants.ts): Pre-configured servers for 45+ languages
+- **LANGUAGE_EXTENSIONS** (lsp/constants.ts): Extension to LSP language ID mapping
 - **LSP_INSTALL_HINTS** (lsp/constants.ts): Installation instructions per server
+- **NearestRoot** (lsp/constants.ts): Factory for root pattern matching functions
 
-#### Grep Configuration
-- **Safety limits** (grep/constants.ts): Max depth, filesize, count, columns, timeout
-- **RG_SAFETY_FLAGS**: `--no-follow`, `--color=never`, `--no-heading`, `--line-number`, `--with-filename`
-- **GREP_SAFETY_FLAGS**: `-n`, `-H`, `--color=never`
+#### User LSP Configuration
+- **config-store.ts**: Runtime storage for user-provided LSP config from opencode.json
+- Merged at runtime: built-in servers + user config (user config overrides command/extensions/env, root patterns preserved from built-in)
+- Can disable servers with `"disabled": true`
 
 #### AST-grep Configuration
-- **CLI_LANGUAGES** (ast-grep/types.ts): 25 supported languages
-- **LANG_EXTENSIONS** (ast-grep/constants.ts): Language to file extension mapping
+- **CLI_LANGUAGES** (ast-grep/types.ts): Supported languages
 - **Safety limits**: Timeout (300s), max output (1MB), max matches (500)
 
 ### Binary Management
 
-#### Ripgrep (grep/downloader.ts)
-- **Version**: 14.1.1
-- **Platforms**: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64
-- **Install location**: `~/.cache/oh-my-opencode-slim/bin/rg` (Linux/macOS), `%LOCALAPPDATA%\oh-my-opencode-slim\bin\rg.exe` (Windows)
-- **Fallback**: System grep if ripgrep unavailable
-
 #### AST-grep (ast-grep/downloader.ts)
 - **Version**: 0.40.0 (synced with @ast-grep/cli package)
 - **Platforms**: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64, win32-arm64, win32-ia32
 - **Install location**: `~/.cache/oh-my-opencode-slim/bin/sg` (Linux/macOS), `%LOCALAPPDATA%\oh-my-opencode-slim\bin\sg.exe` (Windows)
-- **Fallback**: Manual installation instructions
-
-### Error Handling Integration
-
-All tools follow a consistent error handling pattern:
-1. Try-catch around execution
-2. Return formatted error messages
-3. Include installation hints for missing binaries
-4. Graceful degradation (fallback tools)
-5. Timeout handling with process cleanup
+- **Fallback chain**: @ast-grep/cli → platform-specific package → Homebrew → download from GitHub
 
 ### Performance Considerations
 
@@ -324,31 +279,22 @@ All tools follow a consistent error handling pattern:
 
 ### Root Level
 - **index.ts**: Central export point for all tools
-- **background.ts**: Background task management (3 tools: launch, output, cancel)
+- **background.ts**: Background task management (3 tools: background_task, background_output, background_cancel)
 
-### grep/
-- **index.ts**: Re-exports grep module
-- **cli.ts**: `runRg()`, `runRgCount()` - subprocess execution with timeout
-- **tools.ts**: `grep` tool definition
-- **types.ts**: `GrepMatch`, `GrepResult`, `CountResult`, `GrepOptions`
-- **utils.ts**: `formatGrepResult()` - output formatting
-- **constants.ts**: Safety limits, `resolveGrepCli()`, `resolveGrepCliWithAutoInstall()`
-- **downloader.ts**: `downloadAndInstallRipgrep()`, `getInstalledRipgrepPath()`
+### ast-grep/
+- **index.ts**: Re-exports ast-grep module and types
+- **cli.ts**: `runSg()`, `getAstGrepPath()`, `startBackgroundInit()`, `isCliAvailable()`, `ensureCliAvailable()` - CLI execution layer
+- **types.ts**: `CliLanguage`, `CliMatch`, `SgResult`, `CLI_LANGUAGES` - TypeScript interfaces
+- **utils.ts**: `formatSearchResult()`, `formatReplaceResult()`, `getEmptyResultHint()` - Output formatting
+- **constants.ts**: `findSgCliPathSync()`, `getSgCliPath()`, `setSgCliPath()`, `checkEnvironment()`, `formatEnvironmentCheck()`, safety limits
+- **downloader.ts**: `downloadAstGrep()`, `ensureAstGrepBinary()`, `getCacheDir()`, `getCachedBinaryPath()` - Binary management
 
 ### lsp/
-- **index.ts**: Re-exports LSP module and types
-- **client.ts**: `LSPServerManager` (singleton), `LSPClient` class
+- **index.ts**: Re-exports LSP module, tools, and types
+- **client.ts**: `LSPServerManager` (singleton), `LSPClient` class - full connection lifecycle management
 - **tools.ts**: 4 tools: `lsp_goto_definition`, `lsp_find_references`, `lsp_diagnostics`, `lsp_rename`
-- **types.ts**: LSP type re-exports from vscode-languageserver-protocol
-- **utils.ts**: `withLspClient()`, formatters, `applyWorkspaceEdit()`
-- **config.ts**: `findServerForExtension()`, `getLanguageId()`, `isServerInstalled()`
-- **constants.ts**: `BUILTIN_SERVERS`, `EXT_TO_LANG`, `LSP_INSTALL_HINTS`, safety limits
-
-### ast-grep/
-- **index.ts**: Re-exports ast-grep module
-- **cli.ts**: `runSg()`, `getAstGrepPath()`, `startBackgroundInit()`
-- **tools.ts**: 2 tools: `ast_grep_search`, `ast_grep_replace`
-- **types.ts**: `CliLanguage`, `CliMatch`, `SgResult`, `CLI_LANGUAGES`
-- **utils.ts**: `formatSearchResult()`, `formatReplaceResult()`, `getEmptyResultHint()`
-- **constants.ts**: `findSgCliPathSync()`, `checkEnvironment()`, safety limits
-- **downloader.ts**: `downloadAstGrep()`, `ensureAstGrepBinary()`, cache management
+- **types.ts**: LSP type re-exports from vscode-languageserver-protocol (`Diagnostic`, `Location`, `WorkspaceEdit`, etc.)
+- **utils.ts**: `withLspClient()`, `findServerProjectRoot()`, formatters, `applyWorkspaceEdit()`, `formatApplyResult()`
+- **config.ts**: `findServerForExtension()`, `getLanguageId()`, `isServerInstalled()`, `buildMergedServers()`
+- **config-store.ts**: `setUserLspConfig()`, `getUserLspConfig()`, `getAllUserLspConfigs()`, `hasUserLspConfig()`
+- **constants.ts**: `BUILTIN_SERVERS` (45+ servers), `LANGUAGE_EXTENSIONS`, `LSP_INSTALL_HINTS`, `NearestRoot()`, safety limits

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

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

+ 0 - 253
src/tools/grep/cli.ts

@@ -1,253 +0,0 @@
-import { spawn } from 'bun';
-import {
-  DEFAULT_MAX_COLUMNS,
-  DEFAULT_MAX_COUNT,
-  DEFAULT_MAX_DEPTH,
-  DEFAULT_MAX_FILESIZE,
-  DEFAULT_MAX_OUTPUT_BYTES,
-  DEFAULT_TIMEOUT_MS,
-  GREP_SAFETY_FLAGS,
-  type GrepBackend,
-  RG_SAFETY_FLAGS,
-  resolveGrepCli,
-} from './constants';
-import type { CountResult, GrepMatch, GrepOptions, GrepResult } from './types';
-
-function buildRgArgs(options: GrepOptions): string[] {
-  const args: string[] = [
-    ...RG_SAFETY_FLAGS,
-    `--max-depth=${Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)}`,
-    `--max-filesize=${options.maxFilesize ?? DEFAULT_MAX_FILESIZE}`,
-    `--max-count=${Math.min(options.maxCount ?? DEFAULT_MAX_COUNT, DEFAULT_MAX_COUNT)}`,
-    `--max-columns=${Math.min(options.maxColumns ?? DEFAULT_MAX_COLUMNS, DEFAULT_MAX_COLUMNS)}`,
-  ];
-
-  if (options.context !== undefined && options.context > 0) {
-    args.push(`-C${Math.min(options.context, 10)}`);
-  }
-
-  if (options.caseSensitive) {
-    args.push('--case-sensitive');
-  } else {
-    args.push('-i');
-  }
-  if (options.wholeWord) args.push('-w');
-  if (options.fixedStrings) args.push('-F');
-  if (options.multiline) args.push('-U');
-  if (options.hidden) args.push('--hidden');
-  if (options.noIgnore) args.push('--no-ignore');
-
-  if (options.fileType?.length) {
-    for (const type of options.fileType) {
-      args.push(`--type=${type}`);
-    }
-  }
-
-  if (options.globs) {
-    for (const glob of options.globs) {
-      args.push(`--glob=${glob}`);
-    }
-  }
-
-  if (options.excludeGlobs) {
-    for (const glob of options.excludeGlobs) {
-      args.push(`--glob=!${glob}`);
-    }
-  }
-
-  return args;
-}
-
-function buildGrepArgs(options: GrepOptions): string[] {
-  const args: string[] = [...GREP_SAFETY_FLAGS, '-r'];
-
-  if (options.context !== undefined && options.context > 0) {
-    args.push(`-C${Math.min(options.context, 10)}`);
-  }
-
-  if (!options.caseSensitive) args.push('-i');
-  if (options.wholeWord) args.push('-w');
-  if (options.fixedStrings) args.push('-F');
-
-  if (options.globs?.length) {
-    for (const glob of options.globs) {
-      args.push(`--include=${glob}`);
-    }
-  }
-
-  if (options.excludeGlobs?.length) {
-    for (const glob of options.excludeGlobs) {
-      args.push(`--exclude=${glob}`);
-    }
-  }
-
-  args.push('--exclude-dir=.git', '--exclude-dir=node_modules');
-
-  return args;
-}
-
-function buildArgs(options: GrepOptions, backend: GrepBackend): string[] {
-  return backend === 'rg' ? buildRgArgs(options) : buildGrepArgs(options);
-}
-
-function parseOutput(output: string): GrepMatch[] {
-  if (!output.trim()) return [];
-
-  const matches: GrepMatch[] = [];
-  // Handle both Unix (\n) and Windows (\r\n) line endings
-  const lines = output.trim().split(/\r?\n/);
-
-  for (const line of lines) {
-    if (!line.trim()) continue;
-
-    const match = line.match(/^(.+?):(\d+):(.*)$/);
-    if (match) {
-      matches.push({
-        file: match[1],
-        line: parseInt(match[2], 10),
-        text: match[3],
-      });
-    }
-  }
-
-  return matches;
-}
-
-function parseCountOutput(output: string): CountResult[] {
-  if (!output.trim()) return [];
-
-  const results: CountResult[] = [];
-  // Handle both Unix (\n) and Windows (\r\n) line endings
-  const lines = output.trim().split(/\r?\n/);
-
-  for (const line of lines) {
-    if (!line.trim()) continue;
-
-    const match = line.match(/^(.+?):(\d+)$/);
-    if (match) {
-      results.push({
-        file: match[1],
-        count: parseInt(match[2], 10),
-      });
-    }
-  }
-
-  return results;
-}
-
-export async function runRg(options: GrepOptions): Promise<GrepResult> {
-  const cli = resolveGrepCli();
-  const args = buildArgs(options, cli.backend);
-  const timeout = Math.min(
-    options.timeout ?? DEFAULT_TIMEOUT_MS,
-    DEFAULT_TIMEOUT_MS,
-  );
-
-  if (cli.backend === 'rg') {
-    args.push('--', options.pattern);
-  } else {
-    args.push('-e', options.pattern);
-  }
-
-  const paths = options.paths?.length ? options.paths : ['.'];
-  args.push(...paths);
-  const proc = spawn([cli.path, ...args], {
-    stdout: 'pipe',
-    stderr: 'pipe',
-  });
-
-  const timeoutPromise = new Promise<never>((_, reject) => {
-    const id = setTimeout(() => {
-      proc.kill();
-      reject(new Error(`Search timeout after ${timeout}ms`));
-    }, timeout);
-    proc.exited.then(() => clearTimeout(id));
-  });
-
-  try {
-    const stdout = await Promise.race([
-      new Response(proc.stdout).text(),
-      timeoutPromise,
-    ]);
-    const stderr = await new Response(proc.stderr).text();
-    const exitCode = await proc.exited;
-
-    const truncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES;
-    const outputToProcess = truncated
-      ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES)
-      : stdout;
-
-    if (exitCode > 1 && stderr.trim()) {
-      return {
-        matches: [],
-        totalMatches: 0,
-        filesSearched: 0,
-        truncated: false,
-        error: stderr.trim(),
-      };
-    }
-
-    const matches = parseOutput(outputToProcess);
-    const filesSearched = new Set(matches.map((m) => m.file)).size;
-
-    return {
-      matches,
-      totalMatches: matches.length,
-      filesSearched,
-      truncated,
-    };
-  } catch (e) {
-    return {
-      matches: [],
-      totalMatches: 0,
-      filesSearched: 0,
-      truncated: false,
-      error: e instanceof Error ? e.message : String(e),
-    };
-  }
-}
-
-export async function runRgCount(
-  options: Omit<GrepOptions, 'context'>,
-): Promise<CountResult[]> {
-  const cli = resolveGrepCli();
-  const args = buildArgs({ ...options, context: 0 }, cli.backend);
-
-  if (cli.backend === 'rg') {
-    args.push('--count', '--', options.pattern);
-  } else {
-    args.push('-c', '-e', options.pattern);
-  }
-
-  const paths = options.paths?.length ? options.paths : ['.'];
-  args.push(...paths);
-
-  const timeout = Math.min(
-    options.timeout ?? DEFAULT_TIMEOUT_MS,
-    DEFAULT_TIMEOUT_MS,
-  );
-  const proc = spawn([cli.path, ...args], {
-    stdout: 'pipe',
-    stderr: 'pipe',
-  });
-
-  const timeoutPromise = new Promise<never>((_, reject) => {
-    const id = setTimeout(() => {
-      proc.kill();
-      reject(new Error(`Search timeout after ${timeout}ms`));
-    }, timeout);
-    proc.exited.then(() => clearTimeout(id));
-  });
-
-  try {
-    const stdout = await Promise.race([
-      new Response(proc.stdout).text(),
-      timeoutPromise,
-    ]);
-    return parseCountOutput(stdout);
-  } catch (e) {
-    throw new Error(
-      `Count search failed: ${e instanceof Error ? e.message : String(e)}`,
-    );
-  }
-}

File diff suppressed because it is too large
+ 0 - 19
src/tools/grep/codemap.md


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

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

+ 0 - 143
src/tools/grep/constants.ts

@@ -1,143 +0,0 @@
-import { spawnSync } from 'node:child_process';
-import { existsSync } from 'node:fs';
-import { dirname, join } from 'node:path';
-import {
-  downloadAndInstallRipgrep,
-  getInstalledRipgrepPath,
-} from './downloader';
-
-export type GrepBackend = 'rg' | 'grep';
-
-interface ResolvedCli {
-  path: string;
-  backend: GrepBackend;
-}
-
-let cachedCli: ResolvedCli | null = null;
-let autoInstallAttempted = false;
-
-function findExecutable(name: string): string | null {
-  const isWindows = process.platform === 'win32';
-  const cmd = isWindows ? 'where' : 'which';
-
-  try {
-    const result = spawnSync(cmd, [name], { encoding: 'utf-8', timeout: 5000 });
-    if (result.status === 0 && result.stdout.trim()) {
-      // Handle both Unix (\n) and Windows (\r\n) line endings
-      return result.stdout.trim().split(/\r?\n/)[0];
-    }
-  } catch {
-    // Command execution failed
-  }
-  return null;
-}
-
-export function getDataDir(): string {
-  if (process.platform === 'win32') {
-    return (
-      process.env.LOCALAPPDATA ||
-      process.env.APPDATA ||
-      join(process.env.USERPROFILE || '.', 'AppData', 'Local')
-    );
-  }
-  return (
-    process.env.XDG_DATA_HOME ||
-    join(process.env.HOME || '.', '.local', 'share')
-  );
-}
-function getOpenCodeBundledRg(): string | null {
-  const execPath = process.execPath;
-  const execDir = dirname(execPath);
-
-  const isWindows = process.platform === 'win32';
-  const rgName = isWindows ? 'rg.exe' : 'rg';
-
-  const candidates = [
-    // OpenCode XDG data path (highest priority - where OpenCode installs rg)
-    join(getDataDir(), 'opencode', 'bin', rgName),
-    // Legacy paths relative to execPath
-    join(execDir, rgName),
-    join(execDir, 'bin', rgName),
-    join(execDir, '..', 'bin', rgName),
-    join(execDir, '..', 'libexec', rgName),
-  ];
-
-  for (const candidate of candidates) {
-    if (existsSync(candidate)) {
-      return candidate;
-    }
-  }
-
-  return null;
-}
-
-export function resolveGrepCli(): ResolvedCli {
-  if (cachedCli) return cachedCli;
-
-  const bundledRg = getOpenCodeBundledRg();
-  if (bundledRg) {
-    cachedCli = { path: bundledRg, backend: 'rg' };
-    return cachedCli;
-  }
-
-  const systemRg = findExecutable('rg');
-  if (systemRg) {
-    cachedCli = { path: systemRg, backend: 'rg' };
-    return cachedCli;
-  }
-
-  const installedRg = getInstalledRipgrepPath();
-  if (installedRg) {
-    cachedCli = { path: installedRg, backend: 'rg' };
-    return cachedCli;
-  }
-
-  const grep = findExecutable('grep');
-  if (grep) {
-    cachedCli = { path: grep, backend: 'grep' };
-    return cachedCli;
-  }
-
-  cachedCli = { path: 'rg', backend: 'rg' };
-  return cachedCli;
-}
-
-export async function resolveGrepCliWithAutoInstall(): Promise<ResolvedCli> {
-  const current = resolveGrepCli();
-
-  if (current.backend === 'rg') {
-    return current;
-  }
-
-  if (autoInstallAttempted) {
-    return current;
-  }
-
-  autoInstallAttempted = true;
-
-  try {
-    const rgPath = await downloadAndInstallRipgrep();
-    cachedCli = { path: rgPath, backend: 'rg' };
-    return cachedCli;
-  } catch {
-    return current;
-  }
-}
-
-export const DEFAULT_MAX_DEPTH = 20;
-export const DEFAULT_MAX_FILESIZE = '10M';
-export const DEFAULT_MAX_COUNT = 500;
-export const DEFAULT_MAX_COLUMNS = 1000;
-export const DEFAULT_CONTEXT = 2;
-export const DEFAULT_TIMEOUT_MS = 300_000;
-export const DEFAULT_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
-
-export const RG_SAFETY_FLAGS = [
-  '--no-follow',
-  '--color=never',
-  '--no-heading',
-  '--line-number',
-  '--with-filename',
-] as const;
-
-export const GREP_SAFETY_FLAGS = ['-n', '-H', '--color=never'] as const;

+ 0 - 166
src/tools/grep/downloader.ts

@@ -1,166 +0,0 @@
-import {
-  chmodSync,
-  existsSync,
-  mkdirSync,
-  readdirSync,
-  unlinkSync,
-} from 'node:fs';
-import { join } from 'node:path';
-import { spawn } from 'bun';
-import { extractZip } from '../../utils';
-
-export function findFileRecursive(
-  dir: string,
-  filename: string,
-): string | null {
-  try {
-    const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
-    for (const entry of entries) {
-      if (entry.isFile() && entry.name === filename) {
-        return join(entry.parentPath ?? dir, entry.name);
-      }
-    }
-  } catch {
-    return null;
-  }
-  return null;
-}
-
-const RG_VERSION = '14.1.1';
-
-// Platform key format: ${process.platform}-${process.arch} (consistent with ast-grep)
-const PLATFORM_CONFIG: Record<
-  string,
-  { platform: string; extension: 'tar.gz' | 'zip' } | undefined
-> = {
-  'darwin-arm64': { platform: 'aarch64-apple-darwin', extension: 'tar.gz' },
-  'darwin-x64': { platform: 'x86_64-apple-darwin', extension: 'tar.gz' },
-  'linux-arm64': { platform: 'aarch64-unknown-linux-gnu', extension: 'tar.gz' },
-  'linux-x64': { platform: 'x86_64-unknown-linux-musl', extension: 'tar.gz' },
-  'win32-x64': { platform: 'x86_64-pc-windows-msvc', extension: 'zip' },
-};
-
-function getPlatformKey(): string {
-  return `${process.platform}-${process.arch}`;
-}
-
-function getInstallDir(): string {
-  const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
-  return join(homeDir, '.cache', 'oh-my-opencode-slim', 'bin');
-}
-
-function getRgPath(): string {
-  const isWindows = process.platform === 'win32';
-  return join(getInstallDir(), isWindows ? 'rg.exe' : 'rg');
-}
-
-async function downloadFile(url: string, destPath: string): Promise<void> {
-  const response = await fetch(url);
-  if (!response.ok) {
-    throw new Error(
-      `Failed to download: ${response.status} ${response.statusText}`,
-    );
-  }
-
-  const buffer = await response.arrayBuffer();
-  await Bun.write(destPath, buffer);
-}
-
-async function extractTarGz(
-  archivePath: string,
-  destDir: string,
-): Promise<void> {
-  const args = ['tar', '-xzf', archivePath, '--strip-components=1'];
-
-  if (process.platform === 'darwin') {
-    args.push('--include=*/rg');
-  } else if (process.platform === 'linux') {
-    args.push('--wildcards', '*/rg');
-  }
-
-  const proc = spawn(args, {
-    cwd: destDir,
-    stdout: 'pipe',
-    stderr: 'pipe',
-  });
-
-  const exitCode = await proc.exited;
-  if (exitCode !== 0) {
-    const stderr = await new Response(proc.stderr).text();
-    throw new Error(`Failed to extract tar.gz: ${stderr}`);
-  }
-}
-
-async function extractZipArchive(
-  archivePath: string,
-  destDir: string,
-): Promise<void> {
-  await extractZip(archivePath, destDir);
-
-  const binaryName = process.platform === 'win32' ? 'rg.exe' : 'rg';
-  const foundPath = findFileRecursive(destDir, binaryName);
-  if (foundPath) {
-    const destPath = join(destDir, binaryName);
-    if (foundPath !== destPath) {
-      const { renameSync } = await import('node:fs');
-      renameSync(foundPath, destPath);
-    }
-  }
-}
-
-export async function downloadAndInstallRipgrep(): Promise<string> {
-  const platformKey = getPlatformKey();
-  const config = PLATFORM_CONFIG[platformKey];
-
-  if (!config) {
-    throw new Error(`Unsupported platform: ${platformKey}`);
-  }
-
-  const installDir = getInstallDir();
-  const rgPath = getRgPath();
-
-  if (existsSync(rgPath)) {
-    return rgPath;
-  }
-
-  mkdirSync(installDir, { recursive: true });
-
-  const filename = `ripgrep-${RG_VERSION}-${config.platform}.${config.extension}`;
-  const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`;
-  const archivePath = join(installDir, filename);
-
-  try {
-    console.log(`[oh-my-opencode-slim] Downloading ripgrep...`);
-    await downloadFile(url, archivePath);
-
-    if (config.extension === 'tar.gz') {
-      await extractTarGz(archivePath, installDir);
-    } else {
-      await extractZipArchive(archivePath, installDir);
-    }
-
-    if (process.platform !== 'win32') {
-      chmodSync(rgPath, 0o755);
-    }
-
-    if (!existsSync(rgPath)) {
-      throw new Error('ripgrep binary not found after extraction');
-    }
-
-    console.log(`[oh-my-opencode-slim] ripgrep ready.`);
-    return rgPath;
-  } finally {
-    if (existsSync(archivePath)) {
-      try {
-        unlinkSync(archivePath);
-      } catch {
-        // Cleanup failures are non-critical
-      }
-    }
-  }
-}
-
-export function getInstalledRipgrepPath(): string | null {
-  const rgPath = getRgPath();
-  return existsSync(rgPath) ? rgPath : null;
-}

+ 0 - 229
src/tools/grep/grep.test.ts

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

+ 0 - 8
src/tools/grep/index.ts

@@ -1,8 +0,0 @@
-export { runRg, runRgCount } from './cli';
-export { resolveGrepCli, resolveGrepCliWithAutoInstall } from './constants';
-export {
-  downloadAndInstallRipgrep,
-  getInstalledRipgrepPath,
-} from './downloader';
-export { grep } from './tools';
-export type { CountResult, GrepMatch, GrepOptions, GrepResult } from './types';

+ 0 - 64
src/tools/grep/tools.ts

@@ -1,64 +0,0 @@
-import { type ToolDefinition, tool } from '@opencode-ai/plugin/tool';
-import { runRg } from './cli';
-import { formatGrepResult } from './utils';
-
-export const grep: ToolDefinition = tool({
-  description:
-    'Fast content search tool with safety limits (60s timeout, 10MB output). ' +
-    'Searches file contents using regular expressions. ' +
-    'Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.). ' +
-    'Filter files by pattern with the include parameter (e.g. "*.js", "*.{ts,tsx}"). ' +
-    'Returns file paths with matches sorted by modification time.',
-  args: {
-    pattern: tool.schema
-      .string()
-      .describe('The regex pattern to search for in file contents'),
-    include: tool.schema
-      .string()
-      .optional()
-      .describe(
-        'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
-      ),
-    path: tool.schema
-      .string()
-      .optional()
-      .describe(
-        'The directory to search in. Defaults to the current working directory.',
-      ),
-    caseSensitive: tool.schema
-      .boolean()
-      .optional()
-      .default(false)
-      .describe('Perform case-sensitive search (default: false)'),
-    wholeWord: tool.schema
-      .boolean()
-      .optional()
-      .default(false)
-      .describe('Match whole words only (default: false)'),
-    fixedStrings: tool.schema
-      .boolean()
-      .optional()
-      .default(false)
-      .describe('Treat pattern as literal string (default: false)'),
-  },
-  execute: async (args) => {
-    try {
-      const globs = args.include ? [args.include] : undefined;
-      const paths = args.path ? [args.path] : undefined;
-
-      const result = await runRg({
-        pattern: args.pattern,
-        paths,
-        globs,
-        context: 0,
-        caseSensitive: args.caseSensitive ?? false,
-        wholeWord: args.wholeWord ?? false,
-        fixedStrings: args.fixedStrings ?? false,
-      });
-
-      return formatGrepResult(result);
-    } catch (e) {
-      return `Error: ${e instanceof Error ? e.message : String(e)}`;
-    }
-  },
-});

+ 0 - 38
src/tools/grep/types.ts

@@ -1,38 +0,0 @@
-export interface GrepMatch {
-  file: string;
-  line: number;
-  text: string;
-}
-
-export interface GrepResult {
-  matches: GrepMatch[];
-  totalMatches: number;
-  filesSearched: number;
-  truncated: boolean;
-  error?: string;
-}
-
-export interface CountResult {
-  file: string;
-  count: number;
-}
-
-export interface GrepOptions {
-  pattern: string;
-  paths?: string[];
-  globs?: string[];
-  excludeGlobs?: string[];
-  context?: number;
-  caseSensitive?: boolean;
-  wholeWord?: boolean;
-  fixedStrings?: boolean;
-  multiline?: boolean;
-  hidden?: boolean;
-  noIgnore?: boolean;
-  fileType?: string[];
-  maxDepth?: number;
-  maxFilesize?: string;
-  maxCount?: number;
-  maxColumns?: number;
-  timeout?: number;
-}

+ 0 - 37
src/tools/grep/utils.ts

@@ -1,37 +0,0 @@
-import type { GrepResult } from './types';
-
-export function formatGrepResult(result: GrepResult): string {
-  if (result.error) {
-    return `Error: ${result.error}`;
-  }
-
-  if (result.matches.length === 0) {
-    return 'No matches found.';
-  }
-
-  const lines: string[] = [];
-
-  // Group matches by file
-  const byFile = new Map<string, { line: number; text: string }[]>();
-  for (const match of result.matches) {
-    const existing = byFile.get(match.file) || [];
-    existing.push({ line: match.line, text: match.text });
-    byFile.set(match.file, existing);
-  }
-
-  for (const [file, matches] of byFile) {
-    lines.push(`\n${file}:`);
-    for (const match of matches) {
-      lines.push(`  ${match.line}: ${match.text}`);
-    }
-  }
-
-  const summary = `Found ${result.totalMatches} matches in ${result.filesSearched} files`;
-  if (result.truncated) {
-    lines.push(`\n${summary} (output truncated)`);
-  } else {
-    lines.push(`\n${summary}`);
-  }
-
-  return lines.join('\n');
-}

+ 0 - 2
src/tools/index.ts

@@ -1,8 +1,6 @@
 // AST-grep tools
 export { ast_grep_replace, ast_grep_search } from './ast-grep';
 export { createBackgroundTools } from './background';
-// Grep tool (ripgrep-based)
-export { grep } from './grep';
 export {
   lsp_diagnostics,
   lsp_find_references,

File diff suppressed because it is too large
+ 47 - 5
src/utils/codemap.md