Alvin Unreal 2 months ago
parent
commit
065d46c752

+ 244 - 0
.orchestrator/plans/2026-01-21-codebase-cleanup.md

@@ -0,0 +1,244 @@
+# Codebase Cleanup & Simplification Plan
+
+**Created:** 2026-01-21
+**Goal:** Review, simplify, and clean up the codebase following YAGNI principles before writing tests
+**Estimated Effort:** 30-45 hours
+
+---
+
+## Goal & Constraints
+
+### Primary Goals
+1. **Remove complexity** - Simplify overly complex functions and classes
+2. **Eliminate YAGNI violations** - Remove unused code, over-engineered solutions
+3. **Standardize patterns** - Consistent error handling, logging, and coding style
+4. **Improve maintainability** - Make code easier to understand and test
+
+### Constraints
+- Maintain backward compatibility
+- Don't break existing functionality
+- Keep changes atomic and verifiable
+- Focus on cleanup only (no new features)
+
+---
+
+## Stage 1: Quick Wins (Low Effort, High Impact)
+
+### Tasks
+
+#### 1.1 Remove Commented-Out Code
+- [ ] Remove commented code in `cli/install.ts:159-162`
+- [ ] Remove commented code in `cli/install.ts:238-240`
+- [ ] Search entire codebase for other commented blocks
+- [ ] Remove all commented-out code (keep meaningful comments)
+
+#### 1.2 Standardize Logging
+- [ ] Replace `console.log()` with `log()` in `cli/install.ts`
+- [ ] Replace `console.error()` with `log()` in `cli/index.ts`
+- [ ] Replace `console.warn()` with `log()` in `cli/config-manager.ts`
+- [ ] Replace `console.log/error()` in `tools/grep/downloader.ts`
+- [ ] Replace `console.log/error()` in `tools/ast-grep/downloader.ts`
+- [ ] Verify all logging uses the `log()` function from `shared/logger.ts`
+
+#### 1.3 Remove Unused Imports
+- [ ] Check `tools/lsp/client.ts` for unused imports
+- [ ] Check `tools/lsp/utils.ts` for unused imports
+- [ ] Check `cli/config-manager.ts` for unused imports
+- [ ] Run TypeScript compiler to find unused imports
+- [ ] Remove all unused imports
+
+#### 1.4 Verify Stage 1 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 2: Standardize Error Handling
+
+### Tasks
+
+#### 2.1 Define Error Handling Standard
+- [ ] Document the chosen error handling pattern (throw errors with context)
+- [ ] Create error types if needed (e.g., `ConfigError`, `LSPError`, `ToolError`)
+- [ ] Document error handling guidelines in a comment or doc
+
+#### 2.2 Update Error Handling in Tools
+- [ ] Update `tools/lsp/client.ts` - Ensure all errors are thrown with context
+- [ ] Update `tools/lsp/utils.ts` - Standardize error handling
+- [ ] Update `tools/grep/cli.ts` - Change from returning error objects to throwing
+- [ ] Update `tools/grep/downloader.ts` - Standardize error handling
+- [ ] Update `tools/ast-grep/cli.ts` - Standardize error handling
+- [ ] Update `tools/ast-grep/downloader.ts` - Standardize error handling
+
+#### 2.3 Update Error Handling in Other Modules
+- [ ] Update `cli/config-manager.ts` - Remove silent catches, throw errors
+- [ ] Update `hooks/auto-update-checker/checker.ts` - Remove silent catches
+- [ ] Update `utils/tmux.ts` - Standardize error handling
+- [ ] Update `tools/background.ts` - Standardize error handling
+- [ ] Update `tools/skill/mcp-manager.ts` - Standardize error handling
+
+#### 2.4 Verify Stage 2 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 3: Simplify Complex Functions
+
+### Tasks
+
+#### 3.1 Extract LSP Buffer Processing
+- [ ] Extract `processBuffer()` from `tools/lsp/client.ts:266-317` to new module
+- [ ] Create `tools/lsp/buffer-processor.ts` with extracted logic
+- [ ] Update `tools/lsp/client.ts` to use the new module
+- [ ] Add JSDoc comments to the new module
+
+#### 3.2 Break Down Background Task Execution
+- [ ] Extract session creation logic from `tools/background.ts:140-292`
+- [ ] Extract polling logic from `tools/background.ts:217-249`
+- [ ] Extract message extraction logic from `tools/background.ts`
+- [ ] Create helper functions with clear names
+- [ ] Update `executeSync()` to use the new helper functions
+
+#### 3.3 Extract Text Edit Logic
+- [ ] Extract `applyTextEditsToFile()` from `tools/lsp/utils.ts:145-179`
+- [ ] Create `tools/lsp/text-editor.ts` with extracted logic
+- [ ] Update `tools/lsp/utils.ts` to use the new module
+- [ ] Add JSDoc comments to the new module
+
+#### 3.4 Simplify Config Manager
+- [ ] Extract `MODEL_MAPPINGS` from `cli/config-manager.ts:293-318` to separate file
+- [ ] Create `cli/model-mappings.ts` with the constant
+- [ ] Simplify provider config logic in `cli/config-manager.ts`
+- [ ] Consider using `jsonc-parser` library for JSONC parsing (optional)
+
+#### 3.5 Verify Stage 3 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 4: Remove YAGNI Violations
+
+### Tasks
+
+#### 4.1 Evaluate LSP Connection Pooling
+- [ ] Review `tools/lsp/client.ts:18-153` - LSPServerManager class
+- [ ] Determine if connection pooling is actually needed
+- [ ] If not needed, simplify to single connection
+- [ ] If needed, document why it's necessary
+- [ ] Remove idle timeout cleanup if not needed
+
+#### 4.2 Simplify Skill Template
+- [ ] Move hardcoded template from `tools/skill/builtin.ts:20-100` to external file
+- [ ] Create `tools/skill/template.md` or similar
+- [ ] Update `tools/skill/builtin.ts` to load template from file
+- [ ] Consider making template configurable
+
+#### 4.3 Remove Unused Functions
+- [ ] Check if `formatSymbolKind()` in `tools/lsp/utils.ts:106-113` is used
+- [ ] Check if `formatSeverity()` in `tools/lsp/utils.ts` is used
+- [ ] Check if `ensureCliAvailable()` in `tools/ast-grep/cli.ts:224-232` is used
+- [ ] Remove unused functions
+- [ ] Search for other potentially unused exports
+
+#### 4.4 Consolidate Binary Download Logic
+- [ ] Create shared downloader utility in `tools/shared/downloader.ts`
+- [ ] Unify tar.gz extraction logic from `tools/grep/downloader.ts`
+- [ ] Unify zip extraction logic from `tools/ast-grep/downloader.ts`
+- [ ] Standardize error handling in downloader
+- [ ] Update both modules to use the shared utility
+
+#### 4.5 Verify Stage 4 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 5: Code Quality & Consistency
+
+### Tasks
+
+#### 5.1 Standardize Async Patterns
+- [ ] Replace promise chaining with async/await where appropriate
+- [ ] Update `tools/lsp/client.ts:334` - Use async/await instead of setTimeout
+- [ ] Review all `.then()`/`.catch()` usage
+- [ ] Convert to async/await for consistency
+
+#### 5.2 Improve Naming Consistency
+- [ ] Review all type names for consistency (PascalCase for types)
+- [ ] Review all function names for consistency (camelCase)
+- [ ] Add `is` prefix to boolean functions where missing
+- [ ] Ensure parameter names follow conventions
+
+#### 5.3 Add Missing JSDoc Comments
+- [ ] Add JSDoc to complex functions
+- [ ] Add JSDoc to exported functions
+- [ ] Add JSDoc to classes and interfaces
+- [ ] Document parameters and return types
+
+#### 5.4 Run TypeScript Compiler
+- [ ] Run `tsc --noEmit` to check for type errors
+- [ ] Fix any type errors found
+- [ ] Ensure strict mode compliance
+
+#### 5.5 Verify Stage 5 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 6: Final Verification
+
+### Tasks
+
+#### 6.1 Run Existing Tests
+- [ ] Run all existing tests to ensure nothing is broken
+- [ ] Fix any failing tests
+- [ ] Ensure test suite passes
+
+#### 6.2 Manual Testing
+- [ ] Test basic plugin functionality
+- [ ] Test LSP features
+- [ ] Test background tasks
+- [ ] Test MCP skills
+- [ ] Test CLI commands
+
+#### 6.3 Code Review Checklist
+- [ ] All commented code removed
+- [ ] All logging uses `log()` function
+- [ ] All unused imports removed
+- [ ] Error handling is consistent
+- [ ] Complex functions extracted
+- [ ] YAGNI violations removed
+- [ ] Async patterns standardized
+- [ ] Naming is consistent
+- [ ] JSDoc comments added
+- [ ] No TypeScript errors
+
+#### 6.4 Update Documentation
+- [ ] Update README if needed
+- [ ] Update any inline documentation
+- [ ] Document any breaking changes
+
+#### 6.5 Verify Stage 6 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Success Criteria
+
+✅ Code is easier to understand and maintain
+✅ No commented-out code remains
+✅ Logging is consistent throughout
+✅ Error handling follows a standard pattern
+✅ Complex functions are broken down
+✅ YAGNI violations are removed
+✅ All existing tests pass
+✅ No TypeScript errors
+✅ Manual testing confirms functionality
+
+---
+
+## Notes
+
+- This plan focuses on cleanup only - no new features
+- Each stage should be verified before moving to the next
+- Update this plan as needed based on findings during execution
+- Keep changes atomic and testable
+- Document any decisions made during the process

+ 26 - 0
docs/error-handling.md

@@ -0,0 +1,26 @@
+# Error Handling Guidelines
+
+To maintain consistency and reliability across the `oh-my-opencode-slim` codebase, follow these guidelines:
+
+## Core Principles
+
+- **Throw `Error`** for invariant violations, programmer errors, or when the caller cannot reasonably continue.
+    - *Example*: Missing LSP server in `src/tools/lsp/utils.ts` or failed binary download in `src/tools/grep/downloader.ts`.
+- **Return Structured Results** for tool/CLI boundaries and long-running workflows.
+    - *Format*: `{ success: false, error: string }`.
+    - *Example*: Configuration merging or model mapping results.
+- **Prefix Tool Outputs** with `Error:` for LLM-facing results.
+    - *Example*: `src/tools/background.ts` outputs. Keep formatting stable for automated consumers.
+- **Include Context** in every message.
+    - *Format*: `[component] operation: <message>`
+    - *Example*: `[tmux] findTmuxPath: command not found`
+- **Normalize Unknown Errors**. Use `err instanceof Error ? err.message : String(err)`. Avoid stringifying full error objects in user-facing messages.
+- **Log and Continue** only for optional integrations.
+    - *Example*: Tmux, auto-update, background init. Never swallow errors in required setup paths.
+- **No Custom Error Classes** unless programmatic branching is strictly required across module boundaries.
+
+## Compatibility Notes
+
+- `src/tools/background.ts`: Tool outputs must stay string-based and prefixed with `Error:`.
+- `src/tools/ast-grep/cli.ts` & `src/tools/grep/cli.ts`: Return `{ error }` fields in result objects instead of throwing.
+- `src/cli/index.ts`: The top-level `main().catch` is the final safety net. Fatal errors should propagate here.

+ 1 - 0
src/agents/index.ts

@@ -121,6 +121,7 @@ export function getAgentConfigs(config?: PluginConfig): Record<string, SDKAgentC
       // Apply classification-based visibility and mode
       if (isSubagent(a.name)) {
         sdkConfig.mode = "subagent";
+        sdkConfig.hidden = true;
       } else if (a.name === "orchestrator") {
         sdkConfig.mode = "primary";
       }

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

@@ -3,6 +3,8 @@ import { homedir } from "node:os"
 import { join } from "node:path"
 import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
 import { DEFAULT_AGENT_SKILLS } from "../tools/skill/builtin"
+import { MODEL_MAPPINGS } from "./model-mappings"
+import { log } from "../shared/logger"
 
 const PACKAGE_NAME = "oh-my-opencode-slim"
 
@@ -70,7 +72,8 @@ function parseConfigFile(path: string): OpenCodeConfig | null {
     const content = readFileSync(path, "utf-8")
     if (content.trim().length === 0) return null
     return JSON.parse(stripJsonComments(content)) as OpenCodeConfig
-  } catch {
+  } catch (err) {
+    log(`[config-manager] parse: failed to parse ${path}: ${err instanceof Error ? err.message : String(err)}`)
     return null
   }
 }
@@ -102,7 +105,7 @@ function getExistingConfigPath(): string {
  */
 function writeConfig(configPath: string, config: OpenCodeConfig): void {
   if (configPath.endsWith(".jsonc")) {
-    console.warn(
+    log(
       "[config-manager] Writing to .jsonc file - comments will not be preserved"
     )
   }
@@ -117,7 +120,8 @@ export async function isOpenCodeInstalled(): Promise<boolean> {
     })
     await proc.exited
     return proc.exitCode === 0
-  } catch {
+  } catch (err) {
+    log(`[config-manager] check: failed to check opencode installation: ${err instanceof Error ? err.message : String(err)}`)
     return false
   }
 }
@@ -130,7 +134,8 @@ export async function isTmuxInstalled(): Promise<boolean> {
     })
     await proc.exited
     return proc.exitCode === 0
-  } catch {
+  } catch (err) {
+    log(`[config-manager] check: failed to check tmux installation: ${err instanceof Error ? err.message : String(err)}`)
     return false
   }
 }
@@ -144,7 +149,8 @@ export async function getOpenCodeVersion(): Promise<string | null> {
     const output = await new Response(proc.stdout).text()
     await proc.exited
     return proc.exitCode === 0 ? output.trim() : null
-  } catch {
+  } catch (err) {
+    log(`[config-manager] version: failed to get opencode version: ${err instanceof Error ? err.message : String(err)}`)
     return null
   }
 }
@@ -155,7 +161,8 @@ export async function fetchLatestVersion(packageName: string): Promise<string |
     if (!res.ok) return null
     const data = (await res.json()) as { version: string }
     return data.version
-  } catch {
+  } catch (err) {
+    log(`[config-manager] fetch: failed to fetch latest version for ${packageName}: ${err instanceof Error ? err.message : String(err)}`)
     return null
   }
 }
@@ -167,7 +174,7 @@ export async function addPluginToOpenCodeConfig(): Promise<ConfigMergeResult> {
     return {
       success: false,
       configPath: getConfigDir(),
-      error: `Failed to create config directory: ${err}`,
+      error: `[config-manager] install: failed to create config directory: ${err instanceof Error ? err.message : String(err)}`,
     }
   }
 
@@ -193,7 +200,7 @@ export async function addPluginToOpenCodeConfig(): Promise<ConfigMergeResult> {
     return {
       success: false,
       configPath,
-      error: `Failed to update opencode config: ${err}`,
+      error: `[config-manager] install: failed to update opencode config: ${err instanceof Error ? err.message : String(err)}`,
     }
   }
 }
@@ -224,7 +231,7 @@ export async function addAuthPlugins(installConfig: InstallConfig): Promise<Conf
     return {
       success: false,
       configPath,
-      error: `Failed to add auth plugins: ${err}`,
+      error: `[config-manager] auth: failed to add auth plugins: ${err instanceof Error ? err.message : String(err)}`,
     }
   }
 }
@@ -284,39 +291,12 @@ export function addProviderConfig(installConfig: InstallConfig): ConfigMergeResu
     return {
       success: false,
       configPath,
-      error: `Failed to add provider config: ${err}`,
+      error: `[config-manager] provider: failed to add provider config: ${err instanceof Error ? err.message : String(err)}`,
     }
   }
 }
 
 
-// Model mappings by provider priority
-const MODEL_MAPPINGS = {
-  antigravity: {
-    orchestrator: "google/claude-opus-4-5-thinking",
-    oracle: "google/claude-opus-4-5-thinking",
-    librarian: "google/gemini-3-flash",
-    explorer: "google/gemini-3-flash",
-    designer: "google/gemini-3-flash",
-    fixer: "google/gemini-3-flash",
-  },
-  openai: {
-    orchestrator: "openai/gpt-5.2-codex",
-    oracle: "openai/gpt-5.2-codex",
-    librarian: "openai/gpt-5.1-codex-mini",
-    explorer: "openai/gpt-5.1-codex-mini",
-    designer: "openai/gpt-5.1-codex-mini",
-    fixer: "openai/gpt-5.1-codex-mini",
-  },
-  opencode: {
-    orchestrator: "opencode/glm-4.7-free",
-    oracle: "opencode/glm-4.7-free",
-    librarian: "opencode/glm-4.7-free",
-    explorer: "opencode/glm-4.7-free",
-    designer: "opencode/glm-4.7-free",
-  },
-} as const;
-
 export function generateLiteConfig(installConfig: InstallConfig): Record<string, unknown> {
   // Priority: antigravity > openai > opencode (Zen free models)
   const baseProvider = installConfig.hasAntigravity
@@ -370,7 +350,7 @@ export function writeLiteConfig(installConfig: InstallConfig): ConfigMergeResult
     return {
       success: false,
       configPath,
-      error: `Failed to write lite config: ${err}`,
+      error: `[config-manager] lite: failed to write lite config: ${err instanceof Error ? err.message : String(err)}`,
     }
   }
 }
@@ -396,7 +376,7 @@ export function disableDefaultAgents(): ConfigMergeResult {
     return {
       success: false,
       configPath,
-      error: `Failed to disable default agents: ${err}`,
+      error: `[config-manager] agents: failed to disable default agents: ${err instanceof Error ? err.message : String(err)}`,
     }
   }
 }

+ 5 - 4
src/cli/index.ts

@@ -1,6 +1,7 @@
 #!/usr/bin/env bun
 import { install } from "./install"
 import type { InstallArgs, BooleanArg } from "./types"
+import { log } from "../shared/logger"
 
 function parseArgs(args: string[]): InstallArgs {
   const result: InstallArgs = {
@@ -28,7 +29,7 @@ function parseArgs(args: string[]): InstallArgs {
 }
 
 function printHelp(): void {
-  console.log(`
+  log(`
 oh-my-opencode-slim installer
 
 Usage: bunx oh-my-opencode-slim install [OPTIONS]
@@ -58,13 +59,13 @@ async function main(): Promise<void> {
     printHelp()
     process.exit(0)
   } else {
-    console.error(`Unknown command: ${args[0]}`)
-    console.error("Run with --help for usage information")
+    log(`Unknown command: ${args[0]}`)
+    log("Run with --help for usage information")
     process.exit(1)
   }
 }
 
 main().catch((err) => {
-  console.error("Fatal error:", err)
+  log("Fatal error:", err)
   process.exit(1)
 })

+ 37 - 56
src/cli/install.ts

@@ -12,6 +12,7 @@ import {
   isTmuxInstalled,
   generateLiteConfig,
 } from "./config-manager"
+import { log } from "../shared/logger"
 
 // Colors
 const GREEN = "\x1b[32m"
@@ -33,30 +34,30 @@ const SYMBOLS = {
 }
 
 function printHeader(isUpdate: boolean): void {
-  console.log()
-  console.log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`)
-  console.log("=".repeat(30))
-  console.log()
+  log("")
+  log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`)
+  log("=".repeat(30))
+  log("")
 }
 
 function printStep(step: number, total: number, message: string): void {
-  console.log(`${DIM}[${step}/${total}]${RESET} ${message}`)
+  log(`${DIM}[${step}/${total}]${RESET} ${message}`)
 }
 
 function printSuccess(message: string): void {
-  console.log(`${SYMBOLS.check} ${message}`)
+  log(`${SYMBOLS.check} ${message}`)
 }
 
 function printError(message: string): void {
-  console.log(`${SYMBOLS.cross} ${RED}${message}${RESET}`)
+  log(`${SYMBOLS.cross} ${RED}${message}${RESET}`)
 }
 
 function printInfo(message: string): void {
-  console.log(`${SYMBOLS.info} ${message}`)
+  log(`${SYMBOLS.info} ${message}`)
 }
 
 function printWarning(message: string): void {
-  console.log(`${SYMBOLS.warn} ${YELLOW}${message}${RESET}`)
+  log(`${SYMBOLS.warn} ${YELLOW}${message}${RESET}`)
 }
 
 async function checkOpenCodeInstalled(): Promise<{ ok: boolean; version?: string }> {
@@ -64,7 +65,7 @@ async function checkOpenCodeInstalled(): Promise<{ ok: boolean; version?: string
   if (!installed) {
     printError("OpenCode is not installed on this system.")
     printInfo("Install it with:")
-    console.log(`     ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`)
+    log(`     ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`)
     return { ok: false }
   }
   const version = await getOpenCodeVersion()
@@ -100,17 +101,17 @@ function printAgentModels(config: InstallConfig): void {
 
   if (!agents || Object.keys(agents).length === 0) return
 
-  console.log(`${BOLD}Agent Configuration:${RESET}`)
-  console.log()
+  log(`${BOLD}Agent Configuration:${RESET}`)
+  log("")
 
   const maxAgentLen = Math.max(...Object.keys(agents).map((a) => a.length))
 
   for (const [agent, info] of Object.entries(agents)) {
     const padding = " ".repeat(maxAgentLen - agent.length)
     const skillsStr = info.skills.length > 0 ? ` ${DIM}[${info.skills.join(", ")}]${RESET}` : ""
-    console.log(`  ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}${skillsStr}`)
+    log(`  ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}${skillsStr}`)
   }
-  console.log()
+  log("")
 }
 
 function argsToConfig(args: InstallArgs): InstallConfig {
@@ -138,30 +139,17 @@ async function askYesNo(
 
 async function runInteractiveMode(detected: DetectedConfig): Promise<InstallConfig> {
   const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
-  // TODO: tmux has a bug, disabled for now
-  // const tmuxInstalled = await isTmuxInstalled()
-  // const totalQuestions = tmuxInstalled ? 3 : 2
   const totalQuestions = 2
 
   try {
-    console.log(`${BOLD}Question 1/${totalQuestions}:${RESET}`)
+    log(`${BOLD}Question 1/${totalQuestions}:${RESET}`)
     printInfo("The Pantheon is tuned for Antigravity's model routing. Other models work, but results may vary.")
     const antigravity = await askYesNo(rl, "Do you have an Antigravity subscription?", "yes")
-    console.log()
+    log("")
 
-    console.log(`${BOLD}Question 2/${totalQuestions}:${RESET}`)
+    log(`${BOLD}Question 2/${totalQuestions}:${RESET}`)
     const openai = await askYesNo(rl, "Do you have access to OpenAI API?", detected.hasOpenAI ? "yes" : "no")
-    console.log()
-
-    // TODO: tmux has a bug, disabled for now
-    // let tmux: BooleanArg = "no"
-    // if (tmuxInstalled) {
-    //   console.log(`${BOLD}Question 3/3:${RESET}`)
-    //   printInfo(`${BOLD}Tmux detected!${RESET} We can enable tmux integration for you.`)
-    //   printInfo("This will spawn new panes for sub-agents, letting you watch them work in real-time.")
-    //   tmux = await askYesNo(rl, "Enable tmux integration?", detected.hasTmux ? "yes" : "no")
-    //   console.log()
-    // }
+    log("")
 
     return {
       hasAntigravity: antigravity === "yes",
@@ -213,9 +201,9 @@ async function runInstall(config: InstallConfig): Promise<number> {
   if (!handleStepResult(liteResult, "Config written")) return 1
 
   // Summary
-  console.log()
-  console.log(formatConfigSummary(config))
-  console.log()
+  log("")
+  log(formatConfigSummary(config))
+  log("")
 
   printAgentModels(config)
 
@@ -223,26 +211,19 @@ async function runInstall(config: InstallConfig): Promise<number> {
     printWarning("No providers configured. Zen free models will be used as fallback.")
   }
 
-  console.log(`${SYMBOLS.star} ${BOLD}${GREEN}${isUpdate ? "Configuration updated!" : "Installation complete!"}${RESET}`)
-  console.log()
-  console.log(`${BOLD}Next steps:${RESET}`)
-  console.log()
+  log(`${SYMBOLS.star} ${BOLD}${GREEN}${isUpdate ? "Configuration updated!" : "Installation complete!"}${RESET}`)
+  log("")
+  log(`${BOLD}Next steps:${RESET}`)
+  log("")
 
   let nextStep = 1
-  console.log(`  ${nextStep++}. Authenticate with your providers:`)
-  console.log(`     ${BLUE}$ opencode auth login${RESET}`)
-  console.log()
-
-  // TODO: tmux has a bug, disabled for now
-  // if (config.hasTmux) {
-  //   console.log(`  ${nextStep++}. Run OpenCode inside tmux:`)
-  //   console.log(`     ${BLUE}$ tmux${RESET}`)
-  //   console.log(`     ${BLUE}$ opencode${RESET}`)
-  // } else {
-  console.log(`  ${nextStep++}. Start OpenCode:`)
-  console.log(`     ${BLUE}$ opencode${RESET}`)
-  // }
-  console.log()
+  log(`  ${nextStep++}. Authenticate with your providers:`)
+  log(`     ${BLUE}$ opencode auth login${RESET}`)
+  log("")
+
+  log(`  ${nextStep++}. Start OpenCode:`)
+  log(`     ${BLUE}$ opencode${RESET}`)
+  log("")
 
   return 0
 }
@@ -260,11 +241,11 @@ export async function install(args: InstallArgs): Promise<number> {
       printHeader(false)
       printError("Missing or invalid arguments:")
       for (const key of errors) {
-        console.log(`  ${SYMBOLS.bullet} --${key}=<yes|no>`)
+        log(`  ${SYMBOLS.bullet} --${key}=<yes|no>`)
       }
-      console.log()
+      log("")
       printInfo("Usage: bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no> --tmux=<yes|no>")
-      console.log()
+      log("")
       return 1
     }
 
@@ -279,7 +260,7 @@ export async function install(args: InstallArgs): Promise<number> {
   printStep(1, 1, "Checking OpenCode installation...")
   const { ok } = await checkOpenCodeInstalled()
   if (!ok) return 1
-  console.log()
+  log("")
 
   const config = await runInteractiveMode(detected)
   return runInstall(config)

+ 26 - 0
src/cli/model-mappings.ts

@@ -0,0 +1,26 @@
+// Model mappings by provider priority
+export const MODEL_MAPPINGS = {
+  antigravity: {
+    orchestrator: "google/claude-opus-4-5-thinking",
+    oracle: "google/claude-opus-4-5-thinking",
+    librarian: "google/gemini-3-flash",
+    explorer: "google/gemini-3-flash",
+    designer: "google/gemini-3-flash",
+    fixer: "google/gemini-3-flash",
+  },
+  openai: {
+    orchestrator: "openai/gpt-5.2-codex",
+    oracle: "openai/gpt-5.2-codex",
+    librarian: "openai/gpt-5.1-codex-mini",
+    explorer: "openai/gpt-5.1-codex-mini",
+    designer: "openai/gpt-5.1-codex-mini",
+    fixer: "openai/gpt-5.1-codex-mini",
+  },
+  opencode: {
+    orchestrator: "opencode/glm-4.7-free",
+    oracle: "opencode/glm-4.7-free",
+    librarian: "opencode/glm-4.7-free",
+    explorer: "opencode/glm-4.7-free",
+    designer: "opencode/glm-4.7-free",
+  },
+} as const;

+ 2 - 1
src/features/background-manager.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test, beforeEach, mock } from "bun:test"
 import { BackgroundTaskManager, type BackgroundTask, type LaunchOptions } from "./background-manager"
+import { sleep } from "../utils/polling"
 
 // Mock the plugin context
 function createMockContext(overrides?: {
@@ -130,7 +131,7 @@ describe("BackgroundTaskManager", () => {
       })
 
       // Wait a bit for polling to complete the task
-      await new Promise(r => setTimeout(r, 100))
+      await sleep(100)
 
       const result = await manager.getResult(task.id, true)
       expect(result).toBeDefined()

+ 3 - 2
src/features/background-manager.ts

@@ -3,6 +3,7 @@ import { POLL_INTERVAL_BACKGROUND_MS, POLL_INTERVAL_SLOW_MS } from "../config";
 import type { TmuxConfig } from "../config/schema";
 import type { PluginConfig } from "../config";
 import { applyAgentVariant, resolveAgentVariant } from "../utils";
+import { sleep } from "../utils/polling";
 import { log } from "../shared/logger";
 type PromptBody = {
   messageID?: string;
@@ -84,7 +85,7 @@ export class BackgroundTaskManager {
     // Give TmuxSessionManager time to spawn the pane via event hook
     // before we send the prompt (so the TUI can receive streaming updates)
     if (this.tmuxEnabled) {
-      await new Promise((r) => setTimeout(r, 500));
+      await sleep(500);
     }
 
     const promptQuery: Record<string, string> = {
@@ -136,7 +137,7 @@ export class BackgroundTaskManager {
       if (status === "completed" || status === "failed") {
         return task;
       }
-      await new Promise((r) => setTimeout(r, POLL_INTERVAL_SLOW_MS));
+      await sleep(POLL_INTERVAL_SLOW_MS);
     }
 
     return task;

+ 8 - 7
src/hooks/auto-update-checker/checker.ts

@@ -176,7 +176,7 @@ export function getCachedVersion(): string | null {
       if (pkg.version) return pkg.version
     }
   } catch (err) {
-    log("[auto-update-checker] Failed to resolve version from current directory:", err)
+    log("[auto-update] check: Failed to resolve version from current directory:", err instanceof Error ? err.message : String(err))
   }
 
   return null
@@ -189,7 +189,7 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
     
     const pluginMatch = content.match(/"plugin"\s*:\s*\[/)
     if (!pluginMatch || pluginMatch.index === undefined) {
-      log(`[auto-update-checker] No "plugin" array found in ${configPath}`)
+      log(`[auto-update] update: No "plugin" array found in ${configPath}`)
       return false
     }
     
@@ -211,7 +211,7 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
     const regex = new RegExp(`["']${escapedOldEntry}["']`)
     
     if (!regex.test(pluginArrayContent)) {
-      log(`[auto-update-checker] Entry "${oldEntry}" not found in plugin array of ${configPath}`)
+      log(`[auto-update] update: Entry "${oldEntry}" not found in plugin array of ${configPath}`)
       return false
     }
     
@@ -219,15 +219,15 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
     const updatedContent = before + updatedPluginArray + after
     
     if (updatedContent === content) {
-      log(`[auto-update-checker] No changes made to ${configPath}`)
+      log(`[auto-update] update: No changes made to ${configPath}`)
       return false
     }
     
     fs.writeFileSync(configPath, updatedContent, "utf-8")
-    log(`[auto-update-checker] Updated ${configPath}: ${oldEntry} → ${newEntry}`)
+    log(`[auto-update] update: Updated ${configPath}: ${oldEntry} → ${newEntry}`)
     return true
   } catch (err) {
-    log(`[auto-update-checker] Failed to update config file ${configPath}:`, err)
+    log(`[auto-update] update: Failed to update config file ${configPath}:`, err instanceof Error ? err.message : String(err))
     return false
   }
 }
@@ -246,7 +246,8 @@ export async function getLatestVersion(channel: string = "latest"): Promise<stri
 
     const data = (await response.json()) as NpmDistTags
     return data[channel] ?? data.latest ?? null
-  } catch {
+  } catch (err) {
+    log(`[auto-update] fetch: failed to fetch latest version: ${err instanceof Error ? err.message : String(err)}`)
     return null
   } finally {
     clearTimeout(timeoutId)

+ 29 - 8
src/tools/ast-grep/cli.ts

@@ -24,6 +24,10 @@ export interface RunOptions {
 // Use a single init promise to avoid race conditions
 let initPromise: Promise<string | null> | null = null
 
+/**
+ * Resolves the path to the ast-grep binary, downloading it if necessary.
+ * @returns A promise resolving to the binary path, or null if it couldn't be found/downloaded.
+ */
 export async function getAstGrepPath(): Promise<string | null> {
   const currentPath = getSgCliPath()
   if (currentPath !== "sg" && existsSync(currentPath)) {
@@ -53,6 +57,9 @@ export async function getAstGrepPath(): Promise<string | null> {
   return initPromise
 }
 
+/**
+ * Starts background initialization of the ast-grep binary.
+ */
 export function startBackgroundInit(): void {
   if (!initPromise) {
     initPromise = getAstGrepPath()
@@ -60,6 +67,11 @@ export function startBackgroundInit(): void {
   }
 }
 
+/**
+ * Runs an ast-grep search or rewrite with the provided options.
+ * @param options - ast-grep configuration.
+ * @returns A promise resolving to the search/rewrite results.
+ */
 export async function runSg(options: RunOptions): Promise<SgResult> {
   const args = ["run", "-p", options.pattern, "--lang", options.lang, "--json=compact"]
 
@@ -99,12 +111,13 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
     stderr: "pipe",
   })
 
-  const timeoutPromise = new Promise<never>((_, reject) => {
+  const timeoutPromise = new Promise<never>(async (_, reject) => {
     const id = setTimeout(() => {
       proc.kill()
-      reject(new Error(`Search timeout after ${timeout}ms`))
+      reject(new Error(`[ast-grep] run: Search timeout after ${timeout}ms`))
     }, timeout)
-    proc.exited.then(() => clearTimeout(id))
+    await proc.exited
+    clearTimeout(id)
   })
 
   let stdout: string
@@ -123,7 +136,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
         totalMatches: 0,
         truncated: true,
         truncatedReason: "timeout",
-        error: error.message,
+        error: `[ast-grep] run: ${error.message}`,
       }
     }
 
@@ -143,7 +156,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
           totalMatches: 0,
           truncated: false,
           error:
-            `ast-grep CLI binary not found.\n\n` +
+            `[ast-grep] run: ast-grep CLI binary not found.\n\n` +
             `Auto-download failed. Manual install options:\n` +
             `  bun add -D @ast-grep/cli\n` +
             `  cargo install ast-grep --locked\n` +
@@ -156,7 +169,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
       matches: [],
       totalMatches: 0,
       truncated: false,
-      error: `Failed to spawn ast-grep: ${error.message}`,
+      error: `[ast-grep] run: Failed to spawn ast-grep: ${error.message}`,
     }
   }
 
@@ -165,7 +178,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
       return { matches: [], totalMatches: 0, truncated: false }
     }
     if (stderr.trim()) {
-      return { matches: [], totalMatches: 0, truncated: false, error: stderr.trim() }
+      return { matches: [], totalMatches: 0, truncated: false, error: `[ast-grep] run: ${stderr.trim()}` }
     }
     return { matches: [], totalMatches: 0, truncated: false }
   }
@@ -197,7 +210,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
           totalMatches: 0,
           truncated: true,
           truncatedReason: "max_output_bytes",
-          error: "Output too large and could not be parsed",
+          error: "[ast-grep] run: Output too large and could not be parsed",
         }
       }
     } else {
@@ -221,11 +234,19 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
   }
 }
 
+/**
+ * Checks if the ast-grep CLI is available on the system.
+ * @returns True if the CLI is available, false otherwise.
+ */
 export function isCliAvailable(): boolean {
   const path = findSgCliPathSync()
   return path !== null && existsSync(path)
 }
 
+/**
+ * Ensures that the ast-grep CLI is available, downloading it if necessary.
+ * @returns A promise resolving to true if available, false otherwise.
+ */
 export async function ensureCliAvailable(): Promise<boolean> {
   const path = await getAstGrepPath()
   return path !== null && existsSync(path)

+ 14 - 59
src/tools/ast-grep/downloader.ts

@@ -1,8 +1,8 @@
-import { existsSync, mkdirSync, chmodSync, unlinkSync } from "node:fs"
+import { existsSync } from "node:fs"
 import { join } from "node:path"
-import { homedir } from "node:os"
 import { createRequire } from "node:module"
-import { extractZip } from "../../shared"
+import { getDefaultInstallDir, ensureBinary } from "../shared/downloader-utils"
+export { getDefaultInstallDir as getCacheDir } from "../shared/downloader-utils"
 
 const REPO = "ast-grep/ast-grep"
 
@@ -35,24 +35,12 @@ const PLATFORM_MAP: Record<string, PlatformInfo> = {
   "win32-ia32": { arch: "i686", os: "pc-windows-msvc" },
 }
 
-export function getCacheDir(): string {
-  if (process.platform === "win32") {
-    const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
-    const base = localAppData || join(homedir(), "AppData", "Local")
-    return join(base, "oh-my-opencode-slim", "bin")
-  }
-
-  const xdgCache = process.env.XDG_CACHE_HOME
-  const base = xdgCache || join(homedir(), ".cache")
-  return join(base, "oh-my-opencode-slim", "bin")
-}
-
 export function getBinaryName(): string {
   return process.platform === "win32" ? "sg.exe" : "sg"
 }
 
 export function getCachedBinaryPath(): string | null {
-  const binaryPath = join(getCacheDir(), getBinaryName())
+  const binaryPath = join(getDefaultInstallDir(), getBinaryName())
   return existsSync(binaryPath) ? binaryPath : null
 }
 
@@ -61,56 +49,23 @@ export async function downloadAstGrep(version: string = DEFAULT_VERSION): Promis
   const platformInfo = PLATFORM_MAP[platformKey]
 
   if (!platformInfo) {
-    console.error(`[oh-my-opencode-slim] Unsupported platform for ast-grep: ${platformKey}`)
     return null
   }
 
-  const cacheDir = getCacheDir()
-  const binaryName = getBinaryName()
-  const binaryPath = join(cacheDir, binaryName)
-
-  if (existsSync(binaryPath)) {
-    return binaryPath
-  }
-
   const { arch, os } = platformInfo
   const assetName = `app-${arch}-${os}.zip`
-  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`
-
-  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`)
+  const url = `https://github.com/${REPO}/releases/download/${version}/${assetName}`
 
   try {
-    if (!existsSync(cacheDir)) {
-      mkdirSync(cacheDir, { recursive: true })
-    }
-
-    const response = await fetch(downloadUrl, { redirect: "follow" })
-
-    if (!response.ok) {
-      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
-    }
-
-    const archivePath = join(cacheDir, assetName)
-    const arrayBuffer = await response.arrayBuffer()
-    await Bun.write(archivePath, arrayBuffer)
-
-    await extractZip(archivePath, cacheDir)
-
-    if (existsSync(archivePath)) {
-      unlinkSync(archivePath)
-    }
-
-    if (process.platform !== "win32" && existsSync(binaryPath)) {
-      chmodSync(binaryPath, 0o755)
-    }
-
-    console.log(`[oh-my-opencode-slim] ast-grep binary ready.`)
-
-    return binaryPath
-  } catch (err) {
-    console.error(
-      `[oh-my-opencode-slim] Failed to download ast-grep: ${err instanceof Error ? err.message : err}`
-    )
+    return await ensureBinary({
+      binaryName: "sg",
+      version,
+      url,
+      installDir: getDefaultInstallDir(),
+      archiveName: assetName,
+      isZip: true,
+    })
+  } catch {
     return null
   }
 }

+ 17 - 0
src/tools/ast-grep/utils.ts

@@ -1,5 +1,10 @@
 import type { SgResult, CliLanguage } from "./types"
 
+/**
+ * Formats an ast-grep search result into a human-readable summary.
+ * @param result - The SgResult containing matches or error.
+ * @returns A formatted string summary.
+ */
 export function formatSearchResult(result: SgResult): string {
   if (result.error) {
     return `Error: ${result.error}`
@@ -39,6 +44,12 @@ export function formatSearchResult(result: SgResult): string {
   return lines.join("\n")
 }
 
+/**
+ * Formats an ast-grep replacement result into a human-readable summary.
+ * @param result - The SgResult containing matches/replacements.
+ * @param isDryRun - Whether this was a dry run.
+ * @returns A formatted string summary.
+ */
 export function formatReplaceResult(result: SgResult, isDryRun: boolean): string {
   if (result.error) {
     return `Error: ${result.error}`
@@ -83,6 +94,12 @@ export function formatReplaceResult(result: SgResult, isDryRun: boolean): string
   return lines.join("\n")
 }
 
+/**
+ * Provides helpful hints for common mistakes when an ast-grep search returns no results.
+ * @param pattern - The pattern that was searched.
+ * @param lang - The language used for the search.
+ * @returns A hint string if a common mistake is detected, otherwise null.
+ */
 export function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null {
   const src = pattern.trim()
 

+ 121 - 64
src/tools/background.ts

@@ -10,6 +10,7 @@ import {
 import type { TmuxConfig } from "../config/schema";
 import type { PluginConfig } from "../config";
 import { applyAgentVariant, resolveAgentVariant } from "../utils";
+import { sleep } from "../utils/polling";
 import { log } from "../shared/logger";
 
 const z = tool.schema;
@@ -21,6 +22,14 @@ type ToolContext = {
   abort: AbortSignal;
 };
 
+/**
+ * Creates background task tools (background_task, background_output, background_cancel).
+ * @param ctx - The plugin input context.
+ * @param manager - The background task manager instance.
+ * @param tmuxConfig - Optional tmux configuration for spawning panes.
+ * @param pluginConfig - Optional plugin configuration for agent variants.
+ * @returns A record of tool definitions.
+ */
 export function createBackgroundTools(
   ctx: PluginInput,
   manager: BackgroundTaskManager,
@@ -85,7 +94,7 @@ Use \`background_output\` with task_id="${task.id}" to get results.`;
 
       const task = await manager.getResult(taskId, block, timeout);
       if (!task) {
-        return `Task not found: ${taskId}`;
+        return `Error: [background] task not found: ${taskId}`;
       }
 
       const duration = task.completedAt
@@ -147,37 +156,11 @@ async function executeSync(
   pluginConfig?: PluginConfig,
   existingSessionId?: string
 ): Promise<string> {
-  let sessionID: string;
-
-  if (existingSessionId) {
-    const sessionResult = await ctx.client.session.get({ path: { id: existingSessionId } });
-    if (sessionResult.error) {
-      return `Error: Failed to get session: ${sessionResult.error}`;
-    }
-    sessionID = existingSessionId;
-  } else {
-    const parentSession = await ctx.client.session.get({ path: { id: toolContext.sessionID } }).catch(() => null);
-    const parentDirectory = parentSession?.data?.directory ?? ctx.directory;
-
-    const createResult = await ctx.client.session.create({
-      body: {
-        parentID: toolContext.sessionID,
-        title: `${description} (@${agent})`,
-      },
-      query: { directory: parentDirectory },
-    });
-
-    if (createResult.error) {
-      return `Error: Failed to create session: ${createResult.error}`;
-    }
-    sessionID = createResult.data.id;
-
-    // Give TmuxSessionManager time to spawn the pane via event hook
-    // before we send the prompt (so the TUI can receive streaming updates)
-    if (tmuxConfig?.enabled) {
-      await new Promise((r) => setTimeout(r, 500));
-    }
+  const sessionResult = await getOrCreateSession(description, agent, toolContext, ctx, tmuxConfig, existingSessionId);
+  if ("error" in sessionResult) {
+    return sessionResult.error;
   }
+  const sessionID = sessionResult.sessionID;
 
   // Disable recursive delegation tools to prevent infinite loops
   log(`[background-sync] launching sync task for agent="${agent}"`, { description });
@@ -203,27 +186,116 @@ async function executeSync(
       body: promptBody,
     });
   } catch (error) {
-    return `Error: Failed to send prompt: ${error instanceof Error ? error.message : String(error)}
+    return `Error: [background] executeSync: failed to send prompt: ${error instanceof Error ? error.message : String(error)}
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  }
+
+  const pollStatus = await pollForCompletion(sessionID, toolContext, ctx);
+  if (pollStatus === "aborted") {
+    return `Task aborted.
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  } else if (pollStatus === "timeout") {
+    return `Error: [background] executeSync: agent timed out after 5 minutes.
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  }
+
+  const messagesResult = await ctx.client.session.messages({ path: { id: sessionID } });
+  if (messagesResult.error) {
+    return `Error: [background] executeSync: failed to get messages: ${messagesResult.error}`;
+  }
+
+  const responseText = extractAssistantResponse(
+    messagesResult.data as Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }>
+  );
+
+  if (responseText === null) {
+    return `Error: [background] executeSync: no response from agent.
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  }
+
+  // Pane closing is handled by TmuxSessionManager via polling
+  return `${responseText}
 
 <task_metadata>
 session_id: ${sessionID}
 </task_metadata>`;
+}
+
+/**
+ * Retrieves an existing session or creates a new one for background task execution.
+ */
+async function getOrCreateSession(
+  description: string,
+  agent: string,
+  toolContext: ToolContext,
+  ctx: PluginInput,
+  tmuxConfig?: TmuxConfig,
+  existingSessionId?: string
+): Promise<{ sessionID: string } | { error: string }> {
+  if (existingSessionId) {
+    const sessionResult = await ctx.client.session.get({ path: { id: existingSessionId } });
+    if (sessionResult.error) {
+      return { error: `Error: [background] executeSync: failed to get session: ${sessionResult.error}` };
+    }
+    return { sessionID: existingSessionId };
+  }
+
+  const parentSession = await ctx.client.session.get({ path: { id: toolContext.sessionID } }).catch(() => null);
+  const parentDirectory = parentSession?.data?.directory ?? ctx.directory;
+
+  const createResult = await ctx.client.session.create({
+    body: {
+      parentID: toolContext.sessionID,
+      title: `${description} (@${agent})`,
+    },
+    query: { directory: parentDirectory },
+  });
+
+  if (createResult.error) {
+    return { error: `Error: [background] executeSync: failed to create session: ${createResult.error}` };
+  }
+
+  const sessionID = createResult.data.id;
+
+  // Give TmuxSessionManager time to spawn the pane via event hook
+  // before we send the prompt (so the TUI can receive streaming updates)
+  if (tmuxConfig?.enabled) {
+    await sleep(500);
   }
 
+  return { sessionID };
+}
+
+/**
+ * Polls for the completion of a session's execution.
+ */
+async function pollForCompletion(
+  sessionID: string,
+  toolContext: ToolContext,
+  ctx: PluginInput
+): Promise<"completed" | "aborted" | "timeout"> {
   const pollStart = Date.now();
   let lastMsgCount = 0;
   let stablePolls = 0;
 
   while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
     if (toolContext.abort?.aborted) {
-      return `Task aborted.
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
+      return "aborted";
     }
 
-    await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
+    await sleep(POLL_INTERVAL_MS);
 
     const statusResult = await ctx.client.session.status();
     const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
@@ -241,35 +313,26 @@ session_id: ${sessionID}
 
     if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
       stablePolls++;
-      if (stablePolls >= STABLE_POLLS_THRESHOLD) break;
+      if (stablePolls >= STABLE_POLLS_THRESHOLD) return "completed";
     } else {
       stablePolls = 0;
       lastMsgCount = currentMsgCount;
     }
   }
 
-  if (Date.now() - pollStart >= MAX_POLL_TIME_MS) {
-    return `Error: Agent timed out after 5 minutes.
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
-  }
-
-  const messagesResult = await ctx.client.session.messages({ path: { id: sessionID } });
-  if (messagesResult.error) {
-    return `Error: Failed to get messages: ${messagesResult.error}`;
-  }
+  return "timeout";
+}
 
-  const messages = messagesResult.data as Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }>;
+/**
+ * Extracts the assistant's response text from the session messages.
+ */
+function extractAssistantResponse(
+  messages: Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }>
+): string | null {
   const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
 
   if (assistantMessages.length === 0) {
-    return `Error: No response from agent.
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
+    return null;
   }
 
   const extractedContent: string[] = [];
@@ -282,11 +345,5 @@ session_id: ${sessionID}
   }
 
   const responseText = extractedContent.filter((t) => t.length > 0).join("\n\n");
-
-  // Pane closing is handled by TmuxSessionManager via polling
-  return `${responseText}
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
+  return responseText.length > 0 ? responseText : null;
 }

+ 23 - 10
src/tools/grep/cli.ts

@@ -129,6 +129,12 @@ function parseCountOutput(output: string): CountResult[] {
   return results
 }
 
+/**
+ * Runs a ripgrep (or grep) search with the provided options.
+ * @param options - Grep search configuration.
+ * @returns A promise resolving to the search results.
+ * @throws Error if the search times out.
+ */
 export async function runRg(options: GrepOptions): Promise<GrepResult> {
   const cli = resolveGrepCli()
   const args = buildArgs(options, cli.backend)
@@ -147,12 +153,13 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
     stderr: "pipe",
   })
 
-  const timeoutPromise = new Promise<never>((_, reject) => {
+  const timeoutPromise = new Promise<never>(async (_, reject) => {
     const id = setTimeout(() => {
       proc.kill()
-      reject(new Error(`Search timeout after ${timeout}ms`))
+      reject(new Error(`[grep] run: Search timeout after ${timeout}ms`))
     }, timeout)
-    proc.exited.then(() => clearTimeout(id))
+    await proc.exited
+    clearTimeout(id)
   })
 
   try {
@@ -169,7 +176,7 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
         totalMatches: 0,
         filesSearched: 0,
         truncated: false,
-        error: stderr.trim(),
+        error: `[grep] run: ${stderr.trim()}`,
       }
     }
 
@@ -188,11 +195,16 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
       totalMatches: 0,
       filesSearched: 0,
       truncated: false,
-      error: e instanceof Error ? e.message : String(e),
+      error: `[grep] run: ${e instanceof Error ? e.message : String(e)}`,
     }
   }
 }
-
+/**
+ * Runs a ripgrep (or grep) count search with the provided options.
+ * @param options - Grep search configuration (context ignored).
+ * @returns A promise resolving to an array of per-file match counts.
+ * @throws Error if the search fails or times out.
+ */
 export async function runRgCount(options: Omit<GrepOptions, "context">): Promise<CountResult[]> {
   const cli = resolveGrepCli()
   const args = buildArgs({ ...options, context: 0 }, cli.backend)
@@ -212,18 +224,19 @@ export async function runRgCount(options: Omit<GrepOptions, "context">): Promise
     stderr: "pipe",
   })
 
-  const timeoutPromise = new Promise<never>((_, reject) => {
+  const timeoutPromise = new Promise<never>(async (_, reject) => {
     const id = setTimeout(() => {
       proc.kill()
-      reject(new Error(`Search timeout after ${timeout}ms`))
+      reject(new Error(`[grep] run: Search timeout after ${timeout}ms`))
     }, timeout)
-    proc.exited.then(() => clearTimeout(id))
+    await proc.exited
+    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)}`)
+    throw new Error(`[grep] run: Count search failed: ${e instanceof Error ? e.message : String(e)}`)
   }
 }

+ 14 - 110
src/tools/grep/downloader.ts

@@ -1,25 +1,10 @@
-import { existsSync, mkdirSync, chmodSync, unlinkSync, readdirSync } from "node:fs"
+import { existsSync } from "node:fs"
 import { join } from "node:path"
-import { spawn } from "bun"
-import { extractZip } from "../../shared"
-
-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
-}
+import { getDefaultInstallDir, ensureBinary } from "../shared/downloader-utils"
 
 const RG_VERSION = "14.1.1"
 
-// Platform key format: ${process.platform}-${process.arch} (consistent with ast-grep)
+// Platform key format: ${process.platform}-${process.arch}
 const PLATFORM_CONFIG: Record<string, { platform: string; extension: "tar.gz" | "zip" } | undefined> =
   {
     "darwin-arm64": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
@@ -33,60 +18,9 @@ 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)
-    }
-  }
+  return join(getDefaultInstallDir(), isWindows ? "rg.exe" : "rg")
 }
 
 export async function downloadAndInstallRipgrep(): Promise<string> {
@@ -94,51 +28,21 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
   const config = PLATFORM_CONFIG[platformKey]
 
   if (!config) {
-    throw new Error(`Unsupported platform: ${platformKey}`)
-  }
-
-  const installDir = getInstallDir()
-  const rgPath = getRgPath()
-
-  if (existsSync(rgPath)) {
-    return rgPath
+    throw new Error(`[grep] download: Unsupported platform: ${platformKey}`)
   }
 
-  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
-      }
-    }
-  }
+  return ensureBinary({
+    binaryName: "rg",
+    version: RG_VERSION,
+    url,
+    installDir: getDefaultInstallDir(),
+    archiveName: filename,
+    isZip: config.extension === "zip",
+    tarInclude: "*/rg",
+  })
 }
 
 export function getInstalledRipgrepPath(): string | null {

+ 81 - 93
src/tools/lsp/client.ts

@@ -6,6 +6,8 @@ import { extname, resolve } from "path"
 import { pathToFileURL } from "node:url"
 import { getLanguageId } from "./config"
 import type { Diagnostic, ResolvedServer } from "./types"
+import { parseMessages } from "./protocol-parser"
+import { sleep } from "../../utils/polling"
 
 interface ManagedClient {
   client: LSPClient
@@ -119,7 +121,7 @@ class LSPServerManager {
       }
     } catch (err) {
       this.clients.delete(key)
-      throw err
+      throw new Error(`[lsp-client] getClient: ${err instanceof Error ? err.message : String(err)}`)
     }
 
     return client
@@ -182,18 +184,19 @@ export class LSPClient {
     })
 
     if (!this.proc) {
-      throw new Error(`Failed to spawn LSP server: ${this.server.command.join(" ")}`)
+      throw new Error(`[lsp-client] start: Failed to spawn LSP server: ${this.server.command.join(" ")}`)
     }
 
     this.startReading()
     this.startStderrReading()
 
-    await new Promise((resolve) => setTimeout(resolve, 100))
+    await sleep(100)
 
     if (this.proc.exitCode !== null) {
       const stderr = this.stderrBuffer.join("\n")
       throw new Error(
-        `LSP server exited immediately with code ${this.proc.exitCode}` + (stderr ? `\nstderr: ${stderr}` : "")
+        `[lsp-client] start: LSP server exited immediately with code ${this.proc.exitCode}` +
+          (stderr ? `\nstderr: ${stderr}` : "")
       )
     }
   }
@@ -208,7 +211,7 @@ export class LSPClient {
           const { done, value } = await reader.read()
           if (done) {
             this.processExited = true
-            this.rejectAllPending("LSP server stdout closed")
+            this.rejectAllPending("[lsp-client] startReading: LSP server stdout closed")
             break
           }
           const newBuf = new Uint8Array(this.buffer.length + value.length)
@@ -219,7 +222,8 @@ export class LSPClient {
         }
       } catch (err) {
         this.processExited = true
-        this.rejectAllPending(`LSP stdout read error: ${err}`)
+        const message = err instanceof Error ? err.message : String(err)
+        this.rejectAllPending(`[lsp-client] startReading: ${message}`)
       }
     }
     read()
@@ -253,50 +257,12 @@ export class LSPClient {
     }
   }
 
-  private findSequence(haystack: Uint8Array, needle: number[]): number {
-    outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
-      for (let j = 0; j < needle.length; j++) {
-        if (haystack[i + j] !== needle[j]) continue outer
-      }
-      return i
-    }
-    return -1
-  }
-
   private processBuffer(): void {
-    const decoder = new TextDecoder()
-    const CONTENT_LENGTH = [67, 111, 110, 116, 101, 110, 116, 45, 76, 101, 110, 103, 116, 104, 58]
-    const CRLF_CRLF = [13, 10, 13, 10]
-    const LF_LF = [10, 10]
-
-    while (true) {
-      const headerStart = this.findSequence(this.buffer, CONTENT_LENGTH)
-      if (headerStart === -1) break
-      if (headerStart > 0) this.buffer = this.buffer.slice(headerStart)
-
-      let headerEnd = this.findSequence(this.buffer, CRLF_CRLF)
-      let sepLen = 4
-      if (headerEnd === -1) {
-        headerEnd = this.findSequence(this.buffer, LF_LF)
-        sepLen = 2
-      }
-      if (headerEnd === -1) break
-
-      const header = decoder.decode(this.buffer.slice(0, headerEnd))
-      const match = header.match(/Content-Length:\s*(\d+)/i)
-      if (!match) break
-
-      const len = parseInt(match[1], 10)
-      const start = headerEnd + sepLen
-      const end = start + len
-      if (this.buffer.length < end) break
-
-      const content = decoder.decode(this.buffer.slice(start, end))
-      this.buffer = this.buffer.slice(end)
+    const { messages, remainingBuffer } = parseMessages(this.buffer)
+    this.buffer = remainingBuffer
 
+    for (const msg of messages) {
       try {
-        const msg = JSON.parse(content)
-
         if ("method" in msg && !("id" in msg)) {
           if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
             this.diagnosticsStore.set(msg.params.uri, msg.params.diagnostics ?? [])
@@ -307,21 +273,25 @@ export class LSPClient {
           const handler = this.pending.get(msg.id)!
           this.pending.delete(msg.id)
           if ("error" in msg) {
-            handler.reject(new Error(msg.error.message))
+            handler.reject(new Error(`[lsp-client] response: ${msg.error.message}`))
           } else {
             handler.resolve(msg.result)
           }
         }
-      } catch {}
+      } catch (err) {
+        console.error(`[lsp-client] Error handling message: ${err instanceof Error ? err.message : String(err)}`)
+      }
     }
   }
 
   private send(method: string, params?: unknown): Promise<unknown> {
-    if (!this.proc) throw new Error("LSP client not started")
+    if (!this.proc) throw new Error("[lsp-client] send: LSP client not started")
 
     if (this.processExited || this.proc.exitCode !== null) {
       const stderr = this.stderrBuffer.slice(-10).join("\n")
-      throw new Error(`LSP server already exited (code: ${this.proc.exitCode})` + (stderr ? `\nstderr: ${stderr}` : ""))
+      throw new Error(
+        `[lsp-client] send: LSP server already exited (code: ${this.proc.exitCode})` + (stderr ? `\nstderr: ${stderr}` : "")
+      )
     }
 
     const id = ++this.requestIdCounter
@@ -335,18 +305,28 @@ export class LSPClient {
         if (this.pending.has(id)) {
           this.pending.delete(id)
           const stderr = this.stderrBuffer.slice(-5).join("\n")
-          reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
+          reject(
+            new Error(
+              `[lsp-client] send: LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")
+            )
+          )
         }
       }, 15000)
     })
   }
 
   private notify(method: string, params?: unknown): void {
-    if (!this.proc) return
-    if (this.processExited || this.proc.exitCode !== null) return
+    if (!this.proc) throw new Error("[lsp-client] notify: LSP client not started")
+    if (this.processExited || this.proc.exitCode !== null) {
+      throw new Error("[lsp-client] notify: LSP server already exited")
+    }
 
     const msg = JSON.stringify({ jsonrpc: "2.0", method, params })
-    this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
+    try {
+      this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
+    } catch (err) {
+      throw new Error(`[lsp-client] notify: ${err instanceof Error ? err.message : String(err)}`)
+    }
   }
 
   private respond(id: number | string, result: unknown): void {
@@ -373,37 +353,41 @@ export class LSPClient {
   }
 
   async initialize(): Promise<void> {
-    const rootUri = pathToFileURL(this.root).href
-    await this.send("initialize", {
-      processId: process.pid,
-      rootUri,
-      rootPath: this.root,
-      workspaceFolders: [{ uri: rootUri, name: "workspace" }],
-      capabilities: {
-        textDocument: {
-          hover: { contentFormat: ["markdown", "plaintext"] },
-          definition: { linkSupport: true },
-          references: {},
-          documentSymbol: { hierarchicalDocumentSymbolSupport: true },
-          publishDiagnostics: {},
-          rename: {
-            prepareSupport: true,
-            prepareSupportDefaultBehavior: 1,
-            honorsChangeAnnotations: true,
+    try {
+      const rootUri = pathToFileURL(this.root).href
+      await this.send("initialize", {
+        processId: process.pid,
+        rootUri,
+        rootPath: this.root,
+        workspaceFolders: [{ uri: rootUri, name: "workspace" }],
+        capabilities: {
+          textDocument: {
+            hover: { contentFormat: ["markdown", "plaintext"] },
+            definition: { linkSupport: true },
+            references: {},
+            documentSymbol: { hierarchicalDocumentSymbolSupport: true },
+            publishDiagnostics: {},
+            rename: {
+              prepareSupport: true,
+              prepareSupportDefaultBehavior: 1,
+              honorsChangeAnnotations: true,
+            },
+          },
+          workspace: {
+            symbol: {},
+            workspaceFolders: true,
+            configuration: true,
+            applyEdit: true,
+            workspaceEdit: { documentChanges: true },
           },
         },
-        workspace: {
-          symbol: {},
-          workspaceFolders: true,
-          configuration: true,
-          applyEdit: true,
-          workspaceEdit: { documentChanges: true },
-        },
-      },
-      ...this.server.initialization,
-    })
-    this.notify("initialized")
-    await new Promise((r) => setTimeout(r, 300))
+        ...this.server.initialization,
+      })
+      this.notify("initialized")
+      await sleep(300)
+    } catch (err) {
+      throw new Error(`[lsp-client] initialize: ${err instanceof Error ? err.message : String(err)}`)
+    }
   }
 
   async openFile(filePath: string): Promise<void> {
@@ -424,7 +408,7 @@ export class LSPClient {
     })
     this.openedFiles.add(absPath)
 
-    await new Promise((r) => setTimeout(r, 1000))
+    await sleep(1000)
   }
 
   async definition(filePath: string, line: number, character: number): Promise<unknown> {
@@ -450,7 +434,7 @@ export class LSPClient {
     const absPath = resolve(filePath)
     const uri = pathToFileURL(absPath).href
     await this.openFile(absPath)
-    await new Promise((r) => setTimeout(r, 500))
+    await sleep(500)
 
     try {
       const result = await this.send("textDocument/diagnostic", {
@@ -480,12 +464,16 @@ export class LSPClient {
 
   async stop(): Promise<void> {
     try {
-      this.notify("shutdown", {})
-      this.notify("exit")
-    } catch {}
-    this.proc?.kill()
-    this.proc = null
-    this.processExited = true
-    this.diagnosticsStore.clear()
+      try {
+        this.notify("shutdown", {})
+        this.notify("exit")
+      } catch {}
+      this.proc?.kill()
+      this.proc = null
+      this.processExited = true
+      this.diagnosticsStore.clear()
+    } catch (err) {
+      throw new Error(`[lsp-client] stop: ${err instanceof Error ? err.message : String(err)}`)
+    }
   }
 }

+ 16 - 0
src/tools/lsp/config.ts

@@ -6,6 +6,11 @@ import { homedir } from "os"
 import { BUILTIN_SERVERS, EXT_TO_LANG, LSP_INSTALL_HINTS } from "./constants"
 import type { ResolvedServer, ServerLookupResult } from "./types"
 
+/**
+ * Finds a suitable LSP server for a given file extension.
+ * @param ext - The file extension (including dot).
+ * @returns A result indicating if a server was found, not installed, or not configured.
+ */
 export function findServerForExtension(ext: string): ServerLookupResult {
   // Find matching server
   for (const [id, config] of Object.entries(BUILTIN_SERVERS)) {
@@ -33,10 +38,21 @@ export function findServerForExtension(ext: string): ServerLookupResult {
   return { status: "not_configured", extension: ext }
 }
 
+/**
+ * Maps a file extension to its corresponding LSP language identifier.
+ * @param ext - The file extension (including dot).
+ * @returns The language identifier (e.g., 'typescript', 'python').
+ */
 export function getLanguageId(ext: string): string {
   return EXT_TO_LANG[ext] || "plaintext"
 }
 
+/**
+ * Checks if an LSP server command is available on the system.
+ * Looks in PATH, local node_modules/.bin, and global opencode bin.
+ * @param command - The command array (e.g., ['typescript-language-server', '--stdio']).
+ * @returns True if the server is installed and executable.
+ */
 export function isServerInstalled(command: string[]): boolean {
   if (command.length === 0) return false
 

+ 87 - 0
src/tools/lsp/protocol-parser.ts

@@ -0,0 +1,87 @@
+/**
+ * Utility for parsing LSP (Language Server Protocol) messages from a raw byte buffer.
+ * LSP uses a header-based protocol similar to HTTP.
+ */
+
+const decoder = new TextDecoder();
+const CONTENT_LENGTH = [67, 111, 110, 116, 101, 110, 116, 45, 76, 101, 110, 103, 116, 104, 58]; // "Content-Length:"
+const CRLF_CRLF = [13, 10, 13, 10]; // "\r\n\r\n"
+const LF_LF = [10, 10]; // "\n\n"
+
+/**
+ * Finds the first occurrence of a byte sequence in a Uint8Array.
+ * 
+ * @param haystack The buffer to search in.
+ * @param needle The byte sequence to find.
+ * @returns The index of the first occurrence, or -1 if not found.
+ */
+function findSequence(haystack: Uint8Array, needle: number[]): number {
+  outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
+    for (let j = 0; j < needle.length; j++) {
+      if (haystack[i + j] !== needle[j]) continue outer;
+    }
+    return i;
+  }
+  return -1;
+}
+
+/**
+ * Parses LSP messages from a raw byte buffer.
+ * 
+ * @param buffer The raw bytes received from the LSP server.
+ * @returns An object containing the parsed messages and any remaining bytes.
+ */
+export function parseMessages(buffer: Uint8Array): { messages: any[]; remainingBuffer: Uint8Array } {
+  const messages: any[] = [];
+  let currentBuffer = buffer;
+
+  while (true) {
+    const headerStart = findSequence(currentBuffer, CONTENT_LENGTH);
+    if (headerStart === -1) break;
+    
+    // Discard any data before Content-Length
+    if (headerStart > 0) {
+      currentBuffer = currentBuffer.slice(headerStart);
+    }
+
+    let headerEnd = findSequence(currentBuffer, CRLF_CRLF);
+    let sepLen = 4;
+    if (headerEnd === -1) {
+      headerEnd = findSequence(currentBuffer, LF_LF);
+      sepLen = 2;
+    }
+    
+    if (headerEnd === -1) break;
+
+    const header = decoder.decode(currentBuffer.slice(0, headerEnd));
+    const match = header.match(/Content-Length:\s*(\d+)/i);
+    if (!match) {
+      // If we found something that looks like a header but doesn't have Content-Length,
+      // skip past it to avoid infinite loops.
+      currentBuffer = currentBuffer.slice(headerEnd + sepLen);
+      continue;
+    }
+
+    const len = parseInt(match[1], 10);
+    const start = headerEnd + sepLen;
+    const end = start + len;
+
+    if (currentBuffer.length < end) break;
+
+    const content = decoder.decode(currentBuffer.slice(start, end));
+    currentBuffer = currentBuffer.slice(end);
+
+    try {
+      const msg = JSON.parse(content);
+      messages.push(msg);
+    } catch (err) {
+      // We log but don't throw to avoid crashing the whole client on one malformed message
+      console.error(`[protocol-parser] Failed to parse LSP message: ${err instanceof Error ? err.message : String(err)}`);
+    }
+  }
+
+  return {
+    messages,
+    remainingBuffer: currentBuffer,
+  };
+}

+ 49 - 0
src/tools/lsp/text-editor.ts

@@ -0,0 +1,49 @@
+import { readFileSync, writeFileSync } from "fs"
+import type { TextEdit } from "./types"
+
+/**
+ * Applies a list of text edits to a file.
+ *
+ * @param filePath - The absolute path to the file to modify.
+ * @param edits - An array of TextEdit objects to apply.
+ * @returns An object indicating success, the number of edits applied, and an optional error message.
+ */
+export function applyTextEditsToFile(
+  filePath: string,
+  edits: TextEdit[]
+): { success: boolean; editCount: number; error?: string } {
+  try {
+    const content = readFileSync(filePath, "utf-8")
+    const lines = content.split("\n")
+
+    const sortedEdits = [...edits].sort((a, b) => {
+      if (b.range.start.line !== a.range.start.line) {
+        return b.range.start.line - a.range.start.line
+      }
+      return b.range.start.character - a.range.start.character
+    })
+
+    for (const edit of sortedEdits) {
+      const startLine = edit.range.start.line
+      const startChar = edit.range.start.character
+      const endLine = edit.range.end.line
+      const endChar = edit.range.end.character
+
+      if (startLine === endLine) {
+        const line = lines[startLine] || ""
+        lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar)
+      } else {
+        const firstLine = lines[startLine] || ""
+        const lastLine = lines[endLine] || ""
+        const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar)
+        lines.splice(startLine, endLine - startLine + 1, ...newContent.split("\n"))
+      }
+    }
+
+    writeFileSync(filePath, lines.join("\n"), "utf-8")
+    return { success: true, editCount: edits.length }
+  } catch (err) {
+    const message = err instanceof Error ? err.message : String(err)
+    return { success: false, editCount: 0, error: `[lsp-utils] applyTextEdits: ${message}` }
+  }
+}

+ 52 - 49
src/tools/lsp/utils.ts

@@ -6,7 +6,8 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync } from "f
 import { lspManager } from "./client"
 import type { LSPClient } from "./client"
 import { findServerForExtension } from "./config"
-import { SYMBOL_KIND_MAP, SEVERITY_MAP } from "./constants"
+import { SEVERITY_MAP } from "./constants"
+import { applyTextEditsToFile } from "./text-editor"
 import type {
   Location,
   LocationLink,
@@ -16,6 +17,11 @@ import type {
   ServerLookupResult,
 } from "./types"
 
+/**
+ * Finds the workspace root for a given file by looking for common markers like .git or package.json.
+ * @param filePath - The path to the file.
+ * @returns The resolved workspace root directory.
+ */
 export function findWorkspaceRoot(filePath: string): string {
   let dir = resolve(filePath)
 
@@ -43,6 +49,11 @@ export function findWorkspaceRoot(filePath: string): string {
   return dirname(resolve(filePath))
 }
 
+/**
+ * Converts a file URI to a local filesystem path.
+ * @param uri - The file URI (e.g., 'file:///path/to/file').
+ * @returns The local filesystem path.
+ */
 export function uriToPath(uri: string): string {
   return fileURLToPath(uri)
 }
@@ -50,7 +61,7 @@ export function uriToPath(uri: string): string {
 export function formatServerLookupError(result: Exclude<ServerLookupResult, { status: "found" }>): string {
   if (result.status === "not_installed") {
     return [
-      `LSP server '${result.server.id}' is NOT INSTALLED.`,
+      `[lsp-utils] findServer: LSP server '${result.server.id}' is NOT INSTALLED.`,
       ``,
       `Command not found: ${result.server.command[0]}`,
       ``,
@@ -58,9 +69,17 @@ export function formatServerLookupError(result: Exclude<ServerLookupResult, { st
     ].join("\n")
   }
 
-  return `No LSP server configured for extension: ${result.extension}`
+  return `[lsp-utils] findServer: No LSP server configured for extension: ${result.extension}`
 }
 
+/**
+ * Executes a callback function with an LSP client for the given file.
+ * Manages client acquisition and release automatically.
+ * @param filePath - The path to the file to get a client for.
+ * @param fn - The callback function to execute with the client.
+ * @returns The result of the callback function.
+ * @throws Error if no suitable LSP server is found or if the client times out.
+ */
 export async function withLspClient<T>(filePath: string, fn: (client: LSPClient) => Promise<T>): Promise<T> {
   const absPath = resolve(filePath)
   const ext = extname(absPath)
@@ -80,7 +99,7 @@ export async function withLspClient<T>(filePath: string, fn: (client: LSPClient)
     if (e instanceof Error && e.message.includes("timeout")) {
       const isInitializing = lspManager.isServerInitializing(root, server.id)
       if (isInitializing) {
-        throw new Error(`LSP server is still initializing. Please retry in a few seconds.`)
+        throw new Error(`[lsp-utils] withLspClient: LSP server is still initializing. Please retry in a few seconds.`)
       }
     }
     throw e
@@ -89,6 +108,11 @@ export async function withLspClient<T>(filePath: string, fn: (client: LSPClient)
   }
 }
 
+/**
+ * Formats an LSP location or location link into a human-readable string (path:line:char).
+ * @param loc - The LSP location or location link.
+ * @returns A formatted string representation.
+ */
 export function formatLocation(loc: Location | LocationLink): string {
   if ("targetUri" in loc) {
     const uri = uriToPath(loc.targetUri)
@@ -103,15 +127,16 @@ export function formatLocation(loc: Location | LocationLink): string {
   return `${uri}:${line}:${char}`
 }
 
-export function formatSymbolKind(kind: number): string {
-  return SYMBOL_KIND_MAP[kind] || `Unknown(${kind})`
-}
-
 export function formatSeverity(severity: number | undefined): string {
   if (!severity) return "unknown"
   return SEVERITY_MAP[severity] || `unknown(${severity})`
 }
 
+/**
+ * Formats an LSP diagnostic into a human-readable string.
+ * @param diag - The LSP diagnostic.
+ * @returns A formatted string representation.
+ */
 export function formatDiagnostic(diag: Diagnostic): string {
   const severity = formatSeverity(diag.severity)
   const line = diag.range.start.line + 1
@@ -142,42 +167,6 @@ export function filterDiagnosticsBySeverity(
 
 // WorkspaceEdit application
 
-function applyTextEditsToFile(filePath: string, edits: TextEdit[]): { success: boolean; editCount: number; error?: string } {
-  try {
-    const content = readFileSync(filePath, "utf-8")
-    const lines = content.split("\n")
-
-    const sortedEdits = [...edits].sort((a, b) => {
-      if (b.range.start.line !== a.range.start.line) {
-        return b.range.start.line - a.range.start.line
-      }
-      return b.range.start.character - a.range.start.character
-    })
-
-    for (const edit of sortedEdits) {
-      const startLine = edit.range.start.line
-      const startChar = edit.range.start.character
-      const endLine = edit.range.end.line
-      const endChar = edit.range.end.character
-
-      if (startLine === endLine) {
-        const line = lines[startLine] || ""
-        lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar)
-      } else {
-        const firstLine = lines[startLine] || ""
-        const lastLine = lines[endLine] || ""
-        const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar)
-        lines.splice(startLine, endLine - startLine + 1, ...newContent.split("\n"))
-      }
-    }
-
-    writeFileSync(filePath, lines.join("\n"), "utf-8")
-    return { success: true, editCount: edits.length }
-  } catch (err) {
-    return { success: false, editCount: 0, error: err instanceof Error ? err.message : String(err) }
-  }
-}
-
 export interface ApplyResult {
   success: boolean
   filesModified: string[]
@@ -185,9 +174,15 @@ export interface ApplyResult {
   errors: string[]
 }
 
+/**
+ * Applies an LSP workspace edit to the local filesystem.
+ * Supports both 'changes' (TextEdits) and 'documentChanges' (Create/Rename/Delete/Edit).
+ * @param edit - The workspace edit to apply.
+ * @returns An object containing the success status, modified files, and any errors.
+ */
 export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
   if (!edit) {
-    return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] }
+    return { success: false, filesModified: [], totalEdits: 0, errors: ["[lsp-utils] applyWorkspaceEdit: No edit provided"] }
   }
 
   const result: ApplyResult = { success: true, filesModified: [], totalEdits: 0, errors: [] }
@@ -202,7 +197,7 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
         result.totalEdits += applyResult.editCount
       } else {
         result.success = false
-        result.errors.push(`${filePath}: ${applyResult.error}`)
+        result.errors.push(`[lsp-utils] applyWorkspaceEdit: ${filePath}: ${applyResult.error?.replace("[lsp-utils] applyTextEdits: ", "")}`)
       }
     }
   }
@@ -217,7 +212,8 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
             result.filesModified.push(filePath)
           } catch (err) {
             result.success = false
-            result.errors.push(`Create ${change.uri}: ${err}`)
+            const message = err instanceof Error ? err.message : String(err)
+            result.errors.push(`[lsp-utils] applyWorkspaceEdit: Create ${change.uri}: ${message}`)
           }
         } else if (change.kind === "rename") {
           try {
@@ -229,7 +225,8 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
             result.filesModified.push(newPath)
           } catch (err) {
             result.success = false
-            result.errors.push(`Rename ${change.oldUri}: ${err}`)
+            const message = err instanceof Error ? err.message : String(err)
+            result.errors.push(`[lsp-utils] applyWorkspaceEdit: Rename ${change.oldUri}: ${message}`)
           }
         } else if (change.kind === "delete") {
           try {
@@ -238,7 +235,8 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
             result.filesModified.push(filePath)
           } catch (err) {
             result.success = false
-            result.errors.push(`Delete ${change.uri}: ${err}`)
+            const message = err instanceof Error ? err.message : String(err)
+            result.errors.push(`[lsp-utils] applyWorkspaceEdit: Delete ${change.uri}: ${message}`)
           }
         }
       } else {
@@ -259,6 +257,11 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
   return result
 }
 
+/**
+ * Formats the result of a workspace edit application into a human-readable summary.
+ * @param result - The apply result from applyWorkspaceEdit.
+ * @returns A formatted summary string.
+ */
 export function formatApplyResult(result: ApplyResult): string {
   const lines: string[] = []
 

+ 2 - 1
src/tools/quota/api.ts

@@ -1,6 +1,7 @@
 import * as path from "path";
 import * as os from "os";
 import * as fs from "fs";
+import { sleep } from "../../utils/polling";
 import type {
   Account,
   AccountsConfig,
@@ -177,7 +178,7 @@ export async function fetchAccountQuota(account: Account): Promise<AccountQuotaR
 export async function fetchAllQuotas(accounts: Account[]): Promise<AccountQuotaResult[]> {
   const results: AccountQuotaResult[] = [];
   for (let i = 0; i < accounts.length; i++) {
-    if (i > 0) await new Promise((r) => setTimeout(r, ACCOUNT_FETCH_DELAY_MS));
+    if (i > 0) await sleep(ACCOUNT_FETCH_DELAY_MS);
     results.push(await fetchAccountQuota(accounts[i]));
   }
   return results;

+ 3 - 2
src/tools/quota/command.ts

@@ -1,6 +1,7 @@
 import * as path from "path";
 import * as os from "os";
 import * as fs from "fs";
+import { log } from "../../shared/logger";
 
 // Define base configuration directory based on OS
 const isWindows = os.platform() === "win32";
@@ -44,6 +45,6 @@ try {
     }
   }
 } catch (error) {
-  console.error("Failed to create command file/directory:", error);
+  log("Failed to create command file/directory:", error);
   // Continue execution, as this might not be fatal for the plugin's core function
-}
+}

+ 162 - 0
src/tools/shared/downloader-utils.ts

@@ -0,0 +1,162 @@
+import { existsSync, mkdirSync, chmodSync, unlinkSync, readdirSync, renameSync } from "node:fs"
+import { join } from "node:path"
+import { homedir } from "node:os"
+import { spawn } from "bun"
+import { extractZip } from "../../shared"
+import { log } from "../../shared/logger"
+
+export interface DownloadOptions {
+  binaryName: string
+  version: string
+  url: string
+  installDir: string
+  archiveName: string
+  isZip: boolean
+  tarInclude?: string
+}
+
+/**
+ * Downloads a file from a URL to a local destination path.
+ * Supports redirects.
+ * @param url - The URL to download from.
+ * @param destPath - The local path to save the file to.
+ * @returns A promise that resolves when the download is complete.
+ * @throws Error if the download fails.
+ */
+export async function downloadFile(url: string, destPath: string): Promise<void> {
+  const response = await fetch(url, { redirect: "follow" })
+  if (!response.ok) {
+    throw new Error(`Download failed: HTTP ${response.status} ${response.statusText}`)
+  }
+  const buffer = await response.arrayBuffer()
+  await Bun.write(destPath, buffer)
+}
+
+/**
+ * Extracts a .tar.gz archive using the system 'tar' command.
+ * @param archivePath - Path to the archive file.
+ * @param destDir - Directory to extract into.
+ * @param include - Optional pattern of files to include.
+ * @returns A promise that resolves when extraction is complete.
+ * @throws Error if extraction fails.
+ */
+export async function extractTarGz(archivePath: string, destDir: string, include?: string): Promise<void> {
+  const args = ["tar", "-xzf", archivePath, "--strip-components=1"]
+  
+  if (include) {
+    if (process.platform === "darwin") {
+      args.push(`--include=${include}`)
+    } else if (process.platform === "linux") {
+      args.push("--wildcards", include)
+    }
+  }
+
+  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}`)
+  }
+}
+
+/**
+ * Recursively searches for a file with a specific name within a directory.
+ * @param dir - The directory to search in.
+ * @param filename - The name of the file to find.
+ * @returns The full path to the found file, or null if not found.
+ */
+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) {
+        // Bun 1.1+ supports entry.parentPath, fallback to dir if not available
+        return join((entry as any).parentPath ?? dir, entry.name)
+      }
+    }
+  } catch {
+    return null
+  }
+  return null
+}
+
+/**
+ * Gets the default installation directory for binaries based on the OS.
+ * @returns The absolute path to the default installation directory.
+ */
+export function getDefaultInstallDir(): string {
+  const home = homedir()
+  if (process.platform === "win32") {
+    const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
+    const base = localAppData || join(home, "AppData", "Local")
+    return join(base, "oh-my-opencode-slim", "bin")
+  }
+
+  const xdgCache = process.env.XDG_CACHE_HOME
+  const base = xdgCache || join(home, ".cache")
+  return join(base, "oh-my-opencode-slim", "bin")
+}
+
+/**
+ * Ensures a binary is available locally, downloading and extracting it if necessary.
+ * @param options - Download and installation options.
+ * @returns A promise resolving to the absolute path of the binary.
+ * @throws Error if download or extraction fails, or if the binary is not found.
+ */
+export async function ensureBinary(options: DownloadOptions): Promise<string> {
+  const { binaryName, url, installDir, archiveName, isZip, tarInclude } = options
+  const isWindows = process.platform === "win32"
+  const fullBinaryName = isWindows ? `${binaryName}.exe` : binaryName
+  const binaryPath = join(installDir, fullBinaryName)
+
+  if (existsSync(binaryPath)) {
+    return binaryPath
+  }
+
+  if (!existsSync(installDir)) {
+    mkdirSync(installDir, { recursive: true })
+  }
+
+  const archivePath = join(installDir, archiveName)
+
+  try {
+    log(`[oh-my-opencode-slim] Downloading ${binaryName}...`)
+    await downloadFile(url, archivePath)
+
+    if (isZip) {
+      await extractZip(archivePath, installDir)
+      
+      // Some zips have nested structures, try to find the binary and move it to the root of installDir
+      const foundPath = findFileRecursive(installDir, fullBinaryName)
+      if (foundPath && foundPath !== binaryPath) {
+        renameSync(foundPath, binaryPath)
+      }
+    } else {
+      await extractTarGz(archivePath, installDir, tarInclude)
+    }
+
+    if (!isWindows && existsSync(binaryPath)) {
+      chmodSync(binaryPath, 0o755)
+    }
+
+    if (!existsSync(binaryPath)) {
+      throw new Error(`${binaryName} binary not found after extraction`)
+    }
+
+    log(`[oh-my-opencode-slim] ${binaryName} ready.`)
+    return binaryPath
+  } finally {
+    if (existsSync(archivePath)) {
+      try {
+        unlinkSync(archivePath)
+      } catch {
+        // Cleanup failures are non-critical
+      }
+    }
+  }
+}

+ 6 - 81
src/tools/skill/builtin.ts

@@ -1,3 +1,5 @@
+import { readFileSync } from "fs";
+import { fileURLToPath } from "url";
 import type { SkillDefinition } from "./types";
 import type { PluginConfig, AgentName } from "../../config/schema";
 
@@ -17,87 +19,10 @@ export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
   fixer: [],
 };
 
-const YAGNI_TEMPLATE = `# YAGNI Enforcement Skill
-
-You are a code simplicity expert specializing in minimalism and the YAGNI (You Aren't Gonna Need It) principle. Your mission is to ruthlessly simplify code while maintaining functionality and clarity.
-
-When reviewing code, you will:
-
-1. **Analyze Every Line**: Question the necessity of each line of code. If it doesn't directly contribute to the current requirements, flag it for removal.
-
-2. **Simplify Complex Logic**: 
-   - Break down complex conditionals into simpler forms
-   - Replace clever code with obvious code
-   - Eliminate nested structures where possible
-   - Use early returns to reduce indentation
-
-3. **Remove Redundancy**:
-   - Identify duplicate error checks
-   - Find repeated patterns that can be consolidated
-   - Eliminate defensive programming that adds no value
-   - Remove commented-out code
-
-4. **Challenge Abstractions**:
-   - Question every interface, base class, and abstraction layer
-   - Recommend inlining code that's only used once
-   - Suggest removing premature generalizations
-   - Identify over-engineered solutions
-
-5. **Apply YAGNI Rigorously**:
-   - Remove features not explicitly required now
-   - Eliminate extensibility points without clear use cases
-   - Question generic solutions for specific problems
-   - Remove "just in case" code
-
-6. **Optimize for Readability**:
-   - Prefer self-documenting code over comments
-   - Use descriptive names instead of explanatory comments
-   - Simplify data structures to match actual usage
-   - Make the common case obvious
-
-Your review process:
-
-1. First, identify the core purpose of the code
-2. List everything that doesn't directly serve that purpose
-3. For each complex section, propose a simpler alternative
-4. Create a prioritized list of simplification opportunities
-5. Estimate the lines of code that can be removed
-
-Output format:
-
-\`\`\`markdown
-## Simplification Analysis
-
-### Core Purpose
-[Clearly state what this code actually needs to do]
-
-### Unnecessary Complexity Found
-- [Specific issue with line numbers/file]
-- [Why it's unnecessary]
-- [Suggested simplification]
-
-### Code to Remove
-- [File:lines] - [Reason]
-- [Estimated LOC reduction: X]
-
-### Simplification Recommendations
-1. [Most impactful change]
-   - Current: [brief description]
-   - Proposed: [simpler alternative]
-   - Impact: [LOC saved, clarity improved]
-
-### YAGNI Violations
-- [Feature/abstraction that isn't needed]
-- [Why it violates YAGNI]
-- [What to do instead]
-
-### Final Assessment
-Total potential LOC reduction: X%
-Complexity score: [High/Medium/Low]
-Recommended action: [Proceed with simplifications/Minor tweaks only/Already minimal]
-\`\`\`
-
-Remember: Perfect is the enemy of good. The simplest code that works is often the best code. Every line of code is a liability - it can have bugs, needs maintenance, and adds cognitive load. Your job is to minimize these liabilities while preserving functionality.`;
+const YAGNI_TEMPLATE = readFileSync(
+  fileURLToPath(new URL("./templates/yagni.md", import.meta.url)),
+  "utf-8"
+);
 
 const PLAYWRIGHT_TEMPLATE = `# Playwright Browser Automation Skill
 

+ 4 - 4
src/tools/skill/mcp-manager.ts

@@ -85,7 +85,7 @@ export class SkillMcpManager {
   ): Promise<Client> {
     if (!("url" in config)) {
       throw new Error(
-        `MCP server "${info.serverName}" missing url for HTTP connection.`
+        `[mcp-manager] init: MCP server "${info.serverName}" missing url for HTTP connection.`
       );
     }
 
@@ -114,7 +114,7 @@ export class SkillMcpManager {
       }
       const errorMessage = error instanceof Error ? error.message : String(error);
       throw new Error(
-        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`
+        `[mcp-manager] connect: failed to connect to MCP server "${info.serverName}": ${errorMessage}`
       );
     }
 
@@ -137,7 +137,7 @@ export class SkillMcpManager {
   ): Promise<Client> {
     if (!("command" in config)) {
       throw new Error(
-        `MCP server "${info.serverName}" missing command for stdio connection.`
+        `[mcp-manager] init: MCP server "${info.serverName}" missing command for stdio connection.`
       );
     }
 
@@ -163,7 +163,7 @@ export class SkillMcpManager {
       }
       const errorMessage = error instanceof Error ? error.message : String(error);
       throw new Error(
-        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`
+        `[mcp-manager] connect: failed to connect to MCP server "${info.serverName}": ${errorMessage}`
       );
     }
 

+ 81 - 0
src/tools/skill/templates/yagni.md

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

+ 6 - 0
src/tools/skill/tools.ts

@@ -108,6 +108,12 @@ async function formatMcpCapabilities(
   return sections.join("\n");
 }
 
+/**
+ * Creates skill tools for agent use, including 'omos_skill' and 'omos_skill_mcp'.
+ * @param manager - The SkillMcpManager to use for MCP operations.
+ * @param pluginConfig - Optional plugin configuration for skill permissions.
+ * @returns An object containing the created tool definitions.
+ */
 export function createSkillTools(
   manager: SkillMcpManager,
   pluginConfig?: PluginConfig

+ 17 - 0
src/utils/agent-variant.ts

@@ -1,11 +1,22 @@
 import type { PluginConfig } from "../config";
 import { log } from "../shared/logger";
 
+/**
+ * Normalizes an agent name by trimming whitespace and removing leading '@'.
+ * @param agentName - The agent name to normalize.
+ * @returns The normalized agent name.
+ */
 export function normalizeAgentName(agentName: string): string {
   const trimmed = agentName.trim();
   return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
 }
 
+/**
+ * Resolves the configured variant for a given agent.
+ * @param config - The plugin configuration.
+ * @param agentName - The name of the agent.
+ * @returns The resolved variant name, or undefined if not found/invalid.
+ */
 export function resolveAgentVariant(
   config: PluginConfig | undefined,
   agentName: string
@@ -28,6 +39,12 @@ export function resolveAgentVariant(
   return trimmed;
 }
 
+/**
+ * Applies a variant to a prompt body if one is provided and not already present.
+ * @param variant - The variant to apply.
+ * @param body - The prompt body to modify.
+ * @returns The updated prompt body with the variant applied.
+ */
 export function applyAgentVariant<T extends { variant?: string }>(
   variant: string | undefined,
   body: T

+ 5 - 3
src/utils/polling.ts

@@ -40,7 +40,7 @@ export async function pollUntilStable<T>(
       return { success: false, aborted: true };
     }
 
-    await new Promise((r) => setTimeout(r, pollInterval));
+    await sleep(pollInterval);
 
     const currentData = await fetchFn();
 
@@ -60,8 +60,10 @@ export async function pollUntilStable<T>(
 }
 
 /**
- * Simple delay utility
+ * Delays execution for a specified number of milliseconds.
+ * @param ms - The number of milliseconds to sleep.
+ * @returns A promise that resolves after the delay.
  */
-export function delay(ms: number): Promise<void> {
+export function sleep(ms: number): Promise<void> {
   return new Promise((r) => setTimeout(r, ms));
 }

+ 39 - 28
src/utils/tmux.ts

@@ -1,6 +1,7 @@
 import { spawn } from "bun";
 import { log } from "../shared/logger";
 import type { TmuxConfig, TmuxLayout } from "../config/schema";
+import { sleep } from "./polling";
 
 let tmuxPath: string | null = null;
 let tmuxChecked = false;
@@ -41,22 +42,25 @@ async function isServerRunning(serverUrl: string): Promise<boolean> {
     if (available) {
       serverCheckUrl = serverUrl;
       serverAvailable = true;
-      log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
+      log("[tmux] health-check: success", { serverUrl, available, attempt });
       return true;
     }
 
     if (attempt < maxAttempts) {
-      await new Promise((r) => setTimeout(r, 250));
+      await sleep(250);
     }
   }
 
-  log("[tmux] isServerRunning: checked", { serverUrl, available: false });
+  log("[tmux] health-check: failed", { serverUrl, available: false });
   return false;
 }
 
 /**
  * Reset the server availability cache (useful when server might have started)
  */
+/**
+ * Resets the server availability cache.
+ */
 export function resetServerCheck(): void {
   serverAvailable = null;
   serverCheckUrl = null;
@@ -77,14 +81,14 @@ async function findTmuxPath(): Promise<string | null> {
 
     const exitCode = await proc.exited;
     if (exitCode !== 0) {
-      log("[tmux] findTmuxPath: 'which tmux' failed", { exitCode });
+      log("[tmux] find: 'which tmux' failed", { exitCode });
       return null;
     }
 
     const stdout = await new Response(proc.stdout).text();
     const path = stdout.trim().split("\n")[0];
     if (!path) {
-      log("[tmux] findTmuxPath: no path in output");
+      log("[tmux] find: no path in output");
       return null;
     }
 
@@ -95,14 +99,14 @@ async function findTmuxPath(): Promise<string | null> {
     });
     const verifyExit = await verifyProc.exited;
     if (verifyExit !== 0) {
-      log("[tmux] findTmuxPath: tmux -V failed", { path, verifyExit });
+      log("[tmux] find: tmux -V failed", { path, verifyExit });
       return null;
     }
 
-    log("[tmux] findTmuxPath: found tmux", { path });
+    log("[tmux] find: found tmux", { path });
     return path;
   } catch (err) {
-    log("[tmux] findTmuxPath: exception", { error: String(err) });
+    log("[tmux] find: exception", { error: err instanceof Error ? err.message : String(err) });
     return null;
   }
 }
@@ -117,13 +121,17 @@ export async function getTmuxPath(): Promise<string | null> {
 
   tmuxPath = await findTmuxPath();
   tmuxChecked = true;
-  log("[tmux] getTmuxPath: initialized", { tmuxPath });
+  log("[tmux] init: initialized", { tmuxPath });
   return tmuxPath;
 }
 
 /**
  * Check if we're running inside tmux
  */
+/**
+ * Checks if the current process is running inside a tmux session.
+ * @returns True if running inside tmux, false otherwise.
+ */
 export function isInsideTmux(): boolean {
   return !!process.env.TMUX;
 }
@@ -160,9 +168,9 @@ async function applyLayout(tmux: string, layout: TmuxLayout, mainPaneSize: numbe
       await reapplyProc.exited;
     }
 
-    log("[tmux] applyLayout: applied", { layout, mainPaneSize });
+    log("[tmux] apply-layout: applied", { layout, mainPaneSize });
   } catch (err) {
-    log("[tmux] applyLayout: exception", { error: String(err) });
+    log("[tmux] apply-layout: exception", { error: err instanceof Error ? err.message : String(err) });
   }
 }
 
@@ -183,15 +191,15 @@ export async function spawnTmuxPane(
   config: TmuxConfig,
   serverUrl: string
 ): Promise<SpawnPaneResult> {
-  log("[tmux] spawnTmuxPane called", { sessionId, description, config, serverUrl });
+  log("[tmux] spawn: start", { sessionId, description, config, serverUrl });
 
   if (!config.enabled) {
-    log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
+    log("[tmux] spawn: disabled in config");
     return { success: false };
   }
 
   if (!isInsideTmux()) {
-    log("[tmux] spawnTmuxPane: not inside tmux, skipping");
+    log("[tmux] spawn: not inside tmux");
     return { success: false };
   }
 
@@ -199,7 +207,7 @@ export async function spawnTmuxPane(
   // This is needed because serverUrl may be a fallback even when no server is running
   const serverRunning = await isServerRunning(serverUrl);
   if (!serverRunning) {
-    log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
+    log("[tmux] spawn: server not running", {
       serverUrl,
       hint: "Start opencode with --port 4096"
     });
@@ -208,7 +216,7 @@ export async function spawnTmuxPane(
 
   const tmux = await getTmuxPath();
   if (!tmux) {
-    log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
+    log("[tmux] spawn: binary not found");
     return { success: false };
   }
 
@@ -231,7 +239,7 @@ export async function spawnTmuxPane(
       opencodeCmd,
     ];
 
-    log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
+    log("[tmux] spawn: executing", { tmux, args, opencodeCmd });
 
     const proc = spawn([tmux, ...args], {
       stdout: "pipe",
@@ -243,7 +251,7 @@ export async function spawnTmuxPane(
     const stderr = await new Response(proc.stderr).text();
     const paneId = stdout.trim(); // e.g., "%42"
 
-    log("[tmux] spawnTmuxPane: split result", { exitCode, paneId, stderr: stderr.trim() });
+    log("[tmux] spawn: result", { exitCode, paneId, stderr: stderr.trim() });
 
     if (exitCode === 0 && paneId) {
       // Rename the pane for visibility
@@ -258,13 +266,13 @@ export async function spawnTmuxPane(
       const mainPaneSize = config.main_pane_size ?? 60;
       await applyLayout(tmux, layout, mainPaneSize);
 
-      log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", { paneId, layout });
+      log("[tmux] spawn: success", { paneId, layout });
       return { success: true, paneId };
     }
 
     return { success: false };
   } catch (err) {
-    log("[tmux] spawnTmuxPane: exception", { error: String(err) });
+    log("[tmux] spawn: exception", { error: err instanceof Error ? err.message : String(err) });
     return { success: false };
   }
 }
@@ -273,16 +281,16 @@ export async function spawnTmuxPane(
  * Close a tmux pane by its ID and reapply layout to rebalance remaining panes
  */
 export async function closeTmuxPane(paneId: string): Promise<boolean> {
-  log("[tmux] closeTmuxPane called", { paneId });
+  log("[tmux] close: start", { paneId });
 
   if (!paneId) {
-    log("[tmux] closeTmuxPane: no paneId provided");
+    log("[tmux] close: no paneId provided");
     return false;
   }
 
   const tmux = await getTmuxPath();
   if (!tmux) {
-    log("[tmux] closeTmuxPane: tmux binary not found");
+    log("[tmux] close: binary not found");
     return false;
   }
 
@@ -295,27 +303,27 @@ export async function closeTmuxPane(paneId: string): Promise<boolean> {
     const exitCode = await proc.exited;
     const stderr = await new Response(proc.stderr).text();
 
-    log("[tmux] closeTmuxPane: result", { exitCode, stderr: stderr.trim() });
+    log("[tmux] close: result", { exitCode, stderr: stderr.trim() });
 
     if (exitCode === 0) {
-      log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
+      log("[tmux] close: success", { paneId });
 
       // Reapply layout to rebalance remaining panes
       if (storedConfig) {
         const layout = storedConfig.layout ?? "main-vertical";
         const mainPaneSize = storedConfig.main_pane_size ?? 60;
         await applyLayout(tmux, layout, mainPaneSize);
-        log("[tmux] closeTmuxPane: layout reapplied", { layout });
+        log("[tmux] close: layout reapplied", { layout });
       }
 
       return true;
     }
 
     // Pane might already be closed (user closed it manually, or process exited)
-    log("[tmux] closeTmuxPane: failed (pane may already be closed)", { paneId });
+    log("[tmux] close: failed (pane may already be closed)", { paneId });
     return false;
   } catch (err) {
-    log("[tmux] closeTmuxPane: exception", { error: String(err) });
+    log("[tmux] close: exception", { error: err instanceof Error ? err.message : String(err) });
     return false;
   }
 }
@@ -323,6 +331,9 @@ export async function closeTmuxPane(paneId: string): Promise<boolean> {
 /**
  * Start background check for tmux availability
  */
+/**
+ * Starts a background check for tmux availability by looking for the binary path.
+ */
 export function startTmuxCheck(): void {
   if (!tmuxChecked) {
     getTmuxPath().catch(() => {});