After adding comprehensive logging to the task loading and plan update pipeline, I've analyzed the data flow from backend to frontend to identify why subtasks fail to display after spec completion.
Backend (Python)
↓
Creates implementation_plan.json
↓
Emits IPC event: 'task:progress' with plan data
↓
Frontend (Electron Renderer)
↓
useIpc.ts: onTaskProgress handler (batched)
↓
task-store.ts: updateTaskFromPlan(taskId, plan)
↓
Creates subtasks from plan.phases.flatMap(phase => phase.subtasks)
↓
UI: TaskSubtasks.tsx renders subtasks
1. Plan Update Handler (apps/frontend/src/renderer/hooks/useIpc.ts:131-135)
window.electronAPI.onTaskProgress(
(taskId: string, plan: ImplementationPlan) => {
queueUpdate(taskId, { plan });
}
);2. Subtask Creation (apps/frontend/src/renderer/stores/task-store.ts:124-133)
const subtasks: Subtask[] = plan.phases.flatMap((phase) =>
phase.subtasks.map((subtask) => ({
id: subtask.id,
title: subtask.description,
description: subtask.description,
status: subtask.status,
files: [],
verification: subtask.verification as Subtask['verification']
}))
);3. Initial Task Loading (apps/frontend/src/main/project-store.ts:461-470)
const subtasks = plan?.phases?.flatMap((phase) => {
const items = phase.subtasks || (phase as { chunks?: PlanSubtask[] }).chunks || [];
return items.map((subtask) => ({
id: subtask.id,
title: subtask.description,
description: subtask.description,
status: subtask.status,
files: []
}));
}) || [];What's Happening:
-
Backend creates
implementation_plan.jsonin stages:- First writes the file with minimal structure:
{ "feature": "...", "phases": [] } - Then adds phases and subtasks incrementally
- Emits IPC event each time the plan is updated
- First writes the file with minimal structure:
-
Frontend receives the FIRST plan update event:
- Plan has
featureand basic metadata - But
phasesarray is EMPTY:[] updateTaskFromPlanis called with this incomplete plan- Subtasks are created as empty array:
plan.phases.flatMap(...)→[]
- Plan has
-
Later plan updates with full subtask data are ignored:
- When backend writes the complete plan with subtasks
- Another IPC event is emitted
- But due to race conditions or event handling issues, this update doesn't reach the frontend
- Or it does reach but the task UI doesn't refresh
Evidence from Code:
Looking at updateTaskFromPlan (task-store.ts:106-190):
- Line 108-114: Logs show
phases: plan.phases?.length || 0 - Line 112: If plan has 0 phases,
totalSubtaskswill be 0 - Line 124-133:
plan.phases.flatMap(...)on empty array createssubtasks = [] - No validation to check if plan is complete before updating state
Why "!" Indicators Appear:
The "!" indicators likely come from the UI attempting to render subtasks when:
- Subtask count shows as 18 (from later plan update metadata)
- But
task.subtasksarray is actually empty[](from early plan update) - This mismatch causes the UI to show warning indicators
A. No Plan Validation Before State Update
Current code in updateTaskFromPlan immediately creates subtasks from whatever plan data it receives:
const subtasks: Subtask[] = plan.phases.flatMap((phase) =>
phase.subtasks.map((subtask) => ({ ... }))
);Problem: No check if plan is "ready" or "complete" before updating state.
B. Missing Reload Trigger After Spec Completion
When spec creation completes and the full plan is written:
- The IPC event might not fire again
- Or the event fires but the batching mechanism drops it
- Frontend state remains stuck with empty subtasks
C. Race Condition in Batch Update Queue
In useIpc.ts:92-112, the batching mechanism queues updates:
function queueUpdate(taskId: string, update: BatchedUpdate): void {
const existing = batchQueue.get(taskId) || {};
batchQueue.set(taskId, { ...existing, ...update });
}Problem: If two plan updates arrive within 16ms:
- First update has empty phases:
{ plan: { phases: [] } } - Second update has full phases:
{ plan: { phases: [...18 subtasks...] } } - Second update overwrites first in the queue
- But if order gets reversed, empty plan overwrites full plan
To confirm this root cause, check console logs for:
[updateTaskFromPlan] called with plan:
taskId: "xxx"
feature: "..."
phases: 0 ← SMOKING GUN: phases array is empty
totalSubtasks: 0 ← No subtasks
If you see phases: 0 followed later by no update with phases: 3 (or more), the early empty plan is stuck in state.
[updateTaskFromPlan] called with plan:
phases: 0
totalSubtasks: 0
[updateTaskFromPlan] called with plan: ← This might never appear
phases: 3
totalSubtasks: 18
If second log never appears, the plan update event isn't firing after spec completion.
[ProjectStore] Loading implementation_plan.json for spec: xxx
[ProjectStore] Loaded plan for xxx:
phaseCount: 0 ← Empty plan loaded from disk
subtaskCount: 0
If plan file on disk has empty phases, the issue is in backend plan writing.
[plan-file-utils] Reading implementation_plan.json to update status
[plan-file-utils] Successfully persisted status ← Plan exists but might be incomplete
Check if plan file reads/writes are happening during spec creation.
File: apps/frontend/src/renderer/stores/task-store.ts
Change: Only update subtasks if plan has valid phases and subtasks:
updateTaskFromPlan: (taskId, plan) =>
set((state) => {
console.log('[updateTaskFromPlan] called with plan:', { ... });
const index = findTaskIndex(state.tasks, taskId);
if (index === -1) {
console.log('[updateTaskFromPlan] Task not found:', taskId);
return state;
}
// VALIDATION: Don't update if plan is incomplete
if (!plan.phases || plan.phases.length === 0) {
console.warn('[updateTaskFromPlan] Plan has no phases, skipping update:', taskId);
return state; // Keep existing state, don't overwrite with empty data
}
const totalSubtasks = plan.phases.reduce((acc, p) => acc + (p.subtasks?.length || 0), 0);
if (totalSubtasks === 0) {
console.warn('[updateTaskFromPlan] Plan has no subtasks, skipping update:', taskId);
return state; // Keep existing state
}
// ... rest of existing code to create subtasks ...
})File: apps/frontend/src/renderer/hooks/useIpc.ts
Change: Add explicit "spec completed" event handler that reloads the task:
// Add new IPC event listener
const cleanupSpecComplete = window.electronAPI.onSpecComplete(
async (taskId: string) => {
console.log('[IPC] Spec completed for task:', taskId);
// Force reload the task from disk to get the complete plan
const task = useTaskStore.getState().tasks.find(t => t.id === taskId);
if (task) {
// Reload plan from file
const result = await window.electronAPI.getTaskPlan(task.projectId, taskId);
if (result.success && result.data) {
updateTaskFromPlan(taskId, result.data);
}
}
}
);File: apps/frontend/src/renderer/hooks/useIpc.ts
Change: Don't overwrite plan if incoming plan has fewer subtasks than existing:
function queueUpdate(taskId: string, update: BatchedUpdate): void {
const existing = batchQueue.get(taskId) || {};
// For plan updates, only accept if it has MORE data than existing
let mergedPlan = existing.plan;
if (update.plan) {
const existingSubtasks = existing.plan?.phases?.flatMap(p => p.subtasks || []).length || 0;
const newSubtasks = update.plan.phases?.flatMap(p => p.subtasks || []).length || 0;
if (newSubtasks >= existingSubtasks) {
mergedPlan = update.plan; // Accept new plan
} else {
console.warn('[IPC Batch] Rejecting plan update with fewer subtasks:',
{ taskId, existing: existingSubtasks, new: newSubtasks });
// Keep existing plan, don't overwrite with less complete data
}
}
// ... rest of existing code ...
}- Create a new task and move it to "In Progress"
- Watch the console logs for:
[updateTaskFromPlan] called with plan: { phases: 0, totalSubtasks: 0 } - Wait for spec to complete (planning phase finishes)
- Check console logs for:
[updateTaskFromPlan] called with plan: { phases: 3, totalSubtasks: 18 } - Expand subtask list in task card
- Verify: Subtasks display with full details, no "!" indicators
- ✅ Empty/incomplete plan updates are ignored
- ✅ Only complete plans with phases and subtasks update the UI
- ✅ Subtasks display with id, description, and status
- ✅ No "!" warning indicators
- ✅ Subtask count shows "0/18 completed" (not "0/0")
- ✅ Plan pulsing animation stops when spec completes
- ✅ Resume functionality works without infinite loop
- ✅ This Investigation - Root cause identified (COMPLETE)
- 🔄 Subtask 2-1 - Implement Fix 1 (validation in updateTaskFromPlan)
- 🔄 Subtask 2-2 - Add data validation before subtask state updates
- 🔄 Subtask 2-3 - Fix pulsing animation condition
- 🔄 Subtask 2-4 - Fix resume logic to reload plan if subtasks missing
- 🔄 Phase 3 - Add comprehensive tests to prevent regressions
Root Cause: Frontend receives and accepts incomplete plan data (empty phases array) during the spec creation process, before subtasks are written. This overwrites any existing subtask data and leaves the UI in a stuck state with no subtasks to display.
Fix Priority: Implement Fix 1 (validation) immediately to prevent incomplete plans from updating state. This is a minimal, low-risk change that will resolve the core issue.
Long-term Solution: Add explicit event handling for spec completion (Fix 2) and improve batch queue logic (Fix 3) to make the system more robust against race conditions and out-of-order updates.