const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); const promptBuilder = require("./prompt-builder"); const contextManager = require("./context-manager"); const featureLoader = require("./feature-loader"); const mcpServerFactory = require("./mcp-server-factory"); const { ModelRegistry } = require("./model-registry"); const { ModelProviderFactory } = require("./model-provider"); // Model name mappings for Claude (legacy - kept for backwards compatibility) const MODEL_MAP = { haiku: "claude-haiku-4-5", sonnet: "claude-sonnet-4-20250514", opus: "claude-opus-4-5-20251101", }; // Thinking level to budget_tokens mapping // These values control how much "thinking time" the model gets for extended thinking const THINKING_BUDGET_MAP = { none: null, // No extended thinking low: 4096, // Light thinking medium: 16384, // Moderate thinking high: 65536, // Deep thinking ultrathink: 262144, // Ultra-deep thinking (maximum reasoning) }; /** * Feature Executor - Handles feature implementation using Claude Agent SDK * Now supports multiple model providers (Claude, Codex/OpenAI) */ class FeatureExecutor { /** * Get the model string based on feature's model setting * Supports both Claude and Codex/OpenAI models */ getModelString(feature) { const modelKey = feature.model || "opus"; // Default to opus // First check if this is a Codex model - they use the model key directly as the string if (ModelRegistry.isCodexModel(modelKey)) { const model = ModelRegistry.getModel(modelKey); if (model && model.modelString) { console.log(`[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${model.modelString} (Codex model)`); return model.modelString; } // If model exists in registry but somehow no modelString, use the key itself console.log(`[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${modelKey} (Codex fallback)`); return modelKey; } // For Claude models, use the registry lookup let modelString = ModelRegistry.getModelString(modelKey); // Fallback to MODEL_MAP if registry doesn't have it (legacy support) if (!modelString) { modelString = MODEL_MAP[modelKey]; } // Final fallback to opus for Claude models only if (!modelString) { modelString = MODEL_MAP.opus; } // Validate model string format - ensure it's not incorrectly constructed // Prevent incorrect formats like "claude-haiku-4-20250514" (mixing haiku with sonnet date) if (modelString.includes('haiku') && modelString.includes('20250514')) { console.error(`[FeatureExecutor] Invalid model string detected: ${modelString}, using correct format`); modelString = MODEL_MAP.haiku || 'claude-haiku-4-5'; } console.log(`[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${modelString}`); return modelString; } /** * Determine if the feature uses a Codex/OpenAI model */ isCodexModel(feature) { const modelKey = feature.model || "opus"; return ModelRegistry.isCodexModel(modelKey); } /** * Get the appropriate provider for the feature's model */ getProvider(feature) { const modelKey = feature.model || "opus"; return ModelProviderFactory.getProviderForModel(modelKey); } /** * Get thinking configuration based on feature's thinkingLevel */ getThinkingConfig(feature) { const modelId = feature.model || "opus"; // Skip thinking config for models that don't support it (e.g., Codex CLI) if (!ModelRegistry.modelSupportsThinking(modelId)) { return null; } const level = feature.thinkingLevel || "none"; const budgetTokens = THINKING_BUDGET_MAP[level]; if (budgetTokens === null) { return null; // No extended thinking } return { type: "enabled", budget_tokens: budgetTokens, }; } /** * Prepare for ultrathink execution - validate and warn */ prepareForUltrathink(feature, thinkingConfig) { if (feature.thinkingLevel !== 'ultrathink') { return { ready: true }; } const warnings = []; const recommendations = []; // Check CLI installation const claudeCliDetector = require('./claude-cli-detector'); const cliInfo = claudeCliDetector.getInstallationInfo(); if (cliInfo.status === 'not_installed') { warnings.push('Claude Code CLI not detected - ultrathink may have timeout issues'); recommendations.push('Install Claude Code CLI for optimal ultrathink performance'); } // Validate budget tokens if (thinkingConfig && thinkingConfig.budget_tokens > 32000) { warnings.push(`Ultrathink budget (${thinkingConfig.budget_tokens} tokens) exceeds recommended 32K - may cause long-running requests`); recommendations.push('Consider using batch processing for budgets above 32K'); } // Cost estimate (rough) const estimatedCost = (thinkingConfig?.budget_tokens || 0) / 1000 * 0.015; // Rough estimate if (estimatedCost > 1.0) { warnings.push(`Estimated cost: ~$${estimatedCost.toFixed(2)} per execution`); } // Time estimate warnings.push('Ultrathink tasks typically take 45-180 seconds'); return { ready: true, warnings, recommendations, estimatedCost, estimatedTime: '45-180 seconds', cliInfo }; } /** * Sleep helper */ sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Implement a single feature using Claude Agent SDK * Uses a Plan-Act-Verify loop with detailed phase logging */ async implementFeature(feature, projectPath, sendToRenderer, execution) { console.log(`[FeatureExecutor] Implementing: ${feature.description}`); // Declare variables outside try block so they're available in catch let modelString; let providerName; let isCodex; try { // ======================================== // PHASE 1: PLANNING // ======================================== const planningMessage = `šŸ“‹ Planning implementation for: ${feature.description}\n`; await contextManager.writeToContextFile(projectPath, feature.id, planningMessage); sendToRenderer({ type: "auto_mode_phase", featureId: feature.id, phase: "planning", message: `Planning implementation for: ${feature.description}`, }); console.log(`[FeatureExecutor] Phase: PLANNING for ${feature.description}`); const abortController = new AbortController(); execution.abortController = abortController; // Create custom MCP server with UpdateFeatureStatus tool const featureToolsServer = mcpServerFactory.createFeatureToolsServer( featureLoader.updateFeatureStatus.bind(featureLoader), projectPath ); // Ensure feature has a model set (for backward compatibility with old features) if (!feature.model) { console.warn(`[FeatureExecutor] Feature ${feature.id} missing model property, defaulting to 'opus'`); feature.model = "opus"; } // Get model and thinking configuration from feature settings const modelString = this.getModelString(feature); const thinkingConfig = this.getThinkingConfig(feature); // Prepare for ultrathink if needed if (feature.thinkingLevel === 'ultrathink') { const preparation = this.prepareForUltrathink(feature, thinkingConfig); console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation); // Log warnings if (preparation.warnings && preparation.warnings.length > 0) { preparation.warnings.forEach(warning => { console.warn(`[FeatureExecutor] āš ļø ${warning}`); }); } // Send preparation info to renderer sendToRenderer({ type: 'auto_mode_ultrathink_preparation', featureId: feature.id, warnings: preparation.warnings || [], recommendations: preparation.recommendations || [], estimatedCost: preparation.estimatedCost, estimatedTime: preparation.estimatedTime }); } providerName = this.isCodexModel(feature) ? 'Codex/OpenAI' : 'Claude'; console.log(`[FeatureExecutor] Using provider: ${providerName}, model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`); // Note: Claude Agent SDK handles authentication automatically - it can use: // 1. CLAUDE_CODE_OAUTH_TOKEN env var (for SDK mode) // 2. Claude CLI's own authentication (if CLI is installed) // 3. ANTHROPIC_API_KEY (fallback) // We don't need to validate here - let the SDK/CLI handle auth errors // Configure options for the SDK query const options = { model: modelString, systemPrompt: promptBuilder.getCodingPrompt(), maxTurns: 1000, cwd: projectPath, mcpServers: { "automaker-tools": featureToolsServer }, allowedTools: [ "Read", "Write", "Edit", "Glob", "Grep", "Bash", "WebSearch", "WebFetch", "mcp__automaker-tools__UpdateFeatureStatus", ], permissionMode: "acceptEdits", sandbox: { enabled: true, autoAllowBashIfSandboxed: true, }, abortController: abortController, }; // Add thinking configuration if enabled if (thinkingConfig) { options.thinking = thinkingConfig; } // Build the prompt for this specific feature const prompt = promptBuilder.buildFeaturePrompt(feature); // Planning: Analyze the codebase and create implementation plan sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: "Analyzing codebase structure and creating implementation plan...", }); // Small delay to show planning phase await this.sleep(500); // ======================================== // PHASE 2: ACTION // ======================================== const actionMessage = `⚔ Executing implementation for: ${feature.description}\n`; await contextManager.writeToContextFile(projectPath, feature.id, actionMessage); sendToRenderer({ type: "auto_mode_phase", featureId: feature.id, phase: "action", message: `Executing implementation for: ${feature.description}`, }); console.log(`[FeatureExecutor] Phase: ACTION for ${feature.description}`); // Send query - use appropriate provider based on model let currentQuery; isCodex = this.isCodexModel(feature); // Validate that model string matches the provider if (isCodex) { // Ensure model string is actually a Codex model, not a Claude model if (modelString.startsWith('claude-')) { console.error(`[FeatureExecutor] ERROR: Codex provider selected but Claude model string detected: ${modelString}`); console.error(`[FeatureExecutor] Feature model: ${feature.model || 'not set'}, modelString: ${modelString}`); throw new Error(`Invalid model configuration: Codex provider cannot use Claude model '${modelString}'. Please check feature model setting.`); } // Use Codex provider for OpenAI models console.log(`[FeatureExecutor] Using Codex provider for model: ${modelString}`); const provider = this.getProvider(feature); currentQuery = provider.executeQuery({ prompt, model: modelString, cwd: projectPath, systemPrompt: promptBuilder.getCodingPrompt(), maxTurns: 20, // Codex CLI typically uses fewer turns allowedTools: options.allowedTools, abortController: abortController, env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY } }); } else { // Ensure model string is actually a Claude model, not a Codex model if (!modelString.startsWith('claude-') && !modelString.match(/^(gpt-|o\d)/)) { console.warn(`[FeatureExecutor] WARNING: Claude provider selected but unexpected model string: ${modelString}`); } // Use Claude SDK (original implementation) currentQuery = query({ prompt, options }); } execution.query = currentQuery; // Stream responses let responseText = ""; let hasStartedToolUse = false; for await (const msg of currentQuery) { // Check if this specific feature was aborted if (!execution.isActive()) break; // Handle error messages if (msg.type === "error") { const errorMsg = `\nāŒ Error: ${msg.error}\n`; await contextManager.writeToContextFile(projectPath, feature.id, errorMsg); sendToRenderer({ type: "auto_mode_error", featureId: feature.id, error: msg.error, }); throw new Error(msg.error); } if (msg.type === "assistant" && msg.message?.content) { for (const block of msg.message.content) { if (block.type === "text") { responseText += block.text; // Write to context file await contextManager.writeToContextFile(projectPath, feature.id, block.text); // Stream progress to renderer sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: block.text, }); } else if (block.type === "thinking") { // Handle thinking output from Codex O-series models const thinkingMsg = `\nšŸ’­ Thinking: ${block.thinking?.substring(0, 200)}...\n`; await contextManager.writeToContextFile(projectPath, feature.id, thinkingMsg); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: thinkingMsg, }); } else if (block.type === "tool_use") { // First tool use indicates we're actively implementing if (!hasStartedToolUse) { hasStartedToolUse = true; const startMsg = "Starting code implementation...\n"; await contextManager.writeToContextFile(projectPath, feature.id, startMsg); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: startMsg, }); } // Write tool use to context file const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; await contextManager.writeToContextFile(projectPath, feature.id, toolMsg); // Notify about tool use sendToRenderer({ type: "auto_mode_tool", featureId: feature.id, tool: block.name, input: block.input, }); } } } } execution.query = null; execution.abortController = null; // ======================================== // PHASE 3: VERIFICATION // ======================================== const verificationMessage = `āœ… Verifying implementation for: ${feature.description}\n`; await contextManager.writeToContextFile(projectPath, feature.id, verificationMessage); sendToRenderer({ type: "auto_mode_phase", featureId: feature.id, phase: "verification", message: `Verifying implementation for: ${feature.description}`, }); console.log(`[FeatureExecutor] Phase: VERIFICATION for ${feature.description}`); const checkingMsg = "Verifying implementation and checking test results...\n"; await contextManager.writeToContextFile(projectPath, feature.id, checkingMsg); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: checkingMsg, }); // Re-load features to check if it was marked as verified or waiting_approval (for skipTests) const updatedFeatures = await featureLoader.loadFeatures(projectPath); const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); // For skipTests features, waiting_approval is also considered a success const passes = updatedFeature?.status === "verified" || (updatedFeature?.skipTests && updatedFeature?.status === "waiting_approval"); // Send verification result const resultMsg = passes ? "āœ“ Verification successful: All tests passed\n" : "āœ— Verification: Tests need attention\n"; await contextManager.writeToContextFile(projectPath, feature.id, resultMsg); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: resultMsg, }); return { passes, message: responseText.substring(0, 500), // First 500 chars }; } catch (error) { if (error instanceof AbortError || error?.name === "AbortError") { console.log("[FeatureExecutor] Feature run aborted"); if (execution) { execution.abortController = null; execution.query = null; } return { passes: false, message: "Auto mode aborted", }; } console.error("[FeatureExecutor] Error implementing feature:", error); // Safely get model info for error logging (may not be set if error occurred early) const modelInfo = modelString ? { message: error.message, stack: error.stack, name: error.name, code: error.code, model: modelString, provider: providerName || 'unknown', isCodex: isCodex !== undefined ? isCodex : 'unknown' } : { message: error.message, stack: error.stack, name: error.name, code: error.code, model: 'not initialized', provider: 'unknown', isCodex: 'unknown' }; console.error("[FeatureExecutor] Error details:", modelInfo); // Check if this is a Claude CLI process error if (error.message && error.message.includes("process exited with code")) { const modelDisplay = modelString ? `Model: ${modelString}` : 'Model: not initialized'; const errorMsg = `Claude Code CLI failed with exit code 1. This might be due to:\n` + `- Invalid or unsupported model (${modelDisplay})\n` + `- Missing or invalid CLAUDE_CODE_OAUTH_TOKEN\n` + `- Claude CLI configuration issue\n` + `- Model not available in your Claude account\n\n` + `Original error: ${error.message}`; await contextManager.writeToContextFile(projectPath, feature.id, `\nāŒ ${errorMsg}\n`); sendToRenderer({ type: "auto_mode_error", featureId: feature.id, error: errorMsg, }); } // Clean up if (execution) { execution.abortController = null; execution.query = null; } throw error; } } /** * Resume feature implementation with previous context */ async resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution) { console.log(`[FeatureExecutor] Resuming with context for: ${feature.description}`); try { const resumeMessage = `\nšŸ”„ Resuming implementation for: ${feature.description}\n`; await contextManager.writeToContextFile(projectPath, feature.id, resumeMessage); sendToRenderer({ type: "auto_mode_phase", featureId: feature.id, phase: "action", message: `Resuming implementation for: ${feature.description}`, }); const abortController = new AbortController(); execution.abortController = abortController; // Create custom MCP server with UpdateFeatureStatus tool const featureToolsServer = mcpServerFactory.createFeatureToolsServer( featureLoader.updateFeatureStatus.bind(featureLoader), projectPath ); // Ensure feature has a model set (for backward compatibility with old features) if (!feature.model) { console.warn(`[FeatureExecutor] Feature ${feature.id} missing model property, defaulting to 'opus'`); feature.model = "opus"; } // Get model and thinking configuration from feature settings const modelString = this.getModelString(feature); const thinkingConfig = this.getThinkingConfig(feature); // Prepare for ultrathink if needed if (feature.thinkingLevel === 'ultrathink') { const preparation = this.prepareForUltrathink(feature, thinkingConfig); console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation); // Log warnings if (preparation.warnings && preparation.warnings.length > 0) { preparation.warnings.forEach(warning => { console.warn(`[FeatureExecutor] āš ļø ${warning}`); }); } // Send preparation info to renderer sendToRenderer({ type: 'auto_mode_ultrathink_preparation', featureId: feature.id, warnings: preparation.warnings || [], recommendations: preparation.recommendations || [], estimatedCost: preparation.estimatedCost, estimatedTime: preparation.estimatedTime }); } const isCodex = this.isCodexModel(feature); const providerName = isCodex ? 'Codex/OpenAI' : 'Claude'; console.log(`[FeatureExecutor] Resuming with provider: ${providerName}, model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`); const options = { model: modelString, systemPrompt: promptBuilder.getVerificationPrompt(), maxTurns: 1000, cwd: projectPath, mcpServers: { "automaker-tools": featureToolsServer }, allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "WebSearch", "WebFetch", "mcp__automaker-tools__UpdateFeatureStatus"], permissionMode: "acceptEdits", sandbox: { enabled: true, autoAllowBashIfSandboxed: true, }, abortController: abortController, }; // Add thinking configuration if enabled if (thinkingConfig) { options.thinking = thinkingConfig; } // Build prompt with previous context const prompt = promptBuilder.buildResumePrompt(feature, previousContext); // Use appropriate provider based on model type let currentQuery; if (isCodex) { // Validate that model string is actually a Codex model if (modelString.startsWith('claude-')) { console.error(`[FeatureExecutor] ERROR: Codex provider selected but Claude model string detected: ${modelString}`); throw new Error(`Invalid model configuration: Codex provider cannot use Claude model '${modelString}'. Please check feature model setting.`); } console.log(`[FeatureExecutor] Using Codex provider for resume with model: ${modelString}`); const provider = this.getProvider(feature); currentQuery = provider.executeQuery({ prompt, model: modelString, cwd: projectPath, systemPrompt: promptBuilder.getVerificationPrompt(), maxTurns: 20, allowedTools: options.allowedTools, abortController: abortController, env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY } }); } else { // Use Claude SDK currentQuery = query({ prompt, options }); } execution.query = currentQuery; let responseText = ""; for await (const msg of currentQuery) { // Check if this specific feature was aborted if (!execution.isActive()) break; if (msg.type === "assistant" && msg.message?.content) { for (const block of msg.message.content) { if (block.type === "text") { responseText += block.text; await contextManager.writeToContextFile(projectPath, feature.id, block.text); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: block.text, }); } else if (block.type === "tool_use") { const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; await contextManager.writeToContextFile(projectPath, feature.id, toolMsg); sendToRenderer({ type: "auto_mode_tool", featureId: feature.id, tool: block.name, input: block.input, }); } } } } execution.query = null; execution.abortController = null; // Check if feature was marked as verified or waiting_approval (for skipTests) const updatedFeatures = await featureLoader.loadFeatures(projectPath); const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); // For skipTests features, waiting_approval is also considered a success const passes = updatedFeature?.status === "verified" || (updatedFeature?.skipTests && updatedFeature?.status === "waiting_approval"); const finalMsg = passes ? "āœ“ Feature successfully verified and completed\n" : "⚠ Feature still in progress - may need additional work\n"; await contextManager.writeToContextFile(projectPath, feature.id, finalMsg); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: finalMsg, }); return { passes, message: responseText.substring(0, 500), }; } catch (error) { if (error instanceof AbortError || error?.name === "AbortError") { console.log("[FeatureExecutor] Resume aborted"); if (execution) { execution.abortController = null; execution.query = null; } return { passes: false, message: "Resume aborted", }; } console.error("[FeatureExecutor] Error resuming feature:", error); if (execution) { execution.abortController = null; execution.query = null; } throw error; } } /** * Commit changes for a feature without doing additional work * Analyzes changes and creates a proper conventional commit message */ async commitChangesOnly(feature, projectPath, sendToRenderer, execution) { console.log(`[FeatureExecutor] Committing changes for: ${feature.description}`); try { const commitMessage = `\nšŸ“ Committing changes for: ${feature.description}\n`; await contextManager.writeToContextFile(projectPath, feature.id, commitMessage); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: "Analyzing changes and creating commit...", }); const abortController = new AbortController(); execution.abortController = abortController; // Create custom MCP server with UpdateFeatureStatus tool const featureToolsServer = mcpServerFactory.createFeatureToolsServer( featureLoader.updateFeatureStatus.bind(featureLoader), projectPath ); const options = { model: "claude-sonnet-4-20250514", // Use sonnet for commit task systemPrompt: `You are a git commit assistant that creates professional conventional commit messages. IMPORTANT RULES: - DO NOT modify any code - DO NOT write tests - DO NOT do anything except analyzing changes and committing them - Use the git command line tools via Bash - Create proper conventional commit messages based on what was actually changed`, maxTurns: 15, // Allow some turns to analyze and commit cwd: projectPath, mcpServers: { "automaker-tools": featureToolsServer }, allowedTools: ["Bash", "mcp__automaker-tools__UpdateFeatureStatus"], permissionMode: "acceptEdits", sandbox: { enabled: false, // Need to run git commands }, abortController: abortController, }; // Prompt that guides the agent to create a proper conventional commit const prompt = `Please commit the current changes with a proper conventional commit message. **Feature Context:** Category: ${feature.category} Description: ${feature.description} **Your Task:** 1. First, run \`git status\` to see all untracked and modified files 2. Run \`git diff\` to see the actual changes (both staged and unstaged) 3. Run \`git log --oneline -5\` to see recent commit message styles in this repo 4. Analyze all the changes and draft a proper conventional commit message: - Use conventional commit format: \`type(scope): description\` - Types: feat, fix, refactor, style, docs, test, chore - The description should be concise (under 72 chars) and focus on "what" was done - Summarize the nature of the changes (new feature, enhancement, bug fix, etc.) - Make sure the commit message accurately reflects the actual code changes 5. Run \`git add .\` to stage all changes 6. Create the commit with a message ending with: šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 Use a HEREDOC for the commit message to ensure proper formatting: \`\`\`bash git commit -m "$(cat <<'EOF' type(scope): Short description here Optional longer description if needed. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 EOF )" \`\`\` **IMPORTANT:** - DO NOT use the feature description verbatim as the commit message - Analyze the actual code changes to determine the appropriate commit message - The commit message should be professional and follow conventional commit standards - DO NOT modify any code or run tests - ONLY commit the existing changes`; const currentQuery = query({ prompt, options }); execution.query = currentQuery; let responseText = ""; for await (const msg of currentQuery) { if (!execution.isActive()) break; if (msg.type === "assistant" && msg.message?.content) { for (const block of msg.message.content) { if (block.type === "text") { responseText += block.text; await contextManager.writeToContextFile(projectPath, feature.id, block.text); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: block.text, }); } else if (block.type === "tool_use") { const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; await contextManager.writeToContextFile(projectPath, feature.id, toolMsg); sendToRenderer({ type: "auto_mode_tool", featureId: feature.id, tool: block.name, input: block.input, }); } } } } execution.query = null; execution.abortController = null; const finalMsg = "āœ“ Changes committed successfully\n"; await contextManager.writeToContextFile(projectPath, feature.id, finalMsg); sendToRenderer({ type: "auto_mode_progress", featureId: feature.id, content: finalMsg, }); return { passes: true, message: responseText.substring(0, 500), }; } catch (error) { if (error instanceof AbortError || error?.name === "AbortError") { console.log("[FeatureExecutor] Commit aborted"); if (execution) { execution.abortController = null; execution.query = null; } return { passes: false, message: "Commit aborted", }; } console.error("[FeatureExecutor] Error committing feature:", error); if (execution) { execution.abortController = null; execution.query = null; } throw error; } } } module.exports = new FeatureExecutor();