From c198c10244889392815d4bd2ffd2ce9803da92bf Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 03:01:45 -0600 Subject: [PATCH 01/14] Complete overhaul for app spec system. Created logic to auto generate kanban stories after the fact as well as added logging logic and visual aids to tell what stage of the process the app spec creation is in. May need refinement for state-based updates as the menu doesnt update as dynamicly as id like --- app/electron/main.js | 56 ++ app/electron/preload.js | 6 + app/electron/services/feature-loader.js | 82 +- app/electron/services/mcp-server-factory.js | 9 +- .../services/spec-regeneration-service.js | 592 +++++++++++++- app/src/components/ui/log-viewer.tsx | 14 +- app/src/components/views/kanban-card.tsx | 17 +- app/src/components/views/spec-view.tsx | 767 ++++++++++++++++-- app/src/lib/electron.ts | 62 ++ app/src/lib/log-parser.ts | 35 +- app/src/types/electron.d.ts | 5 + 11 files changed, 1508 insertions(+), 137 deletions(-) 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}
+
+ )} +
+
+ ); } // Count entries by type diff --git a/app/src/components/views/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index 71cb4416..c941c8c2 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -393,10 +393,10 @@ export const KanbanCard = memo(function KanbanCard({ !isDescriptionExpanded && "line-clamp-3" )} > - {feature.description} + {feature.description || feature.summary || feature.title || feature.id} {/* Show More/Less toggle - only show when description is likely truncated */} - {feature.description.length > 100 && ( + {(feature.description || feature.summary || feature.title || "").length > 100 && ( + {errorMessage && ( +
+

Error:

+

{errorMessage}

+
+ )} + {!isCreating && ( +
+ +
+ )} {/* Create Dialog */} - + { + if (!open && !isCreating) { + setShowCreateDialog(false); + } + }} + > Create App Specification @@ -270,6 +813,7 @@ export function SpecView() { onChange={(e) => setProjectOverview(e.target.value)} placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..." autoFocus + disabled={isCreating} /> @@ -278,11 +822,12 @@ export function SpecView() { id="generate-features" checked={generateFeatures} onCheckedChange={(checked) => setGenerateFeatures(checked === true)} + disabled={isCreating} />
@@ -298,17 +843,27 @@ export function SpecView() { - - Generate Spec + {isCreating ? ( + <> + + Generating... + + ) : ( + <> + + Generate Spec + + )} @@ -333,30 +888,66 @@ export function SpecView() {

-
- - +
+ {(isRegenerating || isCreating || isGeneratingFeatures) && ( +
+
+ +
+
+
+ + {isGeneratingFeatures ? "Generating Features" : isCreating ? "Generating Specification" : "Regenerating Specification"} + + {currentPhase && ( + + {currentPhase === "initialization" && "Initializing..."} + {currentPhase === "setup" && "Setting up tools..."} + {currentPhase === "analysis" && "Analyzing project structure..."} + {currentPhase === "spec_complete" && "Spec created! Generating features..."} + {currentPhase === "feature_generation" && "Creating features from roadmap..."} + {currentPhase === "complete" && "Complete!"} + {currentPhase === "error" && "Error occurred"} + {!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase} + + )} +
+
+ )} + {errorMessage && ( +
+ +
+ Error + {errorMessage} +
+
+ )} +
+ + +
@@ -373,7 +964,14 @@ export function SpecView() {
{/* Regenerate Dialog */} - + { + if (!open && !isRegenerating) { + setShowRegenerateDialog(false); + } + }} + > Regenerate App Specification @@ -403,35 +1001,56 @@ export function SpecView() { - + - - {isRegenerating ? ( + {isGeneratingFeatures ? ( <> - Regenerating... + Generating... ) : ( <> - - Regenerate Spec + + Generate Features )} - + +
+ + + {isRegenerating ? ( + <> + + Regenerating... + + ) : ( + <> + + Regenerate Spec + + )} + +
+ ); } diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 04d7625f..af8a66a4 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -1843,6 +1843,23 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { return { success: true }; }, + generateFeatures: async (projectPath: string) => { + if (mockSpecRegenerationRunning) { + return { + success: false, + error: "Feature generation is already running", + }; + } + + mockSpecRegenerationRunning = true; + console.log(`[Mock] Generating features from existing spec for: ${projectPath}`); + + // Simulate async feature generation + simulateFeatureGeneration(projectPath); + + return { success: true }; + }, + stop: async () => { mockSpecRegenerationRunning = false; if (mockSpecRegenerationTimeout) { @@ -2007,6 +2024,51 @@ async function simulateSpecRegeneration( mockSpecRegenerationTimeout = null; } +async function simulateFeatureGeneration(projectPath: string) { + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Phase: initialization] Starting feature generation from existing app_spec.txt...\n", + }); + + await new Promise((resolve) => { + mockSpecRegenerationTimeout = setTimeout(resolve, 500); + }); + if (!mockSpecRegenerationRunning) return; + + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Phase: feature_generation] Reading implementation roadmap...\n", + }); + + await new Promise((resolve) => { + mockSpecRegenerationTimeout = setTimeout(resolve, 500); + }); + if (!mockSpecRegenerationRunning) return; + + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Feature Creation] Creating features from roadmap...\n", + }); + + await new Promise((resolve) => { + mockSpecRegenerationTimeout = setTimeout(resolve, 1000); + }); + if (!mockSpecRegenerationRunning) return; + + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Phase: complete] All tasks completed!\n", + }); + + emitSpecRegenerationEvent({ + type: "spec_regeneration_complete", + message: "All tasks completed!", + }); + + mockSpecRegenerationRunning = false; + mockSpecRegenerationTimeout = null; +} + // Mock Features API implementation function createMockFeaturesAPI(): FeaturesAPI { // Store features in mock file system using features/{id}/feature.json pattern diff --git a/app/src/lib/log-parser.ts b/app/src/lib/log-parser.ts index a2e6cad1..872b814d 100644 --- a/app/src/lib/log-parser.ts +++ b/app/src/lib/log-parser.ts @@ -72,10 +72,21 @@ function detectEntryType(content: string): LogEntryType { trimmed.startsWith("📋") || trimmed.startsWith("⚡") || trimmed.startsWith("✅") || - trimmed.match(/^(Planning|Action|Verification)/i) + trimmed.match(/^(Planning|Action|Verification)/i) || + trimmed.match(/\[Phase:\s*([^\]]+)\]/) || + trimmed.match(/Phase:\s*\w+/i) ) { return "phase"; } + + // Feature creation events + if ( + trimmed.match(/\[Feature Creation\]/i) || + trimmed.match(/Feature Creation/i) || + trimmed.match(/Creating feature/i) + ) { + return "success"; + } // Errors if (trimmed.startsWith("❌") || trimmed.toLowerCase().includes("error:")) { @@ -138,6 +149,12 @@ function extractPhase(content: string): string | undefined { if (content.includes("⚡")) return "action"; if (content.includes("✅")) return "verification"; + // Extract from [Phase: ...] format + const phaseMatch = content.match(/\[Phase:\s*([^\]]+)\]/); + if (phaseMatch) { + return phaseMatch[1].toLowerCase(); + } + const match = content.match(/^(Planning|Action|Verification)/i); return match?.[1]?.toLowerCase(); } @@ -155,7 +172,14 @@ function generateTitle(type: LogEntryType, content: string): string { return "Tool Input/Result"; case "phase": { const phase = extractPhase(content); - return phase ? `Phase: ${phase.charAt(0).toUpperCase() + phase.slice(1)}` : "Phase Change"; + if (phase) { + // Capitalize first letter of each word + const formatted = phase.split(/\s+/).map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(" "); + return `Phase: ${formatted}`; + } + return "Phase Change"; } case "error": return "Error"; @@ -224,6 +248,13 @@ export function parseLogOutput(rawOutput: string): LogEntry[] { trimmedLine.startsWith("❌") || trimmedLine.startsWith("⚠️") || trimmedLine.startsWith("🧠") || + trimmedLine.match(/\[Phase:\s*([^\]]+)\]/) || + trimmedLine.match(/\[Feature Creation\]/i) || + trimmedLine.match(/\[Tool\]/i) || + trimmedLine.match(/\[Agent\]/i) || + trimmedLine.match(/\[Complete\]/i) || + trimmedLine.match(/\[ERROR\]/i) || + trimmedLine.match(/\[Status\]/i) || trimmedLine.toLowerCase().includes("ultrathink preparation") || trimmedLine.toLowerCase().includes("thinking level") || (trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call"); diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 213b793d..66d0d13f 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -257,6 +257,11 @@ export interface SpecRegenerationAPI { error?: string; }>; + generateFeatures: (projectPath: string) => Promise<{ + success: boolean; + error?: string; + }>; + stop: () => Promise<{ success: boolean; error?: string; From 8d6f825c5cb1f1d4164bdfca3a4a0f45c7ee0c39 Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 03:40:16 -0600 Subject: [PATCH 02/14] Overhaul updates for appspec & feature generation. Added detail back to kanban feature creation --- app/electron/services/feature-loader.js | 40 ++++++++++-- app/electron/services/mcp-server-factory.js | 34 +++++++--- app/electron/services/prompt-builder.js | 18 ++--- .../services/spec-regeneration-service.js | 65 +++++++++++++------ 4 files changed, 115 insertions(+), 42 deletions(-) diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index 02bb9d5f..67e6f5df 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -384,20 +384,30 @@ class FeatureLoader { * @param {string} projectPath - Path to the project * @param {string} [summary] - Optional summary of what was done * @param {string} [error] - Optional error message if feature errored + * @param {string} [description] - Optional detailed description + * @param {string} [category] - Optional category/phase + * @param {string[]} [steps] - Optional array of implementation steps */ - async updateFeatureStatus(featureId, status, projectPath, summary, error) { + async updateFeatureStatus(featureId, status, projectPath, summary, error, description, category, steps) { // Check if feature exists const existingFeature = await this.get(projectPath, featureId); if (!existingFeature) { - // Feature doesn't exist - create it + // Feature doesn't exist - create it with all required fields 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 + description: description || summary || '', // Use provided description, fall back to summary + category: category || "Uncategorized", + steps: steps || [], status: status, - summary: summary || '', + images: [], + imagePaths: [], + skipTests: true, + model: "sonnet", + thinkingLevel: "none", + summary: summary || description || '', createdAt: new Date().toISOString(), }; if (error !== undefined) { @@ -405,7 +415,7 @@ class FeatureLoader { } await this.create(projectPath, newFeature); console.log( - `[FeatureLoader] Created feature ${featureId}: status=${status}${ + `[FeatureLoader] Created feature ${featureId}: status=${status}, category=${category || "Uncategorized"}, steps=${steps?.length || 0}${ summary ? `, summary="${summary}"` : "" }` ); @@ -421,6 +431,15 @@ class FeatureLoader { updates.description = summary; } } + if (description !== undefined) { + updates.description = description; + } + if (category !== undefined) { + updates.category = category; + } + if (steps !== undefined && Array.isArray(steps)) { + updates.steps = steps; + } if (error !== undefined) { updates.error = error; } else { @@ -429,10 +448,21 @@ class FeatureLoader { updates.error = undefined; } } + + // Ensure required fields exist (for features created before this fix) + if (!existingFeature.category && !updates.category) updates.category = "Uncategorized"; + if (!existingFeature.steps && !updates.steps) updates.steps = []; + if (!existingFeature.images) updates.images = []; + if (!existingFeature.imagePaths) updates.imagePaths = []; + if (existingFeature.skipTests === undefined) updates.skipTests = true; + if (!existingFeature.model) updates.model = "sonnet"; + if (!existingFeature.thinkingLevel) updates.thinkingLevel = "none"; await this.update(projectPath, featureId, updates); console.log( `[FeatureLoader] Updated feature ${featureId}: status=${status}${ + category ? `, category="${category}"` : "" + }${steps ? `, steps=${steps.length}` : ""}${ summary ? `, summary="${summary}"` : "" }` ); diff --git a/app/electron/services/mcp-server-factory.js b/app/electron/services/mcp-server-factory.js index 8907b59c..8650e926 100644 --- a/app/electron/services/mcp-server-factory.js +++ b/app/electron/services/mcp-server-factory.js @@ -19,15 +19,18 @@ class McpServerFactory { tools: [ tool( "UpdateFeatureStatus", - "Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.", + "Create or update a feature. Use this tool to create new features with detailed information or update existing feature status. When creating features, provide comprehensive description, category, and implementation steps.", { - 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. Note: If skipTests=true, verified will be converted to waiting_approval automatically."), - summary: z.string().optional().describe("A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: 'Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx'") + featureId: z.string().describe("The ID of the feature (lowercase, hyphens for spaces). Example: 'user-authentication', 'budget-tracking'"), + status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. Use 'backlog' or 'todo' for new features."), + summary: z.string().optional().describe("A brief summary of what was implemented/changed or what the feature does."), + description: z.string().optional().describe("A detailed description of the feature. Be comprehensive - explain what the feature does, its purpose, and key functionality."), + category: z.string().optional().describe("The category/phase for this feature. Example: 'Phase 1: Foundation', 'Phase 2: Core Logic', 'Phase 3: Polish', 'Authentication', 'UI/UX'"), + steps: z.array(z.string()).optional().describe("Array of implementation steps. Each step should be a clear, actionable task. Example: ['Set up database schema', 'Create API endpoints', 'Build UI components', 'Add validation']") }, async (args) => { try { - console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`); + console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}, category=${args.category || "(none)"}, steps=${args.steps?.length || 0}`); console.log(`[Feature Creation] Creating/updating feature "${args.featureId}" with status "${args.status}"`); // Load the feature to check skipTests flag @@ -41,17 +44,30 @@ class McpServerFactory { // If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval let finalStatus = args.status; + // Convert 'todo' to 'backlog' for consistency + if (finalStatus === "todo") { + finalStatus = "backlog"; + } if (feature && args.status === "verified" && feature.skipTests === true) { console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`); finalStatus = "waiting_approval"; } - // Call the provided callback to update feature status with summary - await updateFeatureStatusCallback(args.featureId, finalStatus, projectPath, args.summary); + // Call the provided callback to update feature status with all parameters + await updateFeatureStatusCallback( + args.featureId, + finalStatus, + projectPath, + args.summary, + undefined, // error + args.description, + args.category, + args.steps + ); const statusMessage = finalStatus !== args.status - ? `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}"` : ""}`; + ? `Successfully created/updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}")${args.summary ? ` - ${args.summary}` : ""}` + : `Successfully created/updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` - ${args.summary}` : ""}`; console.log(`[Feature Creation] ✓ ${statusMessage}`); diff --git a/app/electron/services/prompt-builder.js b/app/electron/services/prompt-builder.js index 2c793403..d2e44d9c 100644 --- a/app/electron/services/prompt-builder.js +++ b/app/electron/services/prompt-builder.js @@ -52,11 +52,11 @@ ${memoryContent} **Current Feature to Implement:** ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} +Category: ${feature.category || "Uncategorized"} +Description: ${feature.description || feature.summary || feature.title || "No description provided"} ${skipTestsNote}${imagesNote}${contextFilesPreview} **Steps to Complete:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} +${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} **Your Task:** @@ -195,12 +195,12 @@ ${memoryContent} **Feature to Implement/Verify:** ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} +Category: ${feature.category || "Uncategorized"} +Description: ${feature.description || feature.summary || feature.title || "No description provided"} Current Status: ${feature.status} ${skipTestsNote}${imagesNote}${contextFilesPreview} **Steps that should be implemented:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} +${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} **Your Task:** @@ -335,11 +335,11 @@ ${memoryContent} **Current Feature:** ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} +Category: ${feature.category || "Uncategorized"} +Description: ${feature.description || feature.summary || feature.title || "No description provided"} ${skipTestsNote}${imagesNote}${contextFilesPreview} **Steps to Complete:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} +${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} **Previous Work Context:** diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index 55fe55a1..632b0a1e 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -368,24 +368,42 @@ class SpecRegenerationService { 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. + systemPrompt: `You are a feature management assistant. Your job is to read the app_spec.txt file and create DETAILED, COMPREHENSIVE feature entries based on the implementation_roadmap section. **Your Task:** -1. Read the .automaker/app_spec.txt file +1. Read the .automaker/app_spec.txt file thoroughly 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 +3. For EACH feature in the roadmap, use the UpdateFeatureStatus tool to create a detailed feature entry +4. Set the initial status to "backlog" for all features + +**IMPORTANT - For each feature you MUST provide:** +- **featureId**: A descriptive ID (lowercase, hyphens for spaces). Example: "user-authentication", "budget-tracking" +- **status**: "backlog" for all new features +- **description**: A DETAILED description (2-4 sentences) explaining what the feature does, its purpose, and key functionality +- **category**: The phase from the roadmap (e.g., "Phase 1: Foundation", "Phase 2: Core Logic", "Phase 3: Polish") +- **steps**: An array of 4-8 clear, actionable implementation steps. Each step should be specific and completable. +- **summary**: A brief one-line summary of the feature + +**Example of a well-defined feature:** +{ + "featureId": "user-authentication", + "status": "backlog", + "description": "Implement secure user authentication system with email/password login, OAuth integration for Google and Facebook, password reset functionality, and session management. This forms the foundation for all user-specific features.", + "category": "Phase 1: Foundation", + "steps": [ + "Set up authentication provider (NextAuth.js or similar)", + "Configure email/password authentication", + "Implement social login (Google, Facebook OAuth)", + "Create login and registration UI components", + "Add password reset flow with email verification", + "Implement session management and token refresh" + ], + "summary": "Secure authentication with email/password and social login" +} **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)`, +Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, maxTurns: 50, cwd: projectPath, mcpServers: { @@ -400,15 +418,24 @@ Use the UpdateFeatureStatus tool to create features. The tool will handle creati 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. + const prompt = `Please read the .automaker/app_spec.txt file and create DETAILED 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") +**Your workflow:** +1. Read the app_spec.txt file completely +2. Identify ALL features from the implementation_roadmap section +3. For EACH feature, call UpdateFeatureStatus with ALL required fields: -Start by reading the app_spec.txt file to see the implementation roadmap.`; +**Required for each UpdateFeatureStatus call:** +- featureId: Descriptive ID (lowercase, hyphens). Example: "user-authentication" +- status: "backlog" +- description: 2-4 sentences explaining the feature in detail +- category: The phase name (e.g., "Phase 1: Foundation", "Phase 2: Core Logic") +- steps: Array of 4-8 specific implementation steps +- summary: One-line summary + +**Do NOT create features with just a summary - each feature needs description, category, AND steps.** + +Start by reading the app_spec.txt file, then create each feature with full detail.`; const currentQuery = query({ prompt, options }); execution.query = currentQuery; From 626219fa62fb11fb0c4cbef9ee130309caccfd77 Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 04:01:29 -0600 Subject: [PATCH 03/14] Scans the project once again before generating features that may already be implemented. Soft tested, worked the 1 time I tried, created half as many items for an existing project. --- .../services/spec-regeneration-service.js | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index 632b0a1e..849031cd 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -368,37 +368,50 @@ class SpecRegenerationService { 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 DETAILED, COMPREHENSIVE feature entries based on the implementation_roadmap section. + systemPrompt: `You are a feature management assistant. Your job is to analyze an existing codebase, compare it against the app_spec.txt, and create feature entries for work that still needs to be done. + +**CRITICAL: You must analyze the existing codebase FIRST before creating features.** **Your Task:** -1. Read the .automaker/app_spec.txt file thoroughly -2. Parse the implementation_roadmap section (it contains phases with features listed) -3. For EACH feature in the roadmap, use the UpdateFeatureStatus tool to create a detailed feature entry -4. Set the initial status to "backlog" for all features +1. Read the .automaker/app_spec.txt file thoroughly to understand the planned features +2. **ANALYZE THE EXISTING CODEBASE** - Look at: + - package.json/requirements.txt for installed dependencies + - Source code structure (src/, app/, components/, pages/, etc.) + - Existing components, routes, API endpoints, database schemas + - Configuration files, authentication setup, etc. +3. For EACH feature in the implementation_roadmap: + - Determine if it's ALREADY IMPLEMENTED (fully or partially) + - If fully implemented: Create with status "verified" and note what's done + - If partially implemented: Create with status "in_progress" and note remaining work + - If not started: Create with status "backlog" **IMPORTANT - For each feature you MUST provide:** - **featureId**: A descriptive ID (lowercase, hyphens for spaces). Example: "user-authentication", "budget-tracking" -- **status**: "backlog" for all new features +- **status**: + - "verified" if feature is fully implemented in the codebase + - "in_progress" if partially implemented + - "backlog" if not yet started - **description**: A DETAILED description (2-4 sentences) explaining what the feature does, its purpose, and key functionality - **category**: The phase from the roadmap (e.g., "Phase 1: Foundation", "Phase 2: Core Logic", "Phase 3: Polish") -- **steps**: An array of 4-8 clear, actionable implementation steps. Each step should be specific and completable. -- **summary**: A brief one-line summary of the feature +- **steps**: An array of 4-8 clear, actionable implementation steps. For verified features, these are what WAS done. For backlog, these are what NEEDS to be done. +- **summary**: A brief one-line summary. For verified features, describe what's implemented. + +**Example of analyzing existing code:** +If you find NextAuth.js configured in the codebase with working login pages, the user-authentication feature should be "verified" not "backlog". **Example of a well-defined feature:** { "featureId": "user-authentication", - "status": "backlog", - "description": "Implement secure user authentication system with email/password login, OAuth integration for Google and Facebook, password reset functionality, and session management. This forms the foundation for all user-specific features.", + "status": "verified", // Because we found it's already implemented + "description": "Secure user authentication system with email/password login and session management. Already implemented using NextAuth.js with email provider.", "category": "Phase 1: Foundation", "steps": [ - "Set up authentication provider (NextAuth.js or similar)", - "Configure email/password authentication", - "Implement social login (Google, Facebook OAuth)", - "Create login and registration UI components", - "Add password reset flow with email verification", - "Implement session management and token refresh" + "Set up authentication provider (NextAuth.js) - DONE", + "Configure email/password authentication - DONE", + "Create login and registration UI components - DONE", + "Implement session management - DONE" ], - "summary": "Secure authentication with email/password and social login" + "summary": "Authentication implemented with NextAuth.js email provider" } **Feature Storage:** @@ -418,24 +431,33 @@ Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, abortController: abortController, }; - const prompt = `Please read the .automaker/app_spec.txt file and create DETAILED feature entries for ALL features listed in the implementation_roadmap section. + const prompt = `Analyze this project and create feature entries based on the app_spec.txt implementation roadmap. + +**IMPORTANT: You must analyze the existing codebase to determine what's already implemented.** **Your workflow:** -1. Read the app_spec.txt file completely -2. Identify ALL features from the implementation_roadmap section -3. For EACH feature, call UpdateFeatureStatus with ALL required fields: +1. **First, analyze the existing codebase:** + - Read package.json or similar config files to see what's installed + - Explore the source code structure (use Glob to list directories) + - Look at key files: components, pages, API routes, database schemas + - Check for authentication, routing, state management, etc. -**Required for each UpdateFeatureStatus call:** -- featureId: Descriptive ID (lowercase, hyphens). Example: "user-authentication" -- status: "backlog" -- description: 2-4 sentences explaining the feature in detail -- category: The phase name (e.g., "Phase 1: Foundation", "Phase 2: Core Logic") -- steps: Array of 4-8 specific implementation steps -- summary: One-line summary +2. **Then, read .automaker/app_spec.txt** to see the implementation roadmap -**Do NOT create features with just a summary - each feature needs description, category, AND steps.** +3. **For EACH feature in the roadmap, determine its status:** + - Is it ALREADY IMPLEMENTED in the codebase? → status: "verified" + - Is it PARTIALLY IMPLEMENTED? → status: "in_progress" + - Is it NOT STARTED? → status: "backlog" -Start by reading the app_spec.txt file, then create each feature with full detail.`; +4. **Create each feature with UpdateFeatureStatus including ALL fields:** + - featureId: Descriptive ID (lowercase, hyphens) + - status: "verified", "in_progress", or "backlog" based on your analysis + - description: 2-4 sentences explaining the feature + - category: The phase name from the roadmap + - steps: Array of 4-8 implementation steps + - summary: One-line summary (for verified features, note what's implemented) + +**Start by exploring the project structure, then read the app_spec, then create features with accurate statuses.**`; const currentQuery = query({ prompt, options }); execution.query = currentQuery; From 9381162a8e81d3d5782415535ad1eabfcb275f30 Mon Sep 17 00:00:00 2001 From: Ben <117330199+trueheads@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:50:13 -0600 Subject: [PATCH 04/14] Update app/electron/services/mcp-server-factory.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/electron/services/mcp-server-factory.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/electron/services/mcp-server-factory.js b/app/electron/services/mcp-server-factory.js index 8650e926..4fdb2bd6 100644 --- a/app/electron/services/mcp-server-factory.js +++ b/app/electron/services/mcp-server-factory.js @@ -44,8 +44,8 @@ class McpServerFactory { // If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval let finalStatus = args.status; - // Convert 'todo' to 'backlog' for consistency - if (finalStatus === "todo") { + // Convert 'todo' to 'backlog' for consistency, but only for new features + if (!feature && finalStatus === "todo") { finalStatus = "backlog"; } if (feature && args.status === "verified" && feature.skipTests === true) { From 60065806f486b9f9f0a90b11beee1f2d320c84a1 Mon Sep 17 00:00:00 2001 From: Ben <117330199+trueheads@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:50:30 -0600 Subject: [PATCH 05/14] Update app/electron/services/spec-regeneration-service.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/electron/services/spec-regeneration-service.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index 849031cd..119d6445 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -117,11 +117,10 @@ class SpecRegenerationService { 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( + mcpServerFactory.createFeatureToolsServer( featureLoader.updateFeatureStatus.bind(featureLoader), projectPath ); From 4b1f4576aea6621e1d8e3e2b5545c44b8904870b Mon Sep 17 00:00:00 2001 From: Ben <117330199+trueheads@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:51:06 -0600 Subject: [PATCH 06/14] Update app/src/components/views/spec-view.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/src/components/views/spec-view.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/components/views/spec-view.tsx b/app/src/components/views/spec-view.tsx index d7de8ba2..28e58216 100644 --- a/app/src/components/views/spec-view.tsx +++ b/app/src/components/views/spec-view.tsx @@ -40,9 +40,6 @@ export function SpecView() { // Generate features only state const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false); - // Logs state (kept for internal tracking, but UI removed) - const [logs, setLogs] = useState(""); - const logsRef = useRef(""); // Phase tracking and status const [currentPhase, setCurrentPhase] = useState(""); From a602b1b519d25de039b09cee254b8f6414a68243 Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 09:14:41 -0600 Subject: [PATCH 07/14] adjustments based on gemini review --- app/electron/services/feature-loader.js | 2 +- .../services/spec-regeneration-service.js | 155 +++++++++++------- app/src/components/views/spec-view.tsx | 63 ++++--- 3 files changed, 130 insertions(+), 90 deletions(-) diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index 67e6f5df..d259bbf1 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -404,7 +404,7 @@ class FeatureLoader { status: status, images: [], imagePaths: [], - skipTests: true, + skipTests: false, // Auto-generated features should run tests by default model: "sonnet", thinkingLevel: "none", summary: summary || description || '', diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index 119d6445..9244e62b 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -461,8 +461,7 @@ Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, const currentQuery = query({ prompt, options }); execution.query = currentQuery; - let toolCallCount = 0; - let messageCount = 0; + const counters = { toolCallCount: 0, messageCount: 0 }; try { for await (const msg of currentQuery) { @@ -472,67 +471,11 @@ Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, } 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, - }); - } - } + this._handleAssistantMessage(msg, sendToRenderer, counters); } 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`, - }); - } + this._handleToolResult(msg, sendToRenderer); } 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}`, - }); + this._handleStreamError(msg, sendToRenderer); } } } catch (streamError) { @@ -544,7 +487,7 @@ Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, throw streamError; } - console.log(`[SpecRegeneration] Feature generation completed - ${messageCount} messages, ${toolCallCount} tool calls`); + console.log(`[SpecRegeneration] Feature generation completed - ${counters.messageCount} messages, ${counters.toolCallCount} tool calls`); execution.query = null; execution.abortController = null; @@ -1003,6 +946,94 @@ Use this general structure: Begin by exploring the project structure.`; } + /** + * Handle assistant message in feature generation stream + * @private + */ + _handleAssistantMessage(msg, sendToRenderer, counters) { + counters.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 #${counters.messageCount}: ${preview}...`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + this._handleToolUse(block, sendToRenderer, counters); + } + } + } + + /** + * Handle tool use block in feature generation stream + * @private + */ + _handleToolUse(block, sendToRenderer, counters) { + counters.toolCallCount++; + const toolName = block.name; + const toolInput = block.input; + console.log(`[SpecRegeneration] Feature gen tool call #${counters.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, + }); + } + + /** + * Handle tool result in feature generation stream + * @private + */ + _handleToolResult(msg, sendToRenderer) { + 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`, + }); + } + } + + /** + * Handle error in feature generation stream + * @private + */ + _handleStreamError(msg, sendToRenderer) { + 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}`, + }); + } + /** * Stop the current regeneration */ diff --git a/app/src/components/views/spec-view.tsx b/app/src/components/views/spec-view.tsx index 28e58216..608a25ab 100644 --- a/app/src/components/views/spec-view.tsx +++ b/app/src/components/views/spec-view.tsx @@ -19,6 +19,12 @@ import { Checkbox } from "@/components/ui/checkbox"; import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor"; import type { SpecRegenerationEvent } from "@/types/electron"; +// Delay before reloading spec file to ensure it's written to disk +const SPEC_FILE_WRITE_DELAY = 500; + +// Interval for polling backend status during generation +const STATUS_CHECK_INTERVAL_MS = 2000; + export function SpecView() { const { currentProject, appSpec, setAppSpec } = useAppStore(); const [isLoading, setIsLoading] = useState(true); @@ -40,6 +46,9 @@ export function SpecView() { // Generate features only state const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false); + // Logs state (kept for internal tracking, but UI removed) + const [logs, setLogs] = useState(""); + const logsRef = useRef(""); // Phase tracking and status const [currentPhase, setCurrentPhase] = useState(""); @@ -246,7 +255,7 @@ export function SpecView() { loadSpec(); } } - }, 2000); + }, STATUS_CHECK_INTERVAL_MS); } } } catch (error) { @@ -310,7 +319,7 @@ export function SpecView() { } catch (error) { console.error("[SpecView] Periodic status check error:", error); } - }, 2000); // Check every 2 seconds (more frequent) + }, STATUS_CHECK_INTERVAL_MS); return () => { clearInterval(intervalId); @@ -342,7 +351,7 @@ export function SpecView() { // Small delay to ensure spec file is written setTimeout(() => { loadSpec(); - }, 500); + }, SPEC_FILE_WRITE_DELAY); } } @@ -357,7 +366,7 @@ export function SpecView() { stateRestoredRef.current = false; setTimeout(() => { loadSpec(); - }, 500); + }, SPEC_FILE_WRITE_DELAY); } // Append progress to logs @@ -399,37 +408,37 @@ export function SpecView() { logsRef.current = completionLog; setLogs(completionLog); - // Check if this is the final completion - const isFinalCompletion = event.message?.includes("All tasks completed") || - event.message === "All tasks completed!" || - event.message === "All tasks completed"; + // --- Completion Detection Logic --- + // Check 1: Message explicitly indicates all tasks are done + const isFinalCompletionMessage = event.message?.includes("All tasks completed") || + event.message === "All tasks completed!" || + event.message === "All tasks completed"; - // Check if we've already seen a completion phase in logs (including the message we just added) - const hasSeenCompletePhase = logsRef.current.includes("[Phase: complete]"); + // Check 2: We've seen a [Phase: complete] marker in the logs + const hasCompletePhase = logsRef.current.includes("[Phase: complete]"); - // Check recent logs for feature activity + // Check 3: Feature generation has finished (no recent activity and not actively generating) const recentLogs = logsRef.current.slice(-2000); const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") || - recentLogs.includes("Creating feature") || - recentLogs.includes("UpdateFeatureStatus"); + recentLogs.includes("Creating feature") || + recentLogs.includes("UpdateFeatureStatus"); + const isStillGeneratingFeatures = !isFinalCompletionMessage && + !hasCompletePhase && + (event.message?.includes("Features are being generated") || + event.message?.includes("features are being generated")); + const isFeatureGenerationComplete = currentPhase === "feature_generation" && + !hasRecentFeatureActivity && + !isStillGeneratingFeatures; - // Check if we're still generating features (only for intermediate completion) - const isGeneratingFeatures = !isFinalCompletion && - !hasSeenCompletePhase && - (event.message?.includes("Features are being generated") || - event.message?.includes("features are being generated")); - - // If we're in feature_generation but no recent activity and we see completion, we're done - const shouldComplete = isFinalCompletion || - hasSeenCompletePhase || - (currentPhase === "feature_generation" && !hasRecentFeatureActivity && !isGeneratingFeatures); + // Determine if we should mark everything as complete + const shouldComplete = isFinalCompletionMessage || hasCompletePhase || isFeatureGenerationComplete; if (shouldComplete) { // Fully complete - clear all states immediately console.log("[SpecView] Final completion detected - clearing state", { - isFinalCompletion, - hasSeenCompletePhase, - shouldComplete, + isFinalCompletionMessage, + hasCompletePhase, + isFeatureGenerationComplete, hasRecentFeatureActivity, currentPhase, message: event.message @@ -452,7 +461,7 @@ export function SpecView() { setIsRegenerating(true); setCurrentPhase("feature_generation"); console.log("[SpecView] Spec complete, continuing with feature generation", { - isGeneratingFeatures, + isStillGeneratingFeatures, hasRecentFeatureActivity, currentPhase }); From 74efaadb596564da5144f3f2d4192ad2e17a7642 Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 09:36:38 -0600 Subject: [PATCH 08/14] further gemini reviews and fixes --- app/electron/auto-mode-service.js | 15 +- app/electron/main.js | 3 +- app/electron/services/feature-loader.js | 14 +- app/electron/services/mcp-server-factory.js | 13 +- app/electron/services/mcp-server-stdio.js | 2 +- .../services/spec-regeneration-service.js | 116 +++++------ app/src/components/views/spec-view.tsx | 183 ++++-------------- app/src/lib/electron.ts | 30 ++- app/src/types/electron.d.ts | 1 + 9 files changed, 141 insertions(+), 236 deletions(-) diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 20c75246..cedd6fd6 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -391,8 +391,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -495,8 +494,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -662,8 +660,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -859,8 +856,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -1102,8 +1098,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); diff --git a/app/electron/main.js b/app/electron/main.js index a19555f0..5fc2b0a1 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -898,7 +898,7 @@ ipcMain.handle( featureId, status, projectPath, - summary + { summary } ); // Notify renderer if window is available @@ -1170,6 +1170,7 @@ ipcMain.handle("spec-regeneration:status", () => { isRunning: specRegenerationExecution !== null && specRegenerationExecution.isActive(), + currentPhase: specRegenerationService.getCurrentPhase(), }; }); diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index d259bbf1..d9b69404 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -382,13 +382,15 @@ class FeatureLoader { * @param {string} featureId - The ID of the feature to update * @param {string} status - The new status * @param {string} projectPath - Path to the project - * @param {string} [summary] - Optional summary of what was done - * @param {string} [error] - Optional error message if feature errored - * @param {string} [description] - Optional detailed description - * @param {string} [category] - Optional category/phase - * @param {string[]} [steps] - Optional array of implementation steps + * @param {Object} options - Options object for optional parameters + * @param {string} [options.summary] - Optional summary of what was done + * @param {string} [options.error] - Optional error message if feature errored + * @param {string} [options.description] - Optional detailed description + * @param {string} [options.category] - Optional category/phase + * @param {string[]} [options.steps] - Optional array of implementation steps */ - async updateFeatureStatus(featureId, status, projectPath, summary, error, description, category, steps) { + async updateFeatureStatus(featureId, status, projectPath, options = {}) { + const { summary, error, description, category, steps } = options; // Check if feature exists const existingFeature = await this.get(projectPath, featureId); diff --git a/app/electron/services/mcp-server-factory.js b/app/electron/services/mcp-server-factory.js index 4fdb2bd6..4caddfe2 100644 --- a/app/electron/services/mcp-server-factory.js +++ b/app/electron/services/mcp-server-factory.js @@ -53,16 +53,17 @@ class McpServerFactory { finalStatus = "waiting_approval"; } - // Call the provided callback to update feature status with all parameters + // Call the provided callback to update feature status await updateFeatureStatusCallback( args.featureId, finalStatus, projectPath, - args.summary, - undefined, // error - args.description, - args.category, - args.steps + { + summary: args.summary, + description: args.description, + category: args.category, + steps: args.steps, + } ); const statusMessage = finalStatus !== args.status diff --git a/app/electron/services/mcp-server-stdio.js b/app/electron/services/mcp-server-stdio.js index 44cdd6b4..798f12d1 100644 --- a/app/electron/services/mcp-server-stdio.js +++ b/app/electron/services/mcp-server-stdio.js @@ -215,7 +215,7 @@ async function handleToolsCall(params, id) { // Call the update callback via IPC or direct call // Since we're in a separate process, we need to use IPC to communicate back // For now, we'll call the feature loader directly since it has the update method - await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, summary); + await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, { summary }); const statusMessage = finalStatus !== status ? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}` diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index 9244e62b..a977a0a6 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -86,6 +86,15 @@ const APP_SPEC_XML_TEMPLATE = ` class SpecRegenerationService { constructor() { this.runningRegeneration = null; + this.currentPhase = ""; // Tracks current phase for status queries + } + + /** + * Get the current phase of the regeneration process + * @returns {string} Current phase or empty string if not running + */ + getCurrentPhase() { + return this.currentPhase; } /** @@ -107,14 +116,14 @@ class SpecRegenerationService { const abortController = new AbortController(); execution.abortController = abortController; - // Phase tracking - let currentPhase = "initialization"; + // Phase tracking - use instance property for status queries + this.currentPhase = "initialization"; sendToRenderer({ type: "spec_regeneration_progress", - content: `[Phase: ${currentPhase}] Initializing spec generation process...\n`, + content: `[Phase: ${this.currentPhase}] Initializing spec generation process...\n`, }); - console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); // Create custom MCP server with UpdateFeatureStatus tool if generating features if (generateFeatures) { @@ -135,12 +144,12 @@ class SpecRegenerationService { } } - currentPhase = "setup"; + this.currentPhase = "setup"; sendToRenderer({ type: "spec_regeneration_progress", - content: `[Phase: ${currentPhase}] Configuring AI agent and tools...\n`, + content: `[Phase: ${this.currentPhase}] Configuring AI agent and tools...\n`, }); - console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); // Phase 1: Generate spec WITHOUT UpdateFeatureStatus tool // This prevents features from being created before the spec is complete @@ -160,17 +169,17 @@ class SpecRegenerationService { const prompt = this.buildInitialCreationPrompt(projectOverview); // No feature generation during spec creation - currentPhase = "analysis"; + this.currentPhase = "analysis"; sendToRenderer({ type: "spec_regeneration_progress", - content: `[Phase: ${currentPhase}] Starting project analysis and spec creation...\n`, + content: `[Phase: ${this.currentPhase}] Starting project analysis and spec creation...\n`, }); - console.log(`[SpecRegeneration] Phase: ${currentPhase} - Starting AI agent query`); + console.log(`[SpecRegeneration] Phase: ${this.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`, + content: `[Phase: ${this.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"); } @@ -253,11 +262,11 @@ class SpecRegenerationService { 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`); + this.currentPhase = "spec_complete"; + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Spec creation completed in ${elapsedTime}s`); sendToRenderer({ type: "spec_regeneration_progress", - content: `\n[Phase: ${currentPhase}] ✓ App specification created successfully! (${elapsedTime}s)\n`, + content: `\n[Phase: ${this.currentPhase}] ✓ App specification created successfully! (${elapsedTime}s)\n`, }); if (generateFeatures) { @@ -282,10 +291,10 @@ class SpecRegenerationService { }); } } else { - currentPhase = "complete"; + this.currentPhase = "complete"; sendToRenderer({ type: "spec_regeneration_progress", - content: `[Phase: ${currentPhase}] All tasks completed!\n`, + content: `[Phase: ${this.currentPhase}] All tasks completed!\n`, }); // Send final completion event @@ -344,16 +353,16 @@ class SpecRegenerationService { */ async generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime) { const featureStartTime = Date.now(); - let currentPhase = "feature_generation"; + this.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`, + content: `\n[Phase: ${this.currentPhase}] Starting feature creation from implementation roadmap...\n`, }); - console.log(`[SpecRegeneration] Phase: ${currentPhase} - Starting feature generation query`); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting feature generation query`); try { // Create feature tools server @@ -492,12 +501,12 @@ Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, execution.query = null; execution.abortController = null; - currentPhase = "complete"; + this.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`, + content: `\n[Phase: ${this.currentPhase}] ✓ All tasks completed! (${totalElapsedTime}s total, ${featureElapsedTime}s for features)\n`, }); sendToRenderer({ type: "spec_regeneration_complete", @@ -653,22 +662,22 @@ Begin by exploring the project structure.`; console.log(`[SpecRegeneration] Project definition length: ${projectDefinition.length} characters`); try { - let currentPhase = "initialization"; + this.currentPhase = "initialization"; sendToRenderer({ type: "spec_regeneration_progress", - content: `[Phase: ${currentPhase}] Initializing spec regeneration process...\n`, + content: `[Phase: ${this.currentPhase}] Initializing spec regeneration process...\n`, }); - console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); const abortController = new AbortController(); execution.abortController = abortController; - currentPhase = "setup"; + this.currentPhase = "setup"; sendToRenderer({ type: "spec_regeneration_progress", - content: `[Phase: ${currentPhase}] Configuring AI agent and tools...\n`, + content: `[Phase: ${this.currentPhase}] Configuring AI agent and tools...\n`, }); - console.log(`[SpecRegeneration] Phase: ${currentPhase}`); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); const options = { model: "claude-sonnet-4-20250514", @@ -686,12 +695,12 @@ Begin by exploring the project structure.`; const prompt = this.buildRegenerationPrompt(projectDefinition); - currentPhase = "regeneration"; + this.currentPhase = "regeneration"; sendToRenderer({ type: "spec_regeneration_progress", - content: `[Phase: ${currentPhase}] Starting spec regeneration...\n`, + content: `[Phase: ${this.currentPhase}] Starting spec regeneration...\n`, }); - console.log(`[SpecRegeneration] Phase: ${currentPhase} - Starting AI agent query`); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting AI agent query`); const currentQuery = query({ prompt, options }); execution.query = currentQuery; @@ -725,21 +734,10 @@ Begin by exploring the project structure.`; 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_progress", + content: `\n[Tool] Using ${toolName}...\n`, + }); sendToRenderer({ type: "spec_regeneration_tool", @@ -755,18 +753,10 @@ Begin by exploring the project structure.`; 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: `[Feature Creation] ${result}\n`, - }); - } else { - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Tool Result] ${toolName} completed successfully\n`, - }); - } + 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}`); @@ -791,11 +781,11 @@ Begin by exploring the project structure.`; execution.abortController = null; const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); - currentPhase = "complete"; - console.log(`[SpecRegeneration] Phase: ${currentPhase} - Spec regeneration completed in ${elapsedTime}s`); + this.currentPhase = "complete"; + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Spec regeneration completed in ${elapsedTime}s`); sendToRenderer({ type: "spec_regeneration_progress", - content: `\n[Phase: ${currentPhase}] ✓ Spec regeneration complete! (${elapsedTime}s)\n`, + content: `\n[Phase: ${this.currentPhase}] ✓ Spec regeneration complete! (${elapsedTime}s)\n`, }); sendToRenderer({ @@ -867,9 +857,8 @@ 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. +**Note:** Feature files are stored separately in .automaker/features/{id}/feature.json. +Your task is ONLY to update the app_spec.txt file - feature files will be managed separately. You CAN and SHOULD modify: - .automaker/app_spec.txt (this is your primary target) @@ -1042,6 +1031,7 @@ Begin by exploring the project structure.`; this.runningRegeneration.abortController.abort(); } this.runningRegeneration = null; + this.currentPhase = ""; } } diff --git a/app/src/components/views/spec-view.tsx b/app/src/components/views/spec-view.tsx index 608a25ab..301416d6 100644 --- a/app/src/components/views/spec-view.tsx +++ b/app/src/components/views/spec-view.tsx @@ -105,93 +105,38 @@ export function SpecView() { console.log("[SpecView] Status check on mount:", status); if (status.success && status.isRunning) { - // Something is running - restore state - console.log("[SpecView] Spec generation is running - restoring state"); + // Something is running - restore state using backend's authoritative phase + console.log("[SpecView] Spec generation is running - restoring state", { phase: status.currentPhase }); - // Only restore state if we haven't already done so for this project - // This prevents resetting state when switching tabs if (!stateRestoredRef.current) { setIsCreating(true); setIsRegenerating(true); stateRestoredRef.current = true; } - // Try to extract the current phase from existing logs - let detectedPhase = ""; - if (logsRef.current) { - // Look for the most recent phase in the logs - const phaseMatches = logsRef.current.matchAll(/\[Phase:\s*([^\]]+)\]/g); - const phases = Array.from(phaseMatches); - if (phases.length > 0) { - // Get the last phase mentioned in the logs - detectedPhase = phases[phases.length - 1][1]; - console.log(`[SpecView] Detected phase from logs: ${detectedPhase}`); - } - - // Also check for feature generation indicators in logs - const hasFeatureGeneration = logsRef.current.includes("Feature Generation") || - logsRef.current.includes("Feature Creation") || - logsRef.current.includes("Creating feature") || - logsRef.current.includes("feature_generation"); - - if (hasFeatureGeneration && !detectedPhase) { - detectedPhase = "feature_generation"; - console.log("[SpecView] Detected feature generation from logs"); - } - } - - // Update phase from logs if we found one and don't have a specific phase set - // This allows the phase to update as new events come in - if (detectedPhase) { - setCurrentPhase((prevPhase) => { - // Only update if we don't have a phase or if the detected phase is more recent - if (!prevPhase || prevPhase === "unknown" || prevPhase === "in progress") { - return detectedPhase; - } - return prevPhase; - }); - } else if (!currentPhase) { - // Use a more descriptive default instead of "unknown" + // Use the backend's currentPhase directly - single source of truth + if (status.currentPhase) { + setCurrentPhase(status.currentPhase); + } else { setCurrentPhase("in progress"); } - // Don't clear logs - they may have been persisted + // Add resume message to logs if needed if (!logsRef.current) { const resumeMessage = "[Status] Resumed monitoring existing spec generation process...\n"; logsRef.current = resumeMessage; setLogs(resumeMessage); } else if (!logsRef.current.includes("Resumed monitoring")) { - // Add a resume message to existing logs only if not already present const resumeMessage = "\n[Status] Resumed monitoring existing spec generation process...\n"; logsRef.current = logsRef.current + resumeMessage; setLogs(logsRef.current); } } else if (status.success && !status.isRunning) { - // Check if we might still be in feature generation phase based on logs - const mightBeGeneratingFeatures = logsRef.current && ( - logsRef.current.includes("Feature Generation") || - logsRef.current.includes("Feature Creation") || - logsRef.current.includes("Creating feature") || - logsRef.current.includes("feature_generation") || - currentPhase === "feature_generation" - ); - - if (mightBeGeneratingFeatures && specExists) { - // Spec exists and we might still be generating features - keep state active - console.log("[SpecView] Detected potential feature generation - keeping state active"); - if (!isCreating && !isRegenerating) { - setIsCreating(true); - } - if (currentPhase !== "feature_generation") { - setCurrentPhase("feature_generation"); - } - } else { - // Not running - clear running state - setIsCreating(false); - setIsRegenerating(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - } + // Not running - clear all state + setIsCreating(false); + setIsRegenerating(false); + setCurrentPhase(""); + stateRestoredRef.current = false; } } catch (error) { console.error("[SpecView] Failed to check status:", error); @@ -209,7 +154,7 @@ export function SpecView() { useEffect(() => { const handleVisibilityChange = async () => { if (!document.hidden && currentProject && (isCreating || isRegenerating || isGeneratingFeatures)) { - // Tab became visible and we think we're still generating - verify status + // Tab became visible and we think we're still generating - verify status from backend try { const api = getElectronAPI(); if (!api.specRegeneration) return; @@ -218,45 +163,17 @@ export function SpecView() { console.log("[SpecView] Visibility change - status check:", status); if (!status.isRunning) { - // Not running but we think we are - check if we're truly done - // Look for recent activity in logs (within last 30 seconds worth of content) - const recentLogs = logsRef.current.slice(-5000); // Last ~5000 chars - const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") || - recentLogs.includes("Creating feature") || - recentLogs.match(/\[Feature Creation\].*$/m); - - // Check if we have a completion message or complete phase - const hasCompletion = logsRef.current.includes("All tasks completed") || - logsRef.current.includes("[Complete] All tasks completed") || - logsRef.current.includes("[Phase: complete]"); - - if (hasCompletion || (!hasRecentFeatureActivity && currentPhase !== "feature_generation")) { - // No recent activity and not running - we're done - console.log("[SpecView] Visibility change: Generation appears complete - clearing state"); - setIsCreating(false); - setIsRegenerating(false); - setIsGeneratingFeatures(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - loadSpec(); - } else if (currentPhase === "feature_generation" && !hasRecentFeatureActivity) { - // We were in feature generation but no recent activity - might be done - // Wait a moment and check again - setTimeout(async () => { - if (api.specRegeneration) { - const recheckStatus = await api.specRegeneration.status(); - if (!recheckStatus.isRunning) { - console.log("[SpecView] Re-check after visibility: Still not running - clearing state"); - setIsCreating(false); - setIsRegenerating(false); - setIsGeneratingFeatures(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - loadSpec(); - } - } - }, STATUS_CHECK_INTERVAL_MS); - } + // Backend says not running - clear state + console.log("[SpecView] Visibility change: Backend indicates generation complete - clearing state"); + setIsCreating(false); + setIsRegenerating(false); + setIsGeneratingFeatures(false); + setCurrentPhase(""); + stateRestoredRef.current = false; + loadSpec(); + } else if (status.currentPhase) { + // Still running - update phase from backend + setCurrentPhase(status.currentPhase); } } catch (error) { console.error("[SpecView] Failed to check status on visibility change:", error); @@ -268,7 +185,7 @@ export function SpecView() { return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); }; - }, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]); + }, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec]); // Periodic status check to ensure state stays in sync (only when we think we're running) useEffect(() => { @@ -281,40 +198,22 @@ export function SpecView() { const status = await api.specRegeneration.status(); - // If not running but we think we are, verify we're truly done - if (!status.isRunning && (isCreating || isRegenerating || isGeneratingFeatures)) { - // Check logs for completion indicators - const hasCompletion = logsRef.current.includes("All tasks completed") || - logsRef.current.includes("[Complete] All tasks completed") || - logsRef.current.includes("[Phase: complete]") || - currentPhase === "complete"; - - // Also check if we haven't seen feature activity recently - const recentLogs = logsRef.current.slice(-3000); // Last 3000 chars (more context) - const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") || - recentLogs.includes("Creating feature") || - recentLogs.includes("UpdateFeatureStatus") || - recentLogs.includes("[Tool]") && recentLogs.includes("UpdateFeatureStatus"); - - // If we're in feature_generation phase and not running, we're likely done - // (features are created via tool calls, so when stream ends, they're done) - const isFeatureGenComplete = currentPhase === "feature_generation" && - !hasRecentFeatureActivity; - - if (hasCompletion || isFeatureGenComplete) { - console.log("[SpecView] Periodic check: Generation complete - clearing state", { - hasCompletion, - hasRecentFeatureActivity, - currentPhase, - isFeatureGenComplete - }); - setIsCreating(false); - setIsRegenerating(false); - setIsGeneratingFeatures(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - loadSpec(); - } + if (!status.isRunning) { + // Backend says not running - clear state + console.log("[SpecView] Periodic check: Backend indicates generation complete - clearing state"); + setIsCreating(false); + setIsRegenerating(false); + setIsGeneratingFeatures(false); + setCurrentPhase(""); + stateRestoredRef.current = false; + loadSpec(); + } else if (status.currentPhase && status.currentPhase !== currentPhase) { + // Still running but phase changed - update from backend + console.log("[SpecView] Periodic check: Phase updated from backend", { + old: currentPhase, + new: status.currentPhase + }); + setCurrentPhase(status.currentPhase); } } catch (error) { console.error("[SpecView] Periodic status check error:", error); diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index af8a66a4..0c5e75c5 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -1800,6 +1800,7 @@ async function simulateSuggestionsGeneration( // Mock Spec Regeneration state and implementation let mockSpecRegenerationRunning = false; +let mockSpecRegenerationPhase = ""; let mockSpecRegenerationCallbacks: ((event: SpecRegenerationEvent) => void)[] = []; let mockSpecRegenerationTimeout: NodeJS.Timeout | null = null; @@ -1862,6 +1863,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { stop: async () => { mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; if (mockSpecRegenerationTimeout) { clearTimeout(mockSpecRegenerationTimeout); mockSpecRegenerationTimeout = null; @@ -1873,6 +1875,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { return { success: true, isRunning: mockSpecRegenerationRunning, + currentPhase: mockSpecRegenerationPhase, }; }, @@ -1896,9 +1899,10 @@ async function simulateSpecCreation( projectOverview: string, generateFeatures = true ) { + mockSpecRegenerationPhase = "initialization"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Starting project analysis...\n", + content: "[Phase: initialization] Starting project analysis...\n", }); await new Promise((resolve) => { @@ -1906,6 +1910,7 @@ async function simulateSpecCreation( }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "setup"; emitSpecRegenerationEvent({ type: "spec_regeneration_tool", tool: "Glob", @@ -1917,9 +1922,10 @@ async function simulateSpecCreation( }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "analysis"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Detecting tech stack...\n", + content: "[Phase: analysis] Detecting tech stack...\n", }); await new Promise((resolve) => { @@ -1959,12 +1965,14 @@ async function simulateSpecCreation( // The generateFeatures parameter is kept for API compatibility but features // should be created through the features API + mockSpecRegenerationPhase = "complete"; emitSpecRegenerationEvent({ type: "spec_regeneration_complete", - message: "Initial spec creation complete!", + message: "All tasks completed!", }); mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; mockSpecRegenerationTimeout = null; } @@ -1972,9 +1980,10 @@ async function simulateSpecRegeneration( projectPath: string, projectDefinition: string ) { + mockSpecRegenerationPhase = "initialization"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Starting spec regeneration...\n", + content: "[Phase: initialization] Starting spec regeneration...\n", }); await new Promise((resolve) => { @@ -1982,9 +1991,10 @@ async function simulateSpecRegeneration( }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "analysis"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Analyzing codebase...\n", + content: "[Phase: analysis] Analyzing codebase...\n", }); await new Promise((resolve) => { @@ -2015,16 +2025,19 @@ async function simulateSpecRegeneration( `; + mockSpecRegenerationPhase = "complete"; emitSpecRegenerationEvent({ type: "spec_regeneration_complete", - message: "Spec regeneration complete!", + message: "All tasks completed!", }); mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; mockSpecRegenerationTimeout = null; } async function simulateFeatureGeneration(projectPath: string) { + mockSpecRegenerationPhase = "initialization"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: initialization] Starting feature generation from existing app_spec.txt...\n", @@ -2045,9 +2058,10 @@ async function simulateFeatureGeneration(projectPath: string) { }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "feature_generation"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "[Feature Creation] Creating features from roadmap...\n", + content: "[Phase: feature_generation] Creating features from roadmap...\n", }); await new Promise((resolve) => { @@ -2055,6 +2069,7 @@ async function simulateFeatureGeneration(projectPath: string) { }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "complete"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", content: "[Phase: complete] All tasks completed!\n", @@ -2066,6 +2081,7 @@ async function simulateFeatureGeneration(projectPath: string) { }); mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; mockSpecRegenerationTimeout = null; } diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 66d0d13f..a816218c 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -270,6 +270,7 @@ export interface SpecRegenerationAPI { status: () => Promise<{ success: boolean; isRunning?: boolean; + currentPhase?: string; error?: string; }>; From f460e689f1295c649974b6bd4475ecb0a32b0ba4 Mon Sep 17 00:00:00 2001 From: Ben <117330199+trueheads@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:54:22 -0600 Subject: [PATCH 09/14] Update app/electron/services/feature-loader.js Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- app/electron/services/feature-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index d9b69404..352ad22e 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -456,7 +456,7 @@ class FeatureLoader { if (!existingFeature.steps && !updates.steps) updates.steps = []; if (!existingFeature.images) updates.images = []; if (!existingFeature.imagePaths) updates.imagePaths = []; - if (existingFeature.skipTests === undefined) updates.skipTests = true; + if (existingFeature.skipTests === undefined) updates.skipTests = false; if (!existingFeature.model) updates.model = "sonnet"; if (!existingFeature.thinkingLevel) updates.thinkingLevel = "none"; From 6352a1df19dd196a2a2b535a58ea07ee609acfab Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 09:59:48 -0600 Subject: [PATCH 10/14] final code review --- app/src/components/views/spec-view.tsx | 33 ++++++-------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/app/src/components/views/spec-view.tsx b/app/src/components/views/spec-view.tsx index 301416d6..87173e9b 100644 --- a/app/src/components/views/spec-view.tsx +++ b/app/src/components/views/spec-view.tsx @@ -302,44 +302,29 @@ export function SpecView() { setLogs(newLog); console.log("[SpecView] Tool:", event.tool, event.input); } else if (event.type === "spec_regeneration_complete") { - // Add completion message to logs first (before checking) + // Add completion message to logs first const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`; logsRef.current = completionLog; setLogs(completionLog); // --- Completion Detection Logic --- - // Check 1: Message explicitly indicates all tasks are done + // The backend sends explicit signals for completion: + // 1. "All tasks completed" in the message + // 2. [Phase: complete] marker in logs const isFinalCompletionMessage = event.message?.includes("All tasks completed") || event.message === "All tasks completed!" || event.message === "All tasks completed"; - // Check 2: We've seen a [Phase: complete] marker in the logs const hasCompletePhase = logsRef.current.includes("[Phase: complete]"); - // Check 3: Feature generation has finished (no recent activity and not actively generating) - const recentLogs = logsRef.current.slice(-2000); - const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") || - recentLogs.includes("Creating feature") || - recentLogs.includes("UpdateFeatureStatus"); - const isStillGeneratingFeatures = !isFinalCompletionMessage && - !hasCompletePhase && - (event.message?.includes("Features are being generated") || - event.message?.includes("features are being generated")); - const isFeatureGenerationComplete = currentPhase === "feature_generation" && - !hasRecentFeatureActivity && - !isStillGeneratingFeatures; - - // Determine if we should mark everything as complete - const shouldComplete = isFinalCompletionMessage || hasCompletePhase || isFeatureGenerationComplete; + // Rely solely on explicit backend signals + const shouldComplete = isFinalCompletionMessage || hasCompletePhase; if (shouldComplete) { // Fully complete - clear all states immediately console.log("[SpecView] Final completion detected - clearing state", { isFinalCompletionMessage, hasCompletePhase, - isFeatureGenerationComplete, - hasRecentFeatureActivity, - currentPhase, message: event.message }); setIsRegenerating(false); @@ -359,11 +344,7 @@ export function SpecView() { setIsCreating(true); setIsRegenerating(true); setCurrentPhase("feature_generation"); - console.log("[SpecView] Spec complete, continuing with feature generation", { - isStillGeneratingFeatures, - hasRecentFeatureActivity, - currentPhase - }); + console.log("[SpecView] Intermediate completion, continuing with feature generation"); } console.log("[SpecView] Spec generation event:", event.message); From ca57b9e3ca420b662a5d97422d24f7e8a78614d6 Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 10:15:39 -0600 Subject: [PATCH 11/14] ok one final pr to remove gemini-found race condition --- app/electron/services/feature-loader.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index 352ad22e..d95ba08c 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -170,22 +170,8 @@ 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; - } - + // Read feature.json directly - handle ENOENT in catch block + // This avoids TOCTOU race condition from checking with fs.access first const content = await fs.readFile(featureJsonPath, "utf-8"); const feature = JSON.parse(content); @@ -201,10 +187,9 @@ class FeatureLoader { } catch (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` - ); + // File doesn't exist - this is expected for incomplete feature directories + // Skip silently (feature.json not yet created or was removed) + continue; } else if (error instanceof SyntaxError) { // JSON parse error - log as warning since file exists but is malformed console.warn( From f757270198738bab02569d634ce67381d7641b04 Mon Sep 17 00:00:00 2001 From: trueheads Date: Thu, 11 Dec 2025 10:20:58 -0600 Subject: [PATCH 12/14] including type error commit fixes --- app/src/components/views/kanban-card.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/components/views/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index c941c8c2..7888de3e 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -393,10 +393,10 @@ export const KanbanCard = memo(function KanbanCard({ !isDescriptionExpanded && "line-clamp-3" )} > - {feature.description || feature.summary || feature.title || feature.id} + {feature.description || feature.summary || feature.id} {/* Show More/Less toggle - only show when description is likely truncated */} - {(feature.description || feature.summary || feature.title || "").length > 100 && ( + {(feature.description || feature.summary || "").length > 100 && (