diff --git a/.github/assets/bug-validation-status-inconsistency.png b/.github/assets/bug-validation-status-inconsistency.png new file mode 100644 index 0000000000..bbd10a3490 Binary files /dev/null and b/.github/assets/bug-validation-status-inconsistency.png differ diff --git a/auto-claude-ui/src/main/project-store.ts b/auto-claude-ui/src/main/project-store.ts index 05aec1b4f9..5769bad8dc 100644 --- a/auto-claude-ui/src/main/project-store.ts +++ b/auto-claude-ui/src/main/project-store.ts @@ -2,7 +2,7 @@ import { app } from 'electron'; import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, Dirent } from 'fs'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; -import type { Project, ProjectSettings, Task, TaskStatus, TaskMetadata, ImplementationPlan, ReviewReason, PlanSubtask } from '../shared/types'; +import type { Project, ProjectSettings, Task, TaskStatus, TaskMetadata, ImplementationPlan, ReviewReason, PlanSubtask, ExecutionProgress, ExecutionPhase } from '../shared/types'; import { DEFAULT_PROJECT_SETTINGS, AUTO_BUILD_PATHS, getSpecsDir } from '../shared/constants'; import { getAutoBuildPath, isInitialized } from './project-initializer'; @@ -369,6 +369,80 @@ export class ProjectStore { const stagedInMainProject = planWithStaged?.stagedInMainProject; const stagedAt = planWithStaged?.stagedAt; + // Determine execution progress by checking active or most recent phase from task logs + // Uses same merging logic as TaskLogService: planning from main, coding/validation from worktree + let executionProgress: ExecutionProgress | undefined; + const taskLogPath = path.join(specPath, 'task_log.json'); + // Worktree structure: {projectPath}/.worktrees/{specId}/{specsRelPath}/{specId}/ + const worktreeLogPath = path.join(project.path, '.worktrees', dir.name, specsBaseDir, dir.name, 'task_log.json'); + + // Load and merge logs (same logic as TaskLogService) + let mainLog: any = null; + let worktreeLog: any = null; + + if (existsSync(taskLogPath)) { + try { + mainLog = JSON.parse(readFileSync(taskLogPath, 'utf-8')); + } catch { + // Ignore parse errors + } + } + + if (existsSync(worktreeLogPath)) { + try { + worktreeLog = JSON.parse(readFileSync(worktreeLogPath, 'utf-8')); + } catch { + // Ignore parse errors + } + } + + // Merge logs: planning from main, coding/validation from worktree (if available) + const mergedPhases = { + planning: mainLog?.phases?.planning || worktreeLog?.phases?.planning, + coding: (worktreeLog?.phases?.coding?.entries?.length > 0 || worktreeLog?.phases?.coding?.status !== 'pending') + ? worktreeLog?.phases?.coding + : mainLog?.phases?.coding, + validation: (worktreeLog?.phases?.validation?.entries?.length > 0 || worktreeLog?.phases?.validation?.status !== 'pending') + ? worktreeLog?.phases?.validation + : mainLog?.phases?.validation + }; + + // Find active or most recently completed phase (check in reverse order) + let currentPhase: 'planning' | 'coding' | 'validation' | null = null; + const phases = ['validation', 'coding', 'planning'] as const; + + // First, check for active phases + for (const phase of phases) { + if (mergedPhases[phase]?.status === 'active') { + currentPhase = phase; + break; + } + } + + // If no active phase, find the most recently completed phase + if (!currentPhase) { + for (const phase of phases) { + if (mergedPhases[phase]?.status === 'completed') { + currentPhase = phase; + break; + } + } + } + + // Map task log phase to execution phase + if (currentPhase) { + const phaseMap: Record = { + 'planning': 'planning', + 'coding': 'coding', + 'validation': 'qa_review' + }; + executionProgress = { + phase: phaseMap[currentPhase], + phaseProgress: 0, + overallProgress: 0 + }; + } + // Determine title - check if feature looks like a spec ID (e.g., "054-something-something") let title = plan?.feature || plan?.title || dir.name; const looksLikeSpecId = /^\d{3}-/.test(title); @@ -401,6 +475,7 @@ export class ProjectStore { metadata, stagedInMainProject, stagedAt, + executionProgress, createdAt: new Date(plan?.created_at || Date.now()), updatedAt: new Date(plan?.updated_at || Date.now()) }); @@ -420,6 +495,10 @@ export class ProjectStore { * This method calculates the correct status from subtask progress and QA state, * providing backwards compatibility for existing tasks with incorrect status. * + * CRITICAL FIX: Now checks validation phase status before setting to human_review. + * Tasks must NOT transition to human_review (which allows merge) while validation + * is still running. They should remain in ai_review until validation completes. + * * Review reasons: * - 'completed': All subtasks done, QA passed - ready for merge * - 'errors': Subtasks failed during execution - needs attention @@ -436,22 +515,54 @@ export class ProjectStore { let calculatedStatus: TaskStatus = 'backlog'; let reviewReason: ReviewReason | undefined; + // Check validation phase status from task logs (check both main and worktree logs) + const taskLogPath = path.join(specPath, 'task_log.json'); + let validationPhaseActive = false; + + // First check worktree logs (validation runs in worktree) + const worktreeLogPath = path.join(specPath, '.worktrees', path.basename(specPath), 'task_log.json'); + if (existsSync(worktreeLogPath)) { + try { + const worktreeLog = JSON.parse(readFileSync(worktreeLogPath, 'utf-8')); + validationPhaseActive = worktreeLog?.phases?.validation?.status === 'active'; + } catch { + // Ignore read errors + } + } + + // Fallback to main task log if worktree log doesn't exist or validation not active there + if (!validationPhaseActive && existsSync(taskLogPath)) { + try { + const taskLog = JSON.parse(readFileSync(taskLogPath, 'utf-8')); + validationPhaseActive = taskLog?.phases?.validation?.status === 'active'; + } catch { + // Ignore read errors + } + } + if (allSubtasks.length > 0) { const completed = allSubtasks.filter((s) => s.status === 'completed').length; const inProgress = allSubtasks.filter((s) => s.status === 'in_progress').length; const failed = allSubtasks.filter((s) => s.status === 'failed').length; if (completed === allSubtasks.length) { - // All subtasks completed - check QA status - const qaSignoff = (plan as unknown as Record)?.qa_signoff as { status?: string } | undefined; - if (qaSignoff?.status === 'approved') { - calculatedStatus = 'human_review'; - reviewReason = 'completed'; + // All subtasks completed - check validation phase status first + if (validationPhaseActive) { + // CRITICAL: Validation is still running - keep in ai_review status to prevent premature merge + // The task should NOT be in human_review (which allows merge) until validation completes + calculatedStatus = 'ai_review'; } else { - // Manual tasks skip AI review and go directly to human review - calculatedStatus = metadata?.sourceType === 'manual' ? 'human_review' : 'ai_review'; - if (metadata?.sourceType === 'manual') { + // Validation complete - check QA status + const qaSignoff = (plan as unknown as Record)?.qa_signoff as { status?: string } | undefined; + if (qaSignoff?.status === 'approved') { + calculatedStatus = 'human_review'; reviewReason = 'completed'; + } else { + // Manual tasks skip AI review and go directly to human review + calculatedStatus = metadata?.sourceType === 'manual' ? 'human_review' : 'ai_review'; + if (metadata?.sourceType === 'manual') { + reviewReason = 'completed'; + } } } } else if (failed > 0) { diff --git a/auto-claude-ui/src/renderer/components/TaskCard.tsx b/auto-claude-ui/src/renderer/components/TaskCard.tsx index fb514816b6..a0ab011292 100644 --- a/auto-claude-ui/src/renderer/components/TaskCard.tsx +++ b/auto-claude-ui/src/renderer/components/TaskCard.tsx @@ -45,6 +45,7 @@ export function TaskCard({ task, onClick }: TaskCardProps) { const isRunning = task.status === 'in_progress'; const executionPhase = task.executionProgress?.phase; const hasActiveExecution = executionPhase && executionPhase !== 'idle' && executionPhase !== 'complete' && executionPhase !== 'failed'; + const hasPhaseInfo = executionPhase && executionPhase !== 'idle' && executionPhase !== 'complete' && executionPhase !== 'failed'; // Check if task is in human_review but has no completed subtasks (crashed/incomplete) const isIncomplete = isIncompleteHumanReview(task); @@ -218,8 +219,8 @@ export function TaskCard({ task, onClick }: TaskCardProps) { Archived )} - {/* Execution phase badge - shown when actively running */} - {hasActiveExecution && executionPhase && !isStuck && !isIncomplete && ( + {/* Execution phase badge - shown when phase info is available */} + {hasPhaseInfo && executionPhase && !isStuck && !isIncomplete && ( - + {isRunning && } {EXECUTION_PHASE_LABELS[executionPhase]} )}