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:
Michael Ludlow
2026-01-07 01:27:26 -05:00
committed by GitHub
parent 31519c2a10
commit 5e783908e3
3 changed files with 106 additions and 7 deletions
+52 -6
View File
@@ -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}