Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 120 additions & 9 deletions auto-claude-ui/src/main/project-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<typeof currentPhase, ExecutionPhase> = {
'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);
Expand Down Expand Up @@ -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())
});
Expand All @@ -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
Expand All @@ -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<string, unknown>)?.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<string, unknown>)?.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) {
Expand Down
7 changes: 4 additions & 3 deletions auto-claude-ui/src/renderer/components/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -218,16 +219,16 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
Archived
</Badge>
)}
{/* Execution phase badge - shown when actively running */}
{hasActiveExecution && executionPhase && !isStuck && !isIncomplete && (
{/* Execution phase badge - shown when phase info is available */}
{hasPhaseInfo && executionPhase && !isStuck && !isIncomplete && (
<Badge
variant="outline"
className={cn(
'text-[10px] px-1.5 py-0.5 flex items-center gap-1',
EXECUTION_PHASE_BADGE_COLORS[executionPhase]
)}
>
<Loader2 className="h-2.5 w-2.5 animate-spin" />
{isRunning && <Loader2 className="h-2.5 w-2.5 animate-spin" />}
{EXECUTION_PHASE_LABELS[executionPhase]}
</Badge>
)}
Expand Down
Loading