diff --git a/app/electron/main.js b/app/electron/main.js index 41e96442..a19555f0 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -1234,6 +1234,62 @@ ipcMain.handle( } ); +/** + * Generate features from existing app_spec.txt + * This allows users to generate features retroactively without regenerating the spec + */ +ipcMain.handle( + "spec-regeneration:generate-features", + async (_, { projectPath }) => { + try { + // Add project path to allowed paths + addAllowedPath(projectPath); + + // Check if already running + if (specRegenerationExecution && specRegenerationExecution.isActive()) { + return { success: false, error: "Spec regeneration is already running" }; + } + + // Create execution context + specRegenerationExecution = { + abortController: null, + query: null, + isActive: () => specRegenerationExecution !== null, + }; + + const sendToRenderer = (data) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("spec-regeneration:event", data); + } + }; + + // Start generating features (runs in background) + specRegenerationService + .generateFeaturesOnly(projectPath, sendToRenderer, specRegenerationExecution) + .catch((error) => { + console.error( + "[IPC] spec-regeneration:generate-features background error:", + error + ); + sendToRenderer({ + type: "spec_regeneration_error", + error: error.message, + }); + }) + .finally(() => { + specRegenerationExecution = null; + }); + + // Return immediately + return { success: true }; + } catch (error) { + console.error("[IPC] spec-regeneration:generate-features error:", error); + specRegenerationExecution = null; + return { success: false, error: error.message }; + } + } +); + /** * Merge feature worktree changes back to main branch */ diff --git a/app/electron/preload.js b/app/electron/preload.js index aa3d68c1..85a31baa 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -285,6 +285,12 @@ contextBridge.exposeInMainWorld("electronAPI", { projectDefinition, }), + // Generate features from existing app_spec.txt + generateFeatures: (projectPath) => + ipcRenderer.invoke("spec-regeneration:generate-features", { + projectPath, + }), + // Stop regenerating spec stop: () => ipcRenderer.invoke("spec-regeneration:stop"), diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index 1ff3a7c0..02bb9d5f 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -170,14 +170,53 @@ class FeatureLoader { const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); try { + // Check if feature.json exists before trying to read it + try { + await fs.access(featureJsonPath); + } catch (accessError) { + // File doesn't exist - this is expected for incomplete feature directories + // Skip silently or log at debug level only + if (accessError.code !== "ENOENT") { + console.warn( + `[FeatureLoader] Cannot access feature.json for ${featureId}:`, + accessError.message + ); + } + // Skip this directory - it doesn't have a valid feature.json + continue; + } + const content = await fs.readFile(featureJsonPath, "utf-8"); const feature = JSON.parse(content); + + // Validate that the feature has required fields + if (!feature.id) { + console.warn( + `[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping` + ); + continue; + } + features.push(feature); } catch (error) { - console.error( - `[FeatureLoader] Failed to load feature ${featureId}:`, - error - ); + // Handle different error types appropriately + if (error.code === "ENOENT") { + // File was deleted between access check and read - skip silently + console.debug( + `[FeatureLoader] Feature ${featureId} was removed, skipping` + ); + } else if (error instanceof SyntaxError) { + // JSON parse error - log as warning since file exists but is malformed + console.warn( + `[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}` + ); + } else { + // Other errors - log as error + console.error( + `[FeatureLoader] Failed to load feature ${featureId}:`, + error.message || error + ); + } // Continue loading other features } } @@ -339,6 +378,7 @@ class FeatureLoader { /** * Update feature status (legacy API) * Features are stored in .automaker/features/{id}/feature.json + * Creates the feature if it doesn't exist. * @param {string} featureId - The ID of the feature to update * @param {string} status - The new status * @param {string} projectPath - Path to the project @@ -346,16 +386,46 @@ class FeatureLoader { * @param {string} [error] - Optional error message if feature errored */ async updateFeatureStatus(featureId, status, projectPath, summary, error) { + // Check if feature exists + const existingFeature = await this.get(projectPath, featureId); + + if (!existingFeature) { + // Feature doesn't exist - create it + console.log(`[FeatureLoader] Feature ${featureId} not found - creating new feature`); + const newFeature = { + id: featureId, + title: featureId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '), + description: summary || '', // Use summary as description for display + status: status, + summary: summary || '', + createdAt: new Date().toISOString(), + }; + if (error !== undefined) { + newFeature.error = error; + } + await this.create(projectPath, newFeature); + console.log( + `[FeatureLoader] Created feature ${featureId}: status=${status}${ + summary ? `, summary="${summary}"` : "" + }` + ); + return; + } + + // Feature exists - update it const updates = { status }; if (summary !== undefined) { updates.summary = summary; + // Also update description if it's empty or not set + if (!existingFeature.description) { + updates.description = summary; + } } if (error !== undefined) { updates.error = error; } else { // Clear error if not provided - const feature = await this.get(projectPath, featureId); - if (feature && feature.error) { + if (existingFeature.error) { updates.error = undefined; } } diff --git a/app/electron/services/mcp-server-factory.js b/app/electron/services/mcp-server-factory.js index b890dfbe..8907b59c 100644 --- a/app/electron/services/mcp-server-factory.js +++ b/app/electron/services/mcp-server-factory.js @@ -28,18 +28,20 @@ class McpServerFactory { async (args) => { try { console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`); + console.log(`[Feature Creation] Creating/updating feature "${args.featureId}" with status "${args.status}"`); // Load the feature to check skipTests flag const features = await featureLoader.loadFeatures(projectPath); const feature = features.find((f) => f.id === args.featureId); if (!feature) { - throw new Error(`Feature ${args.featureId} not found`); + console.log(`[Feature Creation] Feature ${args.featureId} not found - this might be a new feature being created`); + // This might be a new feature - try to proceed anyway } // If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval let finalStatus = args.status; - if (args.status === "verified" && feature.skipTests === true) { + if (feature && args.status === "verified" && feature.skipTests === true) { console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`); finalStatus = "waiting_approval"; } @@ -51,6 +53,8 @@ class McpServerFactory { ? `Successfully updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}" because skipTests=true)${args.summary ? ` with summary: "${args.summary}"` : ""}` : `Successfully updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` with summary: "${args.summary}"` : ""}`; + console.log(`[Feature Creation] ✓ ${statusMessage}`); + return { content: [{ type: "text", @@ -59,6 +63,7 @@ class McpServerFactory { }; } catch (error) { console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error); + console.error(`[Feature Creation] ✗ Failed to create/update feature ${args.featureId}: ${error.message}`); return { content: [{ type: "text", diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index 99cad5f1..55fe55a1 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -1,6 +1,8 @@ const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); const fs = require("fs/promises"); const path = require("path"); +const mcpServerFactory = require("./mcp-server-factory"); +const featureLoader = require("./feature-loader"); /** * XML template for app_spec.txt @@ -95,18 +97,60 @@ class SpecRegenerationService { * @param {boolean} generateFeatures - Whether to generate feature entries in features folder */ async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) { - console.log(`[SpecRegeneration] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`); + const startTime = Date.now(); + console.log(`[SpecRegeneration] ===== Starting initial spec creation =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + console.log(`[SpecRegeneration] Generate features: ${generateFeatures}`); + console.log(`[SpecRegeneration] Project overview length: ${projectOverview.length} characters`); try { const abortController = new AbortController(); execution.abortController = abortController; + // Phase tracking + let currentPhase = "initialization"; + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${currentPhase}] Initializing spec generation process...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + + // Create custom MCP server with UpdateFeatureStatus tool if generating features + let featureToolsServer = null; + if (generateFeatures) { + console.log("[SpecRegeneration] Setting up feature generation tools..."); + try { + featureToolsServer = mcpServerFactory.createFeatureToolsServer( + featureLoader.updateFeatureStatus.bind(featureLoader), + projectPath + ); + console.log("[SpecRegeneration] Feature tools server created successfully"); + } catch (error) { + console.error("[SpecRegeneration] ERROR: Failed to create feature tools server:", error); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to initialize feature generation tools: ${error.message}`, + }); + throw error; + } + } + + currentPhase = "setup"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${currentPhase}] Configuring AI agent and tools...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + + // Phase 1: Generate spec WITHOUT UpdateFeatureStatus tool + // This prevents features from being created before the spec is complete const options = { model: "claude-sonnet-4-20250514", - systemPrompt: this.getInitialCreationSystemPrompt(generateFeatures), + systemPrompt: this.getInitialCreationSystemPrompt(false), // Always false - no feature tools during spec gen maxTurns: 50, cwd: projectPath, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], // No UpdateFeatureStatus during spec gen permissionMode: "acceptEdits", sandbox: { enabled: true, @@ -115,54 +159,157 @@ class SpecRegenerationService { abortController: abortController, }; - const prompt = this.buildInitialCreationPrompt(projectOverview, generateFeatures); + const prompt = this.buildInitialCreationPrompt(projectOverview); // No feature generation during spec creation + currentPhase = "analysis"; sendToRenderer({ type: "spec_regeneration_progress", - content: "Starting project analysis and spec creation...\n", + content: `[Phase: ${currentPhase}] Starting project analysis and spec creation...\n`, }); + console.log(`[SpecRegeneration] Phase: ${currentPhase} - Starting AI agent query`); + + if (generateFeatures) { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${currentPhase}] Feature generation is enabled - features will be created after spec is complete.\n`, + }); + console.log("[SpecRegeneration] Feature generation enabled - will create features after spec"); + } const currentQuery = query({ prompt, options }); execution.query = currentQuery; let fullResponse = ""; - for await (const msg of currentQuery) { - if (!execution.isActive()) break; + let toolCallCount = 0; + let messageCount = 0; + + try { + for await (const msg of currentQuery) { + if (!execution.isActive()) { + console.log("[SpecRegeneration] Execution aborted by user"); + break; + } - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - fullResponse += block.text; - sendToRenderer({ - type: "spec_regeneration_progress", - content: block.text, - }); - } else if (block.type === "tool_use") { - sendToRenderer({ - type: "spec_regeneration_tool", - tool: block.name, - input: block.input, - }); + if (msg.type === "assistant" && msg.message?.content) { + messageCount++; + for (const block of msg.message.content) { + if (block.type === "text") { + fullResponse += block.text; + const preview = block.text.substring(0, 100).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Agent message #${messageCount}: ${preview}...`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + toolCallCount++; + const toolName = block.name; + console.log(`[SpecRegeneration] Tool call #${toolCallCount}: ${toolName}`); + console.log(`[SpecRegeneration] Tool input: ${JSON.stringify(block.input).substring(0, 200)}...`); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Tool] Using ${toolName}...\n`, + }); + + sendToRenderer({ + type: "spec_regeneration_tool", + tool: toolName, + input: block.input, + }); + } } + } else if (msg.type === "tool_result") { + const toolName = msg.toolName || "unknown"; + const result = msg.content?.[0]?.text || JSON.stringify(msg.content); + const resultPreview = result.substring(0, 200).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Tool result (${toolName}): ${resultPreview}...`); + + // During spec generation, UpdateFeatureStatus is not available + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Tool Result] ${toolName} completed successfully\n`, + }); + } else if (msg.type === "error") { + const errorMsg = msg.error?.message || JSON.stringify(msg.error); + console.error(`[SpecRegeneration] ERROR in query stream: ${errorMsg}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Error during spec generation: ${errorMsg}`, + }); } } + } catch (streamError) { + console.error("[SpecRegeneration] ERROR in query stream:", streamError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Stream error: ${streamError.message || String(streamError)}`, + }); + throw streamError; } + + console.log(`[SpecRegeneration] Query completed - ${messageCount} messages, ${toolCallCount} tool calls`); execution.query = null; execution.abortController = null; + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + currentPhase = "spec_complete"; + console.log(`[SpecRegeneration] Phase: ${currentPhase} - Spec creation completed in ${elapsedTime}s`); sendToRenderer({ - type: "spec_regeneration_complete", - message: "Initial spec creation complete!", + type: "spec_regeneration_progress", + content: `\n[Phase: ${currentPhase}] ✓ App specification created successfully! (${elapsedTime}s)\n`, }); + + if (generateFeatures) { + // Phase 2: Generate features AFTER spec is complete + console.log(`[SpecRegeneration] Starting Phase 2: Feature generation from app_spec.txt`); + + // Send intermediate completion event for spec creation + sendToRenderer({ + type: "spec_regeneration_complete", + message: "Initial spec creation complete! Features are being generated...", + }); + + // Now start feature generation in a separate query + try { + await this.generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime); + console.log(`[SpecRegeneration] Feature generation completed successfully`); + } catch (featureError) { + console.error(`[SpecRegeneration] Feature generation failed:`, featureError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Feature generation failed: ${featureError.message || String(featureError)}`, + }); + } + } else { + currentPhase = "complete"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${currentPhase}] All tasks completed!\n`, + }); + + // Send final completion event + sendToRenderer({ + type: "spec_regeneration_complete", + message: "Initial spec creation complete!", + }); + } + console.log(`[SpecRegeneration] ===== Initial spec creation finished successfully =====`); return { success: true, message: "Initial spec creation complete", }; } catch (error) { + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[SpecRegeneration] Creation aborted"); + console.log(`[SpecRegeneration] Creation aborted after ${elapsedTime}s`); + sendToRenderer({ + type: "spec_regeneration_error", + error: "Spec generation was aborted by user", + }); if (execution) { execution.abortController = null; execution.query = null; @@ -173,7 +320,252 @@ class SpecRegenerationService { }; } - console.error("[SpecRegeneration] Error creating initial spec:", error); + const errorMessage = error.message || String(error); + const errorStack = error.stack || ""; + console.error(`[SpecRegeneration] ERROR creating initial spec after ${elapsedTime}s:`); + console.error(`[SpecRegeneration] Error message: ${errorMessage}`); + console.error(`[SpecRegeneration] Error stack: ${errorStack}`); + + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to create spec: ${errorMessage}`, + }); + + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } + + /** + * Generate features from the implementation roadmap in app_spec.txt + * This is called AFTER the spec has been created + */ + async generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime) { + const featureStartTime = Date.now(); + let currentPhase = "feature_generation"; + + console.log(`[SpecRegeneration] ===== Starting Phase 2: Feature Generation =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Phase: ${currentPhase}] Starting feature creation from implementation roadmap...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${currentPhase} - Starting feature generation query`); + + try { + // Create feature tools server + const featureToolsServer = mcpServerFactory.createFeatureToolsServer( + featureLoader.updateFeatureStatus.bind(featureLoader), + projectPath + ); + + const abortController = new AbortController(); + execution.abortController = abortController; + + const options = { + model: "claude-sonnet-4-20250514", + systemPrompt: `You are a feature management assistant. Your job is to read the app_spec.txt file and create feature entries based on the implementation_roadmap section. + +**Your Task:** +1. Read the .automaker/app_spec.txt file +2. Parse the implementation_roadmap section (it contains phases with features listed) +3. For each feature listed in the roadmap, use the UpdateFeatureStatus tool to create a feature entry +4. Set the initial status to "todo" for all features +5. Extract a meaningful summary/description for each feature from the roadmap + +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. +Use the UpdateFeatureStatus tool to create features. The tool will handle creating the directory structure and feature.json file. + +**Important:** +- Create features ONLY from the implementation_roadmap section +- Use the UpdateFeatureStatus tool for each feature +- Set status to "todo" initially +- Use a descriptive featureId based on the feature name (lowercase, hyphens for spaces)`, + maxTurns: 50, + 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 = `Please read the .automaker/app_spec.txt file and create feature entries for all features listed in the implementation_roadmap section. + +For each feature in the roadmap: +1. Use the UpdateFeatureStatus tool to create the feature +2. Set status to "todo" +3. Provide a clear summary/description based on what's in the roadmap +4. Use a featureId that's descriptive and follows the pattern: lowercase with hyphens (e.g., "user-authentication", "payment-processing") + +Start by reading the app_spec.txt file to see the implementation roadmap.`; + + const currentQuery = query({ prompt, options }); + execution.query = currentQuery; + + let toolCallCount = 0; + let messageCount = 0; + + try { + for await (const msg of currentQuery) { + if (!execution.isActive()) { + console.log("[SpecRegeneration] Feature generation aborted by user"); + break; + } + + if (msg.type === "assistant" && msg.message?.content) { + messageCount++; + for (const block of msg.message.content) { + if (block.type === "text") { + const preview = block.text.substring(0, 100).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Feature gen message #${messageCount}: ${preview}...`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + toolCallCount++; + const toolName = block.name; + const toolInput = block.input; + console.log(`[SpecRegeneration] Feature gen tool call #${toolCallCount}: ${toolName}`); + + if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { + const featureId = toolInput?.featureId || "unknown"; + const status = toolInput?.status || "unknown"; + const summary = toolInput?.summary || ""; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Feature Creation] Creating feature "${featureId}" with status "${status}"${summary ? `\n Summary: ${summary}` : ""}\n`, + }); + } else { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Tool] Using ${toolName}...\n`, + }); + } + + sendToRenderer({ + type: "spec_regeneration_tool", + tool: toolName, + input: toolInput, + }); + } + } + } else if (msg.type === "tool_result") { + const toolName = msg.toolName || "unknown"; + const result = msg.content?.[0]?.text || JSON.stringify(msg.content); + const resultPreview = result.substring(0, 200).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Feature gen tool result (${toolName}): ${resultPreview}...`); + + if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Feature Creation] ${result}\n`, + }); + } else { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Tool Result] ${toolName} completed successfully\n`, + }); + } + } else if (msg.type === "error") { + const errorMsg = msg.error?.message || JSON.stringify(msg.error); + console.error(`[SpecRegeneration] ERROR in feature generation stream: ${errorMsg}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Error during feature generation: ${errorMsg}`, + }); + } + } + } catch (streamError) { + console.error("[SpecRegeneration] ERROR in feature generation stream:", streamError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Feature generation stream error: ${streamError.message || String(streamError)}`, + }); + throw streamError; + } + + console.log(`[SpecRegeneration] Feature generation completed - ${messageCount} messages, ${toolCallCount} tool calls`); + + execution.query = null; + execution.abortController = null; + + currentPhase = "complete"; + const featureElapsedTime = ((Date.now() - featureStartTime) / 1000).toFixed(1); + const totalElapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Phase: ${currentPhase}] ✓ All tasks completed! (${totalElapsedTime}s total, ${featureElapsedTime}s for features)\n`, + }); + sendToRenderer({ + type: "spec_regeneration_complete", + message: "All tasks completed!", + }); + console.log(`[SpecRegeneration] All tasks completed including feature generation`); + + } catch (error) { + const errorMessage = error.message || String(error); + console.error(`[SpecRegeneration] ERROR generating features: ${errorMessage}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to generate features: ${errorMessage}`, + }); + throw error; + } + } + + /** + * Generate features from existing app_spec.txt + * This is a standalone method that can be called without generating a new spec + * Useful for retroactively generating features from an existing spec + */ + async generateFeaturesOnly(projectPath, sendToRenderer, execution) { + const startTime = Date.now(); + console.log(`[SpecRegeneration] ===== Starting standalone feature generation =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + + try { + // Verify app_spec.txt exists + const specPath = path.join(projectPath, ".automaker", "app_spec.txt"); + try { + await fs.access(specPath); + } catch { + sendToRenderer({ + type: "spec_regeneration_error", + error: "No app_spec.txt found. Please create a spec first before generating features.", + }); + throw new Error("No app_spec.txt found"); + } + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: initialization] Starting feature generation from existing app_spec.txt...\n`, + }); + + // Use the existing feature generation method + await this.generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime); + + console.log(`[SpecRegeneration] ===== Standalone feature generation finished successfully =====`); + return { + success: true, + message: "Feature generation complete", + }; + } catch (error) { + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + const errorMessage = error.message || String(error); + console.error(`[SpecRegeneration] ERROR in standalone feature generation after ${elapsedTime}s: ${errorMessage}`); + if (execution) { execution.abortController = null; execution.query = null; @@ -205,14 +597,12 @@ When analyzing, look at: - Database configurations and schemas - API structures and patterns -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. -Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features. - You CAN and SHOULD modify: - .automaker/app_spec.txt (this is your primary target) -You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`; +You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec. + +**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase.`; } /** @@ -266,12 +656,29 @@ Begin by exploring the project structure.`; * Regenerate the app spec based on user's project definition */ async regenerateSpec(projectPath, projectDefinition, sendToRenderer, execution) { - console.log(`[SpecRegeneration] Regenerating spec for: ${projectPath}`); + const startTime = Date.now(); + console.log(`[SpecRegeneration] ===== Starting spec regeneration =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + console.log(`[SpecRegeneration] Project definition length: ${projectDefinition.length} characters`); try { + let currentPhase = "initialization"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${currentPhase}] Initializing spec regeneration process...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + const abortController = new AbortController(); execution.abortController = abortController; + currentPhase = "setup"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${currentPhase}] Configuring AI agent and tools...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + const options = { model: "claude-sonnet-4-20250514", systemPrompt: this.getSystemPrompt(), @@ -288,52 +695,137 @@ Begin by exploring the project structure.`; const prompt = this.buildRegenerationPrompt(projectDefinition); + currentPhase = "regeneration"; sendToRenderer({ type: "spec_regeneration_progress", - content: "Starting spec regeneration...\n", + content: `[Phase: ${currentPhase}] Starting spec regeneration...\n`, }); + console.log(`[SpecRegeneration] Phase: ${currentPhase} - Starting AI agent query`); const currentQuery = query({ prompt, options }); execution.query = currentQuery; let fullResponse = ""; - for await (const msg of currentQuery) { - if (!execution.isActive()) break; + let toolCallCount = 0; + let messageCount = 0; + + try { + for await (const msg of currentQuery) { + if (!execution.isActive()) { + console.log("[SpecRegeneration] Execution aborted by user"); + break; + } - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - fullResponse += block.text; + if (msg.type === "assistant" && msg.message?.content) { + messageCount++; + for (const block of msg.message.content) { + if (block.type === "text") { + fullResponse += block.text; + const preview = block.text.substring(0, 100).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Agent message #${messageCount}: ${preview}...`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Agent] ${block.text}`, + }); + } else if (block.type === "tool_use") { + toolCallCount++; + const toolName = block.name; + const toolInput = block.input; + console.log(`[SpecRegeneration] Tool call #${toolCallCount}: ${toolName}`); + console.log(`[SpecRegeneration] Tool input: ${JSON.stringify(toolInput).substring(0, 200)}...`); + + // Special handling for UpdateFeatureStatus to show feature creation + if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { + const featureId = toolInput?.featureId || "unknown"; + const status = toolInput?.status || "unknown"; + const summary = toolInput?.summary || ""; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Feature Creation] Creating feature "${featureId}" with status "${status}"${summary ? `\n Summary: ${summary}` : ""}\n`, + }); + } else { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Tool] Using ${toolName}...\n`, + }); + } + + sendToRenderer({ + type: "spec_regeneration_tool", + tool: toolName, + input: toolInput, + }); + } + } + } else if (msg.type === "tool_result") { + // Log tool results for better visibility + const toolName = msg.toolName || "unknown"; + const result = msg.content?.[0]?.text || JSON.stringify(msg.content); + const resultPreview = result.substring(0, 200).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Tool result (${toolName}): ${resultPreview}...`); + + // Special handling for UpdateFeatureStatus results + if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { sendToRenderer({ type: "spec_regeneration_progress", - content: block.text, + content: `[Feature Creation] ${result}\n`, }); - } else if (block.type === "tool_use") { + } else { sendToRenderer({ - type: "spec_regeneration_tool", - tool: block.name, - input: block.input, + type: "spec_regeneration_progress", + content: `[Tool Result] ${toolName} completed successfully\n`, }); } + } else if (msg.type === "error") { + const errorMsg = msg.error?.message || JSON.stringify(msg.error); + console.error(`[SpecRegeneration] ERROR in query stream: ${errorMsg}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Error during spec regeneration: ${errorMsg}`, + }); } } + } catch (streamError) { + console.error("[SpecRegeneration] ERROR in query stream:", streamError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Stream error: ${streamError.message || String(streamError)}`, + }); + throw streamError; } + + console.log(`[SpecRegeneration] Query completed - ${messageCount} messages, ${toolCallCount} tool calls`); execution.query = null; execution.abortController = null; + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + currentPhase = "complete"; + console.log(`[SpecRegeneration] Phase: ${currentPhase} - Spec regeneration completed in ${elapsedTime}s`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Phase: ${currentPhase}] ✓ Spec regeneration complete! (${elapsedTime}s)\n`, + }); + sendToRenderer({ type: "spec_regeneration_complete", message: "Spec regeneration complete!", }); + console.log(`[SpecRegeneration] ===== Spec regeneration finished successfully =====`); return { success: true, message: "Spec regeneration complete", }; } catch (error) { + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[SpecRegeneration] Regeneration aborted"); + console.log(`[SpecRegeneration] Regeneration aborted after ${elapsedTime}s`); + sendToRenderer({ + type: "spec_regeneration_error", + error: "Spec regeneration was aborted by user", + }); if (execution) { execution.abortController = null; execution.query = null; @@ -344,7 +836,17 @@ Begin by exploring the project structure.`; }; } - console.error("[SpecRegeneration] Error regenerating spec:", error); + const errorMessage = error.message || String(error); + const errorStack = error.stack || ""; + console.error(`[SpecRegeneration] ERROR regenerating spec after ${elapsedTime}s:`); + console.error(`[SpecRegeneration] Error message: ${errorMessage}`); + console.error(`[SpecRegeneration] Error stack: ${errorStack}`); + + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to regenerate spec: ${errorMessage}`, + }); + if (execution) { execution.abortController = null; execution.query = null; diff --git a/app/src/components/ui/log-viewer.tsx b/app/src/components/ui/log-viewer.tsx index 37d8fe21..169c626f 100644 --- a/app/src/components/ui/log-viewer.tsx +++ b/app/src/components/ui/log-viewer.tsx @@ -208,7 +208,19 @@ export function LogViewer({ output, className }: LogViewerProps) { }; if (entries.length === 0) { - return null; + return ( +
No log entries yet. Logs will appear here as the process runs.
+ {output && output.trim() && ( +{output}
+