#
tokens: 31709/50000 2/89 files (page 3/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 3. Use http://codebase.md/jhawkins11/task-manager-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── frontend
│   ├── .gitignore
│   ├── .npmrc
│   ├── components.json
│   ├── package-lock.json
│   ├── package.json
│   ├── postcss.config.cjs
│   ├── README.md
│   ├── src
│   │   ├── app.d.ts
│   │   ├── app.html
│   │   ├── app.pcss
│   │   ├── lib
│   │   │   ├── components
│   │   │   │   ├── ImportTasksModal.svelte
│   │   │   │   ├── QuestionModal.svelte
│   │   │   │   ├── TaskFormModal.svelte
│   │   │   │   └── ui
│   │   │   │       ├── badge
│   │   │   │       │   ├── badge.svelte
│   │   │   │       │   └── index.ts
│   │   │   │       ├── button
│   │   │   │       │   ├── button.svelte
│   │   │   │       │   └── index.ts
│   │   │   │       ├── card
│   │   │   │       │   ├── card-content.svelte
│   │   │   │       │   ├── card-description.svelte
│   │   │   │       │   ├── card-footer.svelte
│   │   │   │       │   ├── card-header.svelte
│   │   │   │       │   ├── card-title.svelte
│   │   │   │       │   ├── card.svelte
│   │   │   │       │   └── index.ts
│   │   │   │       ├── checkbox
│   │   │   │       │   ├── checkbox.svelte
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dialog
│   │   │   │       │   ├── dialog-content.svelte
│   │   │   │       │   ├── dialog-description.svelte
│   │   │   │       │   ├── dialog-footer.svelte
│   │   │   │       │   ├── dialog-header.svelte
│   │   │   │       │   ├── dialog-overlay.svelte
│   │   │   │       │   ├── dialog-portal.svelte
│   │   │   │       │   ├── dialog-title.svelte
│   │   │   │       │   └── index.ts
│   │   │   │       ├── input
│   │   │   │       │   ├── index.ts
│   │   │   │       │   └── input.svelte
│   │   │   │       ├── label
│   │   │   │       │   ├── index.ts
│   │   │   │       │   └── label.svelte
│   │   │   │       ├── progress
│   │   │   │       │   ├── index.ts
│   │   │   │       │   └── progress.svelte
│   │   │   │       ├── select
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── select-content.svelte
│   │   │   │       │   ├── select-group-heading.svelte
│   │   │   │       │   ├── select-item.svelte
│   │   │   │       │   ├── select-scroll-down-button.svelte
│   │   │   │       │   ├── select-scroll-up-button.svelte
│   │   │   │       │   ├── select-separator.svelte
│   │   │   │       │   └── select-trigger.svelte
│   │   │   │       ├── separator
│   │   │   │       │   ├── index.ts
│   │   │   │       │   └── separator.svelte
│   │   │   │       └── textarea
│   │   │   │           ├── index.ts
│   │   │   │           └── textarea.svelte
│   │   │   ├── index.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   └── routes
│   │       ├── +layout.server.ts
│   │       ├── +layout.svelte
│   │       └── +page.svelte
│   ├── static
│   │   └── favicon.png
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── vite.config.ts
├── img
│   └── ui.png
├── jest.config.js
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── config
│   │   ├── index.ts
│   │   ├── migrations.sql
│   │   └── schema.sql
│   ├── index.ts
│   ├── lib
│   │   ├── dbUtils.ts
│   │   ├── llmUtils.ts
│   │   ├── logger.ts
│   │   ├── repomixUtils.ts
│   │   ├── utils.ts
│   │   └── winstonLogger.ts
│   ├── models
│   │   └── types.ts
│   ├── server.ts
│   ├── services
│   │   ├── aiService.ts
│   │   ├── databaseService.ts
│   │   ├── planningStateService.ts
│   │   └── webSocketService.ts
│   └── tools
│       ├── adjustPlan.ts
│       ├── markTaskComplete.ts
│       ├── planFeature.ts
│       └── reviewChanges.ts
├── tests
│   ├── json-parser.test.ts
│   ├── llmUtils.unit.test.ts
│   ├── reviewChanges.integration.test.ts
│   └── setupEnv.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/frontend/src/routes/+page.svelte:
--------------------------------------------------------------------------------

```
   1 | <script lang="ts">
   2 | 	import { onMount, onDestroy } from 'svelte';
   3 | 	import { page } from '$app/stores';
   4 | 	import { fade } from 'svelte/transition'; 
   5 | 	import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '$lib/components/ui/card';
   6 | 	import { Button } from '$lib/components/ui/button';
   7 | 	import { Badge } from '$lib/components/ui/badge';
   8 | 	import { Checkbox } from '$lib/components/ui/checkbox';
   9 | 	import * as Select from '$lib/components/ui/select';
  10 | 	import { Progress } from '$lib/components/ui/progress';
  11 | 	import { Loader2, CornerDownLeft, CornerDownRight, Pencil, Trash2, FileText, Eye } from 'lucide-svelte';
  12 | 	import { writable, type Writable } from 'svelte/store';
  13 | 	import type { Task, WebSocketMessage, ShowQuestionPayload, QuestionResponsePayload } from '$lib/types';
  14 | 	import { TaskStatus, TaskEffort } from '$lib/types';
  15 | 	import type { Selected } from 'bits-ui';
  16 | 	import QuestionModal from '$lib/components/QuestionModal.svelte';
  17 | 	import TaskFormModal from '$lib/components/TaskFormModal.svelte';
  18 | 	import ImportTasksModal from '$lib/components/ImportTasksModal.svelte';
  19 | 
  20 | 	// Convert to writable stores for better state management
  21 | 	const tasks: Writable<Task[]> = writable([]);
  22 | 	let nestedTasks: Task[] = [];
  23 | 	const loading: Writable<boolean> = writable(true);
  24 | 	const error: Writable<string | null> = writable(null);
  25 | 	let featureId: string | null = null;
  26 | 	let features: string[] = [];
  27 | 	let loadingFeatures = true;
  28 | 	let ws: WebSocket | null = null;
  29 | 	let wsStatus: 'connecting' | 'connected' | 'disconnected' = 'disconnected';
  30 | 
  31 | 	// Question modal state
  32 | 	let showQuestionModal = false;
  33 | 	let questionData: ShowQuestionPayload | null = null;
  34 | 	let selectedOption = '';
  35 | 	let userResponse = '';
  36 | 
  37 | 	// Task form modal state
  38 | 	let showTaskFormModal = false;
  39 | 	let editingTask: Task | null = null;
  40 | 	let isEditing = false;
  41 | 
  42 | 	let waitingOnLLM = false;
  43 | 
  44 | 	let showImportModal = false;
  45 | 
  46 | 	// Reactive statement to update nestedTasks when tasks store changes
  47 | 	$: {
  48 | 		const taskMap = new Map<string, Task & { children: Task[] }>();
  49 | 		const rootTasks: Task[] = [];
  50 | 
  51 | 		// Use the tasks from the store ($tasks)
  52 | 		$tasks.forEach(task => {
  53 | 			// Ensure the task object has the correct type including children array
  54 | 			const taskWithChildren: Task & { children: Task[] } = {
  55 | 				...task,
  56 | 				children: []
  57 | 			};
  58 | 			taskMap.set(task.id, taskWithChildren);
  59 | 		});
  60 | 
  61 | 		$tasks.forEach(task => {
  62 | 			const currentTask = taskMap.get(task.id)!; // Should always exist
  63 | 			if (task.parentTaskId && taskMap.has(task.parentTaskId)) {
  64 | 				taskMap.get(task.parentTaskId)!.children.push(currentTask);
  65 | 			} else {
  66 | 				rootTasks.push(currentTask);
  67 | 			}
  68 | 		});
  69 | 
  70 | 		nestedTasks = rootTasks;
  71 | 	}
  72 | 
  73 | 	// --- WebSocket Functions ---
  74 | 	function connectWebSocket() {
  75 | 		// Construct WebSocket URL (ws:// or wss:// based on protocol)
  76 | 		const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  77 | 		const wsUrl = `${protocol}//${window.location.host}`;
  78 | 		
  79 | 		console.log(`[WS Client] Attempting to connect to ${wsUrl}...`);
  80 | 		wsStatus = 'connecting';
  81 | 		ws = new WebSocket(wsUrl);
  82 | 
  83 | 		ws.onopen = () => {
  84 | 			console.log('[WS Client] WebSocket connection established.');
  85 | 			wsStatus = 'connected';
  86 | 			// Register this client for the current feature
  87 | 			if (featureId && ws) {
  88 | 				sendWsMessage({ 
  89 | 					type: 'client_registration', 
  90 | 					featureId: featureId,
  91 | 					payload: { featureId: featureId, clientId: `browser-${Date.now()}` } // Basic client ID
  92 | 				});
  93 | 			}
  94 | 		};
  95 | 
  96 | 		ws.onmessage = (event) => {
  97 | 			try {
  98 | 				const message: WebSocketMessage = JSON.parse(event.data);
  99 | 				console.log('[WS Client] Received message:', message);
 100 | 
 101 | 				// Check if the message is for the currently viewed feature
 102 | 				if (message.featureId && message.featureId !== featureId) {
 103 | 					console.log('[WS Client] Ignoring message for different feature:', message.featureId);
 104 | 					return;
 105 | 				}
 106 | 
 107 | 				switch (message.type) {
 108 | 					case 'tasks_updated':
 109 | 						waitingOnLLM = false;
 110 | 						console.log(`[WS Client] Received tasks_updated for feature ${featureId}:`, message.payload.tasks);
 111 | 						if (message.payload?.tasks && Array.isArray(message.payload.tasks) && featureId) {
 112 | 							// Map incoming tasks using the helper function to ensure consistency
 113 | 							const mappedTasks = message.payload.tasks.map((task: any) => 
 114 | 								mapApiTaskToClientTask(task, featureId as string)
 115 | 							);
 116 | 							tasks.set(mappedTasks);
 117 | 							// Detailed logging after update
 118 | 							tasks.subscribe(currentTasks => {
 119 | 								console.log('[WS Client] tasks store after set():', currentTasks);
 120 | 							})();
 121 | 							// Explicitly set loading to false
 122 | 							loading.set(false);
 123 | 							error.set(null); // Clear any previous errors
 124 | 						} else {
 125 | 							console.warn('[WS Client] Invalid or missing tasks payload for tasks_updated message.');
 126 | 							// Optionally handle this case, e.g., set an error or leave tasks unchanged
 127 | 						}
 128 | 						break;
 129 | 					case 'status_changed':
 130 | 						console.log(`[WS Client] Received status_changed for task ${message.payload?.taskId}`);
 131 | 						if (message.payload?.taskId && message.payload?.status) {
 132 | 							// Map incoming status string to TaskStatus enum
 133 | 							let newStatus: TaskStatus;
 134 | 							switch (message.payload.status) {
 135 | 								case 'completed': newStatus = TaskStatus.COMPLETED; break;
 136 | 								case 'in_progress': newStatus = TaskStatus.IN_PROGRESS; break;
 137 | 								case 'decomposed': newStatus = TaskStatus.DECOMPOSED; break;
 138 | 								default: newStatus = TaskStatus.PENDING; break;
 139 | 							}
 140 | 							
 141 | 							tasks.update(currentTasks =>
 142 | 								currentTasks.map(task =>
 143 | 									task.id === message.payload.taskId
 144 | 										? { 
 145 | 											...task, 
 146 | 											status: newStatus, 
 147 | 											// Completed is true ONLY if status is COMPLETED
 148 | 											completed: newStatus === TaskStatus.COMPLETED 
 149 | 										  }
 150 | 										: task
 151 | 								)
 152 | 							);
 153 | 							// Status change doesn't imply general loading state change
 154 | 						}
 155 | 						break;
 156 | 					case 'show_question':
 157 | 						waitingOnLLM = false;
 158 | 						console.log('[WS Client] Received clarification question:', message.payload);
 159 | 						// Store question data and show modal
 160 | 						questionData = message.payload as ShowQuestionPayload;
 161 | 						showQuestionModal = true;
 162 | 						// When question arrives, we should stop loading indicator
 163 | 						loading.set(false);
 164 | 						break;
 165 | 					case 'error':
 166 | 						waitingOnLLM = false;
 167 | 						console.error('[WS Client] Received error message:', message.payload);
 168 | 						// Display user-facing error
 169 | 						error.set(message.payload?.message || 'Received error from server.');
 170 | 						// Error likely means loading is done (with an error)
 171 | 						loading.set(false);
 172 | 						break;
 173 | 					case 'task_created':
 174 | 						console.log('[WS Client] Received task_created:', message.payload);
 175 | 						if (message.payload?.task) {
 176 | 							// Map incoming task to our Task type
 177 | 							const newTask = mapApiTaskToClientTask(message.payload.task, message.featureId || featureId || '');
 178 | 							// Add the new task to the store
 179 | 							tasks.update(currentTasks => [...currentTasks, newTask]);
 180 | 							// Process nested structure
 181 | 							processNestedTasks();
 182 | 						}
 183 | 						break;
 184 | 					case 'task_updated':
 185 | 						console.log('[WS Client] Received task_updated:', message.payload);
 186 | 						if (message.payload?.task) {
 187 | 							// Map incoming task to our Task type
 188 | 							const updatedTask = mapApiTaskToClientTask(message.payload.task, message.featureId || featureId || '');
 189 | 							// Update the existing task in the store
 190 | 							tasks.update(currentTasks =>
 191 | 								currentTasks.map(task =>
 192 | 									task.id === updatedTask.id ? updatedTask : task
 193 | 								)
 194 | 							);
 195 | 							// Process nested structure
 196 | 							processNestedTasks();
 197 | 						}
 198 | 						break;
 199 | 					case 'task_deleted':
 200 | 						console.log('[WS Client] Received task_deleted:', message.payload);
 201 | 						if (message.payload?.taskId) {
 202 | 							// Remove the task from the store
 203 | 							tasks.update(currentTasks =>
 204 | 								currentTasks.filter(task => task.id !== message.payload.taskId)
 205 | 							);
 206 | 							// Process nested structure
 207 | 							processNestedTasks();
 208 | 						}
 209 | 						break;
 210 | 					case 'connection_established':
 211 | 						console.log('[WS Client] Server confirmed connection.');
 212 | 						break;
 213 | 					case 'client_registration':
 214 | 						console.log('[WS Client] Server confirmed registration:', message.payload);
 215 | 						break;
 216 | 					// Add other message type handlers if needed
 217 | 				}
 218 | 			} catch (e) {
 219 | 				console.error('[WS Client] Error processing message:', e);
 220 | 				loading.set(false); // Ensure loading is set to false on error
 221 | 			}
 222 | 		};
 223 | 
 224 | 		ws.onerror = (error) => {
 225 | 			console.error('[WS Client] WebSocket error:', error);
 226 | 			wsStatus = 'disconnected';
 227 | 			loading.set(false); // Ensure loading is false on WebSocket error
 228 | 		};
 229 | 
 230 | 		ws.onclose = (event) => {
 231 | 			console.log(`[WS Client] WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`);
 232 | 			wsStatus = 'disconnected';
 233 | 			ws = null;
 234 | 			// Ensure loading is false when WebSocket disconnects
 235 | 			loading.set(false);
 236 | 		};
 237 | 	}
 238 | 
 239 | 	function sendWsMessage(message: WebSocketMessage) {
 240 | 		if (ws && ws.readyState === WebSocket.OPEN) {
 241 | 			try {
 242 | 				ws.send(JSON.stringify(message));
 243 | 				console.log('[WS Client] Sent message:', message);
 244 | 			} catch (e) {
 245 | 				console.error('[WS Client] Error sending message:', e);
 246 | 			}
 247 | 		} else {
 248 | 			console.warn('[WS Client] Cannot send message, WebSocket not open.');
 249 | 		}
 250 | 	}
 251 | 
 252 | 	// --- Component Lifecycle & Data Fetching ---
 253 | 
 254 | 	async function fetchTasks(selectedFeatureId?: string) {
 255 | 		loading.set(true);
 256 | 		error.set(null);
 257 | 		
 258 | 		try {
 259 | 			// Construct the API endpoint based on whether we have a featureId
 260 | 			const endpoint = selectedFeatureId 
 261 | 				? `/api/tasks/${selectedFeatureId}` 
 262 | 				: '/api/tasks';
 263 | 			
 264 | 			const response = await fetch(endpoint);
 265 | 			if (!response.ok) {
 266 | 				throw new Error(`Failed to fetch tasks: ${response.statusText}`);
 267 | 			}
 268 | 			
 269 | 			const data = await response.json();
 270 | 			
 271 | 			// Convert API response to our Task type
 272 | 			const mappedData = data.map((task: any) => {
 273 | 				// Map incoming status string to TaskStatus enum
 274 | 				let status: TaskStatus;
 275 | 				switch (task.status) {
 276 | 					case 'completed': status = TaskStatus.COMPLETED; break;
 277 | 					case 'in_progress': status = TaskStatus.IN_PROGRESS; break;
 278 | 					case 'decomposed': status = TaskStatus.DECOMPOSED; break;
 279 | 					default: status = TaskStatus.PENDING; break;
 280 | 				}
 281 | 				
 282 | 				// Ensure effort is one of our enum values
 283 | 				let effort: TaskEffort = TaskEffort.MEDIUM; // Default
 284 | 				if (task.effort === 'low') {
 285 | 					effort = TaskEffort.LOW;
 286 | 				} else if (task.effort === 'high') {
 287 | 					effort = TaskEffort.HIGH;
 288 | 				}
 289 | 				
 290 | 				// Derive title from description if not present
 291 | 				const title = task.title || task.description;
 292 | 				
 293 | 				// Ensure completed flag is consistent with status
 294 | 				const completed = status === TaskStatus.COMPLETED;
 295 | 				
 296 | 				// Return the fully mapped task
 297 | 				return {
 298 | 					id: task.id,
 299 | 					title,
 300 | 					description: task.description,
 301 | 					status,
 302 | 					completed,
 303 | 					effort,
 304 | 					feature_id: task.feature_id || selectedFeatureId || undefined,
 305 | 					parentTaskId: task.parentTaskId,
 306 | 					createdAt: task.createdAt,
 307 | 					updatedAt: task.updatedAt,
 308 | 					fromReview: task.fromReview
 309 | 				} as Task;
 310 | 			});
 311 | 			
 312 | 			tasks.set(mappedData);
 313 | 			
 314 | 			if (mappedData.length === 0) {
 315 | 				error.set('No tasks found for this feature.');
 316 | 			}
 317 | 		} catch (err) {
 318 | 			error.set(err instanceof Error ? err.message : 'An error occurred');
 319 | 			// Add more detailed logging
 320 | 			console.error('[fetchTasks] Error fetching tasks:', err);
 321 | 			if (err instanceof Error && err.cause) {
 322 | 				console.error('[fetchTasks] Error Cause:', err.cause);
 323 | 			}
 324 | 			tasks.set([]); // Clear any previous tasks
 325 | 		} finally {
 326 | 			// Always reset loading state when fetch completes
 327 | 			loading.set(false);
 328 | 		}
 329 | 	}
 330 | 
 331 | 	async function fetchFeatures() {
 332 | 		loadingFeatures = true;
 333 | 		try {
 334 | 			const response = await fetch('/api/features');
 335 | 			if (!response.ok) {
 336 | 				throw new Error('Failed to fetch features');
 337 | 			}
 338 | 			features = await response.json();
 339 | 		} catch (err) {
 340 | 			console.error('Error fetching features:', err);
 341 | 			features = [];
 342 | 		} finally {
 343 | 			loadingFeatures = false;
 344 | 		}
 345 | 	}
 346 | 
 347 | 	// New function to fetch pending question
 348 | 	async function fetchPendingQuestion(id: string): Promise<ShowQuestionPayload | null> {
 349 | 		console.log(`[Pending Question] Checking for feature ${id}...`);
 350 | 		try {
 351 | 			const response = await fetch(`/api/features/${id}/pending-question`);
 352 | 			if (!response.ok) {
 353 | 				throw new Error(`HTTP error! status: ${response.status}`);
 354 | 			}
 355 | 			const data: ShowQuestionPayload | null = await response.json();
 356 | 			if (data) {
 357 | 				console.log('[Pending Question] Found pending question:', data);
 358 | 				return data;
 359 | 			} else {
 360 | 				console.log('[Pending Question] No pending question found.');
 361 | 				return null;
 362 | 			}
 363 | 		} catch (err) {
 364 | 			console.error('[Pending Question] Error fetching pending question:', err);
 365 | 			error.set(err instanceof Error ? `Error checking for pending question: ${err.message}` : 'An error occurred while checking for pending questions.');
 366 | 			return null;
 367 | 		}
 368 | 	}
 369 | 
 370 | 	function processNestedTasks() {
 371 | 		// Define the type for map values explicitly
 372 | 		type TaskWithChildren = Task & { children: Task[] };
 373 | 
 374 | 		const taskMap = new Map<string, TaskWithChildren>(
 375 | 			$tasks.map(task => [task.id, { ...task, children: [] }])
 376 | 		);
 377 | 		const rootTasks: Task[] = [];
 378 | 
 379 | 		taskMap.forEach((task: TaskWithChildren) => { 
 380 | 			if (task.parentTaskId && taskMap.has(task.parentTaskId)) {
 381 | 				const parent = taskMap.get(task.parentTaskId);
 382 | 				if (parent) {
 383 | 					parent.children.push(task);
 384 | 				} else {
 385 | 					rootTasks.push(task);
 386 | 				}
 387 | 			} else {
 388 | 				rootTasks.push(task);
 389 | 			}
 390 | 		});
 391 | 
 392 | 		// Optional: Sort root tasks or children if needed
 393 | 		// rootTasks.sort(...); 
 394 | 		// taskMap.forEach(task => task.children.sort(...));
 395 | 
 396 | 		nestedTasks = rootTasks;
 397 | 	}
 398 | 
 399 | 	async function addTask(taskData: { title: string; effort: string; featureId: string, description?: string }) {
 400 | 		try {
 401 | 			const response = await fetch('/api/tasks', {
 402 | 				method: 'POST',
 403 | 				headers: {
 404 | 					'Content-Type': 'application/json'
 405 | 				},
 406 | 				body: JSON.stringify({
 407 | 					...taskData,
 408 | 					description: taskData.description || taskData.title // Use provided description, or title if none
 409 | 				})
 410 | 			});
 411 | 
 412 | 			if (!response.ok) {
 413 | 				throw new Error(`Failed to create task: ${response.statusText}`);
 414 | 			}
 415 | 
 416 | 			const newTask = await response.json();
 417 | 			console.log('[Task] New task created:', newTask);
 418 | 
 419 | 			// Refresh the tasks list
 420 | 			await fetchTasks(taskData.featureId);
 421 | 			
 422 | 			// Clear any errors that might have been shown
 423 | 			error.set(null);
 424 | 		} catch (err) {
 425 | 			console.error('[Task] Error creating task:', err);
 426 | 			error.set(err instanceof Error ? err.message : 'Failed to create task');
 427 | 		}
 428 | 	}
 429 | 
 430 | 	function handleTaskFormSubmit(event: CustomEvent) {
 431 | 		const taskData = event.detail;
 432 | 		if (isEditing && editingTask) {
 433 | 			updateTask(editingTask.id, taskData);
 434 | 		} else {
 435 | 			addTask(taskData);
 436 | 		}
 437 | 		showTaskFormModal = false;
 438 | 		isEditing = false;
 439 | 		editingTask = null;
 440 | 	}
 441 | 
 442 | 	function openEditTaskModal(task: Task) {
 443 | 		editingTask = task;
 444 | 		isEditing = true;
 445 | 		showTaskFormModal = true;
 446 | 	}
 447 | 
 448 | 	async function updateTask(taskId: string, taskData: { title: string; effort: string; featureId: string }) {
 449 | 		try {
 450 | 			const response = await fetch(`/api/tasks/${taskId}`, {
 451 | 				method: 'PUT',
 452 | 				headers: {
 453 | 					'Content-Type': 'application/json'
 454 | 				},
 455 | 				body: JSON.stringify({
 456 | 					...taskData,
 457 | 					description: taskData.title, // Use title as description
 458 | 					featureId: taskData.featureId
 459 | 				})
 460 | 			});
 461 | 
 462 | 			if (!response.ok) {
 463 | 				throw new Error(`Failed to update task: ${response.statusText}`);
 464 | 			}
 465 | 
 466 | 			const updatedTask = await response.json();
 467 | 			console.log('[Task] Task updated:', updatedTask);
 468 | 
 469 | 			// Refresh the tasks list
 470 | 			await fetchTasks(taskData.featureId);
 471 | 			
 472 | 			// Clear any errors that might have been shown
 473 | 			error.set(null);
 474 | 		} catch (err) {
 475 | 			console.error('[Task] Error updating task:', err);
 476 | 			error.set(err instanceof Error ? err.message : 'Failed to update task');
 477 | 		}
 478 | 	}
 479 | 
 480 | 	async function deleteTask(taskId: string, featureId: string) {
 481 | 		if (!confirm('Are you sure you want to delete this task?')) {
 482 | 			return;
 483 | 		}
 484 | 		
 485 | 		try {
 486 | 			const response = await fetch(`/api/tasks/${taskId}?featureId=${featureId}`, {
 487 | 				method: 'DELETE'
 488 | 			});
 489 | 
 490 | 			if (!response.ok) {
 491 | 				throw new Error(`Failed to delete task: ${response.statusText}`);
 492 | 			}
 493 | 
 494 | 			console.log('[Task] Task deleted:', taskId);
 495 | 
 496 | 			// Refresh the tasks list
 497 | 			await fetchTasks(featureId);
 498 | 			
 499 | 			// Clear any errors that might have been shown
 500 | 			error.set(null);
 501 | 		} catch (err) {
 502 | 			console.error('[Task] Error deleting task:', err);
 503 | 			error.set(err instanceof Error ? err.message : 'Failed to delete task');
 504 | 		}
 505 | 	}
 506 | 
 507 | 	onMount(async () => {
 508 | 		loading.set(true); // Set loading true at the start
 509 | 		error.set(null); // Reset error
 510 | 
 511 | 		// Extract featureId from URL query parameters
 512 | 		featureId = $page.url.searchParams.get('featureId');
 513 | 		
 514 | 		// Fetch available features first
 515 | 		await fetchFeatures();
 516 | 		
 517 | 		// Determine the featureId to use (from URL or latest)
 518 | 		if (!featureId && features.length > 0) {
 519 | 			// Attempt to fetch default tasks to find the latest featureId
 520 | 			await fetchTasks(); 
 521 | 			if ($tasks.length > 0 && $tasks[0]?.feature_id) {
 522 | 				featureId = $tasks[0].feature_id;
 523 | 				console.log(`[onMount] Determined latest featureId: ${featureId}`);
 524 | 			} else {
 525 | 				console.log('[onMount] Could not determine latest featureId from default tasks.');
 526 | 				// If no featureId determined, use the first from the list if available
 527 | 				if (features.length > 0) {
 528 | 					featureId = features[0];
 529 | 					console.log(`[onMount] Using first available featureId: ${featureId}`);
 530 | 				}
 531 | 			}
 532 | 		}
 533 | 		
 534 | 		// Now, if we have a featureId, check for pending questions and fetch tasks
 535 | 		if (featureId) {
 536 | 			console.log(`[onMount] Operating with featureId: ${featureId}`);
 537 | 			// Check for pending question first
 538 | 			const pendingQuestion = await fetchPendingQuestion(featureId);
 539 | 			if (pendingQuestion) {
 540 | 				questionData = pendingQuestion;
 541 | 				showQuestionModal = true;
 542 | 				// Still fetch tasks even if question is shown, they might exist
 543 | 				await fetchTasks(featureId);
 544 | 			} else {
 545 | 				// No pending question, just fetch tasks
 546 | 				await fetchTasks(featureId);
 547 | 			}
 548 | 		} else {
 549 | 			// No featureId could be determined
 550 | 			console.log('[onMount] No featureId available.');
 551 | 			if (!$error) { // Only set error if fetchTasks didn't already set one
 552 | 				error.set('No features found. Create a feature first using the task manager CLI.');
 553 | 			}
 554 | 			tasks.set([]); // Ensure tasks are empty
 555 | 			nestedTasks = [];
 556 | 		}
 557 | 
 558 | 		// Connect WebSocket AFTER initial data load and featureId determination
 559 | 		if (featureId) {
 560 | 			connectWebSocket();
 561 | 		}
 562 | 	});
 563 | 
 564 | 	onDestroy(() => {
 565 | 		// Clean up WebSocket connection
 566 | 		if (ws) {
 567 | 			console.log('[WS Client] Closing WebSocket connection.');
 568 | 			ws.close();
 569 | 			ws = null;
 570 | 		}
 571 | 	});
 572 | 
 573 | 	async function toggleTaskStatus(taskId: string) {
 574 | 		const tasksList = $tasks;
 575 | 		const taskIndex = tasksList.findIndex((t) => t.id === taskId);
 576 | 		if (taskIndex !== -1) {
 577 | 			const task = tasksList[taskIndex];
 578 | 			const newStatus = task.status === TaskStatus.COMPLETED ? TaskStatus.PENDING : TaskStatus.COMPLETED;
 579 | 			try {
 580 | 				// Make API call to update status in backend
 581 | 				const response = await fetch(`/api/tasks/${taskId}`, {
 582 | 					method: 'PUT',
 583 | 					headers: { 'Content-Type': 'application/json' },
 584 | 					body: JSON.stringify({
 585 | 						featureId: task.feature_id,
 586 | 						status: newStatus,
 587 | 						completed: newStatus === TaskStatus.COMPLETED
 588 | 					})
 589 | 				});
 590 | 				if (!response.ok) {
 591 | 					throw new Error('Failed to update task status');
 592 | 				}
 593 | 				// Optionally update local store or rely on WebSocket update
 594 | 			} catch (err) {
 595 | 				console.error('Failed to update task status:', err);
 596 | 				// Optionally show error to user
 597 | 			}
 598 | 		}
 599 | 	}
 600 | 
 601 | 	function getEffortBadgeVariant(effort: string) {
 602 | 		switch (effort) {
 603 | 			case TaskEffort.LOW:
 604 | 				return 'secondary';
 605 | 			case TaskEffort.MEDIUM:
 606 | 				return 'default';
 607 | 			case TaskEffort.HIGH:
 608 | 				return 'destructive';
 609 | 			default:
 610 | 				return 'outline';
 611 | 		}
 612 | 	}
 613 | 
 614 | 	function getStatusBadgeVariant(status: TaskStatus): 'default' | 'secondary' | 'destructive' | 'outline' {
 615 | 		switch (status) {
 616 | 			case TaskStatus.COMPLETED:
 617 | 				return 'secondary';
 618 | 			case TaskStatus.IN_PROGRESS:
 619 | 				return 'default';
 620 | 			case TaskStatus.DECOMPOSED:
 621 | 				return 'outline';
 622 | 			case TaskStatus.PENDING:
 623 | 			default:
 624 | 				return 'outline';
 625 | 		}
 626 | 	}
 627 | 
 628 | 	function refreshTasks() {
 629 | 		if ($loading) return;
 630 | 		console.log('[Task List] Refreshing tasks...');
 631 | 		fetchTasks(featureId || undefined);
 632 | 	}
 633 | 
 634 | 	function handleFeatureChange(selectedItem: Selected<string> | undefined) {
 635 | 		const newFeatureId = selectedItem?.value; // Safely get value
 636 | 		
 637 | 		if (newFeatureId && newFeatureId !== featureId) { 
 638 | 			featureId = newFeatureId;
 639 | 			
 640 | 			// Update URL 
 641 | 			const url = new URL(window.location.href);
 642 | 			url.searchParams.set('featureId', newFeatureId);
 643 | 			window.history.pushState({}, '', url);
 644 | 			
 645 | 			// Fetch tasks for the new feature
 646 | 			fetchTasks(newFeatureId);
 647 | 
 648 | 			// Re-register WebSocket for the new feature
 649 | 			if (ws && wsStatus === 'connected') {
 650 | 				sendWsMessage({ 
 651 | 					type: 'client_registration', 
 652 | 					featureId: featureId,
 653 | 					payload: { featureId: featureId, clientId: `browser-${Date.now()}` }
 654 | 				});
 655 | 			}
 656 | 		}
 657 | 	}
 658 | 
 659 | 	// Handle user response to clarification question
 660 | 	function handleQuestionResponse(event: SubmitEvent) {
 661 | 		event.preventDefault();
 662 | 		console.log('[WS Client] User responded to question. Selected:', selectedOption, 'Text:', userResponse);
 663 | 		
 664 | 		if (questionData && featureId) {
 665 | 			const response = selectedOption || userResponse;
 666 | 			
 667 | 			sendWsMessage({
 668 | 				type: 'question_response',
 669 | 				featureId,
 670 | 				payload: {
 671 | 					questionId: questionData.questionId,
 672 | 					response: response
 673 | 				} as QuestionResponsePayload
 674 | 			});
 675 | 			
 676 | 			showQuestionModal = false;
 677 | 			questionData = null;
 678 | 			waitingOnLLM = true;
 679 | 			
 680 | 			// Reset form fields
 681 | 			selectedOption = '';
 682 | 			userResponse = '';
 683 | 		}
 684 | 	}
 685 | 
 686 | 	// Handle user cancellation of question
 687 | 	function handleQuestionCancel() {
 688 | 		console.log('[WS Client] User cancelled question');
 689 | 		showQuestionModal = false;
 690 | 		questionData = null;
 691 | 	}
 692 | 
 693 | 	// ... reactive variables ...
 694 | 	// Filter out decomposed tasks from progress calculation
 695 | 	$: actionableTasks = $tasks.filter(t => t.status !== TaskStatus.DECOMPOSED);
 696 | 	$: completedCount = actionableTasks.filter(t => t.completed).length;
 697 | 	$: totalActionableTasks = actionableTasks.length;
 698 | 	$: progress = totalActionableTasks > 0 ? (completedCount / totalActionableTasks) * 100 : 0;
 699 | 	$: firstPendingTaskIndex = $tasks.findIndex(t => t.status === TaskStatus.PENDING);
 700 | 	$: selectedFeatureLabel = features.find(f => f === featureId) || 'Select Feature';
 701 | 
 702 | 	// Call processNestedTasks whenever the raw tasks array changes
 703 | 	$: {
 704 | 		if ($tasks) {
 705 | 			processNestedTasks();
 706 | 		}
 707 | 	}
 708 | 
 709 | 	// Helper function to map API task response to client Task type
 710 | 	function mapApiTaskToClientTask(apiTask: any, currentFeatureId: string): Task {
 711 | 		// Map incoming status string to TaskStatus enum
 712 | 		let status: TaskStatus;
 713 | 		switch (apiTask.status) {
 714 | 			case 'completed': status = TaskStatus.COMPLETED; break;
 715 | 			case 'in_progress': status = TaskStatus.IN_PROGRESS; break;
 716 | 			case 'decomposed': status = TaskStatus.DECOMPOSED; break;
 717 | 			default: status = TaskStatus.PENDING; break;
 718 | 		}
 719 | 		
 720 | 		// Ensure effort is one of our enum values
 721 | 		let effort: TaskEffort = TaskEffort.MEDIUM; // Default
 722 | 		if (apiTask.effort === 'low') {
 723 | 			effort = TaskEffort.LOW;
 724 | 		} else if (apiTask.effort === 'high') {
 725 | 			effort = TaskEffort.HIGH;
 726 | 		}
 727 | 		
 728 | 		// Derive title from description if not present
 729 | 		const title = apiTask.title || apiTask.description;
 730 | 		
 731 | 		// Ensure completed flag is consistent with status
 732 | 		const completed = status === TaskStatus.COMPLETED;
 733 | 		
 734 | 		// Return the fully mapped task
 735 | 		return {
 736 | 			id: apiTask.id,
 737 | 			title,
 738 | 			description: apiTask.description,
 739 | 			status,
 740 | 			completed,
 741 | 			effort,
 742 | 			feature_id: apiTask.feature_id || currentFeatureId,
 743 | 			parentTaskId: apiTask.parentTaskId,
 744 | 			createdAt: apiTask.createdAt,
 745 | 			updatedAt: apiTask.updatedAt,
 746 | 			fromReview: apiTask.fromReview
 747 | 		} as Task;
 748 | 	}
 749 | 
 750 | 	async function handleImportTasks(event: CustomEvent) {
 751 | 		const { tasks } = event.detail;
 752 | 		if (!Array.isArray(tasks)) return;
 753 | 		for (const t of tasks) {
 754 | 			await addTask({
 755 | 				title: t.title,
 756 | 				effort: t.effort,
 757 | 				featureId: featureId || '',
 758 | 				description: t.description 
 759 | 			});
 760 | 		}
 761 | 		showImportModal = false;
 762 | 	}
 763 | 
 764 | 	function handleCancelImport() {
 765 | 		showImportModal = false;
 766 | 	}
 767 | 
 768 | </script>
 769 | 
 770 | <div class="container mx-auto py-10 px-4 sm:px-6 lg:px-8 max-w-5xl">
 771 | 	<div class="flex justify-between items-center mb-8">
 772 | 		<h1 class="text-3xl font-bold tracking-tight text-foreground">Task Manager</h1>
 773 | 		{#if features.length > 0}
 774 | 			<div class="w-64">
 775 | 				<Select.Root 
 776 | 					onSelectedChange={handleFeatureChange} 
 777 | 					selected={featureId ? { value: featureId, label: featureId } : undefined}
 778 | 					disabled={loadingFeatures}
 779 | 				>
 780 | 					<Select.Trigger class="w-full">
 781 | 						{featureId ? featureId.substring(0, 8) + '...' : 'Select Feature'}
 782 | 					</Select.Trigger>
 783 | 					<Select.Content>
 784 | 						<Select.Group>
 785 | 							<Select.GroupHeading>Available Features</Select.GroupHeading>
 786 | 							{#each features as feature}
 787 | 								<Select.Item value={feature} label={feature}>{feature.substring(0, 8)}...</Select.Item>
 788 | 							{/each}
 789 | 						</Select.Group>
 790 | 					</Select.Content>
 791 | 				</Select.Root>
 792 | 			</div>
 793 | 		{/if}
 794 | 	</div>
 795 | 
 796 | 	{#if questionData}
 797 | 		<div class="flex flex-col items-center justify-center min-h-[300px]">
 798 | 			<div class="max-w-md w-full bg-background border border-border rounded-lg shadow-lg p-6">
 799 | 				<h2 class="text-xl font-semibold mb-4">Clarification Needed</h2>
 800 | 				<p class="text-foreground mb-5">{questionData.question}</p>
 801 | 				<form on:submit|preventDefault={handleQuestionResponse}>
 802 | 					{#if questionData.options && questionData.options.length > 0}
 803 | 						<div class="flex flex-col gap-3 mb-5">
 804 | 							{#each questionData.options as option}
 805 | 								<label class="flex items-center gap-2 p-3 border border-border rounded-md cursor-pointer hover:bg-muted transition-colors">
 806 | 									<input 
 807 | 										type="radio" 
 808 | 										name="option" 
 809 | 										value={option}
 810 | 										bind:group={selectedOption}
 811 | 										class="focus:ring-primary"
 812 | 									/>
 813 | 									<span class="text-foreground">{option}</span>
 814 | 								</label>
 815 | 							{/each}
 816 | 						</div>
 817 | 					{/if}
 818 | 					{#if questionData.allowsText !== false}
 819 | 						<div class="mb-5">
 820 | 							<label for="text-response" class="block mb-2 font-medium text-foreground">
 821 | 								{questionData.options && questionData.options.length > 0 ? 'Or provide a custom response:' : 'Your response:'}
 822 | 							</label>
 823 | 							<textarea 
 824 | 								id="text-response"
 825 | 								rows="3"
 826 | 								bind:value={userResponse}
 827 | 								placeholder="Type your response here..."
 828 | 								class="w-full p-3 border border-border rounded-md resize-y text-foreground bg-background focus:ring-primary focus:border-primary"
 829 | 							></textarea>
 830 | 						</div>
 831 | 					{/if}
 832 | 					<div class="flex justify-end gap-3 pt-2">
 833 | 						<button 
 834 | 							type="submit" 
 835 | 							class="bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md font-medium text-sm disabled:opacity-50"
 836 | 							disabled={!userResponse && (!questionData.options || !selectedOption)}
 837 | 						>
 838 | 							Submit Response
 839 | 						</button>
 840 | 					</div>
 841 | 				</form>
 842 | 			</div>
 843 | 		</div>
 844 | 	{:else if waitingOnLLM}
 845 | 		<div class="flex flex-col items-center justify-center min-h-[300px]">
 846 | 			<Loader2 class="h-12 w-12 animate-spin text-primary mb-4" />
 847 | 			<p class="text-lg text-muted-foreground">Waiting on LLM to plan after clarification...</p>
 848 | 		</div>
 849 | 	{:else if $loading}
 850 | 		<div class="flex justify-center items-center h-64">
 851 | 			<Loader2 class="h-12 w-12 animate-spin text-primary" />
 852 | 		</div>
 853 | 	{:else if $error}
 854 | 		<Card class="mb-6 border-destructive">
 855 | 			<CardHeader>
 856 | 				<CardTitle class="text-destructive">Error Loading Tasks</CardTitle>
 857 | 				<CardDescription class="text-destructive/90">{$error}</CardDescription>
 858 | 			</CardHeader>
 859 | 		</Card>
 860 | 	{:else}
 861 | 		<Card class="shadow-lg">
 862 | 			<CardHeader class="border-b border-border px-6 py-4">
 863 | 				<CardTitle class="text-xl font-semibold flex justify-between items-center">
 864 | 					<span class="flex-1">Tasks</span>
 865 | 					<div class="flex justify-between items-center gap-4 items-center">
 866 | 						<Badge variant="secondary">{$tasks.length}</Badge>
 867 | 						<Button on:click={() => showImportModal = true}>Import Tasks</Button>
 868 | 					</div>
 869 | 				</CardTitle>
 870 | 				<CardDescription class="mt-1">
 871 | 					Manage your tasks and track progress for the selected feature.
 872 | 				</CardDescription>
 873 | 				<div class="pt-4">
 874 | 					<Progress 
 875 | 						value={progress} 
 876 | 						class="w-full h-2 [&>div]:bg-green-500 [&>div]:transition-all [&>div]:duration-300 [&>div]:ease-in-out"
 877 | 					/>
 878 | 				</div>
 879 | 			</CardHeader>
 880 | 			<CardContent class="p-0">
 881 | 				<div class="divide-y divide-border">
 882 | 					{#each nestedTasks as task (task.id)}
 883 | 						{@const taskIndexInFlatList = $tasks.findIndex(t => t.id === task.id)}
 884 | 						{@const isNextPending = taskIndexInFlatList === firstPendingTaskIndex}
 885 | 						{@const isInProgress = task.status === TaskStatus.IN_PROGRESS}
 886 | 						{@const areAllChildrenComplete = task.children && task.children.length > 0 && task.children.every(c => c.status === TaskStatus.COMPLETED)}
 887 | 						<div 
 888 | 							transition:fade={{ duration: 200 }}
 889 | 							class="task-row flex items-start space-x-4 p-4 hover:bg-muted/50 transition-colors 
 890 | 								   {isNextPending ? 'bg-muted/30' : ''} 
 891 | 								   {isInProgress ? 'in-progress-shine relative overflow-hidden' : ''}
 892 | 								   {(task.status === TaskStatus.COMPLETED || (task.status === TaskStatus.DECOMPOSED && areAllChildrenComplete)) ? 'opacity-60' : ''}
 893 | 								   {task.fromReview ? 'from-review-task' : ''}"
 894 | 						>
 895 | 							{#if task.status === TaskStatus.DECOMPOSED}
 896 | 								<div class="flex items-center justify-center h-6 w-6 mt-1 text-muted-foreground">
 897 | 									<CornerDownRight class="h-4 w-4" />
 898 | 								</div>
 899 | 							{:else}
 900 | 								<div class="flex flex-col items-center gap-1">
 901 | 									<Checkbox 
 902 | 										id={`task-${task.id}`} 
 903 | 										checked={task.completed} 
 904 | 										onCheckedChange={() => toggleTaskStatus(task.id)} 
 905 | 										aria-labelledby={`task-label-${task.id}`}
 906 | 										disabled={task.status === TaskStatus.IN_PROGRESS}
 907 | 									/>
 908 | 									{#if task.fromReview}
 909 | 										<span class="review-indicator" title="Task from review">
 910 | 											<Eye size={20} />
 911 | 										</span>
 912 | 									{/if}
 913 | 								</div>
 914 | 							{/if}
 915 | 							<div class="flex-1 grid gap-1">
 916 | 								<div class="flex items-center gap-2">
 917 | 									<label 
 918 | 										for={`task-${task.id}`} 
 919 | 										id={`task-label-${task.id}`}
 920 | 										class={`font-medium cursor-pointer ${(task.status === TaskStatus.COMPLETED || (task.status === TaskStatus.DECOMPOSED && areAllChildrenComplete)) ? 'line-through text-muted-foreground' : ''}`}
 921 | 									>
 922 | 										{task.title}
 923 | 									</label>
 924 | 								</div>
 925 | 								{#if task.description && task.description !== task.title}
 926 | 									<p class="text-sm text-muted-foreground">
 927 | 										{task.description}
 928 | 									</p>
 929 | 								{/if}
 930 | 							</div>
 931 | 							<div class="flex flex-col gap-1.5 items-end min-w-[100px]">
 932 | 								<div class="flex items-center gap-1.5">
 933 | 									<Badge variant={getStatusBadgeVariant(task.status)} class="capitalize">
 934 | 										{task.status.replace('_', ' ')}
 935 | 									</Badge>
 936 | 								</div>
 937 | 								{#if task.effort}
 938 | 									<Badge variant={getEffortBadgeVariant(task.effort)} class="capitalize">
 939 | 										{task.effort}
 940 | 									</Badge>
 941 | 								{/if}
 942 | 							</div>
 943 | 							<div class="flex gap-1 ml-4">
 944 | 								<button
 945 | 									class="text-muted-foreground hover:text-foreground p-1 rounded-sm hover:bg-muted transition-colors"
 946 | 									title="Edit task"
 947 | 									on:click|stopPropagation={() => openEditTaskModal(task)}
 948 | 								>
 949 | 									<Pencil size={16} />
 950 | 								</button>
 951 | 								<button
 952 | 									class="text-muted-foreground hover:text-destructive p-1 rounded-sm hover:bg-muted transition-colors"
 953 | 									title="Delete task"
 954 | 									on:click|stopPropagation={() => deleteTask(task.id, featureId || '')}
 955 | 								>
 956 | 									<Trash2 size={16} />
 957 | 								</button>
 958 | 							</div>
 959 | 						</div>
 960 | 						{#if task.children && task.children.length > 0}
 961 | 							<div class="ml-10 pl-4 py-2 border-l border-border divide-y divide-border">
 962 | 								{#each task.children as childTask (childTask.id)}
 963 | 									{@const childTaskIndexInFlatList = $tasks.findIndex(t => t.id === childTask.id)}
 964 | 									{@const isChildNextPending = childTaskIndexInFlatList === firstPendingTaskIndex}
 965 | 									{@const isChildInProgress = childTask.status === TaskStatus.IN_PROGRESS}
 966 | 									<div 
 967 | 										transition:fade={{ duration: 200 }}
 968 | 										class="task-row flex items-start space-x-4 pt-3 pr-4 mb-3 
 969 | 											   {isChildNextPending ? 'bg-muted/30' : ''} 
 970 | 											   {isChildInProgress ? 'in-progress-shine relative overflow-hidden' : ''}
 971 | 											   {childTask.status === TaskStatus.COMPLETED ? 'opacity-60' : ''}
 972 | 											   {childTask.fromReview ? 'from-review-task' : ''}"
 973 | 									>
 974 | 										{#if childTask.status === TaskStatus.DECOMPOSED}
 975 | 											<div class="flex items-center justify-center h-6 w-6 mt-1 text-muted-foreground">
 976 | 												<CornerDownRight class="h-4 w-4" />
 977 | 											</div>
 978 | 										{:else}
 979 | 											<div class="flex flex-col items-center gap-1">
 980 | 												<Checkbox 
 981 | 													id={`task-${childTask.id}`} 
 982 | 													checked={childTask.completed} 
 983 | 													onCheckedChange={() => toggleTaskStatus(childTask.id)} 
 984 | 													aria-labelledby={`task-label-${childTask.id}`}
 985 | 													disabled={childTask.status === TaskStatus.IN_PROGRESS}
 986 | 												/>
 987 | 												{#if childTask.fromReview}
 988 | 													<span class="review-indicator" title="Task from review">
 989 | 														<Eye size={20} />
 990 | 													</span>
 991 | 												{/if}
 992 | 											</div>
 993 | 										{/if}
 994 | 										<div class="flex-1 grid gap-1">
 995 | 											<div class="flex items-center gap-2">
 996 | 												<label 
 997 | 													for={`task-${childTask.id}`} 
 998 | 													id={`task-label-${childTask.id}`}
 999 | 													class={`font-medium cursor-pointer ${childTask.status === TaskStatus.COMPLETED ? 'line-through text-muted-foreground' : ''}`}
1000 | 												>
1001 | 													{childTask.title}
1002 | 												</label>
1003 | 											</div>
1004 | 											{#if childTask.description && childTask.description !== childTask.title}
1005 | 												<p class="text-sm text-muted-foreground">
1006 | 													{childTask.description}
1007 | 												</p>
1008 | 											{/if}
1009 | 										</div>
1010 | 										<div class="flex flex-col gap-1.5 items-end min-w-[100px]">
1011 | 											<div class="flex items-center gap-1.5">
1012 | 												<Badge variant={getStatusBadgeVariant(childTask.status)} class="capitalize">
1013 | 													{childTask.status.replace('_', ' ')}
1014 | 												</Badge>
1015 | 											</div>
1016 | 											{#if childTask.effort}
1017 | 												<Badge variant={getEffortBadgeVariant(childTask.effort)} class="capitalize">
1018 | 													{childTask.effort}
1019 | 												</Badge>
1020 | 											{/if}
1021 | 										</div>
1022 | 										<div class="flex gap-1 ml-4">
1023 | 											<button
1024 | 												class="text-muted-foreground hover:text-foreground p-1 rounded-sm hover:bg-muted transition-colors"
1025 | 												title="Edit subtask"
1026 | 												on:click|stopPropagation={() => openEditTaskModal(childTask)}
1027 | 											>
1028 | 												<Pencil size={16} />
1029 | 											</button>
1030 | 											<button
1031 | 												class="text-muted-foreground hover:text-destructive p-1 rounded-sm hover:bg-muted transition-colors"
1032 | 												title="Delete subtask"
1033 | 												on:click|stopPropagation={() => deleteTask(childTask.id, featureId || '')}
1034 | 											>
1035 | 												<Trash2 size={16} />
1036 | 											</button>
1037 | 										</div>
1038 | 									</div>
1039 | 								{/each}
1040 | 							</div>
1041 | 						{/if}
1042 | 					{:else}
1043 | 						<div class="text-center py-8 text-muted-foreground">
1044 | 							No tasks found for this feature.
1045 | 						</div>
1046 | 					{/each}
1047 | 				</div>
1048 | 			</CardContent>
1049 | 			<CardFooter class="flex flex-col items-start gap-4 px-6 py-4 border-t border-border">
1050 | 				<div class="w-full flex justify-between items-center">
1051 | 					<span class="text-sm text-muted-foreground">
1052 | 						{completedCount} of {totalActionableTasks} actionable tasks completed
1053 | 					</span>
1054 | 					<div class="flex gap-2">
1055 | 						<Button variant="outline" size="sm" on:click={() => showTaskFormModal = true} disabled={!featureId}>
1056 | 							Add Task
1057 | 						</Button>
1058 | 						<Button variant="outline" size="sm" on:click={refreshTasks} disabled={$loading}>
1059 | 							{#if $loading}
1060 | 								<Loader2 class="mr-2 h-4 w-4 animate-spin" />
1061 | 							{/if}
1062 | 							Refresh
1063 | 						</Button>
1064 | 					</div>
1065 | 				</div>
1066 | 			</CardFooter>
1067 | 		</Card>
1068 | 	{/if}
1069 | 
1070 | 	{#if featureId}
1071 | 		<TaskFormModal
1072 | 			open={showTaskFormModal}
1073 | 			featureId={featureId}
1074 | 			isEditing={isEditing}
1075 | 			editTask={editingTask ? {
1076 | 				id: editingTask.id,
1077 | 				title: editingTask.title || '',
1078 | 				effort: editingTask.effort || 'medium'
1079 | 			} : {
1080 | 				id: '',
1081 | 				title: '',
1082 | 				effort: 'medium'
1083 | 			}}
1084 | 			on:submit={handleTaskFormSubmit}
1085 | 			on:cancel={() => showTaskFormModal = false}
1086 | 		/>
1087 | 	{/if}
1088 | 
1089 | 	
1090 | 	<ImportTasksModal
1091 | 		bind:open={showImportModal}
1092 | 		on:import={handleImportTasks}
1093 | 		on:cancel={handleCancelImport}
1094 | 	/>
1095 | </div>
1096 | 
1097 | <style>
1098 | 	.in-progress-shine::before {
1099 | 		content: '';
1100 | 		position: absolute;
1101 | 		top: 0;
1102 | 		left: -100%; /* Start off-screen */
1103 | 		width: 75%; /* Width of the shine */
1104 | 		height: 100%;
1105 | 		background: linear-gradient(
1106 | 			100deg,
1107 | 			rgba(255, 255, 255, 0) 0%,
1108 | 			rgba(255, 255, 255, 0.15) 50%, /* Subtle white shine */
1109 | 			rgba(255, 255, 255, 0) 100%
1110 | 		);
1111 | 		transform: skewX(-25deg); /* Angle the shine */
1112 | 		animation: shine 2.5s infinite linear; /* Animation properties */
1113 | 		opacity: 0.8;
1114 | 	}
1115 | 
1116 | 	@keyframes shine {
1117 | 		0% {
1118 | 			left: -100%;
1119 | 		}
1120 | 		50%, 100% { /* Speed up the animation and make it pause less */
1121 | 			left: 120%; /* Move across and off-screen */
1122 | 		}
1123 | 	}
1124 | 
1125 | 	.task-row {
1126 | 		position: relative; /* Needed for absolute positioning of ::before */
1127 | 		overflow: hidden; /* Keep shine contained */
1128 | 	}
1129 | 	.review-indicator {
1130 | 		display: inline-flex;
1131 | 		align-items: center;
1132 | 		justify-content: center;
1133 | 		color: #3b82f6;
1134 | 		transition: all 0.2s ease;
1135 | 		margin-top: 10px;
1136 | 	}
1137 | 	
1138 | 	.review-indicator:hover {
1139 | 		opacity: 0.8;
1140 | 	}
1141 | 	
1142 | 	.from-review-task {
1143 | 		background-color: rgba(59, 130, 246, 0.08);
1144 | 	}
1145 | 	
1146 | 	.from-review-task:hover {
1147 | 		background-color: rgba(59, 130, 246, 0.12);
1148 | 	}
1149 | </style>
1150 | 
1151 | 
```

--------------------------------------------------------------------------------
/src/lib/llmUtils.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { GenerateContentResult, GenerativeModel } from '@google/generative-ai'
   2 | import OpenAI from 'openai'
   3 | import crypto from 'crypto'
   4 | import {
   5 |   BreakdownOptions,
   6 |   EffortEstimationSchema,
   7 |   TaskBreakdownSchema,
   8 |   TaskBreakdownResponseSchema,
   9 |   Task,
  10 |   TaskListSchema,
  11 |   HistoryEntry,
  12 |   FeatureHistorySchema,
  13 |   TaskSchema,
  14 |   LLMClarificationRequestSchema,
  15 | } from '../models/types'
  16 | import { aiService } from '../services/aiService'
  17 | import { logToFile } from './logger'
  18 | import { safetySettings, OPENROUTER_MODEL, GEMINI_MODEL } from '../config'
  19 | import { z } from 'zod'
  20 | import { encoding_for_model } from 'tiktoken'
  21 | import { addHistoryEntry } from './dbUtils'
  22 | import webSocketService from '../services/webSocketService'
  23 | import { databaseService } from '../services/databaseService'
  24 | 
  25 | /**
  26 |  * Parses the text response from Gemini into a list of tasks.
  27 |  */
  28 | export function parseGeminiPlanResponse(
  29 |   responseText: string | undefined | null
  30 | ): string[] {
  31 |   // Basic parsing
  32 |   if (!responseText) {
  33 |     return []
  34 |   }
  35 | 
  36 |   // Split by newlines and clean up
  37 |   const lines = responseText
  38 |     .split('\n')
  39 |     .map((line) => line.trim())
  40 |     .filter((line) => line.length > 0 && !line.match(/^[-*+]\s*$/))
  41 | 
  42 |   // Process each line to remove markdown list markers and numbering
  43 |   const cleanedLines = lines.map((line) => {
  44 |     // Remove markdown list markers and numbering
  45 |     return line
  46 |       .replace(/^[-*+]\s*/, '') // Remove list markers like -, *, +
  47 |       .replace(/^\d+\.\s*/, '') // Remove numbered list markers like 1. 2. etc.
  48 |       .replace(/^[a-z]\)\s*/i, '') // Remove lettered list markers like a) b) etc.
  49 |       .replace(/^\([a-z]\)\s*/i, '') // Remove lettered list markers like (a) (b) etc.
  50 |   })
  51 | 
  52 |   // Detect hierarchical structure based on indentation or subtask indicators
  53 |   const tasks: string[] = []
  54 |   let currentParentTask: string | null = null
  55 | 
  56 |   for (const line of cleanedLines) {
  57 |     // Check if this is a parent task or a subtask based on various indicators
  58 |     const isSubtask =
  59 |       line.match(/subtask|sub-task/i) || // Contains "subtask" or "sub-task"
  60 |       line.startsWith('  ') || // Has leading indentation
  61 |       line.match(/^[a-z]\.[\d]+/i) || // Contains notation like "a.1"
  62 |       line.includes('→') || // Contains arrow indicators
  63 |       line.match(/\([a-z]\)/i) // Contains notation like "(a)"
  64 | 
  65 |     if (isSubtask && currentParentTask) {
  66 |       // If it's a subtask and we have a parent, tag it with the parent task info
  67 |       tasks.push(line)
  68 |     } else {
  69 |       // This is a new parent task
  70 |       currentParentTask = line
  71 |       tasks.push(line)
  72 |     }
  73 |   }
  74 | 
  75 |   return tasks
  76 | }
  77 | 
  78 | /**
  79 |  * Determines task effort using an LLM.
  80 |  * Uses structured JSON output for consistent results.
  81 |  * Works with both OpenRouter and Gemini models.
  82 |  */
  83 | export async function determineTaskEffort(
  84 |   description: string,
  85 |   model: GenerativeModel | OpenAI | null
  86 | ): Promise<'low' | 'medium' | 'high'> {
  87 |   if (!model) {
  88 |     console.error('[TaskServer] Cannot determine effort: No model provided.')
  89 |     // Default to medium effort if no model is available
  90 |     return 'medium'
  91 |   }
  92 | 
  93 |   const prompt = `
  94 | Task: ${description}
  95 | 
  96 | Analyze this **coding task** and determine its estimated **effort level** based ONLY on the implementation work involved. A higher effort level often implies the task might need breaking down into sub-steps. Use these criteria:
  97 | - Low: Simple code changes likely contained in one or a few files, minimal logic changes, straightforward bug fixes. (e.g., renaming a variable, adding a console log, simple UI text change). Expected to be quick.
  98 | - Medium: Requires moderate development time, involves changes across several files or components with clear patterns, includes writing new functions or small classes, moderate refactoring. Might benefit from 1-3 sub-steps. (e.g., adding a new simple API endpoint, implementing a small feature).
  99 | - High: Involves significant development time, potentially spanning multiple days. Suggests complex architectural changes, intricate algorithm implementation, deep refactoring affecting multiple core components, requires careful design and likely needs breakdown into multiple sub-steps (3+). (e.g., redesigning a core system, implementing complex data processing).
 100 | 
 101 | Exclude factors like testing procedures, documentation, deployment, or project management overhead.
 102 | 
 103 | Respond with a JSON object that includes the effort level and optionally a short reasoning.
 104 | `
 105 | 
 106 |   try {
 107 |     // Use structured response with schema validation
 108 |     if (model instanceof OpenAI) {
 109 |       // Use OpenRouter with structured output
 110 |       const result = await aiService.callOpenRouterWithSchema(
 111 |         OPENROUTER_MODEL,
 112 |         [{ role: 'user', content: prompt }],
 113 |         EffortEstimationSchema,
 114 |         { temperature: 0.1, max_tokens: 100 }
 115 |       )
 116 | 
 117 |       if (result.success) {
 118 |         return result.data.effort
 119 |       } else {
 120 |         console.warn(
 121 |           `[TaskServer] Could not determine effort using structured output: ${result.error}. Defaulting to medium.`
 122 |         )
 123 |         return 'medium'
 124 |       }
 125 |     } else {
 126 |       // Use Gemini with structured output
 127 |       const result = await aiService.callGeminiWithSchema(
 128 |         GEMINI_MODEL,
 129 |         prompt,
 130 |         EffortEstimationSchema,
 131 |         { temperature: 0.1, maxOutputTokens: 100 }
 132 |       )
 133 | 
 134 |       if (result.success) {
 135 |         return result.data.effort
 136 |       } else {
 137 |         console.warn(
 138 |           `[TaskServer] Could not determine effort using structured output: ${result.error}. Defaulting to medium.`
 139 |         )
 140 |         return 'medium'
 141 |       }
 142 |     }
 143 |   } catch (error) {
 144 |     console.error('[TaskServer] Error determining task effort:', error)
 145 |     return 'medium' // Default to medium on error
 146 |   }
 147 | }
 148 | 
 149 | /**
 150 |  * Breaks down a high-effort task into subtasks using an LLM.
 151 |  * Uses structured JSON output for consistent results.
 152 |  * Works with both OpenRouter and Gemini models.
 153 |  */
 154 | export async function breakDownHighEffortTask(
 155 |   taskDescription: string,
 156 |   parentId: string,
 157 |   model: GenerativeModel | OpenAI | null,
 158 |   options: BreakdownOptions = {}
 159 | ): Promise<string[]> {
 160 |   if (!model) {
 161 |     console.error('[TaskServer] Cannot break down task: No model provided.')
 162 |     return []
 163 |   }
 164 | 
 165 |   // Use provided options or defaults
 166 |   const {
 167 |     minSubtasks = 2,
 168 |     maxSubtasks = 5,
 169 |     preferredEffort = 'medium',
 170 |     maxRetries = 3,
 171 |   } = options
 172 | 
 173 |   // Enhanced prompt with clearer instructions for JSON output
 174 |   const breakdownPrompt = `
 175 | I need to break down this high-effort coding task into smaller, actionable subtasks:
 176 | 
 177 | Task: "${taskDescription}"
 178 | 
 179 | Guidelines:
 180 | 1. Create ${minSubtasks}-${maxSubtasks} subtasks.
 181 | 2. Each subtask should ideally be '${preferredEffort}' effort, focusing on a specific part of the implementation.
 182 | 3. Make each subtask a concrete coding action (e.g., "Create function X", "Refactor module Y", "Add field Z to interface").
 183 | 4. The subtasks should represent a logical sequence for implementation.
 184 | 5. Only include coding tasks, not testing, documentation, or deployment steps.
 185 | 
 186 | IMPORTANT RESPONSE FORMAT INSTRUCTIONS:
 187 | - Return ONLY a valid JSON object
 188 | - The JSON object MUST have a single key named "subtasks"
 189 | - "subtasks" MUST be an array of objects with exactly two fields each:
 190 |   - "description": string - The subtask description
 191 |   - "effort": string - MUST be either "low" or "medium"
 192 | - No other text before or after the JSON object
 193 | - No markdown formatting, code blocks, or comments
 194 | 
 195 | Example of EXACTLY how your response should be formatted:
 196 | {
 197 |   "subtasks": [
 198 |     {
 199 |       "description": "Create the database schema for user profiles",
 200 |       "effort": "medium"
 201 |     },
 202 |     {
 203 |       "description": "Implement the user profile repository class",
 204 |       "effort": "medium"
 205 |     }
 206 |   ]
 207 | }
 208 | `
 209 | 
 210 |   // Function to handle the actual API call with retry logic
 211 |   async function attemptBreakdown(attempt: number): Promise<string[]> {
 212 |     try {
 213 |       // Use structured response with schema validation
 214 |       if (model instanceof OpenAI) {
 215 |         // Use OpenRouter with structured output
 216 |         const result = await aiService.callOpenRouterWithSchema(
 217 |           OPENROUTER_MODEL,
 218 |           [{ role: 'user', content: breakdownPrompt }],
 219 |           TaskBreakdownResponseSchema,
 220 |           { temperature: 0.2 } // Lower temperature for more consistent output
 221 |         )
 222 | 
 223 |         if (result.success) {
 224 |           // Extract the descriptions from the structured response
 225 |           return result.data.subtasks.map(
 226 |             (subtask) => `[${subtask.effort}] ${subtask.description}`
 227 |           )
 228 |         } else {
 229 |           console.warn(
 230 |             `[TaskServer] Could not break down task using structured output (attempt ${attempt}): ${result.error}`
 231 |           )
 232 | 
 233 |           // Retry if attempts remain
 234 |           if (attempt < maxRetries) {
 235 |             logToFile(
 236 |               `[TaskServer] Retrying task breakdown (attempt ${attempt + 1})`
 237 |             )
 238 |             return attemptBreakdown(attempt + 1)
 239 |           }
 240 |           return []
 241 |         }
 242 |       } else {
 243 |         // Use Gemini with structured output
 244 |         const result = await aiService.callGeminiWithSchema(
 245 |           GEMINI_MODEL,
 246 |           breakdownPrompt,
 247 |           TaskBreakdownResponseSchema,
 248 |           { temperature: 0.2 } // Lower temperature for more consistent output
 249 |         )
 250 | 
 251 |         if (result.success) {
 252 |           // Extract the descriptions from the structured response
 253 |           return result.data.subtasks.map(
 254 |             (subtask) => `[${subtask.effort}] ${subtask.description}`
 255 |           )
 256 |         } else {
 257 |           console.warn(
 258 |             `[TaskServer] Could not break down task using structured output (attempt ${attempt}): ${result.error}`
 259 |           )
 260 | 
 261 |           // Retry if attempts remain
 262 |           if (attempt < maxRetries) {
 263 |             logToFile(
 264 |               `[TaskServer] Retrying task breakdown (attempt ${attempt + 1})`
 265 |             )
 266 |             return attemptBreakdown(attempt + 1)
 267 |           }
 268 |           return []
 269 |         }
 270 |       }
 271 |     } catch (error) {
 272 |       console.error(
 273 |         `[TaskServer] Error breaking down high-effort task (attempt ${attempt}):`,
 274 |         error
 275 |       )
 276 | 
 277 |       // Retry if attempts remain
 278 |       if (attempt < maxRetries) {
 279 |         logToFile(
 280 |           `[TaskServer] Retrying task breakdown after error (attempt ${
 281 |             attempt + 1
 282 |           })`
 283 |         )
 284 |         return attemptBreakdown(attempt + 1)
 285 |       }
 286 |       return []
 287 |     }
 288 |   }
 289 | 
 290 |   // Start the breakdown process with first attempt
 291 |   return attemptBreakdown(1)
 292 | }
 293 | 
 294 | /**
 295 |  * Extracts parent task ID from a task description if present.
 296 |  * @param taskDescription The task description to check
 297 |  * @returns An object with the cleaned description and parentTaskId if found
 298 |  */
 299 | export function extractParentTaskId(taskDescription: string): {
 300 |   description: string
 301 |   parentTaskId?: string
 302 | } {
 303 |   const parentTaskMatch = taskDescription.match(/\[parentTask:([a-f0-9-]+)\]$/i)
 304 | 
 305 |   if (parentTaskMatch) {
 306 |     // Extract the parent task ID
 307 |     const parentTaskId = parentTaskMatch[1]
 308 |     // Remove the parent task tag from the description
 309 |     const description = taskDescription.replace(
 310 |       /\s*\[parentTask:[a-f0-9-]+\]$/i,
 311 |       ''
 312 |     )
 313 |     return { description, parentTaskId }
 314 |   }
 315 | 
 316 |   return { description: taskDescription }
 317 | }
 318 | 
 319 | /**
 320 |  * Extracts effort rating from a task description.
 321 |  * @param taskDescription The task description to check
 322 |  * @returns An object with the cleaned description and effort
 323 |  */
 324 | export function extractEffort(taskDescription: string): {
 325 |   description: string
 326 |   effort: 'low' | 'medium' | 'high'
 327 | } {
 328 |   const effortMatch = taskDescription.match(/^\[(low|medium|high)\]/i)
 329 | 
 330 |   if (effortMatch) {
 331 |     const effort = effortMatch[1].toLowerCase() as 'low' | 'medium' | 'high'
 332 |     // Remove the effort tag from the description
 333 |     const description = taskDescription.replace(
 334 |       /^\[(low|medium|high)\]\s*/i,
 335 |       ''
 336 |     )
 337 |     return { description, effort }
 338 |   }
 339 | 
 340 |   // Default to medium if no effort found
 341 |   return { description: taskDescription, effort: 'medium' }
 342 | }
 343 | 
 344 | /**
 345 |  * A more robust approach to parsing LLM-generated JSON that might be malformed due to newlines
 346 |  * or other common issues in AI responses.
 347 |  */
 348 | function robustJsonParse(text: string): any {
 349 |   // First attempt: Try with standard JSON.parse
 350 |   try {
 351 |     return JSON.parse(text)
 352 |   } catch (error: any) {
 353 |     // If standard parsing fails, try more aggressive fixing
 354 |     logToFile(
 355 |       `[robustJsonParse] Standard parsing failed, attempting recovery: ${error}`
 356 |     )
 357 | 
 358 |     try {
 359 |       // Detect the main expected structure type (tasks vs subtasks)
 360 |       const isTasksArray = text.includes('"tasks"')
 361 |       const isSubtasksArray = text.includes('"subtasks"')
 362 |       const hasDescription = text.includes('"description"')
 363 |       const hasEffort = text.includes('"effort"')
 364 | 
 365 |       // Special handling for common OpenRouter/AI model response patterns
 366 |       if ((isTasksArray || isSubtasksArray) && hasDescription && hasEffort) {
 367 |         const arrayKey = isSubtasksArray ? 'subtasks' : 'tasks'
 368 | 
 369 |         // 1. Enhanced regex that works for both tasks and subtasks arrays
 370 |         const taskRegex =
 371 |           /"description"\s*:\s*"((?:[^"\\]|\\"|\\|[\s\S])*?)"\s*,\s*"effort"\s*:\s*"(low|medium|high)"/g
 372 |         const tasks = []
 373 |         let match
 374 | 
 375 |         while ((match = taskRegex.exec(text)) !== null) {
 376 |           try {
 377 |             if (match[1] && match[2]) {
 378 |               tasks.push({
 379 |                 description: match[1].replace(/\\"/g, '"'),
 380 |                 effort: match[2],
 381 |               })
 382 |             }
 383 |           } catch (innerError) {
 384 |             logToFile(`[robustJsonParse] Error extracting task: ${innerError}`)
 385 |           }
 386 |         }
 387 | 
 388 |         if (tasks.length > 0) {
 389 |           logToFile(
 390 |             `[robustJsonParse] Successfully extracted ${tasks.length} ${arrayKey} with regex`
 391 |           )
 392 |           return { [arrayKey]: tasks }
 393 |         }
 394 | 
 395 |         // 2. If regex extraction fails, try extracting JSON objects directly
 396 |         if (tasks.length === 0) {
 397 |           try {
 398 |             const objectsExtracted = extractJSONObjects(text)
 399 |             if (objectsExtracted.length > 0) {
 400 |               // Filter valid task objects
 401 |               const validTasks = objectsExtracted.filter(
 402 |                 (obj) =>
 403 |                   obj &&
 404 |                   typeof obj === 'object' &&
 405 |                   obj.description &&
 406 |                   obj.effort &&
 407 |                   typeof obj.description === 'string' &&
 408 |                   typeof obj.effort === 'string'
 409 |               )
 410 | 
 411 |               if (validTasks.length > 0) {
 412 |                 logToFile(
 413 |                   `[robustJsonParse] Successfully extracted ${validTasks.length} ${arrayKey} with object extraction`
 414 |                 )
 415 |                 return { [arrayKey]: validTasks }
 416 |               }
 417 |             }
 418 |           } catch (objExtractionError) {
 419 |             logToFile(
 420 |               `[robustJsonParse] Object extraction failed: ${objExtractionError}`
 421 |             )
 422 |           }
 423 |         }
 424 |       }
 425 | 
 426 |       // 3. Fall back to manual line-by-line parsing for JSON objects
 427 |       const lines = text.split('\n')
 428 |       let cleanJson = ''
 429 |       let inString = false
 430 | 
 431 |       for (const line of lines) {
 432 |         let processedLine = line
 433 | 
 434 |         // Count quote marks to track if we're inside a string
 435 |         for (let i = 0; i < line.length; i++) {
 436 |           if (line[i] === '"' && (i === 0 || line[i - 1] !== '\\')) {
 437 |             inString = !inString
 438 |           }
 439 |         }
 440 | 
 441 |         // Add a space instead of newline if we're in the middle of a string
 442 |         cleanJson += inString ? ' ' + processedLine : processedLine
 443 |       }
 444 | 
 445 |       // 4. Balance braces and brackets if needed
 446 |       cleanJson = balanceBracesAndBrackets(cleanJson)
 447 | 
 448 |       // Final attempt to parse the cleaned JSON
 449 |       return JSON.parse(cleanJson)
 450 |     } catch (recoveryError) {
 451 |       logToFile(
 452 |         `[robustJsonParse] All recovery attempts failed: ${recoveryError}`
 453 |       )
 454 |       throw new Error(`Failed to parse JSON: ${error.message}`)
 455 |     }
 456 |   }
 457 | }
 458 | 
 459 | /**
 460 |  * Extracts valid JSON objects from a potentially malformed string.
 461 |  * Helps recover objects from truncated or malformed JSON.
 462 |  */
 463 | function extractJSONObjects(text: string): any[] {
 464 |   const objects: any[] = []
 465 | 
 466 |   // First try to find array boundaries
 467 |   const arrayStartIndex = text.indexOf('[')
 468 |   const arrayEndIndex = text.lastIndexOf(']')
 469 | 
 470 |   if (arrayStartIndex !== -1 && arrayEndIndex > arrayStartIndex) {
 471 |     // Extract array content
 472 |     const arrayContent = text.substring(arrayStartIndex + 1, arrayEndIndex)
 473 | 
 474 |     // Split by potential object boundaries, respecting nested objects
 475 |     let depth = 0
 476 |     let currentObject = ''
 477 |     let inString = false
 478 | 
 479 |     for (let i = 0; i < arrayContent.length; i++) {
 480 |       const char = arrayContent[i]
 481 | 
 482 |       // Track string boundaries
 483 |       if (char === '"' && (i === 0 || arrayContent[i - 1] !== '\\')) {
 484 |         inString = !inString
 485 |       }
 486 | 
 487 |       // Only track structure when not in a string
 488 |       if (!inString) {
 489 |         if (char === '{') {
 490 |           depth++
 491 |           if (depth === 1) {
 492 |             // Start of a new object
 493 |             currentObject = '{'
 494 |             continue
 495 |           }
 496 |         } else if (char === '}') {
 497 |           depth--
 498 |           if (depth === 0) {
 499 |             // End of an object, try to parse it
 500 |             currentObject += '}'
 501 |             try {
 502 |               const obj = JSON.parse(currentObject)
 503 |               objects.push(obj)
 504 |             } catch (e) {
 505 |               // If this object can't be parsed, just continue
 506 |             }
 507 |             currentObject = ''
 508 |             continue
 509 |           }
 510 |         } else if (char === ',' && depth === 0) {
 511 |           // Skip commas between objects
 512 |           continue
 513 |         }
 514 |       }
 515 | 
 516 |       // Add character to current object if we're inside one
 517 |       if (depth > 0) {
 518 |         currentObject += char
 519 |       }
 520 |     }
 521 |   }
 522 | 
 523 |   return objects
 524 | }
 525 | 
 526 | /**
 527 |  * Balances braces and brackets in a JSON string to make it valid
 528 |  */
 529 | function balanceBracesAndBrackets(text: string): string {
 530 |   let result = text
 531 | 
 532 |   // Count opening and closing braces/brackets
 533 |   const openBraces = (result.match(/\{/g) || []).length
 534 |   const closeBraces = (result.match(/\}/g) || []).length
 535 |   const openBrackets = (result.match(/\[/g) || []).length
 536 |   const closeBrackets = (result.match(/\]/g) || []).length
 537 | 
 538 |   // Add missing closing braces/brackets
 539 |   if (openBraces > closeBraces) {
 540 |     result += '}'.repeat(openBraces - closeBraces)
 541 |   }
 542 | 
 543 |   if (openBrackets > closeBrackets) {
 544 |     result += ']'.repeat(openBrackets - closeBrackets)
 545 |   }
 546 | 
 547 |   return result
 548 | }
 549 | 
 550 | /**
 551 |  * Parses and validates a JSON response string against a provided Zod schema.
 552 |  *
 553 |  * @param responseText - The raw JSON string from the LLM response
 554 |  * @param schema - The Zod schema to validate against
 555 |  * @returns An object containing either the validated data or error information
 556 |  */
 557 | export function parseAndValidateJsonResponse<T extends z.ZodType>(
 558 |   responseText: string | null | undefined,
 559 |   schema: T
 560 | ):
 561 |   | { success: true; data: z.infer<T> }
 562 |   | { success: false; error: string; rawData: any | null } {
 563 |   // Handle null or empty responses
 564 |   if (!responseText) {
 565 |     return {
 566 |       success: false,
 567 |       error: 'Response text is empty or null',
 568 |       rawData: null,
 569 |     }
 570 |   }
 571 | 
 572 |   // Enhanced logging for debugging
 573 |   try {
 574 |     logToFile(
 575 |       `[parseAndValidateJsonResponse] Raw response text: ${responseText?.substring(
 576 |         0,
 577 |         1000
 578 |       )}`
 579 |     )
 580 |   } catch (logError) {
 581 |     // Ignore logging errors
 582 |   }
 583 | 
 584 |   // Extract JSON from the response if it's wrapped in markdown or other text
 585 |   let jsonString = responseText
 586 | 
 587 |   // Look for JSON in markdown code blocks
 588 |   const jsonBlockMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)\s*```/)
 589 |   if (jsonBlockMatch && jsonBlockMatch[1]) {
 590 |     jsonString = jsonBlockMatch[1]
 591 |   }
 592 | 
 593 |   // --- Additional cleaning: extract first valid JSON object from text ---
 594 |   function extractJsonFromText(text: string): string {
 595 |     // Remove markdown code fences
 596 |     text = text.replace(/```(?:json)?/gi, '').replace(/```/g, '')
 597 |     // Find the first { and last }
 598 |     const firstBrace = text.indexOf('{')
 599 |     const lastBrace = text.lastIndexOf('}')
 600 |     if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
 601 |       return text.substring(firstBrace, lastBrace + 1)
 602 |     }
 603 |     return text.trim()
 604 |   }
 605 |   jsonString = extractJsonFromText(jsonString)
 606 | 
 607 |   // Try to identify expected content type for better recovery
 608 |   const expectsSubtasks =
 609 |     responseText.includes('"subtasks"') || responseText.includes('subtasks')
 610 | 
 611 |   const expectsTasks =
 612 |     responseText.includes('"tasks"') || responseText.includes('tasks')
 613 | 
 614 |   // --- Auto-fix common JSON issues (trailing commas, comments) ---
 615 |   function fixCommonJsonIssues(text: string): string {
 616 |     // Remove JavaScript-style comments
 617 |     text = text.replace(/\/\/.*$/gm, '')
 618 |     text = text.replace(/\/\*[\s\S]*?\*\//g, '')
 619 |     // Remove trailing commas in objects and arrays
 620 |     text = text.replace(/,\s*([}\]])/g, '$1')
 621 | 
 622 |     // Fix broken newlines in the middle of strings
 623 |     text = text.replace(/([^\\])"\s*\n\s*"/g, '$1')
 624 | 
 625 |     // Normalize string values that got broken across lines
 626 |     text = text.replace(/([^\\])"\s*\n\s*([^"])/g, '$1", "$2')
 627 | 
 628 |     // Fix incomplete JSON objects
 629 |     const openBraces = (text.match(/\{/g) || []).length
 630 |     const closeBraces = (text.match(/\}/g) || []).length
 631 |     if (openBraces > closeBraces) {
 632 |       text = text + '}'.repeat(openBraces - closeBraces)
 633 |     }
 634 | 
 635 |     // Fix unclosed quotes at end of string
 636 |     if ((text.match(/"/g) || []).length % 2 !== 0) {
 637 |       // Check if the last quote is an opening quote (likely in the middle of a string)
 638 |       const lastQuotePos = text.lastIndexOf('"')
 639 |       const endsWithOpenQuote =
 640 |         lastQuotePos !== -1 &&
 641 |         text.substring(lastQuotePos).split('"').length === 2
 642 | 
 643 |       if (endsWithOpenQuote) {
 644 |         text = text + '"'
 645 |       }
 646 |     }
 647 | 
 648 |     return text
 649 |   }
 650 |   jsonString = fixCommonJsonIssues(jsonString)
 651 |   // --- End auto-fix ---
 652 | 
 653 |   try {
 654 |     logToFile(
 655 |       `[parseAndValidateJsonResponse] Cleaned JSON string: ${jsonString?.substring(
 656 |         0,
 657 |         1000
 658 |       )}`
 659 |     )
 660 |   } catch (logError) {
 661 |     // Ignore logging errors
 662 |   }
 663 | 
 664 |   // Attempt to parse the JSON using robust parser
 665 |   let parsedData: any
 666 |   try {
 667 |     parsedData = robustJsonParse(jsonString)
 668 |     logToFile(
 669 |       `[parseAndValidateJsonResponse] JSON parsed successfully with robust parser`
 670 |     )
 671 |   } catch (parseError) {
 672 |     // If primary parsing failed, try reconstructing specific expected structures
 673 |     try {
 674 |       // For tasks/subtasks, try to reconstruct using direct object extraction
 675 |       if (expectsTasks || expectsSubtasks) {
 676 |         const arrayKey = expectsSubtasks ? 'subtasks' : 'tasks'
 677 | 
 678 |         // Extract task objects directly from text
 679 |         const extractedObjects = extractJSONObjects(jsonString)
 680 |         if (extractedObjects.length > 0) {
 681 |           // Filter out invalid objects
 682 |           const validItems = extractedObjects.filter(
 683 |             (obj) =>
 684 |               obj &&
 685 |               typeof obj === 'object' &&
 686 |               obj.description &&
 687 |               obj.effort &&
 688 |               typeof obj.description === 'string' &&
 689 |               typeof obj.effort === 'string'
 690 |           )
 691 | 
 692 |           if (validItems.length > 0) {
 693 |             parsedData = { [arrayKey]: validItems }
 694 |             logToFile(
 695 |               `[parseAndValidateJsonResponse] Successfully reconstructed ${arrayKey} array with ${validItems.length} items`
 696 |             )
 697 | 
 698 |             // Validate against schema immediately
 699 |             const validationResult = schema.safeParse(parsedData)
 700 |             if (validationResult.success) {
 701 |               return {
 702 |                 success: true,
 703 |                 data: validationResult.data,
 704 |               }
 705 |             }
 706 |           }
 707 |         }
 708 | 
 709 |         // If we can see where tasks are, try regex extraction
 710 |         const regex = new RegExp(
 711 |           `"(description|desc|name)"\\s*:\\s*"([^"]*)"[\\s\\S]*?"(effort|difficulty)"\\s*:\\s*"(low|medium|high)"`,
 712 |           'gi'
 713 |         )
 714 | 
 715 |         const items = []
 716 |         let match
 717 |         while ((match = regex.exec(responseText)) !== null) {
 718 |           try {
 719 |             items.push({
 720 |               description: match[2],
 721 |               effort: match[4].toLowerCase(),
 722 |             })
 723 |           } catch (e) {
 724 |             // Skip invalid matches
 725 |           }
 726 |         }
 727 | 
 728 |         if (items.length > 0) {
 729 |           parsedData = { [arrayKey]: items }
 730 |           logToFile(
 731 |             `[parseAndValidateJsonResponse] Successfully extracted ${items.length} ${arrayKey} with regex`
 732 |           )
 733 | 
 734 |           // Validate against schema
 735 |           const validationResult = schema.safeParse(parsedData)
 736 |           if (validationResult.success) {
 737 |             return {
 738 |               success: true,
 739 |               data: validationResult.data,
 740 |             }
 741 |           }
 742 |         }
 743 |       }
 744 |     } catch (reconstructionError) {
 745 |       logToFile(
 746 |         `[parseAndValidateJsonResponse] Reconstruction error: ${reconstructionError}`
 747 |       )
 748 |       // Continue to normal error handling
 749 |     }
 750 | 
 751 |     // All parsing methods have failed
 752 |     logToFile(
 753 |       `[parseAndValidateJsonResponse] All parsing attempts failed: ${parseError}`
 754 |     )
 755 |     return {
 756 |       success: false,
 757 |       error: `Failed to parse JSON: ${(parseError as Error).message}`,
 758 |       rawData: responseText,
 759 |     }
 760 |   }
 761 | 
 762 |   // Validate against the schema
 763 |   const validationResult = schema.safeParse(parsedData)
 764 | 
 765 |   if (validationResult.success) {
 766 |     return {
 767 |       success: true,
 768 |       data: validationResult.data,
 769 |     }
 770 |   } else {
 771 |     logToFile(
 772 |       `[parseAndValidateJsonResponse] Schema validation failed. Errors: ${JSON.stringify(
 773 |         validationResult.error.errors
 774 |       )}`
 775 |     )
 776 | 
 777 |     // Attempt to recover partial valid data
 778 |     const recoveredData = attemptPartialResponseRecovery(parsedData, schema)
 779 |     if (recoveredData) {
 780 |       logToFile(
 781 |         `[parseAndValidateJsonResponse] Successfully recovered partial response`
 782 |       )
 783 |       return {
 784 |         success: true,
 785 |         data: recoveredData,
 786 |       }
 787 |     }
 788 | 
 789 |     return {
 790 |       success: false,
 791 |       error: `Schema validation failed: ${validationResult.error.message}`,
 792 |       rawData: parsedData,
 793 |     }
 794 |   }
 795 | }
 796 | 
 797 | /**
 798 |  * Attempts to recover partial valid data from a failed schema validation.
 799 |  * Particularly useful for array of tasks or subtasks where some items might be valid.
 800 |  */
 801 | function attemptPartialResponseRecovery(
 802 |   parsedData: any,
 803 |   schema: z.ZodType
 804 | ): any | null {
 805 |   try {
 806 |     logToFile(
 807 |       `[attemptPartialResponseRecovery] Attempting to recover partial valid response`
 808 |     )
 809 | 
 810 |     // Handle common case: tasks array with valid and invalid items
 811 |     if (
 812 |       parsedData &&
 813 |       ((parsedData.tasks && Array.isArray(parsedData.tasks)) ||
 814 |         (parsedData.subtasks && Array.isArray(parsedData.subtasks)))
 815 |     ) {
 816 |       const isSubtasksArray =
 817 |         parsedData.subtasks && Array.isArray(parsedData.subtasks)
 818 |       const arrayKey = isSubtasksArray ? 'subtasks' : 'tasks'
 819 |       const items = isSubtasksArray ? parsedData.subtasks : parsedData.tasks
 820 | 
 821 |       // Filter out invalid task items
 822 |       const validItems = items.filter(
 823 |         (item: any) =>
 824 |           item &&
 825 |           typeof item === 'object' &&
 826 |           item.description &&
 827 |           item.effort &&
 828 |           typeof item.description === 'string' &&
 829 |           typeof item.effort === 'string'
 830 |       )
 831 | 
 832 |       if (validItems.length > 0) {
 833 |         const recoveredData = { ...parsedData, [arrayKey]: validItems }
 834 |         const validationResult = schema.safeParse(recoveredData)
 835 | 
 836 |         if (validationResult.success) {
 837 |           logToFile(
 838 |             `[attemptPartialResponseRecovery] Recovery successful, found ${validItems.length} valid ${arrayKey}`
 839 |           )
 840 |           return validationResult.data
 841 |         }
 842 |       }
 843 |     }
 844 | 
 845 |     return null
 846 |   } catch (error) {
 847 |     logToFile(
 848 |       `[attemptPartialResponseRecovery] Recovery attempt failed: ${error}`
 849 |     )
 850 |     return null
 851 |   }
 852 | }
 853 | 
 854 | /**
 855 |  * Ensures all task descriptions have an effort rating prefix.
 856 |  * Determines effort using an LLM if missing.
 857 |  */
 858 | export async function ensureEffortRatings(
 859 |   taskDescriptions: string[],
 860 |   model: GenerativeModel | OpenAI | null
 861 | ): Promise<string[]> {
 862 |   const effortRatedTasks: string[] = []
 863 |   for (const taskDesc of taskDescriptions) {
 864 |     const effortMatch = taskDesc.match(/^\[(low|medium|high)\]/i)
 865 |     if (effortMatch) {
 866 |       // Ensure consistent casing
 867 |       const effort = effortMatch[1].toLowerCase() as 'low' | 'medium' | 'high'
 868 |       const cleanDesc = taskDesc.replace(/^\[(low|medium|high)\]\s*/i, '')
 869 |       effortRatedTasks.push(`[${effort}] ${cleanDesc}`)
 870 |     } else {
 871 |       let effort: 'low' | 'medium' | 'high' = 'medium' // Default effort
 872 |       try {
 873 |         if (model) {
 874 |           // Only call if model is available
 875 |           effort = await determineTaskEffort(taskDesc, model)
 876 |         }
 877 |       } catch (error) {
 878 |         console.error(
 879 |           `[TaskServer] Error determining effort for task "${taskDesc.substring(
 880 |             0,
 881 |             40
 882 |           )}...". Defaulting to medium:`,
 883 |           error
 884 |         )
 885 |       }
 886 |       effortRatedTasks.push(`[${effort}] ${taskDesc}`)
 887 |     }
 888 |   }
 889 |   return effortRatedTasks
 890 | }
 891 | 
 892 | /**
 893 |  * Processes tasks: breaks down high-effort ones, ensures effort, and creates Task objects.
 894 |  */
 895 | export async function processAndBreakdownTasks(
 896 |   initialTasksWithEffort: string[],
 897 |   model: GenerativeModel | OpenAI | null,
 898 |   featureId: string,
 899 |   fromReviewContext: boolean
 900 | ): Promise<{ finalTasks: Task[]; complexTaskMap: Map<string, string> }> {
 901 |   const finalProcessedSteps: string[] = []
 902 |   const complexTaskMap = new Map<string, string>()
 903 |   let breakdownSuccesses = 0
 904 |   let breakdownFailures = 0
 905 | 
 906 |   for (const step of initialTasksWithEffort) {
 907 |     const effortMatch = step.match(/^\[(low|medium|high)\]/i)
 908 |     const isHighEffort = effortMatch && effortMatch[1].toLowerCase() === 'high'
 909 | 
 910 |     if (isHighEffort) {
 911 |       const taskDescription = step.replace(/^\[high\]\s*/i, '')
 912 |       const parentId = crypto.randomUUID()
 913 |       complexTaskMap.set(taskDescription, parentId) // Map original description to ID
 914 | 
 915 |       try {
 916 |         await addHistoryEntry(featureId, 'model', {
 917 |           step: 'task_breakdown_attempt',
 918 |           task: step,
 919 |           parentId,
 920 |         })
 921 | 
 922 |         const subtasks = await breakDownHighEffortTask(
 923 |           taskDescription,
 924 |           parentId,
 925 |           model,
 926 |           { minSubtasks: 2, maxSubtasks: 5, preferredEffort: 'medium' }
 927 |         )
 928 | 
 929 |         if (subtasks.length > 0) {
 930 |           // Add parent container task (marked completed later)
 931 |           finalProcessedSteps.push(`${step} [parentContainer]`) // Add marker
 932 | 
 933 |           // Process and add subtasks immediately after parent
 934 |           // Ensure subtasks also have effort ratings
 935 |           const subtasksWithEffort = await ensureEffortRatings(subtasks, model)
 936 |           const subtasksWithParentId = subtasksWithEffort.map((subtaskDesc) => {
 937 |             const { description: cleanSubDesc } = extractEffort(subtaskDesc) // Already has effort
 938 |             return `${subtaskDesc} [parentTask:${parentId}]`
 939 |           })
 940 | 
 941 |           finalProcessedSteps.push(...subtasksWithParentId)
 942 | 
 943 |           await addHistoryEntry(featureId, 'model', {
 944 |             step: 'task_breakdown_success',
 945 |             task: step,
 946 |             parentId,
 947 |             subtasks: subtasksWithParentId,
 948 |           })
 949 |           breakdownSuccesses++
 950 |         } else {
 951 |           // Breakdown failed, keep original high-effort task
 952 |           finalProcessedSteps.push(step)
 953 |           await addHistoryEntry(featureId, 'model', {
 954 |             step: 'task_breakdown_failure',
 955 |             task: step,
 956 |           })
 957 |           breakdownFailures++
 958 |         }
 959 |       } catch (breakdownError) {
 960 |         console.error(
 961 |           `[TaskServer] Error during breakdown for task "${taskDescription.substring(
 962 |             0,
 963 |             40
 964 |           )}...":`,
 965 |           breakdownError
 966 |         )
 967 |         finalProcessedSteps.push(step) // Keep original task on error
 968 |         await addHistoryEntry(featureId, 'model', {
 969 |           step: 'task_breakdown_error',
 970 |           task: step,
 971 |           error:
 972 |             breakdownError instanceof Error
 973 |               ? breakdownError.message
 974 |               : String(breakdownError),
 975 |         })
 976 |         breakdownFailures++
 977 |       }
 978 |     } else {
 979 |       // Keep low/medium effort tasks as is
 980 |       finalProcessedSteps.push(step)
 981 |     }
 982 |   }
 983 | 
 984 |   await logToFile(
 985 |     `[TaskServer] Breakdown processing complete: ${breakdownSuccesses} successes, ${breakdownFailures} failures.`
 986 |   )
 987 | 
 988 |   // --- Create Task Objects ---
 989 |   const finalTasks: Task[] = []
 990 |   const taskCreationErrors: string[] = []
 991 | 
 992 |   for (const step of finalProcessedSteps) {
 993 |     try {
 994 |       const isParentContainer = step.includes('[parentContainer]')
 995 |       const descriptionWithTags = step.replace('[parentContainer]', '').trim()
 996 | 
 997 |       const { description: descWithoutParent, parentTaskId } =
 998 |         extractParentTaskId(descriptionWithTags)
 999 |       const { description: cleanDescription, effort } =
1000 |         extractEffort(descWithoutParent)
1001 | 
1002 |       // Validate effort extracted or default
1003 |       const validatedEffort = ['low', 'medium', 'high'].includes(effort)
1004 |         ? effort
1005 |         : 'medium'
1006 | 
1007 |       // Get the predetermined ID for parent containers, otherwise generate new
1008 |       const originalHighEffortDesc = isParentContainer ? cleanDescription : null
1009 | 
1010 |       const taskId =
1011 |         (originalHighEffortDesc &&
1012 |           complexTaskMap.get(originalHighEffortDesc)) ||
1013 |         crypto.randomUUID()
1014 | 
1015 |       // If it's a parent container, set status to 'decomposed', otherwise 'pending'
1016 |       const status = isParentContainer ? 'decomposed' : 'pending'
1017 | 
1018 |       const taskDataToValidate: Omit<
1019 |         Task,
1020 |         'title' | 'subTasks' | 'dependencies' | 'history' | 'isManual'
1021 |       > = {
1022 |         id: taskId,
1023 |         feature_id: featureId,
1024 |         status,
1025 |         description: cleanDescription,
1026 |         effort: validatedEffort,
1027 |         completed: false, // All new tasks/subtasks start as not completed.
1028 |         ...(parentTaskId && { parentTaskId }),
1029 |         createdAt: new Date().toISOString(),
1030 |         updatedAt: new Date().toISOString(),
1031 |         ...(fromReviewContext && { fromReview: true }), // Set fromReview if in review context
1032 |       }
1033 | 
1034 |       // --- Enhanced Logging ---
1035 |       logToFile(
1036 |         `[processAndBreakdownTasks] Preparing ${
1037 |           isParentContainer ? 'Parent' : parentTaskId ? 'Subtask' : 'Task'
1038 |         } for validation: ID=${taskId}, Status=${status}, Parent=${
1039 |           parentTaskId || 'N/A'
1040 |         }, Desc="${cleanDescription.substring(0, 50)}..."`
1041 |       )
1042 |       logToFile(
1043 |         `[processAndBreakdownTasks] Task data before validation: ${JSON.stringify(
1044 |           taskDataToValidate
1045 |         )}`
1046 |       )
1047 |       // --- End Enhanced Logging ---
1048 | 
1049 |       // Validate against the Task schema before pushing
1050 |       const validationResult = TaskSchema.safeParse(taskDataToValidate)
1051 |       if (validationResult.success) {
1052 |         // --- Enhanced Logging ---
1053 |         logToFile(
1054 |           `[processAndBreakdownTasks] Validation successful for Task ID: ${taskId}`
1055 |         )
1056 |         // --- End Enhanced Logging ---
1057 |         finalTasks.push(validationResult.data)
1058 |       } else {
1059 |         // --- Enhanced Logging ---
1060 |         const errorMsg = `Task "${cleanDescription.substring(
1061 |           0,
1062 |           30
1063 |         )}..." (ID: ${taskId}) failed validation: ${
1064 |           validationResult.error.message
1065 |         }`
1066 |         logToFile(`[processAndBreakdownTasks] ${errorMsg}`)
1067 |         // --- End Enhanced Logging ---
1068 |         taskCreationErrors.push(errorMsg)
1069 |         console.warn(
1070 |           `[TaskServer] Task validation failed for "${cleanDescription.substring(
1071 |             0,
1072 |             30
1073 |           )}..." (ID: ${taskId}):`,
1074 |           validationResult.error.flatten()
1075 |         )
1076 |       }
1077 |     } catch (creationError) {
1078 |       const errorMsg = `Error creating task object for step "${step.substring(
1079 |         0,
1080 |         30
1081 |       )}...": ${
1082 |         creationError instanceof Error
1083 |           ? creationError.message
1084 |           : String(creationError)
1085 |       }`
1086 |       // --- Enhanced Logging ---
1087 |       logToFile(`[processAndBreakdownTasks] ${errorMsg}`)
1088 |       // --- End Enhanced Logging ---
1089 |       taskCreationErrors.push(errorMsg)
1090 |       console.error(
1091 |         `[TaskServer] Error creating task object for step "${step.substring(
1092 |           0,
1093 |           30
1094 |         )}...":`,
1095 |         creationError
1096 |       )
1097 |     }
1098 |   }
1099 | 
1100 |   if (taskCreationErrors.length > 0) {
1101 |     console.error(
1102 |       `[TaskServer] ${taskCreationErrors.length} errors occurred during task object creation/validation.`
1103 |     )
1104 |     await addHistoryEntry(featureId, 'model', {
1105 |       step: 'task_creation_errors',
1106 |       errors: taskCreationErrors,
1107 |     })
1108 |     // Decide if we should throw or return partial results. Returning for now.
1109 |   }
1110 | 
1111 |   return { finalTasks, complexTaskMap }
1112 | }
1113 | 
1114 | /**
1115 |  * Processes raw plan steps, ensures effort ratings are assigned, breaks down high-effort tasks,
1116 |  * saves the final task list, and notifies WebSocket clients of the update.
1117 |  *
1118 |  * @param rawPlanSteps Array of task descriptions (format: "[effort] description").
1119 |  * @param model The generative model to use for effort estimation/task breakdown.
1120 |  * @param featureId The ID of the feature being planned.
1121 |  * @param fromReview Optional flag to set fromReview: true on all saved tasks.
1122 |  * @returns The final list of processed Task objects.
1123 |  */
1124 | export async function processAndFinalizePlan(
1125 |   rawPlanSteps: string[],
1126 |   model: GenerativeModel | OpenAI | null,
1127 |   featureId: string,
1128 |   fromReview: boolean = false // Add default value
1129 | ): Promise<Task[]> {
1130 |   logToFile(
1131 |     `[TaskServer] Processing and finalizing plan for feature ${featureId}...`
1132 |   )
1133 |   let existingTasks: Task[] = []
1134 |   let finalTasks: Task[] = []
1135 |   const complexTaskMap = new Map<string, string>() // To track original description of broken down tasks
1136 | 
1137 |   try {
1138 |     // 1. Ensure all raw steps have effort ratings.
1139 |     // ensureEffortRatings preserves existing [high] prefixes from rawPlanSteps
1140 |     // and assigns effort to those without a prefix.
1141 |     const initialTasksWithEffort = await ensureEffortRatings(
1142 |       rawPlanSteps,
1143 |       model
1144 |     )
1145 | 
1146 |     // Explicitly define the tasks to be sent for breakdown processing.
1147 |     // This includes tasks from rawPlanSteps that were marked [high]
1148 |     // (as ensureEffortRatings preserves such tags) and will be
1149 |     // unconditionally processed by processAndBreakdownTasks.
1150 |     const tasksForBreakdownProcessing = initialTasksWithEffort
1151 | 
1152 |     // 2. Process tasks: Breakdown high-effort ones.
1153 |     // processAndBreakdownTasks will identify and attempt to break down tasks
1154 |     // with a "[high]" prefix within tasksForBreakdownProcessing.
1155 |     const { finalTasks: processedTasks, complexTaskMap: breakdownMap } =
1156 |       await processAndBreakdownTasks(
1157 |         tasksForBreakdownProcessing, // Using the explicitly defined variable
1158 |         model,
1159 |         featureId, // Pass featureId for logging/history
1160 |         fromReview // Pass the fromReview context flag
1161 |       )
1162 | 
1163 |     // Merge complexTaskMap from breakdown
1164 |     breakdownMap.forEach((value, key) => complexTaskMap.set(key, value))
1165 | 
1166 |     // --- Start Database Operations ---
1167 |     await databaseService.connect()
1168 |     logToFile(
1169 |       `[processAndFinalizePlan] Database connected. Fetching existing tasks...`
1170 |     )
1171 | 
1172 |     // 3. Fetch existing tasks to compare
1173 |     existingTasks = await databaseService.getTasksByFeatureId(featureId)
1174 | 
1175 |     const existingTaskMap = new Map(existingTasks.map((t) => [t.id, t]))
1176 |     const processedTaskMap = new Map(processedTasks.map((t) => [t.id, t]))
1177 |     const tasksToAdd: Task[] = []
1178 |     const tasksToUpdate: { id: string; updates: Partial<Task> }[] = []
1179 |     const taskIdsToDelete: string[] = []
1180 | 
1181 |     // 4. Compare processed tasks with existing tasks
1182 |     for (const processedTask of processedTasks) {
1183 |       if (existingTaskMap.has(processedTask.id)) {
1184 |         // Task exists, check for updates
1185 |         const existing = existingTaskMap.get(processedTask.id)!
1186 |         // updates object should only contain keys matching DB columns (snake_case)
1187 |         const updates: Partial<
1188 |           Pick<Task, 'description' | 'effort' | 'fromReview'> & {
1189 |             parentTaskId?: string
1190 |           }
1191 |         > = {}
1192 |         if (existing.description !== processedTask.description) {
1193 |           updates.description = processedTask.description
1194 |         }
1195 |         if (existing.effort !== processedTask.effort) {
1196 |           updates.effort = processedTask.effort
1197 |         }
1198 |         // Compare snake_case from DB (existing) with camelCase from processed Task
1199 |         if (existing.parentTaskId !== processedTask.parentTaskId) {
1200 |           // Add snake_case key to updates object for DB
1201 |           updates.parentTaskId = processedTask.parentTaskId
1202 |         }
1203 |         // Always update the 'fromReview' flag if this process is from review
1204 |         if (fromReview && !existing.fromReview) {
1205 |           // Use camelCase here as Task type expects it, DB service handles conversion to snake_case
1206 |           updates.fromReview = true
1207 |           await logToFile(
1208 |             `[processAndFinalizePlan] Updating task ${existing.id} to set fromReview = true. Context fromReview: ${fromReview}, existing.fromReview: ${existing.fromReview}`
1209 |           )
1210 |         }
1211 | 
1212 |         // Check if any updates are needed using the keys in the updates object
1213 |         if (Object.keys(updates).length > 0) {
1214 |           tasksToUpdate.push({ id: processedTask.id, updates })
1215 |         }
1216 |       } else {
1217 |         // New task to add
1218 |         tasksToAdd.push(processedTask)
1219 |       }
1220 |     }
1221 | 
1222 |     if (!fromReview) {
1223 |       // Identify tasks to delete (exist in DB but not in new plan)
1224 |       for (const existingTask of existingTasks) {
1225 |         if (!processedTaskMap.has(existingTask.id)) {
1226 |           taskIdsToDelete.push(existingTask.id)
1227 |         }
1228 |       }
1229 |     }
1230 | 
1231 |     // 5. Apply changes to the database
1232 |     logToFile(
1233 |       `[processAndFinalizePlan] Applying DB changes: ${tasksToAdd.length} adds, ${tasksToUpdate.length} updates, ${taskIdsToDelete.length} deletes.`
1234 |     )
1235 |     for (const { id, updates } of tasksToUpdate) {
1236 |       // Check if the task being updated was decomposed
1237 |       const isDecomposed = complexTaskMap.has(id)
1238 |       if (isDecomposed) {
1239 |         // If decomposed, mark status as 'decomposed' and completed = true
1240 |         await databaseService.updateTaskStatus(id, 'decomposed', true)
1241 |         // Only update other details if necessary (rare for decomposed tasks)
1242 |         if (Object.keys(updates).length > 0) {
1243 |           // Pass updates object (contains snake_case key) to DB service
1244 |           await databaseService.updateTaskDetails(id, updates)
1245 |         }
1246 |       } else {
1247 |         // Otherwise, just update details
1248 |         // Pass updates object (contains snake_case key) to DB service
1249 |         await databaseService.updateTaskDetails(id, updates)
1250 |       }
1251 |     }
1252 | 
1253 |     for (const task of tasksToAdd) {
1254 |       // Ensure parent task exists if specified using camelCase from Task type
1255 |       if (task.parentTaskId) {
1256 |         const parentExistsInDB = existingTaskMap.has(task.parentTaskId)
1257 |         const parentExistsInProcessed = processedTaskMap.has(task.parentTaskId)
1258 | 
1259 |         if (!parentExistsInDB && !parentExistsInProcessed) {
1260 |           logToFile(
1261 |             `[processAndFinalizePlan] Warning: Parent task ${task.parentTaskId} for task ${task.id} not found. Setting parent to null.`
1262 |           )
1263 |           // Use camelCase when modifying the task object
1264 |           task.parentTaskId = undefined
1265 |         }
1266 |       }
1267 | 
1268 |       // Prepare object for DB insertion with snake_case keys
1269 |       const now = Math.floor(Date.now() / 1000)
1270 |       const dbTaskPayload: any = {
1271 |         id: task.id,
1272 |         title: task.title,
1273 |         description: task.description,
1274 |         status: task.status,
1275 |         completed: task.completed ? 1 : 0,
1276 |         effort: task.effort,
1277 |         feature_id: featureId,
1278 |         created_at:
1279 |           task.createdAt && typeof task.createdAt === 'number'
1280 |             ? Math.floor(new Date(task.createdAt * 1000).getTime() / 1000)
1281 |             : now, // Map and convert
1282 |         updated_at:
1283 |           task.updatedAt && typeof task.updatedAt === 'number'
1284 |             ? Math.floor(new Date(task.updatedAt * 1000).getTime() / 1000)
1285 |             : now, // Map and convert
1286 |         // Use camelCase 'fromReview' to align with the Task interface expected by addTask
1287 |         fromReview: fromReview || task.fromReview || false,
1288 |       }
1289 |       await logToFile(
1290 |         `[processAndFinalizePlan] Adding task ${task.id}. Context fromReview: ${fromReview}, task.fromReview property: ${task.fromReview}, dbTaskPayload.fromReview value: ${dbTaskPayload.fromReview}`,
1291 |         'debug'
1292 |       )
1293 | 
1294 |       try {
1295 |         // Ensure that the object passed to addTask conforms to the Task interface
1296 |         await databaseService.addTask({
1297 |           id: dbTaskPayload.id,
1298 |           title: dbTaskPayload.title,
1299 |           description: dbTaskPayload.description,
1300 |           status: dbTaskPayload.status,
1301 |           completed: dbTaskPayload.completed === 1, // Ensure boolean
1302 |           effort: dbTaskPayload.effort,
1303 |           feature_id: dbTaskPayload.feature_id,
1304 |           created_at: dbTaskPayload.created_at,
1305 |           updated_at: dbTaskPayload.updated_at,
1306 |           fromReview: dbTaskPayload.fromReview, // This is now correctly camelCased
1307 |         })
1308 |       } catch (dbError) {
1309 |         logToFile(
1310 |           `[processAndFinalizePlan] Error adding task to database: ${dbError}`
1311 |         )
1312 |         console.error(`[TaskServer] Error adding task to database:`, dbError)
1313 |         throw dbError
1314 |       }
1315 |     }
1316 | 
1317 |     for (const taskId of taskIdsToDelete) {
1318 |       await databaseService.deleteTask(taskId)
1319 |     }
1320 | 
1321 |     // 6. Fetch the final list of tasks after all modifications
1322 |     finalTasks = await databaseService.getTasksByFeatureId(featureId)
1323 | 
1324 |     logToFile(
1325 |       `[processAndFinalizePlan] Final task count for feature ${featureId}: ${finalTasks.length}`
1326 |     )
1327 |     // --- End Database Operations ---
1328 |   } catch (error) {
1329 |     logToFile(
1330 |       `[processAndFinalizePlan] Error during plan finalization for feature ${featureId}: ${error}`
1331 |     )
1332 |     console.error(`[TaskServer] Error during plan finalization:`, error)
1333 |     // Re-throw the error to be handled by the caller (e.g., tool handler)
1334 |     throw error
1335 |   } finally {
1336 |     // Ensure database connection is closed, even if errors occurred
1337 |     try {
1338 |       await databaseService.close()
1339 |       logToFile(`[processAndFinalizePlan] Database connection closed.`)
1340 |     } catch (closeError) {
1341 |       logToFile(
1342 |         `[processAndFinalizePlan] Error closing database connection: ${closeError}`
1343 |       )
1344 |       console.error(`[TaskServer] Error closing database:`, closeError)
1345 |     }
1346 |   }
1347 | 
1348 |   // 7. Notify UI about the updated tasks (outside the main try/catch for DB ops)
1349 |   try {
1350 |     // Add detailed logging to debug
1351 |     if (finalTasks.length > 0) {
1352 |       await logToFile(
1353 |         `[processAndFinalizePlan] Sample of final task from DB (finalTasks[0]): ${JSON.stringify(
1354 |           finalTasks[0],
1355 |           null,
1356 |           2
1357 |         )}`
1358 |       )
1359 |     } else {
1360 |       await logToFile(
1361 |         `[processAndFinalizePlan] No final tasks to log from DB sample.`
1362 |       )
1363 |     }
1364 | 
1365 |     const formattedTasks = finalTasks.map((task: any) => {
1366 |       // Create a clean task object for the WebSocket
1367 |       return {
1368 |         id: task.id,
1369 |         description: task.description,
1370 |         status: task.status,
1371 |         effort: task.effort,
1372 |         parentTaskId: task.parentTaskId,
1373 |         completed: task.completed,
1374 |         title: task.title,
1375 |         fromReview: task.fromReview || task.from_review === 1, // Handle both camelCase and snake_case
1376 |         createdAt:
1377 |           typeof task.createdAt === 'number'
1378 |             ? new Date(task.createdAt * 1000).toISOString()
1379 |             : undefined,
1380 |         updatedAt:
1381 |           typeof task.updatedAt === 'number'
1382 |             ? new Date(task.updatedAt * 1000).toISOString()
1383 |             : undefined,
1384 |       }
1385 |     })
1386 | 
1387 |     // Log the first formatted task
1388 |     if (formattedTasks.length > 0) {
1389 |       await logToFile(
1390 |         `[processAndFinalizePlan] First formatted task for WebSocket (formattedTasks[0]): ${JSON.stringify(
1391 |           formattedTasks[0],
1392 |           null,
1393 |           2
1394 |         )}`,
1395 |         'debug'
1396 |       )
1397 |     } else {
1398 |       await logToFile(
1399 |         `[processAndFinalizePlan] No formatted tasks to log for WebSocket sample.`,
1400 |         'debug'
1401 |       )
1402 |     }
1403 | 
1404 |     webSocketService.notifyTasksUpdated(featureId, formattedTasks)
1405 |     logToFile(`[processAndFinalizePlan] WebSocket notification sent.`)
1406 |   } catch (wsError) {
1407 |     logToFile(
1408 |       `[processAndFinalizePlan] Error sending WebSocket notification: ${wsError}`
1409 |     )
1410 |     console.error(`[TaskServer] Error sending WebSocket update:`, wsError)
1411 |     // Do not re-throw WS errors, as the main operation succeeded
1412 |   }
1413 | 
1414 |   return finalTasks
1415 | }
1416 | 
1417 | /**
1418 |  * Detects if the LLM response contains a clarification request.
1419 |  * This function searches for both JSON-formatted clarification requests and
1420 |  * special prefix format like [CLARIFICATION_NEEDED].
1421 |  *
1422 |  * @param responseText The raw response from the LLM
1423 |  * @returns An object with success flag and either the parsed clarification request or error message
1424 |  */
1425 | export function detectClarificationRequest(
1426 |   responseText: string | null | undefined
1427 | ):
1428 |   | {
1429 |       detected: true
1430 |       clarificationRequest: z.infer<typeof LLMClarificationRequestSchema>
1431 |       rawResponse: string
1432 |     }
1433 |   | { detected: false; rawResponse: string | null } {
1434 |   if (!responseText) {
1435 |     return { detected: false, rawResponse: null }
1436 |   }
1437 | 
1438 |   // Check for [CLARIFICATION_NEEDED] format
1439 |   const prefixMatch = responseText.match(
1440 |     /\[CLARIFICATION_NEEDED\](.*?)(\[END_CLARIFICATION\]|$)/s
1441 |   )
1442 |   if (prefixMatch) {
1443 |     const questionText = prefixMatch[1].trim()
1444 | 
1445 |     // Parse out options if they exist
1446 |     const optionsMatch = questionText.match(/Options:\s*\[(.*?)\]/)
1447 |     const options = optionsMatch
1448 |       ? optionsMatch[1].split(',').map((o) => o.trim())
1449 |       : undefined
1450 | 
1451 |     // Check if text input is allowed
1452 |     const allowsText = !questionText.includes('MULTIPLE_CHOICE_ONLY')
1453 | 
1454 |     // Create a clarification request object
1455 |     return {
1456 |       detected: true,
1457 |       clarificationRequest: {
1458 |         type: 'clarification_needed',
1459 |         question: questionText
1460 |           .replace(/Options:\s*\[.*?\]/, '')
1461 |           .replace('MULTIPLE_CHOICE_ONLY', '')
1462 |           .trim(),
1463 |         options,
1464 |         allowsText,
1465 |       },
1466 |       rawResponse: responseText,
1467 |     }
1468 |   }
1469 | 
1470 |   // Try to parse as JSON
1471 |   try {
1472 |     // Check if we have a JSON object in the response
1473 |     const jsonMatch = responseText.match(/\{[\s\S]*\}/)
1474 |     if (jsonMatch) {
1475 |       const jsonStr = jsonMatch[0]
1476 |       const parsedJson = JSON.parse(jsonStr)
1477 | 
1478 |       // Check if it's a clarification request
1479 |       if (
1480 |         parsedJson.type === 'clarification_needed' ||
1481 |         parsedJson.clarification_needed ||
1482 |         parsedJson.needs_clarification
1483 |       ) {
1484 |         // Attempt to validate against our schema
1485 |         const result = LLMClarificationRequestSchema.safeParse({
1486 |           type: 'clarification_needed',
1487 |           question: parsedJson.question || parsedJson.message || '',
1488 |           options: parsedJson.options || undefined,
1489 |           allowsText: parsedJson.allowsText !== false,
1490 |         })
1491 | 
1492 |         if (result.success) {
1493 |           return {
1494 |             detected: true,
1495 |             clarificationRequest: result.data,
1496 |             rawResponse: responseText,
1497 |           }
1498 |         }
1499 |       }
1500 |     }
1501 | 
1502 |     return { detected: false, rawResponse: responseText }
1503 |   } catch (error) {
1504 |     // If JSON parsing fails, it's not a JSON-formatted clarification request
1505 |     return { detected: false, rawResponse: responseText }
1506 |   }
1507 | }
1508 | 
```
Page 3/3FirstPrevNextLast