diff --git a/apps/frontend/src/main/agent/agent-queue.ts b/apps/frontend/src/main/agent/agent-queue.ts index 93f716aa..1d18be76 100644 --- a/apps/frontend/src/main/agent/agent-queue.ts +++ b/apps/frontend/src/main/agent/agent-queue.ts @@ -38,6 +38,40 @@ export class AgentQueueManager { this.emitter = emitter; } + /** + * Ensure Python environment is ready before spawning processes. + * Prevents the race condition where generation starts before dependencies are installed, + * which would cause it to fall back to system Python and fail with ModuleNotFoundError. + * + * @param projectId - The project ID for error event emission + * @param eventType - The error event type to emit on failure + * @returns true if environment is ready, false if initialization failed (error already emitted) + */ + private async ensurePythonEnvReady( + projectId: string, + eventType: 'ideation-error' | 'roadmap-error' + ): Promise { + const autoBuildSource = this.processManager.getAutoBuildSourcePath(); + + if (!pythonEnvManager.isEnvReady()) { + debugLog('[Agent Queue] Python environment not ready, waiting for initialization...'); + if (autoBuildSource) { + const status = await pythonEnvManager.initialize(autoBuildSource); + if (!status.ready) { + debugError('[Agent Queue] Python environment initialization failed:', status.error); + this.emitter.emit(eventType, projectId, `Python environment not ready: ${status.error || 'initialization failed'}`); + return false; + } + debugLog('[Agent Queue] Python environment now ready'); + } else { + debugError('[Agent Queue] Cannot initialize Python - auto-build source not found'); + this.emitter.emit(eventType, projectId, 'Python environment not ready: auto-build source not found'); + return false; + } + } + return true; + } + /** * Start roadmap generation process * @@ -195,6 +229,15 @@ export class AgentQueueManager { ): Promise { debugLog('[Agent Queue] Spawning ideation process:', { projectId, projectPath }); + // Run from auto-claude source directory so imports work correctly + const autoBuildSource = this.processManager.getAutoBuildSourcePath(); + const cwd = autoBuildSource || process.cwd(); + + // Ensure Python environment is ready before spawning + if (!await this.ensurePythonEnvReady(projectId, 'ideation-error')) { + return; + } + // Kill existing process for this project if any const wasKilled = this.processManager.killProcess(projectId); if (wasKilled) { @@ -205,9 +248,6 @@ export class AgentQueueManager { const spawnId = this.state.generateSpawnId(); debugLog('[Agent Queue] Generated spawn ID:', spawnId); - // Run from auto-claude source directory so imports work correctly - const autoBuildSource = this.processManager.getAutoBuildSourcePath(); - const cwd = autoBuildSource || process.cwd(); // Get combined environment variables const combinedEnv = this.processManager.getCombinedEnv(projectPath); @@ -516,6 +556,15 @@ export class AgentQueueManager { ): Promise { debugLog('[Agent Queue] Spawning roadmap process:', { projectId, projectPath }); + // Run from auto-claude source directory so imports work correctly + const autoBuildSource = this.processManager.getAutoBuildSourcePath(); + const cwd = autoBuildSource || process.cwd(); + + // Ensure Python environment is ready before spawning + if (!await this.ensurePythonEnvReady(projectId, 'roadmap-error')) { + return; + } + // Kill existing process for this project if any const wasKilled = this.processManager.killProcess(projectId); if (wasKilled) { @@ -526,9 +575,6 @@ export class AgentQueueManager { const spawnId = this.state.generateSpawnId(); debugLog('[Agent Queue] Generated roadmap spawn ID:', spawnId); - // Run from auto-claude source directory so imports work correctly - const autoBuildSource = this.processManager.getAutoBuildSourcePath(); - const cwd = autoBuildSource || process.cwd(); // Get combined environment variables const combinedEnv = this.processManager.getCombinedEnv(projectPath); diff --git a/apps/frontend/src/main/ipc-handlers/roadmap/transformers.ts b/apps/frontend/src/main/ipc-handlers/roadmap/transformers.ts index 0eb8b3aa..62f9faee 100644 --- a/apps/frontend/src/main/ipc-handlers/roadmap/transformers.ts +++ b/apps/frontend/src/main/ipc-handlers/roadmap/transformers.ts @@ -96,6 +96,57 @@ function transformPhase(raw: RawRoadmapPhase): RoadmapPhase { }; } +/** + * Maps all known backend status values to canonical Kanban column statuses. + * Includes valid statuses as identity mappings for consistent lookup. + * Module-level constant for efficiency (not recreated on each call). + */ +const STATUS_MAP: Record = { + // Canonical Kanban statuses (identity mappings) + 'under_review': 'under_review', + 'planned': 'planned', + 'in_progress': 'in_progress', + 'done': 'done', + // Early-stage / ideation statuses → under_review + 'idea': 'under_review', + 'backlog': 'under_review', + 'proposed': 'under_review', + 'pending': 'under_review', + // Approved / scheduled statuses → planned + 'approved': 'planned', + 'scheduled': 'planned', + // Active development statuses → in_progress + 'active': 'in_progress', + 'building': 'in_progress', + // Completed statuses → done + 'complete': 'done', + 'completed': 'done', + 'shipped': 'done' +}; + +/** + * Normalizes a feature status string to a valid Kanban column status. + * Handles case-insensitive matching and maps backend values to canonical statuses. + * + * @param status - The raw status string from the backend + * @returns A valid RoadmapFeature status for Kanban display + */ +function normalizeFeatureStatus(status: string | undefined): RoadmapFeature['status'] { + if (!status) return 'under_review'; + + const normalized = STATUS_MAP[status.toLowerCase()]; + + if (!normalized) { + // Debug log for unmapped statuses to aid future mapping additions + if (process.env.NODE_ENV === 'development') { + console.debug(`[Roadmap] normalizeFeatureStatus: unmapped status "${status}", defaulting to "under_review"`); + } + return 'under_review'; + } + + return normalized; +} + function transformFeature(raw: RawRoadmapFeature): RoadmapFeature { return { id: raw.id, @@ -107,7 +158,7 @@ function transformFeature(raw: RawRoadmapFeature): RoadmapFeature { impact: (raw.impact as RoadmapFeature['impact']) || 'medium', phaseId: raw.phase_id || raw.phaseId || '', dependencies: raw.dependencies || [], - status: (raw.status as RoadmapFeature['status']) || 'under_review', + status: normalizeFeatureStatus(raw.status), acceptanceCriteria: raw.acceptance_criteria || raw.acceptanceCriteria || [], userStories: raw.user_stories || raw.userStories || [], linkedSpecId: raw.linked_spec_id || raw.linkedSpecId, @@ -115,6 +166,7 @@ function transformFeature(raw: RawRoadmapFeature): RoadmapFeature { }; } + export function transformRoadmapFromSnakeCase( raw: RawRoadmap, projectId: string, diff --git a/apps/frontend/src/renderer/components/roadmap/RoadmapTabs.tsx b/apps/frontend/src/renderer/components/roadmap/RoadmapTabs.tsx index 689fd28c..ca5de7d9 100644 --- a/apps/frontend/src/renderer/components/roadmap/RoadmapTabs.tsx +++ b/apps/frontend/src/renderer/components/roadmap/RoadmapTabs.tsx @@ -37,6 +37,7 @@ export function RoadmapTabs({ {/* Kanban View */}