fix(roadmap): normalize feature status values for Kanban display [ACS-115] (#763)
* fix(memory): use Homebrew for Ollama installation on macOS Added macOS-specific branch in getOllamaInstallCommand() to use 'brew install ollama' instead of the Linux-only curl install script. - macOS: now uses 'brew install ollama' (Homebrew) - Linux: continues using 'curl -fsSL https://ollama.com/install.sh | sh' - Windows: unchanged (uses winget) Closes ACS-114 * fix(frontend): force remount of kanban view on roadmap update (ACS-115) * fix(roadmap): normalize feature status values for Kanban display Fixes ACS-115 - roadmap features were not appearing in Kanban columns. Root cause: Backend generates features with status 'idea' but Kanban columns expect 'under_review', 'planned', 'in_progress', or 'done'. The type cast was passing through invalid values unchanged. Changes: - Add normalizeFeatureStatus() to map backend values to valid column IDs - Map 'idea', 'backlog', 'proposed' → 'under_review' - Map 'approved', 'scheduled' → 'planned' - Map 'active', 'building' → 'in_progress' - Map 'complete', 'completed', 'shipped' → 'done' - Fallback unknown values to 'under_review' - Add Python env readiness check in agent-queue.ts * refactor: address reviewer feedback on ACS-115 PR - Extract duplicated Python env check into ensurePythonEnvReady() helper - Move STATUS_MAP to module-level constant for efficiency - Simplify normalizeFeatureStatus with single map lookup - Add debug logging for unmapped status values - Add JSDoc documentation for new methods 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Alex <63423455+AlexMadera@users.noreply.github.com>
This commit is contained in:
@@ -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<boolean> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
|
||||
@@ -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<string, RoadmapFeature['status']> = {
|
||||
// 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,
|
||||
|
||||
@@ -37,6 +37,7 @@ export function RoadmapTabs({
|
||||
{/* Kanban View */}
|
||||
<TabsContent value="kanban" className="flex-1 overflow-hidden">
|
||||
<RoadmapKanbanView
|
||||
key={roadmap.updatedAt?.toString()}
|
||||
roadmap={roadmap}
|
||||
onFeatureClick={onFeatureSelect}
|
||||
onConvertToSpec={onConvertToSpec}
|
||||
|
||||
Reference in New Issue
Block a user