Files
Aperant/apps/frontend/src/main/agent/agent-queue.ts
T
souky-byte 6ec8549f63 Fix/ideation status sync (#212)
* fix(ideation): add missing event forwarders for status sync

- Add event forwarders in ideation-handlers.ts for progress, log,
  type-complete, type-failed, complete, error, and stopped events
- Fix ideation-type-complete to load actual ideas array from JSON files
  instead of emitting only the count

Resolves UI getting stuck at 0/3 complete during ideation generation.

* fix(ideation): fix UI not updating after actions

- Fix getIdeationSummary to count only active ideas (exclude dismissed/archived)
  This ensures header stats match the visible ideas count
- Add transformSessionFromSnakeCase to properly transform session data
  from backend snake_case to frontend camelCase on ideation-complete event
- Transform raw session before emitting ideation-complete event

Resolves header showing stale counts after dismissing/deleting ideas.

* fix(ideation): improve type safety and async handling in ideation type completion

- Replace synchronous readFileSync with async fsPromises.readFile in ideation-type-complete handler
- Wrap async file read in IIFE with proper error handling to prevent unhandled promise rejections
- Add type validation for IdeationType with VALID_IDEATION_TYPES set and isValidIdeationType guard
- Add validateEnabledTypes function to filter out invalid type values and log dropped entries
- Handle ENOENT separately

* fix(ideation): improve generation state management and error handling

- Add explicit isGenerating flag to prevent race conditions during async operations
- Implement 5-minute timeout for generation with automatic cleanup and error state
- Add ideation-stopped event emission when process is intentionally killed
- Replace console.warn/error with proper ideation-error events in agent-queue
- Add resetGeneratingTypes helper to transition all generating types to a target state
- Filter out dismissed/

* refactor(ideation): improve event listener cleanup and timeout management

- Extract event handler functions in ideation-handlers.ts to enable proper cleanup
- Return cleanup function from registerIdeationHandlers to remove all listeners
- Replace single generationTimeoutId with Map to support multiple concurrent projects
- Add clearGenerationTimeout helper to centralize timeout cleanup logic
- Extract loadIdeationType IIFE to named function for better error context
- Enhance error logging with projectId,

* refactor: use async file read for ideation and roadmap session loading

- Replace synchronous readFileSync with async fsPromises.readFile
- Prevents blocking the event loop during file operations
- Consistent with async pattern used elsewhere in the codebase
- Improved error handling with proper event emission

* fix(agent-queue): improve roadmap completion handling and error reporting

- Add transformRoadmapFromSnakeCase to convert backend snake_case to frontend camelCase
- Transform raw roadmap data before emitting roadmap-complete event
- Add roadmap-error emission for unexpected errors during completion
- Add roadmap-error emission when project path is unavailable
- Remove duplicate ideation-type-complete emission from error handler (event already emitted in loadIdeationType)
- Update error log message
2025-12-23 18:02:45 +01:00

766 lines
28 KiB
TypeScript

import { spawn } from 'child_process';
import path from 'path';
import { existsSync, promises as fsPromises } from 'fs';
import { EventEmitter } from 'events';
import { AgentState } from './agent-state';
import { AgentEvents } from './agent-events';
import { AgentProcessManager } from './agent-process';
import { RoadmapConfig } from './types';
import type { IdeationConfig, Idea } from '../../shared/types';
import { MODEL_ID_MAP } from '../../shared/constants';
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from '../rate-limit-detector';
import { debugLog, debugError } from '../../shared/utils/debug-logger';
import { parsePythonCommand } from '../python-detector';
import { transformIdeaFromSnakeCase, transformSessionFromSnakeCase } from '../ipc-handlers/ideation/transformers';
import { transformRoadmapFromSnakeCase } from '../ipc-handlers/roadmap/transformers';
import type { RawIdea } from '../ipc-handlers/ideation/types';
/**
* Queue management for ideation and roadmap generation
*/
export class AgentQueueManager {
private state: AgentState;
private events: AgentEvents;
private processManager: AgentProcessManager;
private emitter: EventEmitter;
constructor(
state: AgentState,
events: AgentEvents,
processManager: AgentProcessManager,
emitter: EventEmitter
) {
this.state = state;
this.events = events;
this.processManager = processManager;
this.emitter = emitter;
}
/**
* Start roadmap generation process
*
* @param refreshCompetitorAnalysis - Force refresh competitor analysis even if it exists.
* This allows refreshing competitor data independently of the general roadmap refresh.
* Use when user explicitly wants new competitor research.
*/
startRoadmapGeneration(
projectId: string,
projectPath: string,
refresh: boolean = false,
enableCompetitorAnalysis: boolean = false,
refreshCompetitorAnalysis: boolean = false,
config?: RoadmapConfig
): void {
debugLog('[Agent Queue] Starting roadmap generation:', {
projectId,
projectPath,
refresh,
enableCompetitorAnalysis,
refreshCompetitorAnalysis,
config
});
const autoBuildSource = this.processManager.getAutoBuildSourcePath();
if (!autoBuildSource) {
debugError('[Agent Queue] Auto-build source path not found');
this.emitter.emit('roadmap-error', projectId, 'Auto-build source path not found. Please configure it in App Settings.');
return;
}
const roadmapRunnerPath = path.join(autoBuildSource, 'runners', 'roadmap_runner.py');
if (!existsSync(roadmapRunnerPath)) {
debugError('[Agent Queue] Roadmap runner not found at:', roadmapRunnerPath);
this.emitter.emit('roadmap-error', projectId, `Roadmap runner not found at: ${roadmapRunnerPath}`);
return;
}
const args = [roadmapRunnerPath, '--project', projectPath];
if (refresh) {
args.push('--refresh');
}
// Add competitor analysis flag if enabled
if (enableCompetitorAnalysis) {
args.push('--competitor-analysis');
}
// Add refresh competitor analysis flag if user wants fresh competitor data
if (refreshCompetitorAnalysis) {
args.push('--refresh-competitor-analysis');
}
// Add model and thinking level from config
if (config?.model) {
const modelId = MODEL_ID_MAP[config.model] || MODEL_ID_MAP['opus'];
args.push('--model', modelId);
}
if (config?.thinkingLevel) {
args.push('--thinking-level', config.thinkingLevel);
}
debugLog('[Agent Queue] Spawning roadmap process with args:', args);
// Use projectId as taskId for roadmap operations
this.spawnRoadmapProcess(projectId, projectPath, args);
}
/**
* Start ideation generation process
*/
startIdeationGeneration(
projectId: string,
projectPath: string,
config: IdeationConfig,
refresh: boolean = false
): void {
debugLog('[Agent Queue] Starting ideation generation:', {
projectId,
projectPath,
config,
refresh
});
const autoBuildSource = this.processManager.getAutoBuildSourcePath();
if (!autoBuildSource) {
debugError('[Agent Queue] Auto-build source path not found');
this.emitter.emit('ideation-error', projectId, 'Auto-build source path not found. Please configure it in App Settings.');
return;
}
const ideationRunnerPath = path.join(autoBuildSource, 'runners', 'ideation_runner.py');
if (!existsSync(ideationRunnerPath)) {
debugError('[Agent Queue] Ideation runner not found at:', ideationRunnerPath);
this.emitter.emit('ideation-error', projectId, `Ideation runner not found at: ${ideationRunnerPath}`);
return;
}
const args = [ideationRunnerPath, '--project', projectPath];
// Add enabled types as comma-separated list
if (config.enabledTypes.length > 0) {
args.push('--types', config.enabledTypes.join(','));
}
// Add context flags (script uses --no-roadmap/--no-kanban negative flags)
if (!config.includeRoadmapContext) {
args.push('--no-roadmap');
}
if (!config.includeKanbanContext) {
args.push('--no-kanban');
}
// Add max ideas per type
if (config.maxIdeasPerType) {
args.push('--max-ideas', config.maxIdeasPerType.toString());
}
if (refresh) {
args.push('--refresh');
}
// Add append flag to preserve existing ideas
if (config.append) {
args.push('--append');
}
// Add model and thinking level from config
if (config.model) {
const modelId = MODEL_ID_MAP[config.model] || MODEL_ID_MAP['opus'];
args.push('--model', modelId);
}
if (config.thinkingLevel) {
args.push('--thinking-level', config.thinkingLevel);
}
debugLog('[Agent Queue] Spawning ideation process with args:', args);
// Use projectId as taskId for ideation operations
this.spawnIdeationProcess(projectId, projectPath, args);
}
/**
* Spawn a Python process for ideation generation
*/
private spawnIdeationProcess(
projectId: string,
projectPath: string,
args: string[]
): void {
debugLog('[Agent Queue] Spawning ideation process:', { projectId, projectPath });
// Kill existing process for this project if any
const wasKilled = this.processManager.killProcess(projectId);
if (wasKilled) {
debugLog('[Agent Queue] Killed existing process for project:', projectId);
}
// Generate unique spawn ID for this process instance
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);
// Get active Claude profile environment (CLAUDE_CODE_OAUTH_TOKEN if not default)
const profileEnv = getProfileEnv();
// Get Python path from process manager (uses venv if configured)
const pythonPath = this.processManager.getPythonPath();
// Build final environment with proper precedence:
// 1. process.env (system)
// 2. combinedEnv (auto-claude/.env for CLI usage)
// 3. profileEnv (Electron app OAuth token - highest priority)
// 4. Our specific overrides
const finalEnv = {
...process.env,
...combinedEnv,
...profileEnv,
PYTHONPATH: autoBuildSource || '', // Allow imports from auto-claude directory
PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
};
// Debug: Show OAuth token source
const tokenSource = profileEnv['CLAUDE_CODE_OAUTH_TOKEN']
? 'Electron app profile'
: (combinedEnv['CLAUDE_CODE_OAUTH_TOKEN'] ? 'auto-claude/.env' : 'not found');
const oauthToken = (finalEnv as Record<string, string | undefined>)['CLAUDE_CODE_OAUTH_TOKEN'];
const hasToken = !!oauthToken;
debugLog('[Agent Queue] OAuth token status:', {
source: tokenSource,
hasToken,
tokenPreview: hasToken ? oauthToken?.substring(0, 20) + '...' : 'none'
});
// Parse Python command to handle space-separated commands like "py -3"
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonPath);
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
cwd,
env: finalEnv
});
this.state.addProcess(projectId, {
taskId: projectId,
process: childProcess,
startedAt: new Date(),
projectPath, // Store project path for loading session on completion
spawnId,
queueProcessType: 'ideation'
});
// Track progress through output
let progressPhase = 'analyzing';
let progressPercent = 10;
// Collect output for rate limit detection
let allOutput = '';
// Helper to emit logs - split multi-line output into individual log lines
const emitLogs = (log: string) => {
const lines = log.split('\n').filter(line => line.trim().length > 0);
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.length > 0) {
this.emitter.emit('ideation-log', projectId, trimmed);
}
}
};
// Track completed types for progress calculation
const completedTypes = new Set<string>();
const totalTypes = 7; // Default all types
// Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support
childProcess.stdout?.on('data', (data: Buffer) => {
const log = data.toString('utf8');
// Collect output for rate limit detection (keep last 10KB)
allOutput = (allOutput + log).slice(-10000);
// Emit all log lines for the activity log
emitLogs(log);
const typeCompleteMatch = log.match(/IDEATION_TYPE_COMPLETE:(\w+):(\d+)/);
if (typeCompleteMatch) {
const [, ideationType, ideasCount] = typeCompleteMatch;
completedTypes.add(ideationType);
debugLog('[Agent Queue] Ideation type completed:', {
projectId,
ideationType,
ideasCount: parseInt(ideasCount, 10),
totalCompleted: completedTypes.size
});
const typeFilePath = path.join(
projectPath,
'.auto-claude',
'ideation',
`${ideationType}_ideas.json`
);
const loadIdeationType = async (): Promise<void> => {
try {
const content = await fsPromises.readFile(typeFilePath, 'utf-8');
const data: Record<string, RawIdea[]> = JSON.parse(content);
const rawIdeas: RawIdea[] = data[ideationType] || [];
const ideas: Idea[] = rawIdeas.map(transformIdeaFromSnakeCase);
debugLog('[Agent Queue] Loaded ideas for type:', {
ideationType,
loadedCount: ideas.length,
filePath: typeFilePath
});
this.emitter.emit('ideation-type-complete', projectId, ideationType, ideas);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
debugError('[Agent Queue] Ideas file not found:', typeFilePath);
} else {
debugError('[Agent Queue] Failed to load ideas for type:', ideationType, err);
}
this.emitter.emit('ideation-type-complete', projectId, ideationType, []);
}
};
loadIdeationType().catch((err: unknown) => {
debugError('[Agent Queue] Unhandled error in ideation type handler (event already emitted):', {
ideationType,
projectId,
typeFilePath
}, err);
});
}
const typeFailedMatch = log.match(/IDEATION_TYPE_FAILED:(\w+)/);
if (typeFailedMatch) {
const [, ideationType] = typeFailedMatch;
completedTypes.add(ideationType);
debugError('[Agent Queue] Ideation type failed:', { projectId, ideationType });
this.emitter.emit('ideation-type-failed', projectId, ideationType);
}
// Parse progress using AgentEvents
const progressUpdate = this.events.parseIdeationProgress(
log,
progressPhase,
progressPercent,
completedTypes,
totalTypes
);
progressPhase = progressUpdate.phase;
progressPercent = progressUpdate.progress;
// Emit progress update with a clean message for the status bar
const statusMessage = log.trim().split('\n')[0].substring(0, 200);
this.emitter.emit('ideation-progress', projectId, {
phase: progressPhase,
progress: progressPercent,
message: statusMessage,
completedTypes: Array.from(completedTypes)
});
});
// Handle stderr - also emit as logs, explicitly decode as UTF-8
childProcess.stderr?.on('data', (data: Buffer) => {
const log = data.toString('utf8');
// Collect stderr for rate limit detection too
allOutput = (allOutput + log).slice(-10000);
console.error('[Ideation STDERR]', log);
emitLogs(log);
this.emitter.emit('ideation-progress', projectId, {
phase: progressPhase,
progress: progressPercent,
message: log.trim().split('\n')[0].substring(0, 200)
});
});
// Handle process exit
childProcess.on('exit', (code: number | null) => {
debugLog('[Agent Queue] Ideation process exited:', { projectId, code, spawnId });
// Check if this process was intentionally stopped by the user
const wasIntentionallyStopped = this.state.wasSpawnKilled(spawnId);
if (wasIntentionallyStopped) {
debugLog('[Agent Queue] Ideation process was intentionally stopped, ignoring exit');
this.state.clearKilledSpawn(spawnId);
this.state.deleteProcess(projectId);
// Emit stopped event to ensure UI updates
this.emitter.emit('ideation-stopped', projectId);
return;
}
// Get the stored project path before deleting from map
const processInfo = this.state.getProcess(projectId);
const storedProjectPath = processInfo?.projectPath;
this.state.deleteProcess(projectId);
// Check for rate limit if process failed
if (code !== 0) {
debugLog('[Agent Queue] Checking for rate limit (non-zero exit)');
const rateLimitDetection = detectRateLimit(allOutput);
if (rateLimitDetection.isRateLimited) {
debugLog('[Agent Queue] Rate limit detected for ideation');
const rateLimitInfo = createSDKRateLimitInfo('ideation', rateLimitDetection, {
projectId
});
this.emitter.emit('sdk-rate-limit', rateLimitInfo);
}
}
if (code === 0) {
debugLog('[Agent Queue] Ideation generation completed successfully');
this.emitter.emit('ideation-progress', projectId, {
phase: 'complete',
progress: 100,
message: 'Ideation generation complete'
});
// Load and emit the complete ideation session
if (storedProjectPath) {
try {
const ideationFilePath = path.join(
storedProjectPath,
'.auto-claude',
'ideation',
'ideation.json'
);
debugLog('[Agent Queue] Loading ideation session from:', ideationFilePath);
if (existsSync(ideationFilePath)) {
const loadSession = async (): Promise<void> => {
try {
const content = await fsPromises.readFile(ideationFilePath, 'utf-8');
const rawSession = JSON.parse(content);
const session = transformSessionFromSnakeCase(rawSession, projectId);
debugLog('[Agent Queue] Loaded ideation session:', {
totalIdeas: session.ideas?.length || 0
});
this.emitter.emit('ideation-complete', projectId, session);
} catch (err) {
debugError('[Ideation] Failed to load ideation session:', err);
this.emitter.emit('ideation-error', projectId,
`Failed to load ideation session: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
loadSession().catch((err: unknown) => {
debugError('[Agent Queue] Unhandled error loading ideation session:', err);
});
} else {
debugError('[Ideation] ideation.json not found at:', ideationFilePath);
this.emitter.emit('ideation-error', projectId,
'Ideation completed but session file not found. Ideas may have been saved to individual type files.');
}
} catch (err) {
debugError('[Ideation] Unexpected error in ideation completion:', err);
this.emitter.emit('ideation-error', projectId,
`Failed to load ideation session: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
} else {
debugError('[Ideation] No project path available to load session');
this.emitter.emit('ideation-error', projectId,
'Ideation completed but project path unavailable');
}
} else {
debugError('[Agent Queue] Ideation generation failed:', { projectId, code });
this.emitter.emit('ideation-error', projectId, `Ideation generation failed with exit code ${code}`);
}
});
// Handle process error
childProcess.on('error', (err: Error) => {
console.error('[Ideation] Process error:', err.message);
this.state.deleteProcess(projectId);
this.emitter.emit('ideation-error', projectId, err.message);
});
}
/**
* Spawn a Python process for roadmap generation
*/
private spawnRoadmapProcess(
projectId: string,
projectPath: string,
args: string[]
): void {
debugLog('[Agent Queue] Spawning roadmap process:', { projectId, projectPath });
// Kill existing process for this project if any
const wasKilled = this.processManager.killProcess(projectId);
if (wasKilled) {
debugLog('[Agent Queue] Killed existing roadmap process for project:', projectId);
}
// Generate unique spawn ID for this process instance
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);
// Get active Claude profile environment (CLAUDE_CODE_OAUTH_TOKEN if not default)
const profileEnv = getProfileEnv();
// Get Python path from process manager (uses venv if configured)
const pythonPath = this.processManager.getPythonPath();
// Build final environment with proper precedence:
// 1. process.env (system)
// 2. combinedEnv (auto-claude/.env for CLI usage)
// 3. profileEnv (Electron app OAuth token - highest priority)
// 4. Our specific overrides
const finalEnv = {
...process.env,
...combinedEnv,
...profileEnv,
PYTHONPATH: autoBuildSource || '', // Allow imports from auto-claude directory
PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
};
// Debug: Show OAuth token source
const tokenSource = profileEnv['CLAUDE_CODE_OAUTH_TOKEN']
? 'Electron app profile'
: (combinedEnv['CLAUDE_CODE_OAUTH_TOKEN'] ? 'auto-claude/.env' : 'not found');
const oauthToken = (finalEnv as Record<string, string | undefined>)['CLAUDE_CODE_OAUTH_TOKEN'];
const hasToken = !!oauthToken;
debugLog('[Agent Queue] OAuth token status:', {
source: tokenSource,
hasToken,
tokenPreview: hasToken ? oauthToken?.substring(0, 20) + '...' : 'none'
});
// Parse Python command to handle space-separated commands like "py -3"
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonPath);
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
cwd,
env: finalEnv
});
this.state.addProcess(projectId, {
taskId: projectId,
process: childProcess,
startedAt: new Date(),
projectPath, // Store project path for loading roadmap on completion
spawnId,
queueProcessType: 'roadmap'
});
// Track progress through output
let progressPhase = 'analyzing';
let progressPercent = 10;
// Collect output for rate limit detection
let allRoadmapOutput = '';
// Helper to emit logs - split multi-line output into individual log lines
const emitLogs = (log: string) => {
const lines = log.split('\n').filter(line => line.trim().length > 0);
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.length > 0) {
this.emitter.emit('roadmap-log', projectId, trimmed);
}
}
};
// Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support
childProcess.stdout?.on('data', (data: Buffer) => {
const log = data.toString('utf8');
// Collect output for rate limit detection (keep last 10KB)
allRoadmapOutput = (allRoadmapOutput + log).slice(-10000);
// Emit all log lines for debugging
emitLogs(log);
// Parse progress using AgentEvents
const progressUpdate = this.events.parseRoadmapProgress(log, progressPhase, progressPercent);
progressPhase = progressUpdate.phase;
progressPercent = progressUpdate.progress;
// Emit progress update
this.emitter.emit('roadmap-progress', projectId, {
phase: progressPhase,
progress: progressPercent,
message: log.trim().substring(0, 200) // Truncate long messages
});
});
// Handle stderr - explicitly decode as UTF-8
childProcess.stderr?.on('data', (data: Buffer) => {
const log = data.toString('utf8');
// Collect stderr for rate limit detection too
allRoadmapOutput = (allRoadmapOutput + log).slice(-10000);
console.error('[Roadmap STDERR]', log);
emitLogs(log);
this.emitter.emit('roadmap-progress', projectId, {
phase: progressPhase,
progress: progressPercent,
message: log.trim().substring(0, 200)
});
});
// Handle process exit
childProcess.on('exit', (code: number | null) => {
debugLog('[Agent Queue] Roadmap process exited:', { projectId, code, spawnId });
// Check if this process was intentionally stopped by the user
const wasIntentionallyStopped = this.state.wasSpawnKilled(spawnId);
if (wasIntentionallyStopped) {
debugLog('[Agent Queue] Roadmap process was intentionally stopped, ignoring exit');
this.state.clearKilledSpawn(spawnId);
this.state.deleteProcess(projectId);
return;
}
// Get the stored project path before deleting from map
const processInfo = this.state.getProcess(projectId);
const storedProjectPath = processInfo?.projectPath;
this.state.deleteProcess(projectId);
// Check for rate limit if process failed
if (code !== 0) {
debugLog('[Agent Queue] Checking for rate limit (non-zero exit)');
const rateLimitDetection = detectRateLimit(allRoadmapOutput);
if (rateLimitDetection.isRateLimited) {
debugLog('[Agent Queue] Rate limit detected for roadmap');
const rateLimitInfo = createSDKRateLimitInfo('roadmap', rateLimitDetection, {
projectId
});
this.emitter.emit('sdk-rate-limit', rateLimitInfo);
}
}
if (code === 0) {
debugLog('[Agent Queue] Roadmap generation completed successfully');
this.emitter.emit('roadmap-progress', projectId, {
phase: 'complete',
progress: 100,
message: 'Roadmap generation complete'
});
// Load and emit the complete roadmap
if (storedProjectPath) {
try {
const roadmapFilePath = path.join(
storedProjectPath,
'.auto-claude',
'roadmap',
'roadmap.json'
);
debugLog('[Agent Queue] Loading roadmap from:', roadmapFilePath);
if (existsSync(roadmapFilePath)) {
const loadRoadmap = async (): Promise<void> => {
try {
const content = await fsPromises.readFile(roadmapFilePath, 'utf-8');
const rawRoadmap = JSON.parse(content);
const transformedRoadmap = transformRoadmapFromSnakeCase(rawRoadmap, projectId);
debugLog('[Agent Queue] Loaded roadmap:', {
featuresCount: transformedRoadmap.features?.length || 0,
phasesCount: transformedRoadmap.phases?.length || 0
});
this.emitter.emit('roadmap-complete', projectId, transformedRoadmap);
} catch (err) {
debugError('[Roadmap] Failed to load roadmap:', err);
this.emitter.emit('roadmap-error', projectId,
`Failed to load roadmap: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
loadRoadmap().catch((err: unknown) => {
debugError('[Agent Queue] Unhandled error loading roadmap:', err);
});
} else {
debugError('[Roadmap] roadmap.json not found at:', roadmapFilePath);
this.emitter.emit('roadmap-error', projectId,
'Roadmap completed but file not found.');
}
} catch (err) {
debugError('[Roadmap] Unexpected error in roadmap completion:', err);
this.emitter.emit('roadmap-error', projectId,
`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
} else {
debugError('[Roadmap] No project path available for roadmap completion');
this.emitter.emit('roadmap-error', projectId, 'Roadmap completed but project path not found.');
}
} else {
debugError('[Agent Queue] Roadmap generation failed:', { projectId, code });
this.emitter.emit('roadmap-error', projectId, `Roadmap generation failed with exit code ${code}`);
}
});
// Handle process error
childProcess.on('error', (err: Error) => {
console.error('[Roadmap] Process error:', err.message);
this.state.deleteProcess(projectId);
this.emitter.emit('roadmap-error', projectId, err.message);
});
}
/**
* Stop ideation generation for a project
*/
stopIdeation(projectId: string): boolean {
debugLog('[Agent Queue] Stop ideation requested:', { projectId });
const processInfo = this.state.getProcess(projectId);
const isIdeation = processInfo?.queueProcessType === 'ideation';
debugLog('[Agent Queue] Process running?', { projectId, isIdeation, processType: processInfo?.queueProcessType });
if (isIdeation) {
debugLog('[Agent Queue] Killing ideation process:', projectId);
this.processManager.killProcess(projectId);
this.emitter.emit('ideation-stopped', projectId);
return true;
}
debugLog('[Agent Queue] No running ideation process found for:', projectId);
return false;
}
/**
* Check if ideation is running for a project
*/
isIdeationRunning(projectId: string): boolean {
const processInfo = this.state.getProcess(projectId);
return processInfo?.queueProcessType === 'ideation';
}
/**
* Stop roadmap generation for a project
*/
stopRoadmap(projectId: string): boolean {
debugLog('[Agent Queue] Stop roadmap requested:', { projectId });
const processInfo = this.state.getProcess(projectId);
const isRoadmap = processInfo?.queueProcessType === 'roadmap';
debugLog('[Agent Queue] Roadmap process running?', { projectId, isRoadmap, processType: processInfo?.queueProcessType });
if (isRoadmap) {
debugLog('[Agent Queue] Killing roadmap process:', projectId);
this.processManager.killProcess(projectId);
this.emitter.emit('roadmap-stopped', projectId);
return true;
}
debugLog('[Agent Queue] No running roadmap process found for:', projectId);
return false;
}
/**
* Check if roadmap is running for a project
*/
isRoadmapRunning(projectId: string): boolean {
const processInfo = this.state.getProcess(projectId);
return processInfo?.queueProcessType === 'roadmap';
}
}