-
Notifications
You must be signed in to change notification settings - Fork 7
t008.4: oh-my-opencode compatibility for aidevops-opencode plugin #1157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,12 @@ | |||||||||||||||||||||||||||||||||
| const QUALITY_LOG = join(LOGS_DIR, "quality-hooks.log"); | ||||||||||||||||||||||||||||||||||
| const IS_MACOS = platform() === "darwin"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Cached oh-my-opencode detection result. | ||||||||||||||||||||||||||||||||||
| * @type {{ detected: boolean, version: string, mcps: string[], hooks: string[], configPath: string } | null} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| let _omocState = null; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
| // Utility helpers | ||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
|
|
@@ -92,6 +98,184 @@ | |||||||||||||||||||||||||||||||||
| return { data, body }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
| // Phase 0: oh-my-opencode (OMOC) Detection & Compatibility (t008.4) | ||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * MCPs known to be managed by oh-my-opencode. | ||||||||||||||||||||||||||||||||||
| * When OMOC is detected, aidevops skips registering these to avoid duplicates. | ||||||||||||||||||||||||||||||||||
| * Maps OMOC MCP name → aidevops registry name (if different) or null (no equivalent). | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| const OMOC_MANAGED_MCPS = { | ||||||||||||||||||||||||||||||||||
| websearch: null, // Exa web search — no aidevops equivalent | ||||||||||||||||||||||||||||||||||
| context7: "context7", // Both plugins register context7 | ||||||||||||||||||||||||||||||||||
| grep_app: null, // GitHub code search — no aidevops equivalent | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Hooks known to be provided by oh-my-opencode. | ||||||||||||||||||||||||||||||||||
| * aidevops skips overlapping hook behaviour when these are active. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| const OMOC_HOOK_NAMES = [ | ||||||||||||||||||||||||||||||||||
| "comment-checker", | ||||||||||||||||||||||||||||||||||
| "todo-enforcer", | ||||||||||||||||||||||||||||||||||
| "todo-continuation-enforcer", | ||||||||||||||||||||||||||||||||||
| "aggressive-truncation", | ||||||||||||||||||||||||||||||||||
| "auto-resume", | ||||||||||||||||||||||||||||||||||
| "think-mode", | ||||||||||||||||||||||||||||||||||
| "ralph-loop", | ||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Detect oh-my-opencode presence and capabilities. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Detection strategy (ordered by reliability): | ||||||||||||||||||||||||||||||||||
| * 1. Check OpenCode config for OMOC in plugin array | ||||||||||||||||||||||||||||||||||
| * 2. Check for OMOC config files (project-level, then user-level) | ||||||||||||||||||||||||||||||||||
| * 3. Check for OMOC npm installation | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Results are cached after first call. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * @param {string} [directory] - Project directory to check for local config | ||||||||||||||||||||||||||||||||||
| * @returns {{ detected: boolean, version: string, mcps: string[], hooks: string[], configPath: string }} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| function detectOhMyOpenCode(directory) { | ||||||||||||||||||||||||||||||||||
| if (_omocState !== null) return _omocState; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| _omocState = { | ||||||||||||||||||||||||||||||||||
| detected: false, | ||||||||||||||||||||||||||||||||||
| version: "", | ||||||||||||||||||||||||||||||||||
| mcps: [], | ||||||||||||||||||||||||||||||||||
| hooks: [], | ||||||||||||||||||||||||||||||||||
| configPath: "", | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // 1. Check OpenCode config for OMOC in plugin array | ||||||||||||||||||||||||||||||||||
| const ocConfigPaths = [ | ||||||||||||||||||||||||||||||||||
| join(HOME, ".config", "opencode", "opencode.json"), | ||||||||||||||||||||||||||||||||||
| join(HOME, ".config", "opencode", "opencode.jsonc"), | ||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (const configPath of ocConfigPaths) { | ||||||||||||||||||||||||||||||||||
| const content = readIfExists(configPath); | ||||||||||||||||||||||||||||||||||
| if (!content) continue; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| // Strip JSONC comments for parsing | ||||||||||||||||||||||||||||||||||
| const cleaned = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); | ||||||||||||||||||||||||||||||||||
| const config = JSON.parse(cleaned); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (Array.isArray(config.plugin) && config.plugin.includes("oh-my-opencode")) { | ||||||||||||||||||||||||||||||||||
| _omocState.detected = true; | ||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||
| // JSON parse error — try next config | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // 2. Check for OMOC config files | ||||||||||||||||||||||||||||||||||
| const omocConfigPaths = [ | ||||||||||||||||||||||||||||||||||
| directory ? join(directory, ".opencode", "oh-my-opencode.json") : "", | ||||||||||||||||||||||||||||||||||
| join(HOME, ".config", "opencode", "oh-my-opencode.json"), | ||||||||||||||||||||||||||||||||||
| ].filter(Boolean); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (const configPath of omocConfigPaths) { | ||||||||||||||||||||||||||||||||||
| if (existsSync(configPath)) { | ||||||||||||||||||||||||||||||||||
| _omocState.detected = true; | ||||||||||||||||||||||||||||||||||
| _omocState.configPath = configPath; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Parse OMOC config to discover disabled hooks and MCP overrides | ||||||||||||||||||||||||||||||||||
| const content = readIfExists(configPath); | ||||||||||||||||||||||||||||||||||
| if (content) { | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const cleaned = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); | ||||||||||||||||||||||||||||||||||
| const omocConfig = JSON.parse(cleaned); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Discover which hooks are active (all enabled unless in disabled_hooks) | ||||||||||||||||||||||||||||||||||
| const disabledHooks = omocConfig.disabled_hooks || []; | ||||||||||||||||||||||||||||||||||
| _omocState.hooks = OMOC_HOOK_NAMES.filter((h) => !disabledHooks.includes(h)); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Discover which MCPs are active (all enabled unless explicitly disabled) | ||||||||||||||||||||||||||||||||||
| const mcpConfig = omocConfig.mcp || {}; | ||||||||||||||||||||||||||||||||||
| _omocState.mcps = Object.keys(OMOC_MANAGED_MCPS).filter((name) => { | ||||||||||||||||||||||||||||||||||
| const mcpEntry = mcpConfig[name]; | ||||||||||||||||||||||||||||||||||
| // MCP is active unless explicitly disabled in OMOC config | ||||||||||||||||||||||||||||||||||
| return !mcpEntry || mcpEntry.enabled !== false; | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||
| // Parse error — assume defaults (all MCPs and hooks active) | ||||||||||||||||||||||||||||||||||
| _omocState.mcps = Object.keys(OMOC_MANAGED_MCPS); | ||||||||||||||||||||||||||||||||||
| _omocState.hooks = [...OMOC_HOOK_NAMES]; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // 3. If not yet detected, check npm for OMOC installation | ||||||||||||||||||||||||||||||||||
| if (!_omocState.detected) { | ||||||||||||||||||||||||||||||||||
| const npmCheck = run("npm ls oh-my-opencode --json 2>/dev/null", 5000); | ||||||||||||||||||||||||||||||||||
| if (npmCheck && npmCheck.includes("oh-my-opencode")) { | ||||||||||||||||||||||||||||||||||
| _omocState.detected = true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+220
to
+222
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking for the package by using
Suggested change
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // 4. Get OMOC version if detected | ||||||||||||||||||||||||||||||||||
| if (_omocState.detected) { | ||||||||||||||||||||||||||||||||||
| const version = run("npm view oh-my-opencode version 2>/dev/null", 5000); | ||||||||||||||||||||||||||||||||||
| if (version) { | ||||||||||||||||||||||||||||||||||
| _omocState.version = version; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Default MCPs and hooks if not populated from config | ||||||||||||||||||||||||||||||||||
| if (_omocState.mcps.length === 0) { | ||||||||||||||||||||||||||||||||||
| _omocState.mcps = Object.keys(OMOC_MANAGED_MCPS); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (_omocState.hooks.length === 0) { | ||||||||||||||||||||||||||||||||||
| _omocState.hooks = [...OMOC_HOOK_NAMES]; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| console.error( | ||||||||||||||||||||||||||||||||||
| `[aidevops] oh-my-opencode detected${_omocState.version ? ` (v${_omocState.version})` : ""}: ` + | ||||||||||||||||||||||||||||||||||
| `${_omocState.mcps.length} MCPs, ${_omocState.hooks.length} hooks active — ` + | ||||||||||||||||||||||||||||||||||
| `aidevops will complement (not duplicate) OMOC features`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return _omocState; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Check if a specific MCP is managed by oh-my-opencode. | ||||||||||||||||||||||||||||||||||
| * @param {string} mcpName - aidevops MCP registry name | ||||||||||||||||||||||||||||||||||
| * @returns {boolean} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| function isMcpManagedByOmoc(mcpName) { | ||||||||||||||||||||||||||||||||||
| const omoc = detectOhMyOpenCode(); | ||||||||||||||||||||||||||||||||||
| if (!omoc.detected) return false; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Check if any OMOC MCP maps to this aidevops MCP name | ||||||||||||||||||||||||||||||||||
| for (const [omocName, aidevopsName] of Object.entries(OMOC_MANAGED_MCPS)) { | ||||||||||||||||||||||||||||||||||
| if (aidevopsName === mcpName && omoc.mcps.includes(omocName)) { | ||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Check if a specific hook type is handled by oh-my-opencode. | ||||||||||||||||||||||||||||||||||
| * @param {string} hookName - OMOC hook name to check | ||||||||||||||||||||||||||||||||||
| * @returns {boolean} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| function isHookManagedByOmoc(hookName) { | ||||||||||||||||||||||||||||||||||
| const omoc = detectOhMyOpenCode(); | ||||||||||||||||||||||||||||||||||
| if (!omoc.detected) return false; | ||||||||||||||||||||||||||||||||||
| return omoc.hooks.includes(hookName); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
| // Phase 1: Agent Loader | ||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
|
|
@@ -402,8 +586,9 @@ | |||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Oh-My-OpenCode tool patterns to disable globally. | ||||||||||||||||||||||||||||||||||
| * These MCPs may exist from old configs or OmO installations. | ||||||||||||||||||||||||||||||||||
| * Oh-My-OpenCode tool patterns to disable globally when OMOC is NOT detected. | ||||||||||||||||||||||||||||||||||
| * When OMOC IS detected, these are left alone (OMOC manages them). | ||||||||||||||||||||||||||||||||||
| * These MCPs may exist from old configs or stale OmO installations. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| const OMO_DISABLED_PATTERNS = ["grep_app_*", "websearch_*", "gh_grep_*"]; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -415,65 +600,75 @@ | |||||||||||||||||||||||||||||||||
| * @param {object} config - OpenCode Config object (mutable) | ||||||||||||||||||||||||||||||||||
| * @returns {number} Number of MCPs registered | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| function registerMcpServers(config) { | ||||||||||||||||||||||||||||||||||
| if (!config.mcp) config.mcp = {}; | ||||||||||||||||||||||||||||||||||
| if (!config.tools) config.tools = {}; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const registry = getMcpRegistry(); | ||||||||||||||||||||||||||||||||||
| let registered = 0; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (const mcp of registry) { | ||||||||||||||||||||||||||||||||||
| // Skip MCPs managed by oh-my-opencode to avoid duplicates (t008.4) | ||||||||||||||||||||||||||||||||||
| if (isMcpManagedByOmoc(mcp.name)) { | ||||||||||||||||||||||||||||||||||
| console.error(`[aidevops] Skipping MCP '${mcp.name}' — managed by oh-my-opencode`); | ||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Skip macOS-only MCPs on other platforms | ||||||||||||||||||||||||||||||||||
| if (mcp.macOnly && !IS_MACOS) continue; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Skip local MCPs whose binary isn't installed | ||||||||||||||||||||||||||||||||||
| if (mcp.requiresBinary) { | ||||||||||||||||||||||||||||||||||
| const binaryPath = run(`which ${mcp.requiresBinary}`); | ||||||||||||||||||||||||||||||||||
| if (!binaryPath) { | ||||||||||||||||||||||||||||||||||
| // Disable tools if binary not available | ||||||||||||||||||||||||||||||||||
| if (mcp.toolPattern) { | ||||||||||||||||||||||||||||||||||
| config.tools[mcp.toolPattern] = false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Register MCP server if not already configured (or if alwaysOverwrite) | ||||||||||||||||||||||||||||||||||
| if (!config.mcp[mcp.name] || mcp.alwaysOverwrite) { | ||||||||||||||||||||||||||||||||||
| if (mcp.type === "remote" && mcp.url) { | ||||||||||||||||||||||||||||||||||
| config.mcp[mcp.name] = { | ||||||||||||||||||||||||||||||||||
| type: "remote", | ||||||||||||||||||||||||||||||||||
| url: mcp.url, | ||||||||||||||||||||||||||||||||||
| enabled: mcp.eager, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| config.mcp[mcp.name] = { | ||||||||||||||||||||||||||||||||||
| type: "local", | ||||||||||||||||||||||||||||||||||
| command: mcp.command, | ||||||||||||||||||||||||||||||||||
| enabled: mcp.eager, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| registered++; | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| // Enforce loading policy on existing MCPs | ||||||||||||||||||||||||||||||||||
| config.mcp[mcp.name].enabled = mcp.eager; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Set global tool permissions | ||||||||||||||||||||||||||||||||||
| if (mcp.toolPattern) { | ||||||||||||||||||||||||||||||||||
| config.tools[mcp.toolPattern] = mcp.globallyEnabled; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Disable Oh-My-OpenCode tool patterns globally | ||||||||||||||||||||||||||||||||||
| for (const pattern of OMO_DISABLED_PATTERNS) { | ||||||||||||||||||||||||||||||||||
| if (!(pattern in config.tools)) { | ||||||||||||||||||||||||||||||||||
| config.tools[pattern] = false; | ||||||||||||||||||||||||||||||||||
| // Disable stale Oh-My-OpenCode tool patterns — but only when OMOC is NOT active. | ||||||||||||||||||||||||||||||||||
| // When OMOC is detected, it manages its own tool permissions. | ||||||||||||||||||||||||||||||||||
| const omoc = detectOhMyOpenCode(); | ||||||||||||||||||||||||||||||||||
| if (!omoc.detected) { | ||||||||||||||||||||||||||||||||||
| for (const pattern of OMO_DISABLED_PATTERNS) { | ||||||||||||||||||||||||||||||||||
| if (!(pattern in config.tools)) { | ||||||||||||||||||||||||||||||||||
| config.tools[pattern] = false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return registered; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Apply per-agent MCP tool permissions. | ||||||||||||||||||||||||||||||||||
|
|
@@ -573,84 +768,84 @@ | |||||||||||||||||||||||||||||||||
| * @param {string} filePath | ||||||||||||||||||||||||||||||||||
| * @returns {{ violations: number, details: string[] }} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| function validateReturnStatements(filePath) { | ||||||||||||||||||||||||||||||||||
| const details = []; | ||||||||||||||||||||||||||||||||||
| let violations = 0; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const content = readFileSync(filePath, "utf-8"); | ||||||||||||||||||||||||||||||||||
| const lines = content.split("\n"); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Find function definitions and check for return statements | ||||||||||||||||||||||||||||||||||
| let inFunction = false; | ||||||||||||||||||||||||||||||||||
| let functionName = ""; | ||||||||||||||||||||||||||||||||||
| let functionStart = 0; | ||||||||||||||||||||||||||||||||||
| let braceDepth = 0; | ||||||||||||||||||||||||||||||||||
| let hasReturn = false; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (let i = 0; i < lines.length; i++) { | ||||||||||||||||||||||||||||||||||
| const line = lines[i]; | ||||||||||||||||||||||||||||||||||
| const trimmed = line.trim(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Detect function definition: name() { or function name { | ||||||||||||||||||||||||||||||||||
| const funcMatch = trimmed.match( | ||||||||||||||||||||||||||||||||||
| /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\)\s*\{/, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| const funcMatch2 = trimmed.match(/^function\s+([a-zA-Z_][a-zA-Z0-9_]*)/); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (funcMatch || funcMatch2) { | ||||||||||||||||||||||||||||||||||
| if (inFunction && !hasReturn) { | ||||||||||||||||||||||||||||||||||
| details.push( | ||||||||||||||||||||||||||||||||||
| ` Line ${functionStart}: function '${functionName}' missing explicit return`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| violations++; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| inFunction = true; | ||||||||||||||||||||||||||||||||||
| functionName = funcMatch ? funcMatch[1] : funcMatch2[1]; | ||||||||||||||||||||||||||||||||||
| functionStart = i + 1; | ||||||||||||||||||||||||||||||||||
| braceDepth = trimmed.includes("{") ? 1 : 0; | ||||||||||||||||||||||||||||||||||
| hasReturn = false; | ||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (inFunction) { | ||||||||||||||||||||||||||||||||||
| // Track brace depth | ||||||||||||||||||||||||||||||||||
| for (const ch of trimmed) { | ||||||||||||||||||||||||||||||||||
| if (ch === "{") braceDepth++; | ||||||||||||||||||||||||||||||||||
| else if (ch === "}") braceDepth--; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Check for return statement | ||||||||||||||||||||||||||||||||||
| if (/\breturn\s+[0-9]/.test(trimmed) || /\breturn\s*$/.test(trimmed)) { | ||||||||||||||||||||||||||||||||||
| hasReturn = true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Function ended | ||||||||||||||||||||||||||||||||||
| if (braceDepth <= 0) { | ||||||||||||||||||||||||||||||||||
| if (!hasReturn) { | ||||||||||||||||||||||||||||||||||
| details.push( | ||||||||||||||||||||||||||||||||||
| ` Line ${functionStart}: function '${functionName}' missing explicit return`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| violations++; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| inFunction = false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Handle last function if file ends inside it | ||||||||||||||||||||||||||||||||||
| if (inFunction && !hasReturn) { | ||||||||||||||||||||||||||||||||||
| details.push( | ||||||||||||||||||||||||||||||||||
| ` Line ${functionStart}: function '${functionName}' missing explicit return`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| violations++; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||
| // File read error — skip validation | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { violations, details }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Validate positional parameter usage in shell scripts. | ||||||||||||||||||||||||||||||||||
|
|
@@ -935,55 +1130,55 @@ | |||||||||||||||||||||||||||||||||
| * @param {object} input - { tool, sessionID, callID } | ||||||||||||||||||||||||||||||||||
| * @param {object} output - { title, output, metadata } (mutable) | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| async function toolExecuteAfter(input, output) { | ||||||||||||||||||||||||||||||||||
| const toolName = input.tool || ""; | ||||||||||||||||||||||||||||||||||
| const title = output.title || ""; | ||||||||||||||||||||||||||||||||||
| const outputText = output.output || ""; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Track git operations for pattern recording | ||||||||||||||||||||||||||||||||||
| if (toolName === "Bash" || toolName === "bash") { | ||||||||||||||||||||||||||||||||||
| if (title.includes("git commit") || title.includes("git push")) { | ||||||||||||||||||||||||||||||||||
| console.error(`[aidevops] Git operation detected: ${title}`); | ||||||||||||||||||||||||||||||||||
| qualityLog("INFO", `Git operation: ${title}`); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Record pattern if pattern-tracker-helper.sh is available | ||||||||||||||||||||||||||||||||||
| const patternTracker = join(SCRIPTS_DIR, "pattern-tracker-helper.sh"); | ||||||||||||||||||||||||||||||||||
| if (existsSync(patternTracker)) { | ||||||||||||||||||||||||||||||||||
| const success = !outputText.includes("error") && !outputText.includes("fatal"); | ||||||||||||||||||||||||||||||||||
| const patternType = success ? "SUCCESS_PATTERN" : "FAILURE_PATTERN"; | ||||||||||||||||||||||||||||||||||
| run( | ||||||||||||||||||||||||||||||||||
| `bash "${patternTracker}" record "${patternType}" "git operation: ${title.substring(0, 100)}" --tag "quality-hook" 2>/dev/null`, | ||||||||||||||||||||||||||||||||||
| 5000, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Track ShellCheck/lint runs in Bash commands | ||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||
| title.includes("shellcheck") || | ||||||||||||||||||||||||||||||||||
| title.includes("linters-local") | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| const passed = !outputText.includes("error") && !outputText.includes("violation"); | ||||||||||||||||||||||||||||||||||
| qualityLog( | ||||||||||||||||||||||||||||||||||
| passed ? "INFO" : "WARN", | ||||||||||||||||||||||||||||||||||
| `Lint run: ${title} — ${passed ? "PASS" : "issues found"}`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Track Write/Edit operations for quality metrics | ||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||
| toolName === "Write" || | ||||||||||||||||||||||||||||||||||
| toolName === "Edit" || | ||||||||||||||||||||||||||||||||||
| toolName === "write" || | ||||||||||||||||||||||||||||||||||
| toolName === "edit" | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| const filePath = output.metadata?.filePath || ""; | ||||||||||||||||||||||||||||||||||
| if (filePath) { | ||||||||||||||||||||||||||||||||||
| qualityLog("INFO", `File modified: ${filePath} via ${toolName}`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
| // Phase 4: Shell Environment | ||||||||||||||||||||||||||||||||||
|
|
@@ -1156,6 +1351,29 @@ | |||||||||||||||||||||||||||||||||
| ].join("\n"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Get oh-my-opencode compatibility state for compaction context. | ||||||||||||||||||||||||||||||||||
| * @returns {string} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| function getOmocState() { | ||||||||||||||||||||||||||||||||||
| const omoc = detectOhMyOpenCode(); | ||||||||||||||||||||||||||||||||||
| if (!omoc.detected) return ""; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const lines = ["## oh-my-opencode Compatibility"]; | ||||||||||||||||||||||||||||||||||
| lines.push(`oh-my-opencode detected${omoc.version ? ` (v${omoc.version})` : ""}`); | ||||||||||||||||||||||||||||||||||
| lines.push("aidevops complements OMOC — no duplicate MCPs or hooks."); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (omoc.mcps.length > 0) { | ||||||||||||||||||||||||||||||||||
| lines.push(`OMOC-managed MCPs (skipped by aidevops): ${omoc.mcps.join(", ")}`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (omoc.hooks.length > 0) { | ||||||||||||||||||||||||||||||||||
| lines.push(`OMOC hooks active: ${omoc.hooks.join(", ")}`); | ||||||||||||||||||||||||||||||||||
| lines.push("aidevops hooks (ShellCheck, return-statements, secrets, MD031) are complementary."); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return lines.join("\n"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Compaction hook — inject aidevops context into compaction summary. | ||||||||||||||||||||||||||||||||||
| * @param {object} _input - { sessionID } | ||||||||||||||||||||||||||||||||||
|
|
@@ -1170,6 +1388,7 @@ | |||||||||||||||||||||||||||||||||
| getRelevantMemories(directory), | ||||||||||||||||||||||||||||||||||
| getGitContext(directory), | ||||||||||||||||||||||||||||||||||
| getMailboxState(), | ||||||||||||||||||||||||||||||||||
| getOmocState(), | ||||||||||||||||||||||||||||||||||
| ].filter(Boolean); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (sections.length === 0) return; | ||||||||||||||||||||||||||||||||||
|
|
@@ -1287,53 +1506,53 @@ | |||||||||||||||||||||||||||||||||
| aidevops_quality_check: { | ||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||
| 'Run quality checks on a file or the full pre-commit pipeline. Args: file (string, path to check) OR command "pre-commit" to run full pipeline on staged files', | ||||||||||||||||||||||||||||||||||
| async execute(args) { | ||||||||||||||||||||||||||||||||||
| const file = args.file || args.command || args; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Full pre-commit pipeline | ||||||||||||||||||||||||||||||||||
| if (file === "pre-commit" || file === "staged") { | ||||||||||||||||||||||||||||||||||
| const hookScript = join(SCRIPTS_DIR, "pre-commit-hook.sh"); | ||||||||||||||||||||||||||||||||||
| if (!existsSync(hookScript)) { | ||||||||||||||||||||||||||||||||||
| return "pre-commit-hook.sh not found — run aidevops update"; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const result = execSync(`bash "${hookScript}"`, { | ||||||||||||||||||||||||||||||||||
| encoding: "utf-8", | ||||||||||||||||||||||||||||||||||
| timeout: 30000, | ||||||||||||||||||||||||||||||||||
| stdio: ["pipe", "pipe", "pipe"], | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| return `Pre-commit quality checks PASSED:\n${result.trim()}`; | ||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||
| const cmdOutput = (err.stdout || "") + (err.stderr || ""); | ||||||||||||||||||||||||||||||||||
| return `Pre-commit quality checks FAILED:\n${cmdOutput.trim()}`; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Single file check | ||||||||||||||||||||||||||||||||||
| if (typeof file === "string" && file.endsWith(".sh")) { | ||||||||||||||||||||||||||||||||||
| const { totalViolations, report } = runShellQualityPipeline(file); | ||||||||||||||||||||||||||||||||||
| return totalViolations > 0 | ||||||||||||||||||||||||||||||||||
| ? `Quality check: ${totalViolations} issue(s) found:\n${report}` | ||||||||||||||||||||||||||||||||||
| : "Quality check: all checks passed."; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (typeof file === "string" && file.endsWith(".md")) { | ||||||||||||||||||||||||||||||||||
| const { totalViolations, report } = runMarkdownQualityPipeline(file); | ||||||||||||||||||||||||||||||||||
| return totalViolations > 0 | ||||||||||||||||||||||||||||||||||
| ? `Markdown check: ${totalViolations} issue(s) found:\n${report}` | ||||||||||||||||||||||||||||||||||
| : "Markdown check: all checks passed."; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Generic secrets scan | ||||||||||||||||||||||||||||||||||
| if (typeof file === "string" && existsSync(file)) { | ||||||||||||||||||||||||||||||||||
| const secretResult = scanForSecrets(file); | ||||||||||||||||||||||||||||||||||
| return secretResult.violations > 0 | ||||||||||||||||||||||||||||||||||
| ? `Secrets scan: ${secretResult.violations} potential issue(s):\n${secretResult.details.join("\n")}` | ||||||||||||||||||||||||||||||||||
| : "Secrets scan: no issues found."; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return `Usage: pass a file path (.sh or .md) or "pre-commit" for full pipeline`; | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| aidevops_install_hooks: { | ||||||||||||||||||||||||||||||||||
|
|
@@ -1397,6 +1616,7 @@ | |||||||||||||||||||||||||||||||||
| * aidevops OpenCode Plugin | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Provides: | ||||||||||||||||||||||||||||||||||
| * 0. oh-my-opencode detection — detects OMOC presence and deduplicates (t008.4) | ||||||||||||||||||||||||||||||||||
| * 1. Config hook — dynamic agent loading + MCP server registration from ~/.aidevops/agents/ | ||||||||||||||||||||||||||||||||||
| * 2. Custom tools — aidevops CLI, memory, pre-edit check, quality check, hook installer | ||||||||||||||||||||||||||||||||||
| * 3. Quality hooks — full pre-commit pipeline (ShellCheck, return statements, | ||||||||||||||||||||||||||||||||||
|
|
@@ -1409,27 +1629,37 @@ | |||||||||||||||||||||||||||||||||
| * - Enforces eager/lazy loading policy (only osgrep starts at launch) | ||||||||||||||||||||||||||||||||||
| * - Sets global tool permissions and per-agent MCP tool enablement | ||||||||||||||||||||||||||||||||||
| * - Skips MCPs whose required binaries aren't installed | ||||||||||||||||||||||||||||||||||
| * - Skips MCPs managed by oh-my-opencode when OMOC is detected (t008.4) | ||||||||||||||||||||||||||||||||||
| * - Disables Oh-My-OpenCode tool patterns globally | ||||||||||||||||||||||||||||||||||
| * - Complements generate-opencode-agents.sh (shell script takes precedence) | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * oh-my-opencode compatibility (Phase 0, t008.4): | ||||||||||||||||||||||||||||||||||
| * - Detects OMOC via OpenCode config, OMOC config files, and npm | ||||||||||||||||||||||||||||||||||
| * - Skips MCP registration for MCPs managed by OMOC (context7, websearch, grep_app) | ||||||||||||||||||||||||||||||||||
| * - Quality hooks are complementary (aidevops: ShellCheck, secrets; OMOC: comments, todos) | ||||||||||||||||||||||||||||||||||
| * - OMOC state injected into compaction context for session continuity | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * @type {import('@opencode-ai/plugin').Plugin} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export async function AidevopsPlugin({ directory }) { | ||||||||||||||||||||||||||||||||||
| // Phase 0: Detect oh-my-opencode early so all hooks can adapt | ||||||||||||||||||||||||||||||||||
| detectOhMyOpenCode(directory); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| // Phase 1+2: Dynamic agent and config injection | ||||||||||||||||||||||||||||||||||
| config: async (config) => configHook(config), | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Phase 1: Custom tools | ||||||||||||||||||||||||||||||||||
| tool: createTools(), | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Phase 3: Quality hooks | ||||||||||||||||||||||||||||||||||
| // Phase 3: Quality hooks (complementary to OMOC — no overlap) | ||||||||||||||||||||||||||||||||||
| "tool.execute.before": toolExecuteBefore, | ||||||||||||||||||||||||||||||||||
| "tool.execute.after": toolExecuteAfter, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Phase 4: Shell environment | ||||||||||||||||||||||||||||||||||
| "shell.env": shellEnvHook, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Compaction context (existing + improved) | ||||||||||||||||||||||||||||||||||
| // Compaction context (includes OMOC state when detected) | ||||||||||||||||||||||||||||||||||
| "experimental.session.compacting": async (input, output) => | ||||||||||||||||||||||||||||||||||
| compactingHook(input, output, directory), | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of
2>/dev/nullhere and on line 227 appears to violate the repository style guide (line 50), which states that blanket suppression should be avoided. Thenpm lscommand is expected to fail with a non-zero exit code if the package is not found. It would be better to allow therunutility to handle this expected failure gracefully, perhaps with an option to ignore non-zero exit codes, rather than suppressing stderr at the command level.References
2>/dev/null, allowing it only when redirecting to log files. (link)