From 08014f3a4ac36bca303d983f758c28e7168a57d8 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Tue, 9 Dec 2025 13:03:03 -0500 Subject: [PATCH] Refactor Auto Mode Service and add feature verification and project analysis capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored AutoModeService to delegate tasks to specialized services: featureLoader, featureExecutor, featureVerifier, contextManager, and projectAnalyzer. - Implemented feature verification logic in a new FeatureVerifier service, which runs tests and updates feature status. - Added ProjectAnalyzer service to scan project structure and update app_spec.txt. - Removed obsolete methods related to feature loading and context management from AutoModeService. - Updated feature status handling to ensure context files are deleted when features are verified. This refactor enhances modularity and maintainability of the codebase, allowing for better separation of concerns in feature management. šŸ¤– Generated with Claude Code --- .../feature-1765300273422-a8ovhdlwq.md | 85 -- .automaker/feature_list.json | 2 +- app/electron/auto-mode-service.js | 1301 ++--------------- app/electron/services/feature-verifier.js | 148 ++ app/electron/services/project-analyzer.js | 112 ++ 5 files changed, 346 insertions(+), 1302 deletions(-) delete mode 100644 .automaker/agents-context/feature-1765300273422-a8ovhdlwq.md create mode 100644 app/electron/services/feature-verifier.js create mode 100644 app/electron/services/project-analyzer.js diff --git a/.automaker/agents-context/feature-1765300273422-a8ovhdlwq.md b/.automaker/agents-context/feature-1765300273422-a8ovhdlwq.md deleted file mode 100644 index 163a72b5..00000000 --- a/.automaker/agents-context/feature-1765300273422-a8ovhdlwq.md +++ /dev/null @@ -1,85 +0,0 @@ -šŸ“‹ Planning implementation for: I want the ability to press P which will automatically select my projects drop down and show all my projects. And then for each one, put a hotkey in the left that says 12345 and selecting one of those with my keyboard should automatically select that project. - -⚔ Executing implementation for: I want the ability to press P which will automatically select my projects drop down and show all my projects. And then for each one, put a hotkey in the left that says 12345 and selecting one of those with my keyboard should automatically select that project. - -Starting code implementation... - -šŸ”§ Tool: Glob -Let me search for more specific project files: -šŸ”§ Tool: Glob - -šŸ”§ Tool: Read - -šŸ”§ Tool: Read - -šŸ”§ Tool: Read -Now let me read the keyboard shortcuts hook to understand the current shortcut system: -šŸ”§ Tool: Read - -šŸ”§ Tool: Read -Now I have a good understanding of the codebase. Let me check if there are existing tests to understand the testing pattern: -šŸ”§ Tool: Glob - -šŸ”§ Tool: Glob - -šŸ”§ Tool: Bash - -šŸ”§ Tool: Bash - -šŸ”§ Tool: Read - -šŸ”§ Tool: Bash - -šŸ”§ Tool: Read -Now I have a good understanding of the codebase. Let me implement the feature: - -1. Press "P" to open the project selector dropdown -2. Show hotkeys (1-5 or up to available projects) next to each project option -3. Pressing a number key selects that project - -Let me first add the necessary shortcut and then update the sidebar component: -šŸ”§ Tool: TodoWrite - -šŸ”§ Tool: Edit - -šŸ”§ Tool: TodoWrite -Now let me update the sidebar to implement the full functionality. I need to: -1. Add state to control the dropdown open state -2. Add keyboard shortcut for "P" to open the dropdown -3. Handle number key selection when dropdown is open -4. Display hotkey indicators in the dropdown items -šŸ”§ Tool: Edit -Now let me add the state for the dropdown and the keyboard handling: -šŸ”§ Tool: Edit -Now I need to add the keyboard event handler for when the dropdown is open to handle number keys, and add the 'P' key shortcut: -šŸ”§ Tool: Edit -Now I need to update the DropdownMenu to use controlled state and add hotkey indicators: -šŸ”§ Tool: Edit - -šŸ”§ Tool: TodoWrite -Now let me update the utils.ts file to add helpers for testing this feature and then write the tests: -šŸ”§ Tool: Edit -Now let me write the Playwright tests: -šŸ”§ Tool: Write -Now let me run the tests to verify the feature works: -šŸ”§ Tool: Bash -The server seems to be blocked. Let me try a different port or reuse existing server: -šŸ”§ Tool: Bash - -šŸ”§ Tool: Bash -The server is already running. Let me use the reuse flag: -šŸ”§ Tool: Bash - -šŸ”§ Tool: BashOutput - -šŸ”§ Tool: BashOutput - -šŸ”§ Tool: Bash - -šŸ”§ Tool: BashOutput - -šŸ”§ Tool: Bash - -šŸ”§ Tool: BashOutput - -šŸ”§ Tool: Bash diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 99436ddc..d3ef63ef 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -25,7 +25,7 @@ "category": "Kanban", "description": "In creating new cards in Kanban, I need the ability to drag and drop images into the description section, which will attach the image as context in store in the temp directory, so that later on when the agent runs, it can know where to fetch that image from.", "steps": [], - "status": "in_progress" + "status": "verified" }, { "id": "feature-1765301184184-ttvhd8kkt", diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 7ef479e8..601683b2 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -1,11 +1,19 @@ -const { query, AbortError, createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk"); -const path = require("path"); -const fs = require("fs/promises"); -const { z } = require("zod"); +const featureLoader = require("./services/feature-loader"); +const featureExecutor = require("./services/feature-executor"); +const featureVerifier = require("./services/feature-verifier"); +const contextManager = require("./services/context-manager"); +const projectAnalyzer = require("./services/project-analyzer"); /** * Auto Mode Service - Autonomous feature implementation * Automatically picks and implements features from the kanban board + * + * This service acts as the main orchestrator, delegating work to specialized services: + * - featureLoader: Loading and selecting features + * - featureExecutor: Implementing features + * - featureVerifier: Running tests and verification + * - contextManager: Managing context files + * - projectAnalyzer: Analyzing project structure */ class AutoModeService { constructor() { @@ -16,51 +24,17 @@ class AutoModeService { } /** - * Create a custom MCP server with the UpdateFeatureStatus tool - * This tool allows Claude Code to safely update feature status without - * directly modifying the feature_list.json file, preventing race conditions - * and accidental state restoration. + * Helper to create execution context with isActive check */ - createFeatureToolsServer(projectPath) { - const service = this; // Reference to AutoModeService instance - - return createSdkMcpServer({ - name: "automaker-tools", - version: "1.0.0", - tools: [ - tool( - "UpdateFeatureStatus", - "Update the status of a feature in the feature list. Use this tool instead of directly modifying feature_list.json to safely update feature status.", - { - featureId: z.string().describe("The ID of the feature to update"), - status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature") - }, - async (args) => { - try { - console.log(`[AutoMode] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}`); - - // Use the service's updateFeatureStatus method - await service.updateFeatureStatus(args.featureId, args.status, projectPath); - - return { - content: [{ - type: "text", - text: `Successfully updated feature ${args.featureId} to status "${args.status}"` - }] - }; - } catch (error) { - console.error("[AutoMode] UpdateFeatureStatus tool error:", error); - return { - content: [{ - type: "text", - text: `Failed to update feature status: ${error.message}` - }] - }; - } - } - ) - ] - }); + createExecutionContext(featureId) { + const context = { + abortController: null, + query: null, + projectPath: null, + sendToRenderer: null, + isActive: () => this.runningFeatures.has(featureId) + }; + return context; } /** @@ -135,16 +109,14 @@ class AutoModeService { console.log(`[AutoMode] Running specific feature: ${featureId}`); // Register this feature as running - this.runningFeatures.set(featureId, { - abortController: null, - query: null, - projectPath, - sendToRenderer, - }); + const execution = this.createExecutionContext(featureId); + execution.projectPath = projectPath; + execution.sendToRenderer = sendToRenderer; + this.runningFeatures.set(featureId, execution); try { // Load features - const features = await this.loadFeatures(projectPath); + const features = await featureLoader.loadFeatures(projectPath); const feature = features.find((f) => f.id === featureId); if (!feature) { @@ -154,7 +126,7 @@ class AutoModeService { console.log(`[AutoMode] Running feature: ${feature.description}`); // Update feature status to in_progress - await this.updateFeatureStatus(featureId, "in_progress", projectPath); + await featureLoader.updateFeatureStatus(featureId, "in_progress", projectPath); sendToRenderer({ type: "auto_mode_feature_start", @@ -163,11 +135,16 @@ class AutoModeService { }); // Implement the feature - const result = await this.implementFeature(feature, projectPath, sendToRenderer); + const result = await featureExecutor.implementFeature(feature, projectPath, sendToRenderer, execution); // Update feature status based on result const newStatus = result.passes ? "verified" : "backlog"; - await this.updateFeatureStatus(feature.id, newStatus, projectPath); + await featureLoader.updateFeatureStatus(feature.id, newStatus, projectPath); + + // Delete context file if verified + if (newStatus === "verified") { + await contextManager.deleteContextFile(projectPath, feature.id); + } sendToRenderer({ type: "auto_mode_feature_complete", @@ -208,16 +185,14 @@ class AutoModeService { console.log(`[AutoMode] Verifying feature: ${featureId}`); // Register this feature as running - this.runningFeatures.set(featureId, { - abortController: null, - query: null, - projectPath, - sendToRenderer, - }); + const execution = this.createExecutionContext(featureId); + execution.projectPath = projectPath; + execution.sendToRenderer = sendToRenderer; + this.runningFeatures.set(featureId, execution); try { // Load features - const features = await this.loadFeatures(projectPath); + const features = await featureLoader.loadFeatures(projectPath); const feature = features.find((f) => f.id === featureId); if (!feature) { @@ -233,11 +208,16 @@ class AutoModeService { }); // Verify the feature by running tests - const result = await this.verifyFeatureTests(feature, projectPath, sendToRenderer); + const result = await featureVerifier.verifyFeatureTests(feature, projectPath, sendToRenderer, execution); // Update feature status based on result const newStatus = result.passes ? "verified" : "in_progress"; - await this.updateFeatureStatus(featureId, newStatus, projectPath); + await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath); + + // Delete context file if verified + if (newStatus === "verified") { + await contextManager.deleteContextFile(projectPath, featureId); + } sendToRenderer({ type: "auto_mode_feature_complete", @@ -278,16 +258,14 @@ class AutoModeService { console.log(`[AutoMode] Resuming feature: ${featureId}`); // Register this feature as running - this.runningFeatures.set(featureId, { - abortController: null, - query: null, - projectPath, - sendToRenderer, - }); + const execution = this.createExecutionContext(featureId); + execution.projectPath = projectPath; + execution.sendToRenderer = sendToRenderer; + this.runningFeatures.set(featureId, execution); try { // Load features - const features = await this.loadFeatures(projectPath); + const features = await featureLoader.loadFeatures(projectPath); const feature = features.find((f) => f.id === featureId); if (!feature) { @@ -303,10 +281,10 @@ class AutoModeService { }); // Read existing context - const previousContext = await this.readContextFile(projectPath, featureId); + const previousContext = await contextManager.readContextFile(projectPath, featureId); // Resume implementation with context - const result = await this.resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext); + const result = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution); // If the agent ends early without finishing, automatically re-run let attempts = 0; @@ -315,7 +293,7 @@ class AutoModeService { while (!finalResult.passes && attempts < maxAttempts) { // Check if feature is still in progress (not verified) - const updatedFeatures = await this.loadFeatures(projectPath); + const updatedFeatures = await featureLoader.loadFeatures(projectPath); const updatedFeature = updatedFeatures.find((f) => f.id === featureId); if (updatedFeature && updatedFeature.status === "in_progress") { @@ -323,7 +301,7 @@ class AutoModeService { console.log(`[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...`); // Update context file with retry message - await this.writeToContextFile(projectPath, featureId, + await contextManager.writeToContextFile(projectPath, featureId, `\n\nšŸ”„ Auto-retry #${attempts} - Continuing implementation...\n\n`); sendToRenderer({ @@ -333,10 +311,10 @@ class AutoModeService { }); // Read updated context - const retryContext = await this.readContextFile(projectPath, featureId); + const retryContext = await contextManager.readContextFile(projectPath, featureId); // Resume again with full context - finalResult = await this.resumeFeatureWithContext(feature, projectPath, sendToRenderer, retryContext); + finalResult = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, retryContext, execution); } else { break; } @@ -344,7 +322,12 @@ class AutoModeService { // Update feature status based on final result const newStatus = finalResult.passes ? "verified" : "in_progress"; - await this.updateFeatureStatus(featureId, newStatus, projectPath); + await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath); + + // Delete context file if verified + if (newStatus === "verified") { + await contextManager.deleteContextFile(projectPath, featureId); + } sendToRenderer({ type: "auto_mode_feature_complete", @@ -368,202 +351,6 @@ class AutoModeService { } } - /** - * Read context file for a feature - */ - async readContextFile(projectPath, featureId) { - try { - const contextPath = path.join(projectPath, ".automaker", "agents-context", `${featureId}.md`); - const content = await fs.readFile(contextPath, "utf-8"); - return content; - } catch (error) { - console.log(`[AutoMode] No context file found for ${featureId}`); - return null; - } - } - - /** - * Resume feature implementation with previous context - */ - async resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext) { - console.log(`[AutoMode] Resuming with context for: ${feature.description}`); - - // Get the execution context for this feature - const execution = this.runningFeatures.get(feature.id); - if (!execution) { - throw new Error(`Feature ${feature.id} not registered in runningFeatures`); - } - - try { - const resumeMessage = `\nšŸ”„ Resuming implementation for: ${feature.description}\n`; - await this.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 = this.createFeatureToolsServer(projectPath); - - const options = { - model: "claude-opus-4-5-20251101", - systemPrompt: this.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, - }; - - // Build prompt with previous context - const prompt = this.buildResumePrompt(feature, previousContext); - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let responseText = ""; - for await (const msg of currentQuery) { - // Check if this specific feature was aborted - if (!this.runningFeatures.has(feature.id)) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - - await this.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 this.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 - const updatedFeatures = await this.loadFeatures(projectPath); - const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - const passes = updatedFeature?.status === "verified"; - - const finalMsg = passes - ? "āœ“ Feature successfully verified and completed\n" - : "⚠ Feature still in progress - may need additional work\n"; - - await this.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("[AutoMode] Resume aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - passes: false, - message: "Resume aborted", - }; - } - - console.error("[AutoMode] Error resuming feature:", error); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Build prompt for resuming feature with previous context - */ - buildResumePrompt(feature, previousContext) { - return `You are resuming work on a feature implementation that was previously started. - -**Current Feature:** - -ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} - -**Steps to Complete:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} - -**Previous Work Context:** - -${previousContext || "No previous context available - this is a fresh start."} - -**Your Task:** - -Continue where you left off and complete the feature implementation: - -1. Review the previous work context above to understand what has been done -2. Continue implementing the feature according to the description and steps -3. Write Playwright tests to verify the feature works correctly (if not already done) -4. Run the tests and ensure they pass -5. **DELETE the test file(s) you created** - tests are only for immediate verification -6. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json -7. Commit your changes with git - -**IMPORTANT - Updating Feature Status:** - -When all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: -- Call the tool with: featureId="${feature.id}" and status="verified" -- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions -- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data - -**Important Guidelines:** - -- Review what was already done in the previous context -- Don't redo work that's already complete - continue from where it left off -- Focus on completing any remaining tasks -- Write comprehensive Playwright tests if not already done -- Ensure all tests pass before marking as verified -- **CRITICAL: Delete test files after verification** -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** -- Make a git commit when complete - -Begin by assessing what's been done and what remains to be completed.`; - } - /** * Main autonomous loop - picks and implements features */ @@ -572,10 +359,10 @@ Begin by assessing what's been done and what remains to be completed.`; let currentFeatureId = null; try { // Load features from .automaker/feature_list.json - const features = await this.loadFeatures(projectPath); + const features = await featureLoader.loadFeatures(projectPath); // Find highest priority incomplete feature - const nextFeature = this.selectNextFeature(features); + const nextFeature = featureLoader.selectNextFeature(features); if (!nextFeature) { console.log("[AutoMode] No more features to implement"); @@ -604,19 +391,22 @@ Begin by assessing what's been done and what remains to be completed.`; }); // Register this feature as running - this.runningFeatures.set(currentFeatureId, { - abortController: null, - query: null, - projectPath, - sendToRenderer, - }); + const execution = this.createExecutionContext(currentFeatureId); + execution.projectPath = projectPath; + execution.sendToRenderer = sendToRenderer; + this.runningFeatures.set(currentFeatureId, execution); // Implement the feature - const result = await this.implementFeature(nextFeature, projectPath, sendToRenderer); + const result = await featureExecutor.implementFeature(nextFeature, projectPath, sendToRenderer, execution); // Update feature status based on result const newStatus = result.passes ? "verified" : "backlog"; - await this.updateFeatureStatus(nextFeature.id, newStatus, projectPath); + await featureLoader.updateFeatureStatus(nextFeature.id, newStatus, projectPath); + + // Delete context file if verified + if (newStatus === "verified") { + await contextManager.deleteContextFile(projectPath, nextFeature.id); + } sendToRenderer({ type: "auto_mode_feature_complete", @@ -655,722 +445,6 @@ Begin by assessing what's been done and what remains to be completed.`; this.autoLoopRunning = false; } - /** - * Load features from .automaker/feature_list.json - */ - async loadFeatures(projectPath) { - const featuresPath = path.join( - projectPath, - ".automaker", - "feature_list.json" - ); - - try { - const content = await fs.readFile(featuresPath, "utf-8"); - const features = JSON.parse(content); - - // Ensure each feature has an ID - return features.map((f, index) => ({ - ...f, - id: f.id || `feature-${index}-${Date.now()}`, - })); - } catch (error) { - console.error("[AutoMode] Failed to load features:", error); - return []; - } - } - - /** - * Select the next feature to implement - * Prioritizes: earlier features in the list that are not verified - */ - selectNextFeature(features) { - // Find first feature that is in backlog or in_progress status - return features.find((f) => f.status !== "verified"); - } - - /** - * Write output to feature context file - */ - async writeToContextFile(projectPath, featureId, content) { - if (!projectPath) return; - - try { - const contextDir = path.join(projectPath, ".automaker", "agents-context"); - - // Ensure directory exists - try { - await fs.access(contextDir); - } catch { - await fs.mkdir(contextDir, { recursive: true }); - } - - const filePath = path.join(contextDir, `${featureId}.md`); - - // Append to existing file or create new one - try { - const existing = await fs.readFile(filePath, "utf-8"); - await fs.writeFile(filePath, existing + content, "utf-8"); - } catch { - await fs.writeFile(filePath, content, "utf-8"); - } - } catch (error) { - console.error("[AutoMode] Failed to write to context file:", error); - } - } - - /** - * Delete agent context file for a feature - */ - async deleteContextFile(projectPath, featureId) { - if (!projectPath) return; - - try { - const contextPath = path.join(projectPath, ".automaker", "agents-context", `${featureId}.md`); - await fs.unlink(contextPath); - console.log(`[AutoMode] Deleted agent context for feature ${featureId}`); - } catch (error) { - // File might not exist, which is fine - if (error.code !== 'ENOENT') { - console.error("[AutoMode] Failed to delete context file:", error); - } - } - } - - /** - * Implement a single feature using Claude Agent SDK - * Uses a Plan-Act-Verify loop with detailed phase logging - */ - async implementFeature(feature, projectPath, sendToRenderer) { - console.log(`[AutoMode] Implementing: ${feature.description}`); - - // Get the execution context for this feature - const execution = this.runningFeatures.get(feature.id); - if (!execution) { - throw new Error(`Feature ${feature.id} not registered in runningFeatures`); - } - - try { - // ======================================== - // PHASE 1: PLANNING - // ======================================== - const planningMessage = `šŸ“‹ Planning implementation for: ${feature.description}\n`; - await this.writeToContextFile(projectPath, feature.id, planningMessage); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "planning", - message: `Planning implementation for: ${feature.description}`, - }); - console.log(`[AutoMode] Phase: PLANNING for ${feature.description}`); - - const abortController = new AbortController(); - execution.abortController = abortController; - - // Create custom MCP server with UpdateFeatureStatus tool - const featureToolsServer = this.createFeatureToolsServer(projectPath); - - // Configure options for the SDK query - const options = { - model: "claude-opus-4-5-20251101", - systemPrompt: this.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, - }; - - // Build the prompt for this specific feature - const prompt = this.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 this.writeToContextFile(projectPath, feature.id, actionMessage); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "action", - message: `Executing implementation for: ${feature.description}`, - }); - console.log(`[AutoMode] Phase: ACTION for ${feature.description}`); - - // Send query - const 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 (!this.runningFeatures.has(feature.id)) break; - - 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 this.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 === "tool_use") { - // First tool use indicates we're actively implementing - if (!hasStartedToolUse) { - hasStartedToolUse = true; - const startMsg = "Starting code implementation...\n"; - await this.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 this.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 this.writeToContextFile(projectPath, feature.id, verificationMessage); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "verification", - message: `Verifying implementation for: ${feature.description}`, - }); - console.log(`[AutoMode] Phase: VERIFICATION for ${feature.description}`); - - const checkingMsg = - "Verifying implementation and checking test results...\n"; - await this.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 - const updatedFeatures = await this.loadFeatures(projectPath); - const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - const passes = updatedFeature?.status === "verified"; - - // Send verification result - const resultMsg = passes - ? "āœ“ Verification successful: All tests passed\n" - : "āœ— Verification: Tests need attention\n"; - - await this.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("[AutoMode] Feature run aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - passes: false, - message: "Auto mode aborted", - }; - } - - console.error("[AutoMode] Error implementing feature:", error); - - // Clean up - if (execution) { - execution.abortController = null; - execution.query = null; - } - - throw error; - } - } - - /** - * Update feature status in .automaker/feature_list.json - */ - async updateFeatureStatus(featureId, status, projectPath) { - const features = await this.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - console.error(`[AutoMode] Feature ${featureId} not found`); - return; - } - - // Update the status field - feature.status = status; - - // Save back to file - const featuresPath = path.join( - projectPath, - ".automaker", - "feature_list.json" - ); - const toSave = features.map((f) => ({ - id: f.id, - category: f.category, - description: f.description, - steps: f.steps, - status: f.status, - })); - - await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8"); - console.log(`[AutoMode] Updated feature ${featureId}: status=${status}`); - - // Delete agent context file when feature is verified - if (status === "verified") { - await this.deleteContextFile(projectPath, featureId); - } - } - - /** - * Verify feature tests (runs tests and checks if they pass) - */ - async verifyFeatureTests(feature, projectPath, sendToRenderer) { - console.log(`[AutoMode] Verifying tests for: ${feature.description}`); - - // Get the execution context for this feature - const execution = this.runningFeatures.get(feature.id); - if (!execution) { - throw new Error(`Feature ${feature.id} not registered in runningFeatures`); - } - - try { - const verifyMsg = `\nāœ… Verifying tests for: ${feature.description}\n`; - await this.writeToContextFile(projectPath, feature.id, verifyMsg); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "verification", - message: `Verifying tests for: ${feature.description}`, - }); - - const abortController = new AbortController(); - execution.abortController = abortController; - - // Create custom MCP server with UpdateFeatureStatus tool - const featureToolsServer = this.createFeatureToolsServer(projectPath); - - const options = { - model: "claude-opus-4-5-20251101", - systemPrompt: this.getVerificationPrompt(), - maxTurns: 1000, - cwd: projectPath, - mcpServers: { - "automaker-tools": featureToolsServer - }, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "mcp__automaker-tools__UpdateFeatureStatus"], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = this.buildVerificationPrompt(feature); - - const runningTestsMsg = - "Running Playwright tests to verify feature implementation...\n"; - await this.writeToContextFile(projectPath, feature.id, runningTestsMsg); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: runningTestsMsg, - }); - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let responseText = ""; - for await (const msg of currentQuery) { - // Check if this specific feature was aborted - if (!this.runningFeatures.has(feature.id)) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - - await this.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 this.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; - - // Re-load features to check if it was marked as verified - const updatedFeatures = await this.loadFeatures(projectPath); - const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - const passes = updatedFeature?.status === "verified"; - - const finalMsg = passes - ? "āœ“ Verification successful: All tests passed\n" - : "āœ— Tests failed or not all passing - feature remains in progress\n"; - - await this.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("[AutoMode] Verification aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - passes: false, - message: "Verification aborted", - }; - } - - console.error("[AutoMode] Error verifying feature:", error); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Build the prompt for implementing a specific feature - */ - buildFeaturePrompt(feature) { - return `You are working on a feature implementation task. - -**Current Feature to Implement:** - -ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} - -**Steps to Complete:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} - -**Your Task:** - -1. Read the project files to understand the current codebase structure -2. Implement the feature according to the description and steps -3. Write Playwright tests to verify the feature works correctly -4. Run the tests and ensure they pass -5. **DELETE the test file(s) you created** - tests are only for immediate verification -6. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json -7. Commit your changes with git - -**IMPORTANT - Updating Feature Status:** - -When you have completed the feature and all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: -- Call the tool with: featureId="${feature.id}" and status="verified" -- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions -- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data - -**Important Guidelines:** - -- Focus ONLY on implementing this specific feature -- Write clean, production-quality code -- Add proper error handling -- Write comprehensive Playwright tests -- Ensure all existing tests still pass -- Mark the feature as passing only when all tests are green -- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** -- Make a git commit when complete - -**Testing Utilities (CRITICAL):** - -1. **Create/maintain tests/utils.ts** - Add helper functions for finding elements and common test operations -2. **Use utilities in tests** - Import and use helper functions instead of repeating selectors -3. **Add utilities as needed** - When you write a test, if you need a new helper, add it to utils.ts -4. **Update utilities when functionality changes** - If you modify components, update corresponding utilities - -Example utilities to add: -- getByTestId(page, testId) - Find elements by data-testid -- getButtonByText(page, text) - Find buttons by text -- clickElement(page, testId) - Click an element by test ID -- fillForm(page, formData) - Fill form fields -- waitForElement(page, testId) - Wait for element to appear - -This makes future tests easier to write and maintain! - -**Test Deletion Policy:** -After tests pass, delete them immediately: -\`\`\`bash -rm tests/[feature-name].spec.ts -\`\`\` - -Begin by reading the project structure and then implementing the feature.`; - } - - /** - * Build the prompt for verifying a specific feature - */ - buildVerificationPrompt(feature) { - return `You are implementing and verifying a feature until it is complete and working correctly. - -**Feature to Implement/Verify:** - -ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} -Current Status: ${feature.status} - -**Steps that should be implemented:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} - -**Your Task:** - -1. Read the project files to understand the current implementation -2. If the feature is not fully implemented, continue implementing it -3. Write or update Playwright tests to verify the feature works correctly -4. Run the Playwright tests: npx playwright test tests/[feature-name].spec.ts -5. Check if all tests pass -6. **If ANY tests fail:** - - Analyze the test failures and error messages - - Fix the implementation code to make the tests pass - - Update test utilities in tests/utils.ts if needed - - Re-run the tests to verify the fixes - - **REPEAT this process until ALL tests pass** -7. **If ALL tests pass:** - - **DELETE the test file(s) for this feature** - tests are only for immediate verification - - **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json - - Explain what was implemented/fixed and that all tests passed - - Commit your changes with git - -**IMPORTANT - Updating Feature Status:** - -When all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: -- Call the tool with: featureId="${feature.id}" and status="verified" -- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions -- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data - -**Testing Utilities:** -- Check if tests/utils.ts exists and is being used -- If utilities are outdated due to functionality changes, update them -- Add new utilities as needed for this feature's tests -- Ensure test utilities stay in sync with code changes - -**Test Deletion Policy:** -After tests pass, delete them immediately: -\`\`\`bash -rm tests/[feature-name].spec.ts -\`\`\` - -**Important:** -- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure -- Only mark as "verified" if Playwright tests pass -- **CRITICAL: Delete test files after they pass** - tests should not accumulate -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** -- Update test utilities if functionality changed -- Make a git commit when the feature is complete -- Be thorough and persistent in fixing issues - -Begin by reading the project structure and understanding what needs to be implemented or fixed.`; - } - - /** - * Get the system prompt for verification agent - */ - getVerificationPrompt() { - return `You are an AI implementation and verification agent focused on completing features and ensuring they work. - -Your role is to: -- **Continue implementing features until they are complete** - don't stop at the first failure -- Write or update code to fix failing tests -- Run Playwright tests to verify feature implementations -- If tests fail, analyze errors and fix the implementation -- If other tests fail, verify if those tests are still accurate or should be updated or deleted -- Continue rerunning tests and fixing issues until ALL tests pass -- **DELETE test files after successful verification** - tests are only for immediate feature verification -- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json -- **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code -- Commit working code to git - -**IMPORTANT - UpdateFeatureStatus Tool:** -You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When all tests pass, use this tool to update the feature status: -- Call with featureId and status="verified" -- **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state -- The tool safely updates the status without corrupting other feature data - -**Testing Utilities:** -- Check if tests/utils.ts needs updates based on code changes -- If a component's selectors or behavior changed, update the corresponding utility functions -- Add new utilities as needed for the feature's tests -- Ensure utilities remain accurate and helpful for future tests - -**Test Deletion Policy:** -Tests should NOT accumulate. After a feature is verified: -1. Delete the test file for that feature -2. Use UpdateFeatureStatus tool to mark the feature as "verified" - -This prevents test brittleness as the app changes rapidly. - -You have access to: -- Read and edit files -- Write new code or modify existing code -- Run bash commands (especially Playwright tests) -- Delete files (rm command) -- Analyze test output -- Make git commits -- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status - -**CRITICAL:** Be persistent and thorough - keep iterating on the implementation until all tests pass. Don't give up after the first failure. Always delete tests after they pass, use the UpdateFeatureStatus tool, and commit your work.`; - } - - /** - * Get the system prompt for coding agent - */ - getCodingPrompt() { - return `You are an AI coding agent working autonomously to implement features. - -Your role is to: -- Implement features exactly as specified -- Write production-quality code -- Create comprehensive Playwright tests using testing utilities -- Ensure all tests pass before marking features complete -- **DELETE test files after successful verification** - tests are only for immediate feature verification -- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json -- Commit working code to git -- Be thorough and detail-oriented - -**IMPORTANT - UpdateFeatureStatus Tool:** -You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When all tests pass, use this tool to update the feature status: -- Call with featureId and status="verified" -- **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state -- The tool safely updates the status without corrupting other feature data - -**Testing Utilities (CRITICAL):** -- **Create and maintain tests/utils.ts** with helper functions for finding elements and common operations -- **Always use utilities in tests** instead of repeating selectors -- **Add new utilities as you write tests** - if you need a helper, add it to utils.ts -- **Update utilities when functionality changes** - keep helpers in sync with code changes - -This makes future tests easier to write and more maintainable! - -**Test Deletion Policy:** -Tests should NOT accumulate. After a feature is verified: -1. Run the tests to ensure they pass -2. Delete the test file for that feature -3. Use UpdateFeatureStatus tool to mark the feature as "verified" - -This prevents test brittleness as the app changes rapidly. - -You have full access to: -- Read and write files -- Run bash commands -- Execute tests -- Delete files (rm command) -- Make git commits -- Search and analyze the codebase -- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status - -Focus on one feature at a time and complete it fully before finishing. Always delete tests after they pass and use the UpdateFeatureStatus tool.`; - } - - /** - * Sleep helper - */ - sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - /** * Analyze a new project - scans codebase and updates app_spec.txt * This is triggered when opening a project for the first time @@ -1386,12 +460,10 @@ Focus on one feature at a time and complete it fully before finishing. Always de } // Register as running - this.runningFeatures.set(analysisId, { - abortController: null, - query: null, - projectPath, - sendToRenderer, - }); + const execution = this.createExecutionContext(analysisId); + execution.projectPath = projectPath; + execution.sendToRenderer = sendToRenderer; + this.runningFeatures.set(analysisId, execution); try { sendToRenderer({ @@ -1405,7 +477,7 @@ Focus on one feature at a time and complete it fully before finishing. Always de }); // Perform the analysis - const result = await this.runProjectAnalysis(projectPath, analysisId, sendToRenderer); + const result = await projectAnalyzer.runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution); sendToRenderer({ type: "auto_mode_feature_complete", @@ -1429,213 +501,10 @@ Focus on one feature at a time and complete it fully before finishing. Always de } /** - * Run the project analysis using Claude Agent SDK + * Sleep helper */ - async runProjectAnalysis(projectPath, analysisId, sendToRenderer) { - console.log(`[AutoMode] Running project analysis for: ${projectPath}`); - - const execution = this.runningFeatures.get(analysisId); - if (!execution) { - throw new Error(`Analysis ${analysisId} not registered in runningFeatures`); - } - - try { - sendToRenderer({ - type: "auto_mode_phase", - featureId: analysisId, - phase: "planning", - message: "Scanning project structure...", - }); - - const abortController = new AbortController(); - execution.abortController = abortController; - - const options = { - model: "claude-sonnet-4-20250514", - systemPrompt: this.getProjectAnalysisSystemPrompt(), - maxTurns: 50, - cwd: projectPath, - allowedTools: ["Read", "Glob", "Grep", "Bash"], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = this.buildProjectAnalysisPrompt(projectPath); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: analysisId, - content: "Starting project analysis...\n", - }); - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let responseText = ""; - for await (const msg of currentQuery) { - if (!this.runningFeatures.has(analysisId)) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - sendToRenderer({ - type: "auto_mode_progress", - featureId: analysisId, - content: block.text, - }); - } else if (block.type === "tool_use") { - sendToRenderer({ - type: "auto_mode_tool", - featureId: analysisId, - tool: block.name, - input: block.input, - }); - } - } - } - } - - execution.query = null; - execution.abortController = null; - - sendToRenderer({ - type: "auto_mode_phase", - featureId: analysisId, - phase: "verification", - message: "Project analysis complete", - }); - - return { - success: true, - message: "Project analyzed successfully", - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[AutoMode] Project analysis aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - success: false, - message: "Analysis aborted", - }; - } - - console.error("[AutoMode] Error in project analysis:", error); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Build the prompt for project analysis - */ - buildProjectAnalysisPrompt(projectPath) { - return `You are analyzing a new project that was just opened in Automaker, an autonomous AI development studio. - -**Your Task:** - -Analyze this project's codebase and update the .automaker/app_spec.txt file with accurate information about: - -1. **Project Name** - Detect the name from package.json, README, or directory name -2. **Overview** - Brief description of what the project does -3. **Technology Stack** - Languages, frameworks, libraries detected -4. **Core Capabilities** - Main features and functionality -5. **Implemented Features** - What features are already built - -**Steps to Follow:** - -1. First, explore the project structure: - - Look at package.json, cargo.toml, go.mod, requirements.txt, etc. for tech stack - - Check README.md for project description - - List key directories (src, lib, components, etc.) - -2. Identify the tech stack: - - Frontend framework (React, Vue, Next.js, etc.) - - Backend framework (Express, FastAPI, etc.) - - Database (if any config files exist) - - Testing framework - - Build tools - -3. Update .automaker/app_spec.txt with your findings in this format: - \`\`\`xml - - Detected Name - - - Clear description of what this project does based on your analysis. - - - - - Framework Name - - - - - - - - - - - - - - - - - - - - - - \`\`\` - -4. Ensure .automaker/feature_list.json exists (create as empty array [] if not) - -5. Ensure .automaker/context/ directory exists - -6. Ensure .automaker/agents-context/ directory exists - -**Important:** -- Be concise but accurate -- Only include information you can verify from the codebase -- If unsure about something, note it as "to be determined" -- Don't make up features that don't exist - -Begin by exploring the project structure.`; - } - - /** - * Get system prompt for project analysis agent - */ - getProjectAnalysisSystemPrompt() { - return `You are a project analysis agent that examines codebases to understand their structure, tech stack, and implemented features. - -Your goal is to: -- Quickly scan and understand project structure -- Identify programming languages, frameworks, and libraries -- Detect existing features and capabilities -- Update the .automaker/app_spec.txt with accurate information -- Ensure all required .automaker files and directories exist - -Be efficient - don't read every file, focus on: -- Configuration files (package.json, tsconfig.json, etc.) -- Main entry points -- Directory structure -- README and documentation - -You have read access to files and can run basic bash commands to explore the structure.`; + sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); } } diff --git a/app/electron/services/feature-verifier.js b/app/electron/services/feature-verifier.js new file mode 100644 index 00000000..1d3b5708 --- /dev/null +++ b/app/electron/services/feature-verifier.js @@ -0,0 +1,148 @@ +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"); + +/** + * Feature Verifier - Handles feature verification by running tests + */ +class FeatureVerifier { + /** + * Verify feature tests (runs tests and checks if they pass) + */ + async verifyFeatureTests(feature, projectPath, sendToRenderer, execution) { + console.log(`[FeatureVerifier] Verifying tests for: ${feature.description}`); + + try { + const verifyMsg = `\nāœ… Verifying tests for: ${feature.description}\n`; + await contextManager.writeToContextFile(projectPath, feature.id, verifyMsg); + + sendToRenderer({ + type: "auto_mode_phase", + featureId: feature.id, + phase: "verification", + message: `Verifying tests 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 + ); + + const options = { + model: "claude-opus-4-5-20251101", + systemPrompt: promptBuilder.getVerificationPrompt(), + maxTurns: 1000, + cwd: projectPath, + mcpServers: { + "automaker-tools": featureToolsServer + }, + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "mcp__automaker-tools__UpdateFeatureStatus"], + permissionMode: "acceptEdits", + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + abortController: abortController, + }; + + const prompt = promptBuilder.buildVerificationPrompt(feature); + + const runningTestsMsg = + "Running Playwright tests to verify feature implementation...\n"; + await contextManager.writeToContextFile(projectPath, feature.id, runningTestsMsg); + + sendToRenderer({ + type: "auto_mode_progress", + featureId: feature.id, + content: runningTestsMsg, + }); + + const 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; + + // Re-load features to check if it was marked as verified + const updatedFeatures = await featureLoader.loadFeatures(projectPath); + const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); + const passes = updatedFeature?.status === "verified"; + + const finalMsg = passes + ? "āœ“ Verification successful: All tests passed\n" + : "āœ— Tests failed or not all passing - feature remains in progress\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("[FeatureVerifier] Verification aborted"); + if (execution) { + execution.abortController = null; + execution.query = null; + } + return { + passes: false, + message: "Verification aborted", + }; + } + + console.error("[FeatureVerifier] Error verifying feature:", error); + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } +} + +module.exports = new FeatureVerifier(); diff --git a/app/electron/services/project-analyzer.js b/app/electron/services/project-analyzer.js new file mode 100644 index 00000000..de91e4dd --- /dev/null +++ b/app/electron/services/project-analyzer.js @@ -0,0 +1,112 @@ +const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); +const promptBuilder = require("./prompt-builder"); + +/** + * Project Analyzer - Scans codebase and updates app_spec.txt + */ +class ProjectAnalyzer { + /** + * Run the project analysis using Claude Agent SDK + */ + async runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution) { + console.log(`[ProjectAnalyzer] Running project analysis for: ${projectPath}`); + + try { + sendToRenderer({ + type: "auto_mode_phase", + featureId: analysisId, + phase: "planning", + message: "Scanning project structure...", + }); + + const abortController = new AbortController(); + execution.abortController = abortController; + + const options = { + model: "claude-sonnet-4-20250514", + systemPrompt: promptBuilder.getProjectAnalysisSystemPrompt(), + maxTurns: 50, + cwd: projectPath, + allowedTools: ["Read", "Glob", "Grep", "Bash"], + permissionMode: "acceptEdits", + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + abortController: abortController, + }; + + const prompt = promptBuilder.buildProjectAnalysisPrompt(projectPath); + + sendToRenderer({ + type: "auto_mode_progress", + featureId: analysisId, + content: "Starting project analysis...\n", + }); + + 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; + sendToRenderer({ + type: "auto_mode_progress", + featureId: analysisId, + content: block.text, + }); + } else if (block.type === "tool_use") { + sendToRenderer({ + type: "auto_mode_tool", + featureId: analysisId, + tool: block.name, + input: block.input, + }); + } + } + } + } + + execution.query = null; + execution.abortController = null; + + sendToRenderer({ + type: "auto_mode_phase", + featureId: analysisId, + phase: "verification", + message: "Project analysis complete", + }); + + return { + success: true, + message: "Project analyzed successfully", + }; + } catch (error) { + if (error instanceof AbortError || error?.name === "AbortError") { + console.log("[ProjectAnalyzer] Project analysis aborted"); + if (execution) { + execution.abortController = null; + execution.query = null; + } + return { + success: false, + message: "Analysis aborted", + }; + } + + console.error("[ProjectAnalyzer] Error in project analysis:", error); + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } +} + +module.exports = new ProjectAnalyzer();