#
tokens: 40843/50000 5/89 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/jhawkins11/task-manager-mcp?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

--------------------------------------------------------------------------------
/src/services/webSocketService.ts:
--------------------------------------------------------------------------------

```typescript
import { WebSocket, WebSocketServer } from 'ws'
import { UI_PORT } from '../config'
import { logToFile } from '../lib/logger'
import {
  WebSocketMessage,
  WebSocketMessageType,
  ClientRegistrationPayload,
  ErrorPayload,
  ShowQuestionPayload,
  QuestionResponsePayload,
  PlanFeatureResponseSchema,
  IntermediatePlanningState,
} from '../models/types'
import planningStateService from '../services/planningStateService'
import { aiService } from '../services/aiService'
import { OPENROUTER_MODEL, GEMINI_MODEL } from '../config'
import { addHistoryEntry } from '../lib/dbUtils'
import crypto from 'crypto'
import {
  processAndBreakdownTasks,
  ensureEffortRatings,
  processAndFinalizePlan,
} from '../lib/llmUtils'
import OpenAI from 'openai'
import { GenerativeModel } from '@google/generative-ai'
import { z } from 'zod'
import { databaseService } from '../services/databaseService'

interface WebSocketConnection {
  socket: WebSocket
  featureId?: string
  clientId?: string
  lastActivity: Date
}

class WebSocketService {
  private wss: WebSocketServer | null = null
  private connections: Map<WebSocket, WebSocketConnection> = new Map()
  private static instance: WebSocketService
  private isInitialized = false

  private constructor() {}

  /**
   * Returns the singleton instance of WebSocketService
   */
  public static getInstance(): WebSocketService {
    if (!WebSocketService.instance) {
      WebSocketService.instance = new WebSocketService()
    }
    return WebSocketService.instance
  }

  /**
   * Initializes the WebSocket server using an existing HTTP server
   *
   * @param httpServer The Node.js HTTP server instance from Express
   */
  public async initialize(httpServer: import('http').Server): Promise<void> {
    if (this.isInitialized) {
      await logToFile(
        '[WebSocketService] WebSocket server already initialized.'
      )
      return
    }

    try {
      // Attach WebSocket server to the existing HTTP server
      this.wss = new WebSocketServer({ server: httpServer })

      // Use UI_PORT for logging consistency if needed
      await logToFile(
        `[WebSocketService] WebSocket server attached to HTTP server on port ${UI_PORT}`
      )

      this.wss.on('connection', this.handleConnection.bind(this))
      this.wss.on('error', this.handleServerError.bind(this))

      // Set up connection cleanup interval (runs every minute)
      setInterval(this.cleanupInactiveConnections.bind(this), 60000)

      this.isInitialized = true
    } catch (error) {
      await logToFile(
        `[WebSocketService] Failed to initialize WebSocket server: ${error}`
      )
      throw error
    }
  }

  /**
   * Handles new WebSocket connections
   */
  private handleConnection(socket: WebSocket, _request: any): void {
    // Create a new connection entry
    const connection: WebSocketConnection = {
      socket,
      lastActivity: new Date(),
    }

    this.connections.set(socket, connection)

    logToFile(
      `[WebSocketService] New client connected. Total connections: ${this.connections.size}`
    )

    // Send a connection established message
    this.sendToSocket(socket, {
      type: 'connection_established',
    })

    // Set up event listeners for the socket
    socket.on('message', (data: Buffer) => this.handleMessage(socket, data))
    socket.on('close', () => this.handleDisconnect(socket))
    socket.on('error', (error) => this.handleSocketError(socket, error))
  }

  /**
   * Handles incoming WebSocket messages
   */
  private handleMessage(socket: WebSocket, data: Buffer): void {
    try {
      // Update last activity timestamp
      const connection = this.connections.get(socket)
      if (connection) {
        connection.lastActivity = new Date()
      }

      // Parse the message
      const message = JSON.parse(data.toString()) as WebSocketMessage

      // Handle client registration
      if (message.type === 'client_registration' && message.payload) {
        this.handleClientRegistration(
          socket,
          message.payload as ClientRegistrationPayload
        )
        return
      }

      // Handle question response
      if (message.type === 'question_response' && message.payload) {
        this.handleQuestionResponse(
          message.featureId || '',
          message.payload as QuestionResponsePayload
        )
        return
      }

      // Log the message type
      logToFile(`[WebSocketService] Received message of type: ${message.type}`)

      // Additional message handling logic can be added here
    } catch (error) {
      logToFile(`[WebSocketService] Error handling message: ${error}`)
      this.sendToSocket(socket, {
        type: 'error',
        payload: {
          code: 'MESSAGE_PARSING_ERROR',
          message: 'Failed to parse incoming message',
        } as ErrorPayload,
      })
    }
  }

  /**
   * Handles client registration messages
   */
  private handleClientRegistration(
    socket: WebSocket,
    payload: ClientRegistrationPayload
  ): void {
    const connection = this.connections.get(socket)

    if (connection) {
      connection.featureId = payload.featureId
      connection.clientId = payload.clientId || `client-${Date.now()}`

      logToFile(
        `[WebSocketService] Client registered: ${connection.clientId} for feature: ${connection.featureId}`
      )

      // Confirm registration to the client
      this.sendToSocket(socket, {
        type: 'client_registration',
        featureId: connection.featureId,
        payload: {
          featureId: connection.featureId,
          clientId: connection.clientId,
        },
      })
    }
  }

  /**
   * Handles socket disconnections
   */
  private handleDisconnect(socket: WebSocket): void {
    const connection = this.connections.get(socket)

    if (connection) {
      logToFile(
        `[WebSocketService] Client disconnected: ${
          connection.clientId || 'unknown'
        }`
      )
      this.connections.delete(socket)
    }
  }

  /**
   * Handles socket errors
   */
  private handleSocketError(socket: WebSocket, error: Error): void {
    const connection = this.connections.get(socket)

    logToFile(
      `[WebSocketService] Socket error for client ${
        connection?.clientId || 'unknown'
      }: ${error.message}`
    )

    // Try to send an error message to the client
    this.sendToSocket(socket, {
      type: 'error',
      payload: {
        code: 'SOCKET_ERROR',
        message: 'Socket error occurred',
      } as ErrorPayload,
    })

    // Close the connection after an error
    try {
      socket.terminate()
    } catch (closeError) {
      logToFile(`[WebSocketService] Error closing socket: ${closeError}`)
    }

    // Remove the connection from our map
    this.connections.delete(socket)
  }

  /**
   * Handles server errors
   */
  private async handleServerError(error: Error): Promise<void> {
    await logToFile(
      `[WebSocketService] WebSocket server error: ${error.message}`
    )
  }

  /**
   * Cleans up inactive connections
   */
  private cleanupInactiveConnections(): void {
    const now = new Date()
    const inactivityThreshold = 30 * 60 * 1000 // 30 minutes

    for (const [socket, connection] of this.connections.entries()) {
      const timeSinceLastActivity =
        now.getTime() - connection.lastActivity.getTime()

      if (timeSinceLastActivity > inactivityThreshold) {
        logToFile(
          `[WebSocketService] Closing inactive connection: ${
            connection.clientId || 'unknown'
          }`
        )

        try {
          socket.terminate()
        } catch (error) {
          logToFile(
            `[WebSocketService] Error terminating inactive socket: ${error}`
          )
        }

        this.connections.delete(socket)
      }
    }
  }

  /**
   * Sends a message to all connected clients for a specific feature
   */
  public broadcast(message: WebSocketMessage): void {
    if (!message.featureId) {
      logToFile('[WebSocketService] Cannot broadcast without featureId')
      return
    }

    let recipientCount = 0

    for (const [socket, connection] of this.connections.entries()) {
      // Only send to clients registered for this feature
      if (connection.featureId === message.featureId) {
        this.sendToSocket(socket, message)
        recipientCount++
      }
    }

    logToFile(
      `[WebSocketService] Broadcast message of type '${message.type}' to ${recipientCount} clients for feature: ${message.featureId}`
    )
  }

  /**
   * Sends a message to a specific socket
   */
  private sendToSocket(socket: WebSocket, message: WebSocketMessage): void {
    if (socket.readyState === WebSocket.OPEN) {
      try {
        socket.send(JSON.stringify(message))
      } catch (error) {
        logToFile(
          `[WebSocketService] Error sending message to socket: ${error}`
        )
      }
    }
  }

  /**
   * Gracefully shutdowns the WebSocket server
   */
  public async shutdown(): Promise<void> {
    if (!this.wss) {
      return
    }

    await logToFile('[WebSocketService] Shutting down WebSocket server...')

    // Close all connections
    for (const [socket] of this.connections.entries()) {
      try {
        socket.terminate()
      } catch (error) {
        await logToFile(
          `[WebSocketService] Error terminating socket during shutdown: ${error}`
        )
      }
    }

    this.connections.clear()

    // Close the server
    this.wss.close((error) => {
      if (error) {
        logToFile(`[WebSocketService] Error closing WebSocket server: ${error}`)
      } else {
        logToFile('[WebSocketService] WebSocket server closed successfully')
      }
    })

    this.wss = null
    this.isInitialized = false
  }

  /**
   * Broadcasts a task update notification for a feature
   */
  public notifyTasksUpdated(featureId: string, tasks: any): void {
    // Log tasks to help debug
    console.log(
      'WebSocketService.notifyTasksUpdated - Task fromReview values:',
      tasks.map((t: any) => ({ id: t.id, fromReview: !!t.fromReview }))
    )

    // Make sure fromReview is properly formatted for all tasks
    const formattedTasks = tasks.map((task: any) => ({
      ...task,
      fromReview: task.fromReview === 1 ? true : !!task.fromReview,
    }))

    this.broadcast({
      type: 'tasks_updated',
      featureId,
      payload: {
        tasks: formattedTasks,
        updatedAt: new Date().toISOString(),
      },
    })
  }

  /**
   * Broadcasts a task status change notification
   */
  public notifyTaskStatusChanged(
    featureId: string,
    taskId: string,
    status: 'pending' | 'completed' | 'decomposed'
  ): void {
    this.broadcast({
      type: 'status_changed',
      featureId,
      payload: {
        taskId,
        status,
        updatedAt: new Date().toISOString(),
      },
    })
  }

  /**
   * Broadcasts a notification when a task is created
   */
  public notifyTaskCreated(featureId: string, task: any): void {
    // Make sure fromReview is properly formatted
    const formattedTask = {
      ...task,
      fromReview: task.fromReview === 1 ? true : !!task.fromReview,
    }

    this.broadcast({
      type: 'task_created',
      featureId,
      payload: {
        task: formattedTask,
        featureId,
        createdAt: new Date().toISOString(),
      },
    })

    logToFile(
      `[WebSocketService] Broadcasted task_created for task ID: ${task.id}`
    )
  }

  /**
   * Broadcasts a notification when a task is updated
   */
  public notifyTaskUpdated(featureId: string, task: any): void {
    // Make sure fromReview is properly formatted
    const formattedTask = {
      ...task,
      fromReview: task.fromReview === 1 ? true : !!task.fromReview,
    }

    this.broadcast({
      type: 'task_updated',
      featureId,
      payload: {
        task: formattedTask,
        featureId,
        updatedAt: new Date().toISOString(),
      },
    })

    logToFile(
      `[WebSocketService] Broadcasted task_updated for task ID: ${task.id}`
    )
  }

  /**
   * Broadcasts a notification when a task is deleted
   */
  public notifyTaskDeleted(featureId: string, taskId: string): void {
    this.broadcast({
      type: 'task_deleted',
      featureId,
      payload: {
        taskId,
        featureId,
        deletedAt: new Date().toISOString(),
      },
    })

    logToFile(
      `[WebSocketService] Broadcasted task_deleted for task ID: ${taskId}`
    )
  }

  /**
   * Sends a question to UI clients
   */
  public sendQuestion(
    featureId: string,
    questionId: string,
    question: string,
    options?: string[],
    allowsText?: boolean
  ): void {
    try {
      if (!featureId || !questionId || !question) {
        logToFile(
          '[WebSocketService] Cannot send question: Missing required parameters'
        )
        return
      }

      // Check if any clients are connected for this feature
      let featureClients = 0
      for (const connection of this.connections.values()) {
        if (connection.featureId === featureId) {
          featureClients++
        }
      }

      // Log if no clients are available
      if (featureClients === 0) {
        logToFile(
          `[WebSocketService] Warning: Sending question ${questionId} to feature ${featureId} with no connected clients`
        )
      }

      this.broadcast({
        type: 'show_question',
        featureId,
        payload: {
          questionId,
          question,
          options,
          allowsText,
        } as ShowQuestionPayload,
      })

      logToFile(
        `[WebSocketService] Sent question to ${featureClients} clients for feature ${featureId}: ${question}`
      )
    } catch (error: any) {
      logToFile(`[WebSocketService] Error sending question: ${error.message}`)
    }
  }

  /**
   * Requests a screenshot from UI clients
   */
  public requestScreenshot(
    featureId: string,
    requestId: string,
    target?: string
  ): void {
    this.broadcast({
      type: 'request_screenshot',
      featureId,
      payload: {
        requestId,
        target,
      },
    })
  }

  /**
   * Handles user responses to questions
   */
  private async handleQuestionResponse(
    featureId: string,
    payload: QuestionResponsePayload
  ): Promise<void> {
    try {
      if (!featureId) {
        logToFile(
          '[WebSocketService] Cannot handle question response: Missing featureId'
        )
        return
      }

      const { questionId, response } = payload

      if (!questionId) {
        logToFile(
          '[WebSocketService] Cannot handle question response: Missing questionId'
        )
        this.broadcast({
          type: 'error',
          featureId,
          payload: {
            code: 'INVALID_RESPONSE',
            message: 'Invalid response format: missing questionId',
          } as ErrorPayload,
        })
        return
      }

      logToFile(
        `[WebSocketService] Received response to question ${questionId}: ${response}`
      )

      // Get the stored planning state
      const state = await planningStateService.getStateByQuestionId(questionId)

      if (!state) {
        logToFile(
          `[WebSocketService] No planning state found for question ${questionId}`
        )
        this.broadcast({
          type: 'error',
          featureId,
          payload: {
            code: 'QUESTION_EXPIRED',
            message: 'The question session has expired or is invalid.',
          } as ErrorPayload,
        })
        return
      }

      // Verify feature ID matches
      if (state.featureId !== featureId) {
        logToFile(
          `[WebSocketService] Feature ID mismatch: question belongs to ${state.featureId}, but response came from ${featureId}`
        )
        this.broadcast({
          type: 'error',
          featureId,
          payload: {
            code: 'FEATURE_MISMATCH',
            message:
              'Response came from a different feature than the question.',
          } as ErrorPayload,
        })
        return
      }

      // Add the response to history
      await addHistoryEntry(featureId, 'user', {
        questionId,
        question: state.partialResponse,
        response,
      })

      // Notify UI that response is being processed
      this.broadcast({
        type: 'status_changed',
        featureId,
        payload: {
          status: 'processing_response',
          questionId,
        },
      })

      // Resume planning/adjustment with the user's response
      try {
        const planningModel = aiService.getPlanningModel()
        if (!planningModel) {
          throw new Error('Planning model not available')
        }

        logToFile(
          `[WebSocketService] Resuming ${state.planningType} with user response for feature ${featureId}`
        )

        // Fetch feature information from database
        await databaseService.connect()
        const feature = await databaseService.getFeatureById(featureId)
        await databaseService.close()

        if (!feature) {
          throw new Error(`Feature with ID ${featureId} not found`)
        }

        // Get previous history entries for context
        await databaseService.connect()
        const history = await databaseService.getHistoryByFeatureId(
          featureId,
          10
        )
        await databaseService.close()

        // Extract original feature description
        const originalDescription =
          feature.description || 'Unknown feature description'

        // Create a comprehensive follow-up prompt with complete context
        let followUpPrompt = `You previously received this feature request: "${originalDescription}"

When planning this feature implementation, you asked for clarification with this question:
${state.partialResponse}

The user has now provided this answer to your question: "${response}"
`

        // Call the LLM with the comprehensive follow-up prompt
        if (planningModel instanceof OpenAI) {
          await this.processOpenRouterResumeResponse(
            planningModel,
            followUpPrompt,
            state,
            featureId,
            questionId
          )
        } else {
          await this.processGeminiResumeResponse(
            planningModel,
            followUpPrompt,
            state,
            featureId,
            questionId
          )
        }
      } catch (error: any) {
        logToFile(
          `[WebSocketService] Error resuming planning: ${error.message}`
        )

        // Notify clients of the error
        this.broadcast({
          type: 'error',
          featureId,
          payload: {
            code: 'RESUME_PLANNING_FAILED',
            message: `Failed to process your response: ${error.message}`,
          } as ErrorPayload,
        })

        // Add error history entry
        await addHistoryEntry(featureId, 'tool_response', {
          tool:
            state.planningType === 'feature_planning'
              ? 'plan_feature'
              : 'adjust_plan',
          status: 'failed_after_clarification',
          error: error.message,
        })
      }
    } catch (error: any) {
      logToFile(
        `[WebSocketService] Unhandled error in question response handler: ${error.message}`
      )
      if (featureId) {
        this.broadcast({
          type: 'error',
          featureId,
          payload: {
            code: 'INTERNAL_ERROR',
            message:
              'An internal error occurred while processing your response.',
          } as ErrorPayload,
        })
      }
    }
  }

  /**
   * Process OpenRouter response for resuming planning after clarification
   */
  private async processOpenRouterResumeResponse(
    model: OpenAI,
    prompt: string,
    state: IntermediatePlanningState,
    featureId: string,
    questionId: string
  ): Promise<void> {
    try {
      logToFile(
        `[WebSocketService] Calling OpenRouter with follow-up prompt for feature ${featureId}`
      )

      const result = await aiService.callOpenRouterWithSchema(
        OPENROUTER_MODEL,
        [{ role: 'user', content: prompt }],
        PlanFeatureResponseSchema,
        { temperature: 0.3, max_tokens: 4096 }
      )

      if (result.success) {
        await this.processPlanningSuccess(
          result.data,
          model,
          state,
          featureId,
          questionId
        )
      } else {
        throw new Error(
          `OpenRouter failed to generate a valid response: ${result.error}`
        )
      }
    } catch (error: any) {
      logToFile(
        `[WebSocketService] Error in OpenRouter response processing: ${error.message}`
      )
      throw error // Re-throw to be handled by the caller
    }
  }

  /**
   * Process Gemini response for resuming planning after clarification
   */
  private async processGeminiResumeResponse(
    model: GenerativeModel,
    prompt: string,
    state: IntermediatePlanningState,
    featureId: string,
    questionId: string
  ): Promise<void> {
    try {
      logToFile(
        `[WebSocketService] Calling Gemini with follow-up prompt for feature ${featureId}`
      )

      const result = await aiService.callGeminiWithSchema(
        GEMINI_MODEL,
        prompt,
        PlanFeatureResponseSchema,
        { temperature: 0.3 }
      )

      if (result.success) {
        await this.processPlanningSuccess(
          result.data,
          model,
          state,
          featureId,
          questionId
        )
      } else {
        throw new Error(
          `Gemini failed to generate a valid response: ${result.error}`
        )
      }
    } catch (error: any) {
      logToFile(
        `[WebSocketService] Error in Gemini response processing: ${error.message}`
      )
      throw error // Re-throw to be handled by the caller
    }
  }

  /**
   * Process successful planning result after clarification
   */
  private async processPlanningSuccess(
    data: z.infer<typeof PlanFeatureResponseSchema>,
    model: GenerativeModel | OpenAI,
    state: IntermediatePlanningState,
    featureId: string,
    questionId: string
  ): Promise<void> {
    try {
      // Check if tasks exist before processing
      if (!data.tasks) {
        logToFile(
          `[WebSocketService] Error: processPlanningSuccess called but response contained clarificationNeeded instead of tasks for feature ${featureId}`
        )
        // Optionally, you could try to handle the clarification again here, but throwing seems safer
        throw new Error(
          'processPlanningSuccess received clarification request, expected tasks.'
        )
      }

      // Process the tasks using schema data
      const rawPlanSteps = data.tasks.map(
        (task: { effort: string; description: string }) =>
          `[${task.effort}] ${task.description}`
      )

      // Log the result before detailed processing
      await addHistoryEntry(featureId, 'model', {
        step: 'resumed_planning_response',
        response: JSON.stringify(data),
      })

      logToFile(
        `[WebSocketService] Got ${rawPlanSteps.length} raw tasks after clarification for feature ${featureId}`
      )

      // Call the centralized processing function
      const finalTasks = await processAndFinalizePlan(
        rawPlanSteps,
        model,
        featureId
      )

      logToFile(
        `[WebSocketService] Processed ${finalTasks.length} final tasks after clarification for feature ${featureId}`
      )

      // Clean up the temporary state
      await planningStateService.clearState(questionId)

      // Add success history entry (notification/saving is now handled within processAndFinalizePlan)
      await addHistoryEntry(featureId, 'tool_response', {
        tool:
          state.planningType === 'feature_planning'
            ? 'plan_feature'
            : 'adjust_plan',
        status: 'completed_after_clarification',
        taskCount: finalTasks.length,
      })

      logToFile(
        `[WebSocketService] Successfully completed ${state.planningType} after clarification for feature ${featureId}`
      )
    } catch (error: any) {
      logToFile(
        `[WebSocketService] Error processing successful planning result: ${error.message}`
      )
      throw error // Re-throw to be handled by the caller
    }
  }
}

export default WebSocketService.getInstance()

```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import * as fsSync from 'fs'
import * as fs from 'fs/promises'
import { logToFile } from './lib/logger'
import logger from './lib/winstonLogger'
import { handleMarkTaskComplete } from './tools/markTaskComplete'
import { handlePlanFeature } from './tools/planFeature'
import { handleReviewChanges } from './tools/reviewChanges'
import { AdjustPlanInputSchema, AdjustPlanInput } from './models/types'
import { adjustPlanHandler } from './tools/adjustPlan'
import webSocketService from './services/webSocketService'
import planningStateService from './services/planningStateService'
// Re-add static imports
import express, { Request, Response, NextFunction } from 'express'
import path from 'path'
import crypto from 'crypto'

import { FEATURE_TASKS_DIR, UI_PORT } from './config'
import { Task } from './models/types'
import { detectClarificationRequest } from './lib/llmUtils'
import { databaseService } from './services/databaseService'
import { addHistoryEntry } from './lib/dbUtils'

// Immediately log that we're starting up
logger.info('Starting task manager server...')

// --- MCP Server Setup ---
logger.info('Setting up MCP Server instance...')
const server = new McpServer({
  name: 'task-manager-mcp',
  version: '0.6.3',
  description:
    'MCP Server using Google AI SDK and repomix for planning and review.',
  capabilities: {
    tools: { listChanged: false },
  },
})
logger.info('MCP Server instance created.')

// --- Tool Definitions ---
logger.info('Defining tools...')

// New 'get_next_task' tool
server.tool(
  'get_next_task',
  {
    featureId: z
      .string()
      .uuid({ message: 'Valid feature ID (UUID) is required.' }),
  },
  async (args, _extra) => {
    try {
      const { featureId } = args
      await logToFile(
        `[TaskServer] Handling get_next_task request for feature: ${featureId}`
      )

      // 1. Read tasks for the given feature ID
      await databaseService.connect()
      const tasks = await databaseService.getTasksByFeatureId(featureId)
      await databaseService.close()

      if (tasks.length === 0) {
        const message = `No tasks found for feature ID ${featureId}. The feature may not exist or has not been planned yet.`
        await logToFile(`[TaskServer] ${message}`)
        return {
          content: [{ type: 'text', text: message }],
          isError: false,
        }
      }

      // 2. Find the first pending task in the list
      const nextTask = tasks.find((task) => task.status === 'pending')

      if (!nextTask) {
        const message = `All tasks have been completed for feature ${featureId}.`
        await logToFile(`[TaskServer] ${message}`)
        return {
          content: [{ type: 'text', text: message }],
          isError: false,
        }
      }

      // 3. Format response with task details
      // Include effort in the message if available
      const effortInfo = nextTask.effort ? ` (Effort: ${nextTask.effort})` : ''

      // Include parent info if this is a subtask
      let parentInfo = ''
      if (nextTask.parent_task_id) {
        // Find the parent task
        const parentTask = tasks.find((t) => t.id === nextTask.parent_task_id)
        if (parentTask) {
          const parentDesc =
            parentTask.description && parentTask.description.length > 30
              ? parentTask.description.substring(0, 30) + '...'
              : parentTask.description || '' // Use empty string if description is undefined
          parentInfo = ` (Subtask of: "${parentDesc}")`
        } else {
          parentInfo = ` (Subtask of parent ID: ${nextTask.parent_task_id})` // Fallback if parent not found
        }
      }

      // Embed ID, description, effort, and parent info in the text message
      const message = `Next pending task (ID: ${
        nextTask.id
      })${effortInfo}${parentInfo}: ${
        nextTask.title !== nextTask.description
          ? `${nextTask.title}: ${nextTask.description}`
          : nextTask.description
      }`

      await logToFile(`[TaskServer] Found next task: ${nextTask.id}`)

      return {
        content: [{ type: 'text', text: message }],
        isError: false,
      }
    } catch (error: any) {
      const errorMsg = `Error processing get_next_task request: ${
        error instanceof Error ? error.message : String(error)
      }`
      logger.error(errorMsg)
      await logToFile(`[TaskServer] ${errorMsg}`)

      return {
        content: [{ type: 'text', text: errorMsg }],
        isError: true,
      }
    }
  }
)

// 1. Tool: mark_task_complete
server.tool(
  'mark_task_complete',
  {
    task_id: z.string().uuid({ message: 'Valid task ID (UUID) is required.' }),
    feature_id: z
      .string()
      .uuid({ message: 'Valid feature ID (UUID) is required.' }),
  },
  async (args, _extra) => {
    const result = await handleMarkTaskComplete(args)
    // Transform the content to match SDK expected format
    return {
      content: result.content.map((item) => ({
        type: item.type as 'text',
        text: item.text,
      })),
      isError: result.isError,
    }
  }
)

// 2. Tool: plan_feature
server.tool(
  'plan_feature',
  {
    feature_description: z.string().min(10, {
      message: 'Feature description must be at least 10 characters.',
    }),
    project_path: z
      .string()
      .describe(
        'The absolute path to the project directory to scan with repomix. '
      ),
  },
  async (args, _extra) => {
    const result = await handlePlanFeature(args)
    // Transform the content to match SDK expected format
    // Since handlePlanFeature now always returns Array<{type: 'text', text: string}>
    // we can use a simple map.
    return {
      content: result.content.map((item) => ({
        type: 'text',
        text: item.text,
      })),
      isError: result.isError,
    }
  }
)

// 3. Tool: review_changes
server.tool(
  'review_changes',
  {
    project_path: z
      .string()
      .optional()
      .describe(
        'The absolute path to the project directory where git commands should run. Defaults to the workspace root if not provided.'
      ),
    featureId: z
      .string()
      .uuid({ message: 'Valid feature ID (UUID) is required.' }),
  },
  async (args, _extra) => {
    // Pass the project_path argument to the handler
    const result = await handleReviewChanges({
      featureId: args.featureId,
      project_path: args.project_path,
    })
    // Transform the content to match SDK expected format
    return {
      content: result.content.map((item) => ({
        type: item.type as 'text',
        text: item.text,
      })),
      isError: result.isError,
    }
  }
)

// 4. Tool: adjust_plan
server.tool(
  'adjust_plan',
  {
    featureId: z
      .string()
      .uuid({ message: 'Valid feature ID (UUID) is required.' }),
    adjustment_request: z
      .string()
      .min(1, { message: 'Adjustment request cannot be empty.' }),
  },
  async (args: AdjustPlanInput, _extra) => {
    const result = await adjustPlanHandler(args)

    return {
      content: [{ type: 'text', text: result.message }],
      isError: result.status === 'error',
    }
  }
)

logger.info('Tools defined.')

// --- Error Handlers ---
// Add top-level error handler for synchronous errors during load
process.on('uncaughtException', (error) => {
  // Cannot reliably use async logToFile here
  logger.error('Uncaught Exception:', error)
  // Use synchronous append for critical errors before exit
  try {
    const logDir = process.env.LOG_DIR || './logs'
    const logFile = process.env.LOG_FILE || `${logDir}/debug.log`
    fsSync.mkdirSync(logDir, { recursive: true })
    fsSync.appendFileSync(
      logFile,
      `${new Date().toISOString()} - [TaskServer] FATAL: Uncaught Exception: ${
        error?.message || error
      }\n${error?.stack || ''}\n`
    )
  } catch (logErr) {
    logger.error('Error writing uncaughtException to sync log:', logErr)
  }
  process.exit(1)
})

process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
  try {
    const logDir = process.env.LOG_DIR || './logs'
    const logFile = process.env.LOG_FILE || `${logDir}/debug.log`
    fsSync.mkdirSync(logDir, { recursive: true })
    fsSync.appendFileSync(
      logFile,
      `${new Date().toISOString()} - [TaskServer] FATAL: Unhandled Rejection: ${reason}\n`
    )
  } catch (logErr) {
    logger.error('Error writing unhandledRejection to sync log:', logErr)
  }
  process.exit(1)
})

// Helper function to list all features
async function listFeatures() {
  try {
    // Ensure features directory exists
    await fs.mkdir(FEATURE_TASKS_DIR, { recursive: true })

    // Read all files in the features directory
    const files = await fs.readdir(FEATURE_TASKS_DIR)

    // Filter for task files (ending with _mcp_tasks.json)
    const taskFiles = files.filter((file) => file.endsWith('_mcp_tasks.json'))

    // Extract feature IDs from filenames
    const featureIds = taskFiles.map((file) =>
      file.replace('_mcp_tasks.json', '')
    )

    return featureIds
  } catch (error) {
    logger.error('Error listing features:', error)
    return []
  }
}

// Helper function to format a task for the frontend
function formatTaskForFrontend(task: any, featureId: string) {
  return {
    id: task.id,
    title: task.title || task.description,
    description: task.description,
    status: task.status,
    completed: task.status === 'completed' || Boolean(task.completed),
    effort: task.effort,
    feature_id: featureId,
    // Convert from snake_case to camelCase for frontend compatibility
    parentTaskId: task.parent_task_id,
    createdAt:
      typeof task.created_at === 'number'
        ? new Date(task.created_at * 1000).toISOString()
        : task.createdAt,
    updatedAt:
      typeof task.updated_at === 'number'
        ? new Date(task.updated_at * 1000).toISOString()
        : task.updatedAt,
    fromReview: Boolean(task.fromReview || task.from_review === 1),
  }
}

// --- Server Start ---
async function main() {
  logger.info('Entering main function...')

  // Initialize database *before* starting other services
  try {
    logger.info('Initializing database...')
    await databaseService.initializeDatabase()
    logger.info('Database initialized successfully.')
  } catch (dbError) {
    logger.error(
      'FATAL: Failed to initialize database. Server cannot start.',
      dbError
    )
    process.exit(1) // Exit if database fails to initialize
  }

  await logToFile('[TaskServer] LOG: main() started.')
  logger.info('Main function started')

  try {
    // --- Express Server Setup --- Moved inside main, after MCP connect
    const app = express()
    const PORT = process.env.PORT || UI_PORT || 4999

    // HTTP request logging middleware
    app.use((req: Request, res: Response, next: NextFunction) => {
      const start = new Date().getTime()

      res.on('finish', () => {
        const duration = new Date().getTime() - start
        logger.info({
          method: req.method,
          url: req.url,
          status: res.statusCode,
          duration: `${duration}ms`,
        })
      })

      next()
    })

    // --- MCP Server Connection --- Moved after Express init
    await logToFile('[TaskServer] LOG: Creating transport...')
    const transport = new StdioServerTransport()
    await logToFile('[TaskServer] LOG: Transport created.')

    await logToFile('[TaskServer] LOG: Connecting server to transport...')
    await server.connect(transport)
    await logToFile(
      '[TaskServer] LOG: MCP Task Manager Server connected and running on stdio...'
    )

    // Setup API endpoints

    // Get list of features
    app.get('/api/features', (req: Request, res: Response) => {
      ;(async () => {
        try {
          const featureIds = await listFeatures()
          res.json(featureIds)
        } catch (error: any) {
          logger.error(`Failed to fetch features: ${error?.message || error}`)
          await logToFile(
            `[TaskServer] ERROR fetching features: ${error?.message || error}`
          )
          res.status(500).json({ error: 'Failed to fetch features' })
        }
      })()
    })

    // Get tasks for a specific feature
    app.get('/api/tasks/:featureId', (req: Request, res: Response) => {
      ;(async () => {
        const { featureId } = req.params
        try {
          await databaseService.connect()
          const tasks = await databaseService.getTasksByFeatureId(featureId)
          await databaseService.close()

          // Use the helper function to format tasks
          const formattedTasks = tasks.map((task) =>
            formatTaskForFrontend(task, featureId)
          )

          res.json(formattedTasks)
        } catch (error: any) {
          logger.error(
            `Failed to fetch tasks for feature ${featureId}: ${
              error?.message || error
            }`
          )
          await logToFile(
            `[TaskServer] ERROR fetching tasks for feature ${featureId}: ${
              error?.message || error
            }`
          )
          res.status(500).json({ error: 'Failed to fetch tasks' })
        }
      })()
    })

    // Parse JSON bodies for API requests
    app.use(express.json())

    // POST: Create a new task
    app.post('/api/tasks', (req: Request, res: Response) => {
      ;(async () => {
        try {
          const { featureId, description, title, effort } = req.body

          // Use title if description is missing
          const taskDescription = description || title

          if (!featureId || !taskDescription) {
            return res.status(400).json({
              error:
                'Missing required fields: featureId and title are required',
            })
          }

          // Read existing tasks
          await databaseService.connect()
          const tasks = await databaseService.getTasksByFeatureId(featureId)

          // Create a new task with a UUID
          const now = Math.floor(Date.now() / 1000)
          const newTask = {
            id: crypto.randomUUID(),
            description: taskDescription,
            title: title || taskDescription, // Use title or derived description
            status: 'pending' as const,
            completed: false,
            effort: effort as 'low' | 'medium' | 'high' | undefined,
            feature_id: featureId,
            created_at: now,
            updated_at: now,
          }

          // Convert to DB format and add to database
          await databaseService.addTask(newTask)

          // Add the new task to the list for WS notifications
          tasks.push(newTask)

          // Notify clients via WebSocket - both general task update and specific creation event
          webSocketService.broadcast({
            type: 'tasks_updated',
            featureId,
            payload: {
              tasks: tasks.map((task) =>
                formatTaskForFrontend(task, featureId)
              ),
              updatedAt: new Date().toISOString(),
            },
          })

          // Also send a specific task created notification
          webSocketService.notifyTaskCreated(
            featureId,
            formatTaskForFrontend(newTask, featureId)
          )

          await databaseService.close()
          res.status(201).json(formatTaskForFrontend(newTask, featureId))
        } catch (error: any) {
          logger.error(`Failed to create task: ${error?.message || error}`)
          await logToFile(
            `[TaskServer] ERROR creating task: ${error?.message || error}`
          )
          res.status(500).json({ error: 'Failed to create task' })
        }
      })()
    })

    // PUT: Update an existing task
    app.put('/api/tasks/:taskId', (req: Request, res: Response) => {
      ;(async () => {
        try {
          const { taskId } = req.params
          const { featureId, description, title, status, completed, effort } =
            req.body

          if (!featureId) {
            return res
              .status(400)
              .json({ error: 'Missing required field: featureId' })
          }

          // Read existing tasks
          await databaseService.connect()

          // Check if task exists
          const task = await databaseService.getTaskById(taskId)

          if (!task) {
            await databaseService.close()
            return res.status(404).json({ error: 'Task not found' })
          }

          // First determine what kind of update we need (status or details)
          if (status || completed !== undefined) {
            // Status update
            const newStatus = status || task.status
            const isCompleted =
              completed !== undefined ? completed : task.completed
            await databaseService.updateTaskStatus(
              taskId,
              newStatus,
              isCompleted
            )
          }

          // If we have other fields to update, do that as well
          if (title || description || effort) {
            await databaseService.updateTaskDetails(taskId, {
              title: title,
              description: description,
              effort: effort as 'low' | 'medium' | 'high' | undefined,
            })
          }

          // Get updated tasks for WebSocket notification
          const tasks = await databaseService.getTasksByFeatureId(featureId)
          const updatedTask = await databaseService.getTaskById(taskId)

          // Notify clients via WebSocket - both general task update and specific update event
          webSocketService.broadcast({
            type: 'tasks_updated',
            featureId,
            payload: {
              tasks: tasks.map((task) =>
                formatTaskForFrontend(task, featureId)
              ),
              updatedAt: new Date().toISOString(),
            },
          })

          // Send a specific task updated notification
          webSocketService.notifyTaskUpdated(
            featureId,
            formatTaskForFrontend(updatedTask!, featureId)
          )

          // Also send a status change notification if the status was updated
          if (status) {
            webSocketService.notifyTaskStatusChanged(featureId, taskId, status)
          }

          await databaseService.close()
          res.json(formatTaskForFrontend(updatedTask!, featureId))
        } catch (error: any) {
          logger.error(`Failed to update task: ${error?.message || error}`)
          await logToFile(
            `[TaskServer] ERROR updating task: ${error?.message || error}`
          )
          res.status(500).json({ error: 'Failed to update task' })
        }
      })()
    })

    // DELETE: Remove a task
    app.delete('/api/tasks/:taskId', (req: Request, res: Response) => {
      ;(async () => {
        try {
          const { taskId } = req.params
          const { featureId } = req.query

          if (!featureId) {
            return res
              .status(400)
              .json({ error: 'Missing required query parameter: featureId' })
          }

          // Connect to database
          await databaseService.connect()

          // Get the task before deletion for the response
          const task = await databaseService.getTaskById(taskId)

          if (!task) {
            await databaseService.close()
            return res.status(404).json({ error: 'Task not found' })
          }

          // Delete the task
          const deleted = await databaseService.deleteTask(taskId)

          if (!deleted) {
            await databaseService.close()
            return res.status(404).json({ error: 'Failed to delete task' })
          }

          // Get updated tasks for WebSocket notification
          const remainingTasks = await databaseService.getTasksByFeatureId(
            featureId as string
          )

          // Notify clients via WebSocket - both general task update and specific deletion event
          webSocketService.broadcast({
            type: 'tasks_updated',
            featureId: featureId as string,
            payload: {
              tasks: remainingTasks.map((task) =>
                formatTaskForFrontend(task, featureId as string)
              ),
              updatedAt: new Date().toISOString(),
            },
          })

          // Send a specific task deleted notification
          webSocketService.notifyTaskDeleted(featureId as string, taskId)

          await databaseService.close()
          res.json({
            message: 'Task deleted successfully',
            task: formatTaskForFrontend(task, featureId as string),
          })
        } catch (error: any) {
          logger.error(`Failed to delete task: ${error?.message || error}`)
          await logToFile(
            `[TaskServer] ERROR deleting task: ${error?.message || error}`
          )
          res.status(500).json({ error: 'Failed to delete task' })
        }
      })()
    })

    // Get pending question for a specific feature
    app.get(
      '/api/features/:featureId/pending-question',
      (req: Request, res: Response) => {
        ;(async () => {
          const { featureId } = req.params
          try {
            const state = await planningStateService.getStateByFeatureId(
              featureId
            )
            if (state && state.partialResponse) {
              // Attempt to parse the stored partialResponse as JSON
              let parsedData: any
              try {
                parsedData = JSON.parse(state.partialResponse)
              } catch (parseError) {
                logToFile(
                  `[TaskServer] Error parsing partialResponse JSON for feature ${featureId}: ${parseError}. Content: ${state.partialResponse}`
                )
                res.json(null) // Cannot parse the stored state
                return
              }

              // Check if the parsed data contains the clarificationNeeded structure
              if (parsedData && parsedData.clarificationNeeded) {
                const clarification = parsedData.clarificationNeeded
                logToFile(
                  `[TaskServer] Found pending question ${state.questionId} for feature ${featureId}`
                )
                res.json({
                  questionId: state.questionId, // Use the ID from the stored state
                  question: clarification.question,
                  options: clarification.options,
                  allowsText: clarification.allowsText,
                })
              } else {
                logToFile(
                  `[TaskServer] State found for feature ${featureId}, but partialResponse JSON did not contain 'clarificationNeeded'. Content: ${state.partialResponse}`
                )
                res.json(null) // Parsed data structure unexpected
              }
            } else {
              logToFile(
                `[TaskServer] No pending question found for feature ${featureId}`
              )
              res.json(null) // No pending question found
            }
          } catch (error: any) {
            logToFile(
              `[TaskServer] ERROR fetching pending question for feature ${featureId}: ${
                error?.message || error
              }`
            )
            res.status(500).json({ error: 'Failed to fetch pending question' })
          }
        })()
      }
    )

    // Default endpoint to get tasks from most recent feature
    app.get('/api/tasks', (req: Request, res: Response) => {
      ;(async () => {
        try {
          const featureIds = await listFeatures()

          if (featureIds.length === 0) {
            // Return empty array if no features exist
            return res.json([])
          }

          // Sort feature IDs by creation time (using file stats)
          const featuresWithStats = await Promise.all(
            featureIds.map(async (featureId) => {
              const filePath = path.join(
                FEATURE_TASKS_DIR,
                `${featureId}_mcp_tasks.json`
              )
              const stats = await fs.stat(filePath)
              return { featureId, mtime: stats.mtime }
            })
          )

          // Sort by most recent modification time
          featuresWithStats.sort(
            (a, b) => b.mtime.getTime() - a.mtime.getTime()
          )

          // Get tasks for the most recent feature
          const mostRecentFeatureId = featuresWithStats[0].featureId
          await databaseService.connect()
          const tasks = await databaseService.getTasksByFeatureId(
            mostRecentFeatureId
          )
          await databaseService.close()

          // Use the helper function to format tasks
          const formattedTasks = tasks.map((task) =>
            formatTaskForFrontend(task, mostRecentFeatureId)
          )

          res.json(formattedTasks)
        } catch (error: any) {
          await logToFile(
            `[TaskServer] ERROR fetching default tasks: ${
              error?.message || error
            }`
          )
          res.status(500).json({ error: 'Failed to fetch tasks' })
        }
      })()
    })

    // Serve static frontend files
    const staticFrontendPath = path.join(__dirname, 'frontend-ui')
    app.use(express.static(staticFrontendPath))

    // Catch-all route to serve the SPA for any unmatched routes
    app.get('*', (req: Request, res: Response) => {
      res.sendFile(path.join(staticFrontendPath, 'index.html'))
    })

    // Start the Express server and capture the HTTP server instance
    const httpServer = app.listen(PORT, () => {
      const url = `http://localhost:${PORT}`
      logger.info(`Frontend server running at ${url}`)
    })

    // Initialize WebSocket service with the HTTP server instance
    try {
      await webSocketService.initialize(httpServer)
      await logToFile(
        '[TaskServer] LOG: WebSocket server attached to HTTP server.'
      )
    } catch (wsError) {
      await logToFile(
        `[TaskServer] WARN: Failed to initialize WebSocket server: ${wsError}`
      )
      logger.error('WebSocket server initialization failed:', wsError)
      // Decide if this is fatal or can continue
    }

    // Handle process termination gracefully
    process.on('SIGINT', async () => {
      await logToFile(
        '[TaskServer] LOG: Received SIGINT. Shutting down gracefully...'
      )

      // Shutdown WebSocket server
      try {
        await webSocketService.shutdown()
        await logToFile(
          '[TaskServer] LOG: WebSocket server shut down successfully.'
        )
      } catch (error) {
        await logToFile(
          `[TaskServer] ERROR: Error shutting down WebSocket server: ${error}`
        )
      }

      process.exit(0)
    })

    process.on('SIGTERM', async () => {
      await logToFile(
        '[TaskServer] LOG: Received SIGTERM. Shutting down gracefully...'
      )

      // Shutdown WebSocket server
      try {
        await webSocketService.shutdown()
        await logToFile(
          '[TaskServer] LOG: WebSocket server shut down successfully.'
        )
      } catch (error) {
        await logToFile(
          `[TaskServer] ERROR: Error shutting down WebSocket server: ${error}`
        )
      }

      process.exit(0)
    })
  } catch (connectError) {
    logger.error('CRITICAL ERROR during server.connect():', connectError)
    process.exit(1)
  }
}

logger.info('Script execution reaching end of top-level code.')
main().catch((error) => {
  logger.error('CRITICAL ERROR executing main():', error)
  try {
    const logDir = process.env.LOG_DIR || './logs'
    const logFile = process.env.LOG_FILE || `${logDir}/debug.log`
    fsSync.mkdirSync(logDir, { recursive: true })
    fsSync.appendFileSync(
      logFile,
      `${new Date().toISOString()} - [TaskServer] CRITICAL ERROR executing main(): ${
        error?.message || error
      }\n${error?.stack || ''}\n`
    )
  } catch (logErr) {
    logger.error('Error writing main() catch to sync log:', logErr)
  }
  process.exit(1) // Exit if main promise rejects
})

```

--------------------------------------------------------------------------------
/src/tools/planFeature.ts:
--------------------------------------------------------------------------------

```typescript
import path from 'path'
import crypto from 'crypto'
import {
  Task,
  PlanFeatureResponseSchema,
  PlanningOutputSchema,
  PlanningTaskSchema,
} from '../models/types'
import { promisify } from 'util'
import { aiService } from '../services/aiService'
import {
  parseGeminiPlanResponse,
  extractParentTaskId,
  extractEffort,
  ensureEffortRatings,
  processAndBreakdownTasks,
  detectClarificationRequest,
  processAndFinalizePlan,
} from '../lib/llmUtils'
import { logToFile } from '../lib/logger'
import fs from 'fs/promises'
import { exec } from 'child_process'
import * as fsSync from 'fs'
import { encoding_for_model } from 'tiktoken'
import webSocketService from '../services/webSocketService'
import { z } from 'zod'
import { GEMINI_MODEL, OPENROUTER_MODEL, WS_PORT, UI_PORT } from '../config'
import { dynamicImportDefault } from '../lib/utils'
import planningStateService from '../services/planningStateService'
import { databaseService } from '../services/databaseService'
import { addHistoryEntry } from '../lib/dbUtils'
import { getCodebaseContext } from '../lib/repomixUtils'

// Promisify child_process.exec for easier async/await usage
const execPromise = promisify(exec)

interface PlanFeatureParams {
  feature_description: string
  project_path: string
}

// Revert interface to only expect text content for SDK compatibility
interface PlanFeatureResult {
  content: Array<{ type: 'text'; text: string }>
  isError?: boolean
}

// Define a standard structure for the serialized response
interface PlanFeatureStandardResponse {
  status: 'completed' | 'awaiting_clarification' | 'error'
  message: string
  featureId: string
  data?: any // For clarification details or potentially first task info
  uiUrl?: string // Include UI URL for convenience
  firstTask?: {
    id: string
    description: string
    effort: string
  }
}

/**
 * Handles the plan_feature tool request
 */
export async function handlePlanFeature(
  params: PlanFeatureParams
): Promise<PlanFeatureResult> {
  const { feature_description, project_path } = params

  // Generate a unique feature ID *first*
  const featureId = crypto.randomUUID()
  // Define UI URL early
  const uiUrl = `http://localhost:${UI_PORT || 4999}?featureId=${featureId}`
  let browserOpened = false // Flag to track if browser was opened for clarification

  await logToFile(
    `[TaskServer] Handling plan_feature request: "${feature_description}" (Path: ${
      project_path || 'CWD'
    }), Feature ID: ${featureId}`
  )

  // Define message and isError outside the try block to ensure they are always available
  let message: string
  let isError = false
  let task_count: number | undefined = undefined // Keep track of task count

  try {
    // Record tool call in history *early*
    await addHistoryEntry(featureId, 'tool_call', {
      tool: 'plan_feature',
      params: { feature_description, project_path },
    })

    // Create the feature record with project_path
    try {
      await databaseService.createFeature(
        featureId,
        feature_description,
        project_path
      )
      await logToFile(
        `[TaskServer] Created feature record with ID: ${featureId}, Project Path: ${project_path}`
      )
    } catch (featureError) {
      await logToFile(
        `[TaskServer] Error creating feature record: ${featureError}`
      )
      // Continue even if feature creation fails - we can recover later
    }

    const planningModel = aiService.getPlanningModel()

    if (!planningModel) {
      await logToFile(
        '[TaskServer] Planning model not initialized (check API key).'
      )
      message = 'Error: Planning model not initialized. Check API Key.'
      isError = true

      // Record error in history, handling potential logging errors
      try {
        await addHistoryEntry(featureId, 'tool_response', {
          tool: 'plan_feature',
          isError: true,
          message,
        })
      } catch (historyError) {
        console.error(
          `[TaskServer] Failed to add error history entry during model init failure: ${historyError}`
        )
      }

      // Return the structured error object *serialized*
      const errorResponse: PlanFeatureStandardResponse = {
        status: 'error',
        message: message,
        featureId: featureId, // Include featureId even in early errors
      }
      return {
        content: [{ type: 'text', text: JSON.stringify(errorResponse) }],
        isError,
      }
    }

    // --- Get Codebase Context using Utility Function ---
    const targetDir = project_path || '.' // Keep targetDir logic
    const { context: codebaseContext, error: contextError } =
      await getCodebaseContext(
        targetDir,
        featureId // Use featureId as log context
      )

    // Handle potential errors from getCodebaseContext
    if (contextError) {
      message = contextError // Use the user-friendly error from the utility
      isError = true
      // Record error in history, handling potential logging errors
      try {
        await addHistoryEntry(featureId, 'tool_response', {
          tool: 'plan_feature',
          isError: true,
          message,
          step: 'repomix_context_gathering',
        })
      } catch (historyError) {
        console.error(
          `[TaskServer] Failed to add error history entry during context gathering failure: ${historyError}`
        )
      }
      return { content: [{ type: 'text', text: message }], isError }
    }

    // Optional: Add token counting / compression logic here if needed,
    // operating on the returned `codebaseContext`.
    // This part is kept separate from the core getCodebaseContext utility for now.
    // ... (Token counting and compression logic could go here)

    // --- LLM Planning & Task Generation ---
    let planSteps: string[] = []

    try {
      await logToFile('[TaskServer] Calling LLM API for planning...')
      const contextPromptPart = codebaseContext
        ? `Based on the following codebase context:\n\`\`\`\n${codebaseContext}\n\`\`\`\n\n`
        : 'Based on the provided feature description (no codebase context available):\n\n'

      // Define the structured planning prompt incorporating the new schema
      const structuredPlanningPrompt = `${contextPromptPart}Generate a detailed, step-by-step coding implementation plan for the feature: \"${feature_description}\".
      
        The plan should ONLY include actionable tasks a developer needs to perform within the code. Exclude steps related to project management, deployment, manual testing, documentation updates, or obtaining approvals.
        
        For each coding task, include an effort rating (low, medium, or high) based on implementation work involved. High effort tasks often require breakdown.
        
        Use these effort definitions:
        - Low: Simple, quick changes in one or few files, minimal logic changes.
        - Medium: Requires moderate development time, involves changes across several files/components, includes writing new functions/classes.
        - High: Significant development time, complex architectural changes, intricate algorithms, deep refactoring.
        
        **RESPONSE FORMAT:**
        You MUST respond with a single JSON object.
        
        **Case 1: Clarification Needed**
        If you require clarification before creating the plan, respond with this JSON structure:
        \`\`\`json
        {\n          "clarificationNeeded": {\n            "question": "Your specific question here. Be precise.",
        \n            "options": ["Option A", "Option B"] // Optional: Provide options if helpful
        \n            "allowsText": true // Optional: Set to false if only options are valid
        \n          }\n        }\`\`\`
        
        **Case 2: No Clarification Needed**
        If you DO NOT need clarification, respond with this JSON structure, containing a non-empty array of tasks:
        \`\`\`json
        {\n          "tasks": [\n            { "description": "Description of the first task", "effort": "low" | "medium" | "high" },\n            { "description": "Description of the second task", "effort": "low" | "medium" | "high" },\n            ...\n          ]\n        }\`\`\`
        
        **IMPORTANT:** Respond *only* with the valid JSON object conforming to one of the two cases described above. Do not include any introductory text, concluding remarks, or markdown formatting outside the JSON structure.`

      // Log truncated structured prompt for history
      await addHistoryEntry(featureId, 'model', {
        step: 'structured_planning_prompt',
        prompt: `Generate structured plan or clarification for: "${feature_description}" ${
          codebaseContext ? '(with context)' : '(no context)'
        }...`,
      })

      if ('chat' in planningModel) {
        // It's OpenRouter - Use structured output
        const structuredResult = await aiService.callOpenRouterWithSchema(
          OPENROUTER_MODEL,
          [{ role: 'user', content: structuredPlanningPrompt }],
          PlanFeatureResponseSchema,
          { temperature: 0.7 }
        )

        logToFile(
          `[TaskServer] Structured result (OpenRouter): ${JSON.stringify(
            structuredResult
          )}`
        )

        if (structuredResult.success) {
          // Check if clarification is needed
          if (structuredResult.data.clarificationNeeded) {
            logToFile(
              '[TaskServer] Clarification needed based on structured response.'
            )
            const clarification = structuredResult.data.clarificationNeeded

            // Open the browser *now* to show the question
            try {
              logToFile(`[TaskServer] Launching UI for clarification: ${uiUrl}`)
              const open = await dynamicImportDefault('open')
              await open(uiUrl)
              browserOpened = true // Mark browser as opened
              logToFile(
                '[TaskServer] Browser launch for clarification initiated.'
              )
            } catch (openError: any) {
              logToFile(
                `[TaskServer] Error launching browser for clarification: ${openError.message}`
              )
              // Continue even if browser launch fails, WS should still work if UI is open
            }

            // Store the intermediate state
            const questionId =
              await planningStateService.storeIntermediateState(
                featureId,
                structuredPlanningPrompt,
                JSON.stringify(structuredResult.data),
                'feature_planning'
              )
            // Send WebSocket message
            webSocketService.broadcast({
              type: 'show_question',
              featureId,
              payload: {
                questionId,
                question: clarification.question,
                options: clarification.options,
                allowsText: clarification.allowsText,
              },
            })
            // Record in history
            await addHistoryEntry(featureId, 'tool_response', {
              tool: 'plan_feature',
              status: 'awaiting_clarification',
              questionId,
            })
            // Return structured clarification info *serialized as text*
            const clarificationData = {
              questionId: questionId,
              question: clarification.question,
              options: clarification.options,
              allowsText: clarification.allowsText,
            }
            const clarificationResponse: PlanFeatureStandardResponse = {
              status: 'awaiting_clarification',
              message: `Planning paused for feature ${featureId}. User clarification needed via UI (${uiUrl}). Once submitted, call 'get_next_task' with featureId '${featureId}' to retrieve the first task.`,
              featureId: featureId,
              data: clarificationData,
              uiUrl: uiUrl,
            }
            return {
              // Serialize the standard response structure
              content: [
                { type: 'text', text: JSON.stringify(clarificationResponse) },
              ],
              isError: false, // Not an error, just waiting
            }
          } else if (structuredResult.data.tasks) {
            logToFile('[TaskServer] Tasks received in structured response.')
            // Convert the structured response to the expected format for processing
            planSteps = structuredResult.data.tasks.map(
              (task) => `[${task.effort}] ${task.description}`
            )
            await addHistoryEntry(featureId, 'model', {
              step: 'structured_planning_response',
              response: JSON.stringify(structuredResult.data),
            })
          } else {
            // Schema validation should prevent this, but handle defensively
            throw new Error(
              'Structured response valid but contained neither tasks nor clarification.'
            )
          }
        } else {
          // Fallback to unstructured response if structured fails
          console.warn(
            `[TaskServer] Structured planning failed: ${structuredResult.error}. Falling back to unstructured format.`
          )

          // Use traditional prompt and formatting
          const unstructuredFallbackPrompt = `${contextPromptPart}Generate a detailed, step-by-step coding implementation plan for the feature: "${feature_description}".
          
          The plan should ONLY include actionable tasks a developer needs to perform within the code. Exclude steps related to project management, deployment, manual testing, documentation updates, or obtaining approvals.
          
          For each coding task, include an effort rating (low, medium, or high) based on implementation work involved. High effort tasks often require breakdown.
          
          Use these effort definitions:
          - Low: Simple, quick changes in one or few files, minimal logic changes.
          - Medium: Requires moderate development time, involves changes across several files/components, includes writing new functions/classes.
          - High: Significant development time, complex architectural changes, intricate algorithms, deep refactoring.
          
          IF YOU NEED CLARIFICATION BEFORE YOU CAN PROPERLY CREATE THE PLAN:
          Instead of returning a task list, use the following format to ask for clarification:
          [CLARIFICATION_NEEDED]
          Your specific question here. Be precise about what information you need to proceed.
          Options: [Option A, Option B, Option C] (include this line only if providing multiple-choice options)
          MULTIPLE_CHOICE_ONLY (include this if only the listed options are valid, omit if free text is also acceptable)
          [END_CLARIFICATION]
          
          For example:
          [CLARIFICATION_NEEDED]
          Should the feature implementation use SQL or NoSQL database for data storage?
          Options: [SQL, NoSQL]
          [END_CLARIFICATION]
          
          IF NO CLARIFICATION IS NEEDED:
          **IMPORTANT: Do NOT include any introductory sentences, summaries, concluding remarks, or any text other than the formatted task list itself. The response must start *directly* with the first task (e.g., "[low] Add console log...").**
          
          Now, generate the list of coding tasks, ensuring each task strictly follows this format: [effort] Task Description`

          let result = await planningModel.chat.completions.create({
            model: OPENROUTER_MODEL,
            messages: [{ role: 'user', content: unstructuredFallbackPrompt }],
            temperature: 0.7,
          })
          // We need to get the text response here for the old parsing logic to work
          let responseText = aiService.extractTextFromResponse(result)

          if (responseText === null) {
            message =
              'Error: Failed to get planning response from LLM or response was blocked. AI Agent: Do not try to call again. Simply alert the user.'
            isError = true
            // Record error in history, handling potential logging errors
            try {
              await addHistoryEntry(featureId, 'tool_response', {
                tool: 'plan_feature',
                isError: true,
                message,
                step: 'llm_response_processing',
              })
            } catch (historyError) {
              console.error(
                `[TaskServer] Failed to add error history entry during LLM processing failure: ${historyError}`
              )
            }
            return { content: [{ type: 'text', text: message }], isError }
          }

          // If no plan steps were extracted from structured response, try text parsing from fallback
          if (!planSteps.length && responseText) {
            logToFile(
              '[TaskServer] Attempting text parsing on unstructured fallback response.'
            )
            // IMPORTANT: Ensure parseGeminiPlanResponse ONLY extracts tasks and doesn't get confused by potential JSON remnants
            planSteps = parseGeminiPlanResponse(responseText)
            if (planSteps.length > 0) {
              logToFile(
                `[TaskServer] Extracted ${planSteps.length} tasks via text parsing.`
              )
            } else {
              // If still no tasks, log error and return *serialized*
              message =
                'Error: The planning model did not return a recognizable list of tasks.'
              isError = true
              // Record error in history, handling potential logging errors
              try {
                await addHistoryEntry(featureId, 'tool_response', {
                  tool: 'plan_feature',
                  isError: true,
                  message,
                  step: 'response_parsing',
                })
              } catch (historyError) {
                console.error(
                  `[TaskServer] Failed to add error history entry during response parsing failure: ${historyError}`
                )
              }
              const errorResponse: PlanFeatureStandardResponse = {
                status: 'error',
                message: message,
                featureId: featureId,
              }
              return {
                content: [
                  { type: 'text', text: JSON.stringify(errorResponse) },
                ],
                isError: true,
              }
            }
          }
        }
      } else {
        // It's Gemini - Use structured output
        const structuredResult = await aiService.callGeminiWithSchema(
          GEMINI_MODEL,
          structuredPlanningPrompt,
          PlanFeatureResponseSchema,
          { temperature: 0.7 }
        )

        logToFile(
          `[TaskServer] Structured result (Gemini): ${JSON.stringify(
            structuredResult
          )}`
        )

        if (structuredResult.success) {
          // Check if clarification is needed
          if (structuredResult.data.clarificationNeeded) {
            logToFile(
              '[TaskServer] Clarification needed based on structured response.'
            )
            const clarification = structuredResult.data.clarificationNeeded

            // Open the browser *now* to show the question
            try {
              logToFile(`[TaskServer] Launching UI for clarification: ${uiUrl}`)
              const open = await dynamicImportDefault('open')
              await open(uiUrl)
              browserOpened = true // Mark browser as opened
              logToFile(
                '[TaskServer] Browser launch for clarification initiated.'
              )
            } catch (openError: any) {
              logToFile(
                `[TaskServer] Error launching browser for clarification: ${openError.message}`
              )
              // Continue even if browser launch fails, WS should still work if UI is open
            }

            // Store the intermediate state
            const questionId =
              await planningStateService.storeIntermediateState(
                featureId,
                structuredPlanningPrompt,
                JSON.stringify(structuredResult.data),
                'feature_planning'
              )
            // Send WebSocket message
            webSocketService.broadcast({
              type: 'show_question',
              featureId,
              payload: {
                questionId,
                question: clarification.question,
                options: clarification.options,
                allowsText: clarification.allowsText,
              },
            })
            // Record in history
            await addHistoryEntry(featureId, 'tool_response', {
              tool: 'plan_feature',
              status: 'awaiting_clarification',
              questionId,
            })
            // Return structured clarification info *serialized as text*
            const clarificationData = {
              questionId: questionId,
              question: clarification.question,
              options: clarification.options,
              allowsText: clarification.allowsText,
            }
            const clarificationResponse: PlanFeatureStandardResponse = {
              status: 'awaiting_clarification',
              message: `Planning paused for feature ${featureId}. User clarification needed via UI (${uiUrl}). Once submitted, call 'get_next_task' with featureId '${featureId}' to retrieve the first task.`,
              featureId: featureId,
              data: clarificationData,
              uiUrl: uiUrl,
            }
            return {
              // Serialize the standard response structure
              content: [
                { type: 'text', text: JSON.stringify(clarificationResponse) },
              ],
              isError: false, // Not an error, just waiting
            }
          } else if (structuredResult.data.tasks) {
            logToFile('[TaskServer] Tasks received in structured response.')
            // Convert the structured response to the expected format for processing
            planSteps = structuredResult.data.tasks.map(
              (task) => `[${task.effort}] ${task.description}`
            )
            await addHistoryEntry(featureId, 'model', {
              step: 'structured_planning_response',
              response: JSON.stringify(structuredResult.data),
            })
          } else {
            // Schema validation should prevent this, but handle defensively
            throw new Error(
              'Structured response valid but contained neither tasks nor clarification.'
            )
          }
        } else {
          // Fallback to unstructured response if structured fails
          console.warn(
            `[TaskServer] Structured planning failed: ${structuredResult.error}. Falling back to unstructured format.`
          )

          // Use traditional Gemini call
          const unstructuredFallbackPrompt = `${contextPromptPart}Generate a detailed, step-by-step coding implementation plan for the feature: "${feature_description}".

          Engineer it in the best way possible, considering all side effects and edge cases. Be extremely thorough and meticulous.
          
          The plan should ONLY include actionable tasks a developer needs to perform within the code. Exclude steps related to project management, deployment, manual testing, documentation updates, or obtaining approvals.
          
          For each coding task, include an effort rating (low, medium, or high) based on implementation work involved. High effort tasks often require breakdown.
          
          Use these effort definitions:
          - Low: Simple, quick changes in one or few files, minimal logic changes.
          - Medium: Requires moderate development time, involves changes across several files/components, includes writing new functions/classes.
          - High: Significant development time, complex architectural changes, intricate algorithms, deep refactoring.
          
          IF YOU NEED CLARIFICATION BEFORE YOU CAN PROPERLY CREATE THE PLAN:
          Instead of returning a task list, use the following format to ask for clarification:
          [CLARIFICATION_NEEDED]
          Your specific question here. Be precise about what information you need to proceed.
          Options: [Option A, Option B, Option C] (include this line only if providing multiple-choice options)
          MULTIPLE_CHOICE_ONLY (include this if only the listed options are valid, omit if free text is also acceptable)
          [END_CLARIFICATION]
          
          For example:
          [CLARIFICATION_NEEDED]
          Should the feature implementation use SQL or NoSQL database for data storage?
          Options: [SQL, NoSQL]
          [END_CLARIFICATION]
          
          IF NO CLARIFICATION IS NEEDED:
          **IMPORTANT: Do NOT include any introductory sentences, summaries, concluding remarks, or any text other than the formatted task list itself. The response must start *directly* with the first task (e.g., "[low] Add console log...").**
          
          Now, generate the list of coding tasks, ensuring each task strictly follows this format: [effort] Task Description`

          let result = await planningModel.generateContent({
            contents: [
              { role: 'user', parts: [{ text: unstructuredFallbackPrompt }] },
            ],
            generationConfig: {
              temperature: 0.7,
            },
          })
          // We need to get the text response here for the old parsing logic to work
          let responseText = aiService.extractTextFromResponse(result)

          if (responseText === null) {
            message =
              'Error: Failed to get planning response from LLM or response was blocked. AI Agent: Do not try to call again. Simply alert the user.'
            isError = true
            // Record error in history, handling potential logging errors
            try {
              await addHistoryEntry(featureId, 'tool_response', {
                tool: 'plan_feature',
                isError: true,
                message,
                step: 'llm_response_processing',
              })
            } catch (historyError) {
              console.error(
                `[TaskServer] Failed to add error history entry during LLM processing failure: ${historyError}`
              )
            }
            return { content: [{ type: 'text', text: message }], isError }
          }

          // If no plan steps were extracted from structured response, try text parsing from fallback
          if (!planSteps.length && responseText) {
            logToFile(
              '[TaskServer] Attempting text parsing on unstructured fallback response.'
            )
            // IMPORTANT: Ensure parseGeminiPlanResponse ONLY extracts tasks and doesn't get confused by potential JSON remnants
            planSteps = parseGeminiPlanResponse(responseText)
            if (planSteps.length > 0) {
              logToFile(
                `[TaskServer] Extracted ${planSteps.length} tasks via text parsing.`
              )
            } else {
              // If still no tasks, log error and return *serialized*
              message =
                'Error: The planning model did not return a recognizable list of tasks.'
              isError = true
              // Record error in history, handling potential logging errors
              try {
                await addHistoryEntry(featureId, 'tool_response', {
                  tool: 'plan_feature',
                  isError: true,
                  message,
                  step: 'response_parsing',
                })
              } catch (historyError) {
                console.error(
                  `[TaskServer] Failed to add error history entry during response parsing failure: ${historyError}`
                )
              }
              const errorResponse: PlanFeatureStandardResponse = {
                status: 'error',
                message: message,
                featureId: featureId,
              }
              return {
                content: [
                  { type: 'text', text: JSON.stringify(errorResponse) },
                ],
                isError: true,
              }
            }
          }
        }
      }

      // Process the plan steps using the centralized function
      const finalTasks = await processAndFinalizePlan(
        planSteps, // Use the extracted/parsed plan steps
        planningModel,
        featureId
      )

      task_count = finalTasks.length

      message = `Successfully planned feature '${feature_description}' with ${task_count} tasks.`
      logToFile(`[TaskServer] ${message}`)

      // Record final success in history
      await addHistoryEntry(featureId, 'tool_response', {
        tool: 'plan_feature',
        status: 'completed',
        taskCount: task_count,
      })
    } catch (error: any) {
      message = `Error during feature planning: ${error.message}`
      isError = true
      await logToFile(`[TaskServer] ${message} Stack: ${error.stack}`)

      // Record error in history, handling potential logging errors
      try {
        await addHistoryEntry(featureId, 'tool_response', {
          tool: 'plan_feature',
          isError: true,
          message: error.message,
          step: 'planning_execution', // Indicate where the error occurred
        })
      } catch (historyError) {
        console.error(
          `[TaskServer] Failed to add error history entry during planning execution failure: ${historyError}`
        )
      }
    }
  } catch (outerError: any) {
    // Catch errors happening before LLM call (e.g., history writing)
    message = `[TaskServer] Unexpected error in handlePlanFeature: ${
      outerError?.message || String(outerError)
    }`
    isError = true
    await logToFile(`${message} Stack: ${outerError?.stack}`, 'error')

    // Record error in history, handling potential logging errors
    try {
      await addHistoryEntry(featureId, 'tool_response', {
        tool: 'plan_feature',
        isError: true,
        message: outerError?.message || String(outerError),
        step: 'outer_catch_block', // Indicate where the error was caught
        errorDetails: outerError?.stack, // Include stack trace if available
      })
    } catch (historyError: any) {
      // Log the failure to record history, but don't crash the main process
      await logToFile(
        `[TaskServer] Failed to record history entry for outer error: ${
          historyError?.message || String(historyError)
        }`,
        'error'
      )
    }
  }

  // Open the UI in the browser *if not already opened for clarification*
  if (!browserOpened) {
    try {
      await logToFile(
        `[TaskServer] Planning complete/failed. Launching UI: ${uiUrl}`
      )
      const open = await dynamicImportDefault('open')
      await open(uiUrl)
      await logToFile('[TaskServer] Browser launch initiated successfully')
    } catch (openError: any) {
      await logToFile(
        `[TaskServer] Error launching browser post-process: ${openError.message}`
      )
      // Continue even if browser launch fails
    }
  }

  // Prepare the final return content *as standard response object*
  let responseData: PlanFeatureStandardResponse
  if (!isError && task_count && task_count > 0) {
    let firstTaskDesc: string | undefined
    let updatedTasks: Task[] = []
    try {
      // Use databaseService instead of readTasks
      await databaseService.connect()
      updatedTasks = await databaseService.getTasksByFeatureId(featureId)
      await databaseService.close()

      if (updatedTasks.length > 0) {
        const firstTask = updatedTasks[0]
        // Format the first task for the return message
        firstTaskDesc = firstTask.description // Store first task desc
      } else {
        // Fallback if tasks array is somehow empty after successful planning
        firstTaskDesc = undefined
      }
    } catch (readError) {
      logToFile(
        `[TaskServer] Error reading tasks after finalization: ${readError}`
      )
      // Fallback to the original message if reading fails
      firstTaskDesc = undefined
    }

    // Construct success response
    responseData = {
      status: 'completed',
      message: `Successfully planned ${task_count || 0} tasks.${
        firstTaskDesc ? ' First task: "' + firstTaskDesc + '"' : ''
      }`,
      featureId: featureId,
      uiUrl: uiUrl,
      firstTask:
        updatedTasks.length > 0
          ? {
              id: updatedTasks[0].id,
              description: updatedTasks[0].description || '',
              effort: updatedTasks[0].effort || 'medium',
            }
          : undefined,
    }
  } else {
    // Construct error or no-tasks response
    responseData = {
      status: isError ? 'error' : 'completed', // 'completed' but with 0 tasks is possible
      message: message, // Use the message determined earlier (could be error or success-with-0-tasks)
      featureId: featureId,
      uiUrl: uiUrl,
    }
  }

  // Final return structure using the standard serialized format
  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(responseData),
      },
    ],
    isError, // Keep isError consistent with internal state for SDK
  }
}

```

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

```
<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import { page } from '$app/stores';
	import { fade } from 'svelte/transition'; 
	import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '$lib/components/ui/card';
	import { Button } from '$lib/components/ui/button';
	import { Badge } from '$lib/components/ui/badge';
	import { Checkbox } from '$lib/components/ui/checkbox';
	import * as Select from '$lib/components/ui/select';
	import { Progress } from '$lib/components/ui/progress';
	import { Loader2, CornerDownLeft, CornerDownRight, Pencil, Trash2, FileText, Eye } from 'lucide-svelte';
	import { writable, type Writable } from 'svelte/store';
	import type { Task, WebSocketMessage, ShowQuestionPayload, QuestionResponsePayload } from '$lib/types';
	import { TaskStatus, TaskEffort } from '$lib/types';
	import type { Selected } from 'bits-ui';
	import QuestionModal from '$lib/components/QuestionModal.svelte';
	import TaskFormModal from '$lib/components/TaskFormModal.svelte';
	import ImportTasksModal from '$lib/components/ImportTasksModal.svelte';

	// Convert to writable stores for better state management
	const tasks: Writable<Task[]> = writable([]);
	let nestedTasks: Task[] = [];
	const loading: Writable<boolean> = writable(true);
	const error: Writable<string | null> = writable(null);
	let featureId: string | null = null;
	let features: string[] = [];
	let loadingFeatures = true;
	let ws: WebSocket | null = null;
	let wsStatus: 'connecting' | 'connected' | 'disconnected' = 'disconnected';

	// Question modal state
	let showQuestionModal = false;
	let questionData: ShowQuestionPayload | null = null;
	let selectedOption = '';
	let userResponse = '';

	// Task form modal state
	let showTaskFormModal = false;
	let editingTask: Task | null = null;
	let isEditing = false;

	let waitingOnLLM = false;

	let showImportModal = false;

	// Reactive statement to update nestedTasks when tasks store changes
	$: {
		const taskMap = new Map<string, Task & { children: Task[] }>();
		const rootTasks: Task[] = [];

		// Use the tasks from the store ($tasks)
		$tasks.forEach(task => {
			// Ensure the task object has the correct type including children array
			const taskWithChildren: Task & { children: Task[] } = {
				...task,
				children: []
			};
			taskMap.set(task.id, taskWithChildren);
		});

		$tasks.forEach(task => {
			const currentTask = taskMap.get(task.id)!; // Should always exist
			if (task.parentTaskId && taskMap.has(task.parentTaskId)) {
				taskMap.get(task.parentTaskId)!.children.push(currentTask);
			} else {
				rootTasks.push(currentTask);
			}
		});

		nestedTasks = rootTasks;
	}

	// --- WebSocket Functions ---
	function connectWebSocket() {
		// Construct WebSocket URL (ws:// or wss:// based on protocol)
		const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
		const wsUrl = `${protocol}//${window.location.host}`;
		
		console.log(`[WS Client] Attempting to connect to ${wsUrl}...`);
		wsStatus = 'connecting';
		ws = new WebSocket(wsUrl);

		ws.onopen = () => {
			console.log('[WS Client] WebSocket connection established.');
			wsStatus = 'connected';
			// Register this client for the current feature
			if (featureId && ws) {
				sendWsMessage({ 
					type: 'client_registration', 
					featureId: featureId,
					payload: { featureId: featureId, clientId: `browser-${Date.now()}` } // Basic client ID
				});
			}
		};

		ws.onmessage = (event) => {
			try {
				const message: WebSocketMessage = JSON.parse(event.data);
				console.log('[WS Client] Received message:', message);

				// Check if the message is for the currently viewed feature
				if (message.featureId && message.featureId !== featureId) {
					console.log('[WS Client] Ignoring message for different feature:', message.featureId);
					return;
				}

				switch (message.type) {
					case 'tasks_updated':
						waitingOnLLM = false;
						console.log(`[WS Client] Received tasks_updated for feature ${featureId}:`, message.payload.tasks);
						if (message.payload?.tasks && Array.isArray(message.payload.tasks) && featureId) {
							// Map incoming tasks using the helper function to ensure consistency
							const mappedTasks = message.payload.tasks.map((task: any) => 
								mapApiTaskToClientTask(task, featureId as string)
							);
							tasks.set(mappedTasks);
							// Detailed logging after update
							tasks.subscribe(currentTasks => {
								console.log('[WS Client] tasks store after set():', currentTasks);
							})();
							// Explicitly set loading to false
							loading.set(false);
							error.set(null); // Clear any previous errors
						} else {
							console.warn('[WS Client] Invalid or missing tasks payload for tasks_updated message.');
							// Optionally handle this case, e.g., set an error or leave tasks unchanged
						}
						break;
					case 'status_changed':
						console.log(`[WS Client] Received status_changed for task ${message.payload?.taskId}`);
						if (message.payload?.taskId && message.payload?.status) {
							// Map incoming status string to TaskStatus enum
							let newStatus: TaskStatus;
							switch (message.payload.status) {
								case 'completed': newStatus = TaskStatus.COMPLETED; break;
								case 'in_progress': newStatus = TaskStatus.IN_PROGRESS; break;
								case 'decomposed': newStatus = TaskStatus.DECOMPOSED; break;
								default: newStatus = TaskStatus.PENDING; break;
							}
							
							tasks.update(currentTasks =>
								currentTasks.map(task =>
									task.id === message.payload.taskId
										? { 
											...task, 
											status: newStatus, 
											// Completed is true ONLY if status is COMPLETED
											completed: newStatus === TaskStatus.COMPLETED 
										  }
										: task
								)
							);
							// Status change doesn't imply general loading state change
						}
						break;
					case 'show_question':
						waitingOnLLM = false;
						console.log('[WS Client] Received clarification question:', message.payload);
						// Store question data and show modal
						questionData = message.payload as ShowQuestionPayload;
						showQuestionModal = true;
						// When question arrives, we should stop loading indicator
						loading.set(false);
						break;
					case 'error':
						waitingOnLLM = false;
						console.error('[WS Client] Received error message:', message.payload);
						// Display user-facing error
						error.set(message.payload?.message || 'Received error from server.');
						// Error likely means loading is done (with an error)
						loading.set(false);
						break;
					case 'task_created':
						console.log('[WS Client] Received task_created:', message.payload);
						if (message.payload?.task) {
							// Map incoming task to our Task type
							const newTask = mapApiTaskToClientTask(message.payload.task, message.featureId || featureId || '');
							// Add the new task to the store
							tasks.update(currentTasks => [...currentTasks, newTask]);
							// Process nested structure
							processNestedTasks();
						}
						break;
					case 'task_updated':
						console.log('[WS Client] Received task_updated:', message.payload);
						if (message.payload?.task) {
							// Map incoming task to our Task type
							const updatedTask = mapApiTaskToClientTask(message.payload.task, message.featureId || featureId || '');
							// Update the existing task in the store
							tasks.update(currentTasks =>
								currentTasks.map(task =>
									task.id === updatedTask.id ? updatedTask : task
								)
							);
							// Process nested structure
							processNestedTasks();
						}
						break;
					case 'task_deleted':
						console.log('[WS Client] Received task_deleted:', message.payload);
						if (message.payload?.taskId) {
							// Remove the task from the store
							tasks.update(currentTasks =>
								currentTasks.filter(task => task.id !== message.payload.taskId)
							);
							// Process nested structure
							processNestedTasks();
						}
						break;
					case 'connection_established':
						console.log('[WS Client] Server confirmed connection.');
						break;
					case 'client_registration':
						console.log('[WS Client] Server confirmed registration:', message.payload);
						break;
					// Add other message type handlers if needed
				}
			} catch (e) {
				console.error('[WS Client] Error processing message:', e);
				loading.set(false); // Ensure loading is set to false on error
			}
		};

		ws.onerror = (error) => {
			console.error('[WS Client] WebSocket error:', error);
			wsStatus = 'disconnected';
			loading.set(false); // Ensure loading is false on WebSocket error
		};

		ws.onclose = (event) => {
			console.log(`[WS Client] WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`);
			wsStatus = 'disconnected';
			ws = null;
			// Ensure loading is false when WebSocket disconnects
			loading.set(false);
		};
	}

	function sendWsMessage(message: WebSocketMessage) {
		if (ws && ws.readyState === WebSocket.OPEN) {
			try {
				ws.send(JSON.stringify(message));
				console.log('[WS Client] Sent message:', message);
			} catch (e) {
				console.error('[WS Client] Error sending message:', e);
			}
		} else {
			console.warn('[WS Client] Cannot send message, WebSocket not open.');
		}
	}

	// --- Component Lifecycle & Data Fetching ---

	async function fetchTasks(selectedFeatureId?: string) {
		loading.set(true);
		error.set(null);
		
		try {
			// Construct the API endpoint based on whether we have a featureId
			const endpoint = selectedFeatureId 
				? `/api/tasks/${selectedFeatureId}` 
				: '/api/tasks';
			
			const response = await fetch(endpoint);
			if (!response.ok) {
				throw new Error(`Failed to fetch tasks: ${response.statusText}`);
			}
			
			const data = await response.json();
			
			// Convert API response to our Task type
			const mappedData = data.map((task: any) => {
				// Map incoming status string to TaskStatus enum
				let status: TaskStatus;
				switch (task.status) {
					case 'completed': status = TaskStatus.COMPLETED; break;
					case 'in_progress': status = TaskStatus.IN_PROGRESS; break;
					case 'decomposed': status = TaskStatus.DECOMPOSED; break;
					default: status = TaskStatus.PENDING; break;
				}
				
				// Ensure effort is one of our enum values
				let effort: TaskEffort = TaskEffort.MEDIUM; // Default
				if (task.effort === 'low') {
					effort = TaskEffort.LOW;
				} else if (task.effort === 'high') {
					effort = TaskEffort.HIGH;
				}
				
				// Derive title from description if not present
				const title = task.title || task.description;
				
				// Ensure completed flag is consistent with status
				const completed = status === TaskStatus.COMPLETED;
				
				// Return the fully mapped task
				return {
					id: task.id,
					title,
					description: task.description,
					status,
					completed,
					effort,
					feature_id: task.feature_id || selectedFeatureId || undefined,
					parentTaskId: task.parentTaskId,
					createdAt: task.createdAt,
					updatedAt: task.updatedAt,
					fromReview: task.fromReview
				} as Task;
			});
			
			tasks.set(mappedData);
			
			if (mappedData.length === 0) {
				error.set('No tasks found for this feature.');
			}
		} catch (err) {
			error.set(err instanceof Error ? err.message : 'An error occurred');
			// Add more detailed logging
			console.error('[fetchTasks] Error fetching tasks:', err);
			if (err instanceof Error && err.cause) {
				console.error('[fetchTasks] Error Cause:', err.cause);
			}
			tasks.set([]); // Clear any previous tasks
		} finally {
			// Always reset loading state when fetch completes
			loading.set(false);
		}
	}

	async function fetchFeatures() {
		loadingFeatures = true;
		try {
			const response = await fetch('/api/features');
			if (!response.ok) {
				throw new Error('Failed to fetch features');
			}
			features = await response.json();
		} catch (err) {
			console.error('Error fetching features:', err);
			features = [];
		} finally {
			loadingFeatures = false;
		}
	}

	// New function to fetch pending question
	async function fetchPendingQuestion(id: string): Promise<ShowQuestionPayload | null> {
		console.log(`[Pending Question] Checking for feature ${id}...`);
		try {
			const response = await fetch(`/api/features/${id}/pending-question`);
			if (!response.ok) {
				throw new Error(`HTTP error! status: ${response.status}`);
			}
			const data: ShowQuestionPayload | null = await response.json();
			if (data) {
				console.log('[Pending Question] Found pending question:', data);
				return data;
			} else {
				console.log('[Pending Question] No pending question found.');
				return null;
			}
		} catch (err) {
			console.error('[Pending Question] Error fetching pending question:', err);
			error.set(err instanceof Error ? `Error checking for pending question: ${err.message}` : 'An error occurred while checking for pending questions.');
			return null;
		}
	}

	function processNestedTasks() {
		// Define the type for map values explicitly
		type TaskWithChildren = Task & { children: Task[] };

		const taskMap = new Map<string, TaskWithChildren>(
			$tasks.map(task => [task.id, { ...task, children: [] }])
		);
		const rootTasks: Task[] = [];

		taskMap.forEach((task: TaskWithChildren) => { 
			if (task.parentTaskId && taskMap.has(task.parentTaskId)) {
				const parent = taskMap.get(task.parentTaskId);
				if (parent) {
					parent.children.push(task);
				} else {
					rootTasks.push(task);
				}
			} else {
				rootTasks.push(task);
			}
		});

		// Optional: Sort root tasks or children if needed
		// rootTasks.sort(...); 
		// taskMap.forEach(task => task.children.sort(...));

		nestedTasks = rootTasks;
	}

	async function addTask(taskData: { title: string; effort: string; featureId: string, description?: string }) {
		try {
			const response = await fetch('/api/tasks', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					...taskData,
					description: taskData.description || taskData.title // Use provided description, or title if none
				})
			});

			if (!response.ok) {
				throw new Error(`Failed to create task: ${response.statusText}`);
			}

			const newTask = await response.json();
			console.log('[Task] New task created:', newTask);

			// Refresh the tasks list
			await fetchTasks(taskData.featureId);
			
			// Clear any errors that might have been shown
			error.set(null);
		} catch (err) {
			console.error('[Task] Error creating task:', err);
			error.set(err instanceof Error ? err.message : 'Failed to create task');
		}
	}

	function handleTaskFormSubmit(event: CustomEvent) {
		const taskData = event.detail;
		if (isEditing && editingTask) {
			updateTask(editingTask.id, taskData);
		} else {
			addTask(taskData);
		}
		showTaskFormModal = false;
		isEditing = false;
		editingTask = null;
	}

	function openEditTaskModal(task: Task) {
		editingTask = task;
		isEditing = true;
		showTaskFormModal = true;
	}

	async function updateTask(taskId: string, taskData: { title: string; effort: string; featureId: string }) {
		try {
			const response = await fetch(`/api/tasks/${taskId}`, {
				method: 'PUT',
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					...taskData,
					description: taskData.title, // Use title as description
					featureId: taskData.featureId
				})
			});

			if (!response.ok) {
				throw new Error(`Failed to update task: ${response.statusText}`);
			}

			const updatedTask = await response.json();
			console.log('[Task] Task updated:', updatedTask);

			// Refresh the tasks list
			await fetchTasks(taskData.featureId);
			
			// Clear any errors that might have been shown
			error.set(null);
		} catch (err) {
			console.error('[Task] Error updating task:', err);
			error.set(err instanceof Error ? err.message : 'Failed to update task');
		}
	}

	async function deleteTask(taskId: string, featureId: string) {
		if (!confirm('Are you sure you want to delete this task?')) {
			return;
		}
		
		try {
			const response = await fetch(`/api/tasks/${taskId}?featureId=${featureId}`, {
				method: 'DELETE'
			});

			if (!response.ok) {
				throw new Error(`Failed to delete task: ${response.statusText}`);
			}

			console.log('[Task] Task deleted:', taskId);

			// Refresh the tasks list
			await fetchTasks(featureId);
			
			// Clear any errors that might have been shown
			error.set(null);
		} catch (err) {
			console.error('[Task] Error deleting task:', err);
			error.set(err instanceof Error ? err.message : 'Failed to delete task');
		}
	}

	onMount(async () => {
		loading.set(true); // Set loading true at the start
		error.set(null); // Reset error

		// Extract featureId from URL query parameters
		featureId = $page.url.searchParams.get('featureId');
		
		// Fetch available features first
		await fetchFeatures();
		
		// Determine the featureId to use (from URL or latest)
		if (!featureId && features.length > 0) {
			// Attempt to fetch default tasks to find the latest featureId
			await fetchTasks(); 
			if ($tasks.length > 0 && $tasks[0]?.feature_id) {
				featureId = $tasks[0].feature_id;
				console.log(`[onMount] Determined latest featureId: ${featureId}`);
			} else {
				console.log('[onMount] Could not determine latest featureId from default tasks.');
				// If no featureId determined, use the first from the list if available
				if (features.length > 0) {
					featureId = features[0];
					console.log(`[onMount] Using first available featureId: ${featureId}`);
				}
			}
		}
		
		// Now, if we have a featureId, check for pending questions and fetch tasks
		if (featureId) {
			console.log(`[onMount] Operating with featureId: ${featureId}`);
			// Check for pending question first
			const pendingQuestion = await fetchPendingQuestion(featureId);
			if (pendingQuestion) {
				questionData = pendingQuestion;
				showQuestionModal = true;
				// Still fetch tasks even if question is shown, they might exist
				await fetchTasks(featureId);
			} else {
				// No pending question, just fetch tasks
				await fetchTasks(featureId);
			}
		} else {
			// No featureId could be determined
			console.log('[onMount] No featureId available.');
			if (!$error) { // Only set error if fetchTasks didn't already set one
				error.set('No features found. Create a feature first using the task manager CLI.');
			}
			tasks.set([]); // Ensure tasks are empty
			nestedTasks = [];
		}

		// Connect WebSocket AFTER initial data load and featureId determination
		if (featureId) {
			connectWebSocket();
		}
	});

	onDestroy(() => {
		// Clean up WebSocket connection
		if (ws) {
			console.log('[WS Client] Closing WebSocket connection.');
			ws.close();
			ws = null;
		}
	});

	async function toggleTaskStatus(taskId: string) {
		const tasksList = $tasks;
		const taskIndex = tasksList.findIndex((t) => t.id === taskId);
		if (taskIndex !== -1) {
			const task = tasksList[taskIndex];
			const newStatus = task.status === TaskStatus.COMPLETED ? TaskStatus.PENDING : TaskStatus.COMPLETED;
			try {
				// Make API call to update status in backend
				const response = await fetch(`/api/tasks/${taskId}`, {
					method: 'PUT',
					headers: { 'Content-Type': 'application/json' },
					body: JSON.stringify({
						featureId: task.feature_id,
						status: newStatus,
						completed: newStatus === TaskStatus.COMPLETED
					})
				});
				if (!response.ok) {
					throw new Error('Failed to update task status');
				}
				// Optionally update local store or rely on WebSocket update
			} catch (err) {
				console.error('Failed to update task status:', err);
				// Optionally show error to user
			}
		}
	}

	function getEffortBadgeVariant(effort: string) {
		switch (effort) {
			case TaskEffort.LOW:
				return 'secondary';
			case TaskEffort.MEDIUM:
				return 'default';
			case TaskEffort.HIGH:
				return 'destructive';
			default:
				return 'outline';
		}
	}

	function getStatusBadgeVariant(status: TaskStatus): 'default' | 'secondary' | 'destructive' | 'outline' {
		switch (status) {
			case TaskStatus.COMPLETED:
				return 'secondary';
			case TaskStatus.IN_PROGRESS:
				return 'default';
			case TaskStatus.DECOMPOSED:
				return 'outline';
			case TaskStatus.PENDING:
			default:
				return 'outline';
		}
	}

	function refreshTasks() {
		if ($loading) return;
		console.log('[Task List] Refreshing tasks...');
		fetchTasks(featureId || undefined);
	}

	function handleFeatureChange(selectedItem: Selected<string> | undefined) {
		const newFeatureId = selectedItem?.value; // Safely get value
		
		if (newFeatureId && newFeatureId !== featureId) { 
			featureId = newFeatureId;
			
			// Update URL 
			const url = new URL(window.location.href);
			url.searchParams.set('featureId', newFeatureId);
			window.history.pushState({}, '', url);
			
			// Fetch tasks for the new feature
			fetchTasks(newFeatureId);

			// Re-register WebSocket for the new feature
			if (ws && wsStatus === 'connected') {
				sendWsMessage({ 
					type: 'client_registration', 
					featureId: featureId,
					payload: { featureId: featureId, clientId: `browser-${Date.now()}` }
				});
			}
		}
	}

	// Handle user response to clarification question
	function handleQuestionResponse(event: SubmitEvent) {
		event.preventDefault();
		console.log('[WS Client] User responded to question. Selected:', selectedOption, 'Text:', userResponse);
		
		if (questionData && featureId) {
			const response = selectedOption || userResponse;
			
			sendWsMessage({
				type: 'question_response',
				featureId,
				payload: {
					questionId: questionData.questionId,
					response: response
				} as QuestionResponsePayload
			});
			
			showQuestionModal = false;
			questionData = null;
			waitingOnLLM = true;
			
			// Reset form fields
			selectedOption = '';
			userResponse = '';
		}
	}

	// Handle user cancellation of question
	function handleQuestionCancel() {
		console.log('[WS Client] User cancelled question');
		showQuestionModal = false;
		questionData = null;
	}

	// ... reactive variables ...
	// Filter out decomposed tasks from progress calculation
	$: actionableTasks = $tasks.filter(t => t.status !== TaskStatus.DECOMPOSED);
	$: completedCount = actionableTasks.filter(t => t.completed).length;
	$: totalActionableTasks = actionableTasks.length;
	$: progress = totalActionableTasks > 0 ? (completedCount / totalActionableTasks) * 100 : 0;
	$: firstPendingTaskIndex = $tasks.findIndex(t => t.status === TaskStatus.PENDING);
	$: selectedFeatureLabel = features.find(f => f === featureId) || 'Select Feature';

	// Call processNestedTasks whenever the raw tasks array changes
	$: {
		if ($tasks) {
			processNestedTasks();
		}
	}

	// Helper function to map API task response to client Task type
	function mapApiTaskToClientTask(apiTask: any, currentFeatureId: string): Task {
		// Map incoming status string to TaskStatus enum
		let status: TaskStatus;
		switch (apiTask.status) {
			case 'completed': status = TaskStatus.COMPLETED; break;
			case 'in_progress': status = TaskStatus.IN_PROGRESS; break;
			case 'decomposed': status = TaskStatus.DECOMPOSED; break;
			default: status = TaskStatus.PENDING; break;
		}
		
		// Ensure effort is one of our enum values
		let effort: TaskEffort = TaskEffort.MEDIUM; // Default
		if (apiTask.effort === 'low') {
			effort = TaskEffort.LOW;
		} else if (apiTask.effort === 'high') {
			effort = TaskEffort.HIGH;
		}
		
		// Derive title from description if not present
		const title = apiTask.title || apiTask.description;
		
		// Ensure completed flag is consistent with status
		const completed = status === TaskStatus.COMPLETED;
		
		// Return the fully mapped task
		return {
			id: apiTask.id,
			title,
			description: apiTask.description,
			status,
			completed,
			effort,
			feature_id: apiTask.feature_id || currentFeatureId,
			parentTaskId: apiTask.parentTaskId,
			createdAt: apiTask.createdAt,
			updatedAt: apiTask.updatedAt,
			fromReview: apiTask.fromReview
		} as Task;
	}

	async function handleImportTasks(event: CustomEvent) {
		const { tasks } = event.detail;
		if (!Array.isArray(tasks)) return;
		for (const t of tasks) {
			await addTask({
				title: t.title,
				effort: t.effort,
				featureId: featureId || '',
				description: t.description 
			});
		}
		showImportModal = false;
	}

	function handleCancelImport() {
		showImportModal = false;
	}

</script>

<div class="container mx-auto py-10 px-4 sm:px-6 lg:px-8 max-w-5xl">
	<div class="flex justify-between items-center mb-8">
		<h1 class="text-3xl font-bold tracking-tight text-foreground">Task Manager</h1>
		{#if features.length > 0}
			<div class="w-64">
				<Select.Root 
					onSelectedChange={handleFeatureChange} 
					selected={featureId ? { value: featureId, label: featureId } : undefined}
					disabled={loadingFeatures}
				>
					<Select.Trigger class="w-full">
						{featureId ? featureId.substring(0, 8) + '...' : 'Select Feature'}
					</Select.Trigger>
					<Select.Content>
						<Select.Group>
							<Select.GroupHeading>Available Features</Select.GroupHeading>
							{#each features as feature}
								<Select.Item value={feature} label={feature}>{feature.substring(0, 8)}...</Select.Item>
							{/each}
						</Select.Group>
					</Select.Content>
				</Select.Root>
			</div>
		{/if}
	</div>

	{#if questionData}
		<div class="flex flex-col items-center justify-center min-h-[300px]">
			<div class="max-w-md w-full bg-background border border-border rounded-lg shadow-lg p-6">
				<h2 class="text-xl font-semibold mb-4">Clarification Needed</h2>
				<p class="text-foreground mb-5">{questionData.question}</p>
				<form on:submit|preventDefault={handleQuestionResponse}>
					{#if questionData.options && questionData.options.length > 0}
						<div class="flex flex-col gap-3 mb-5">
							{#each questionData.options as option}
								<label class="flex items-center gap-2 p-3 border border-border rounded-md cursor-pointer hover:bg-muted transition-colors">
									<input 
										type="radio" 
										name="option" 
										value={option}
										bind:group={selectedOption}
										class="focus:ring-primary"
									/>
									<span class="text-foreground">{option}</span>
								</label>
							{/each}
						</div>
					{/if}
					{#if questionData.allowsText !== false}
						<div class="mb-5">
							<label for="text-response" class="block mb-2 font-medium text-foreground">
								{questionData.options && questionData.options.length > 0 ? 'Or provide a custom response:' : 'Your response:'}
							</label>
							<textarea 
								id="text-response"
								rows="3"
								bind:value={userResponse}
								placeholder="Type your response here..."
								class="w-full p-3 border border-border rounded-md resize-y text-foreground bg-background focus:ring-primary focus:border-primary"
							></textarea>
						</div>
					{/if}
					<div class="flex justify-end gap-3 pt-2">
						<button 
							type="submit" 
							class="bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md font-medium text-sm disabled:opacity-50"
							disabled={!userResponse && (!questionData.options || !selectedOption)}
						>
							Submit Response
						</button>
					</div>
				</form>
			</div>
		</div>
	{:else if waitingOnLLM}
		<div class="flex flex-col items-center justify-center min-h-[300px]">
			<Loader2 class="h-12 w-12 animate-spin text-primary mb-4" />
			<p class="text-lg text-muted-foreground">Waiting on LLM to plan after clarification...</p>
		</div>
	{:else if $loading}
		<div class="flex justify-center items-center h-64">
			<Loader2 class="h-12 w-12 animate-spin text-primary" />
		</div>
	{:else if $error}
		<Card class="mb-6 border-destructive">
			<CardHeader>
				<CardTitle class="text-destructive">Error Loading Tasks</CardTitle>
				<CardDescription class="text-destructive/90">{$error}</CardDescription>
			</CardHeader>
		</Card>
	{:else}
		<Card class="shadow-lg">
			<CardHeader class="border-b border-border px-6 py-4">
				<CardTitle class="text-xl font-semibold flex justify-between items-center">
					<span class="flex-1">Tasks</span>
					<div class="flex justify-between items-center gap-4 items-center">
						<Badge variant="secondary">{$tasks.length}</Badge>
						<Button on:click={() => showImportModal = true}>Import Tasks</Button>
					</div>
				</CardTitle>
				<CardDescription class="mt-1">
					Manage your tasks and track progress for the selected feature.
				</CardDescription>
				<div class="pt-4">
					<Progress 
						value={progress} 
						class="w-full h-2 [&>div]:bg-green-500 [&>div]:transition-all [&>div]:duration-300 [&>div]:ease-in-out"
					/>
				</div>
			</CardHeader>
			<CardContent class="p-0">
				<div class="divide-y divide-border">
					{#each nestedTasks as task (task.id)}
						{@const taskIndexInFlatList = $tasks.findIndex(t => t.id === task.id)}
						{@const isNextPending = taskIndexInFlatList === firstPendingTaskIndex}
						{@const isInProgress = task.status === TaskStatus.IN_PROGRESS}
						{@const areAllChildrenComplete = task.children && task.children.length > 0 && task.children.every(c => c.status === TaskStatus.COMPLETED)}
						<div 
							transition:fade={{ duration: 200 }}
							class="task-row flex items-start space-x-4 p-4 hover:bg-muted/50 transition-colors 
								   {isNextPending ? 'bg-muted/30' : ''} 
								   {isInProgress ? 'in-progress-shine relative overflow-hidden' : ''}
								   {(task.status === TaskStatus.COMPLETED || (task.status === TaskStatus.DECOMPOSED && areAllChildrenComplete)) ? 'opacity-60' : ''}
								   {task.fromReview ? 'from-review-task' : ''}"
						>
							{#if task.status === TaskStatus.DECOMPOSED}
								<div class="flex items-center justify-center h-6 w-6 mt-1 text-muted-foreground">
									<CornerDownRight class="h-4 w-4" />
								</div>
							{:else}
								<div class="flex flex-col items-center gap-1">
									<Checkbox 
										id={`task-${task.id}`} 
										checked={task.completed} 
										onCheckedChange={() => toggleTaskStatus(task.id)} 
										aria-labelledby={`task-label-${task.id}`}
										disabled={task.status === TaskStatus.IN_PROGRESS}
									/>
									{#if task.fromReview}
										<span class="review-indicator" title="Task from review">
											<Eye size={20} />
										</span>
									{/if}
								</div>
							{/if}
							<div class="flex-1 grid gap-1">
								<div class="flex items-center gap-2">
									<label 
										for={`task-${task.id}`} 
										id={`task-label-${task.id}`}
										class={`font-medium cursor-pointer ${(task.status === TaskStatus.COMPLETED || (task.status === TaskStatus.DECOMPOSED && areAllChildrenComplete)) ? 'line-through text-muted-foreground' : ''}`}
									>
										{task.title}
									</label>
								</div>
								{#if task.description && task.description !== task.title}
									<p class="text-sm text-muted-foreground">
										{task.description}
									</p>
								{/if}
							</div>
							<div class="flex flex-col gap-1.5 items-end min-w-[100px]">
								<div class="flex items-center gap-1.5">
									<Badge variant={getStatusBadgeVariant(task.status)} class="capitalize">
										{task.status.replace('_', ' ')}
									</Badge>
								</div>
								{#if task.effort}
									<Badge variant={getEffortBadgeVariant(task.effort)} class="capitalize">
										{task.effort}
									</Badge>
								{/if}
							</div>
							<div class="flex gap-1 ml-4">
								<button
									class="text-muted-foreground hover:text-foreground p-1 rounded-sm hover:bg-muted transition-colors"
									title="Edit task"
									on:click|stopPropagation={() => openEditTaskModal(task)}
								>
									<Pencil size={16} />
								</button>
								<button
									class="text-muted-foreground hover:text-destructive p-1 rounded-sm hover:bg-muted transition-colors"
									title="Delete task"
									on:click|stopPropagation={() => deleteTask(task.id, featureId || '')}
								>
									<Trash2 size={16} />
								</button>
							</div>
						</div>
						{#if task.children && task.children.length > 0}
							<div class="ml-10 pl-4 py-2 border-l border-border divide-y divide-border">
								{#each task.children as childTask (childTask.id)}
									{@const childTaskIndexInFlatList = $tasks.findIndex(t => t.id === childTask.id)}
									{@const isChildNextPending = childTaskIndexInFlatList === firstPendingTaskIndex}
									{@const isChildInProgress = childTask.status === TaskStatus.IN_PROGRESS}
									<div 
										transition:fade={{ duration: 200 }}
										class="task-row flex items-start space-x-4 pt-3 pr-4 mb-3 
											   {isChildNextPending ? 'bg-muted/30' : ''} 
											   {isChildInProgress ? 'in-progress-shine relative overflow-hidden' : ''}
											   {childTask.status === TaskStatus.COMPLETED ? 'opacity-60' : ''}
											   {childTask.fromReview ? 'from-review-task' : ''}"
									>
										{#if childTask.status === TaskStatus.DECOMPOSED}
											<div class="flex items-center justify-center h-6 w-6 mt-1 text-muted-foreground">
												<CornerDownRight class="h-4 w-4" />
											</div>
										{:else}
											<div class="flex flex-col items-center gap-1">
												<Checkbox 
													id={`task-${childTask.id}`} 
													checked={childTask.completed} 
													onCheckedChange={() => toggleTaskStatus(childTask.id)} 
													aria-labelledby={`task-label-${childTask.id}`}
													disabled={childTask.status === TaskStatus.IN_PROGRESS}
												/>
												{#if childTask.fromReview}
													<span class="review-indicator" title="Task from review">
														<Eye size={20} />
													</span>
												{/if}
											</div>
										{/if}
										<div class="flex-1 grid gap-1">
											<div class="flex items-center gap-2">
												<label 
													for={`task-${childTask.id}`} 
													id={`task-label-${childTask.id}`}
													class={`font-medium cursor-pointer ${childTask.status === TaskStatus.COMPLETED ? 'line-through text-muted-foreground' : ''}`}
												>
													{childTask.title}
												</label>
											</div>
											{#if childTask.description && childTask.description !== childTask.title}
												<p class="text-sm text-muted-foreground">
													{childTask.description}
												</p>
											{/if}
										</div>
										<div class="flex flex-col gap-1.5 items-end min-w-[100px]">
											<div class="flex items-center gap-1.5">
												<Badge variant={getStatusBadgeVariant(childTask.status)} class="capitalize">
													{childTask.status.replace('_', ' ')}
												</Badge>
											</div>
											{#if childTask.effort}
												<Badge variant={getEffortBadgeVariant(childTask.effort)} class="capitalize">
													{childTask.effort}
												</Badge>
											{/if}
										</div>
										<div class="flex gap-1 ml-4">
											<button
												class="text-muted-foreground hover:text-foreground p-1 rounded-sm hover:bg-muted transition-colors"
												title="Edit subtask"
												on:click|stopPropagation={() => openEditTaskModal(childTask)}
											>
												<Pencil size={16} />
											</button>
											<button
												class="text-muted-foreground hover:text-destructive p-1 rounded-sm hover:bg-muted transition-colors"
												title="Delete subtask"
												on:click|stopPropagation={() => deleteTask(childTask.id, featureId || '')}
											>
												<Trash2 size={16} />
											</button>
										</div>
									</div>
								{/each}
							</div>
						{/if}
					{:else}
						<div class="text-center py-8 text-muted-foreground">
							No tasks found for this feature.
						</div>
					{/each}
				</div>
			</CardContent>
			<CardFooter class="flex flex-col items-start gap-4 px-6 py-4 border-t border-border">
				<div class="w-full flex justify-between items-center">
					<span class="text-sm text-muted-foreground">
						{completedCount} of {totalActionableTasks} actionable tasks completed
					</span>
					<div class="flex gap-2">
						<Button variant="outline" size="sm" on:click={() => showTaskFormModal = true} disabled={!featureId}>
							Add Task
						</Button>
						<Button variant="outline" size="sm" on:click={refreshTasks} disabled={$loading}>
							{#if $loading}
								<Loader2 class="mr-2 h-4 w-4 animate-spin" />
							{/if}
							Refresh
						</Button>
					</div>
				</div>
			</CardFooter>
		</Card>
	{/if}

	{#if featureId}
		<TaskFormModal
			open={showTaskFormModal}
			featureId={featureId}
			isEditing={isEditing}
			editTask={editingTask ? {
				id: editingTask.id,
				title: editingTask.title || '',
				effort: editingTask.effort || 'medium'
			} : {
				id: '',
				title: '',
				effort: 'medium'
			}}
			on:submit={handleTaskFormSubmit}
			on:cancel={() => showTaskFormModal = false}
		/>
	{/if}

	
	<ImportTasksModal
		bind:open={showImportModal}
		on:import={handleImportTasks}
		on:cancel={handleCancelImport}
	/>
</div>

<style>
	.in-progress-shine::before {
		content: '';
		position: absolute;
		top: 0;
		left: -100%; /* Start off-screen */
		width: 75%; /* Width of the shine */
		height: 100%;
		background: linear-gradient(
			100deg,
			rgba(255, 255, 255, 0) 0%,
			rgba(255, 255, 255, 0.15) 50%, /* Subtle white shine */
			rgba(255, 255, 255, 0) 100%
		);
		transform: skewX(-25deg); /* Angle the shine */
		animation: shine 2.5s infinite linear; /* Animation properties */
		opacity: 0.8;
	}

	@keyframes shine {
		0% {
			left: -100%;
		}
		50%, 100% { /* Speed up the animation and make it pause less */
			left: 120%; /* Move across and off-screen */
		}
	}

	.task-row {
		position: relative; /* Needed for absolute positioning of ::before */
		overflow: hidden; /* Keep shine contained */
	}
	.review-indicator {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		color: #3b82f6;
		transition: all 0.2s ease;
		margin-top: 10px;
	}
	
	.review-indicator:hover {
		opacity: 0.8;
	}
	
	.from-review-task {
		background-color: rgba(59, 130, 246, 0.08);
	}
	
	.from-review-task:hover {
		background-color: rgba(59, 130, 246, 0.12);
	}
</style>


```

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

```typescript
import { GenerateContentResult, GenerativeModel } from '@google/generative-ai'
import OpenAI from 'openai'
import crypto from 'crypto'
import {
  BreakdownOptions,
  EffortEstimationSchema,
  TaskBreakdownSchema,
  TaskBreakdownResponseSchema,
  Task,
  TaskListSchema,
  HistoryEntry,
  FeatureHistorySchema,
  TaskSchema,
  LLMClarificationRequestSchema,
} from '../models/types'
import { aiService } from '../services/aiService'
import { logToFile } from './logger'
import { safetySettings, OPENROUTER_MODEL, GEMINI_MODEL } from '../config'
import { z } from 'zod'
import { encoding_for_model } from 'tiktoken'
import { addHistoryEntry } from './dbUtils'
import webSocketService from '../services/webSocketService'
import { databaseService } from '../services/databaseService'

/**
 * Parses the text response from Gemini into a list of tasks.
 */
export function parseGeminiPlanResponse(
  responseText: string | undefined | null
): string[] {
  // Basic parsing
  if (!responseText) {
    return []
  }

  // Split by newlines and clean up
  const lines = responseText
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line.length > 0 && !line.match(/^[-*+]\s*$/))

  // Process each line to remove markdown list markers and numbering
  const cleanedLines = lines.map((line) => {
    // Remove markdown list markers and numbering
    return line
      .replace(/^[-*+]\s*/, '') // Remove list markers like -, *, +
      .replace(/^\d+\.\s*/, '') // Remove numbered list markers like 1. 2. etc.
      .replace(/^[a-z]\)\s*/i, '') // Remove lettered list markers like a) b) etc.
      .replace(/^\([a-z]\)\s*/i, '') // Remove lettered list markers like (a) (b) etc.
  })

  // Detect hierarchical structure based on indentation or subtask indicators
  const tasks: string[] = []
  let currentParentTask: string | null = null

  for (const line of cleanedLines) {
    // Check if this is a parent task or a subtask based on various indicators
    const isSubtask =
      line.match(/subtask|sub-task/i) || // Contains "subtask" or "sub-task"
      line.startsWith('  ') || // Has leading indentation
      line.match(/^[a-z]\.[\d]+/i) || // Contains notation like "a.1"
      line.includes('→') || // Contains arrow indicators
      line.match(/\([a-z]\)/i) // Contains notation like "(a)"

    if (isSubtask && currentParentTask) {
      // If it's a subtask and we have a parent, tag it with the parent task info
      tasks.push(line)
    } else {
      // This is a new parent task
      currentParentTask = line
      tasks.push(line)
    }
  }

  return tasks
}

/**
 * Determines task effort using an LLM.
 * Uses structured JSON output for consistent results.
 * Works with both OpenRouter and Gemini models.
 */
export async function determineTaskEffort(
  description: string,
  model: GenerativeModel | OpenAI | null
): Promise<'low' | 'medium' | 'high'> {
  if (!model) {
    console.error('[TaskServer] Cannot determine effort: No model provided.')
    // Default to medium effort if no model is available
    return 'medium'
  }

  const prompt = `
Task: ${description}

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:
- 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.
- 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).
- 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).

Exclude factors like testing procedures, documentation, deployment, or project management overhead.

Respond with a JSON object that includes the effort level and optionally a short reasoning.
`

  try {
    // Use structured response with schema validation
    if (model instanceof OpenAI) {
      // Use OpenRouter with structured output
      const result = await aiService.callOpenRouterWithSchema(
        OPENROUTER_MODEL,
        [{ role: 'user', content: prompt }],
        EffortEstimationSchema,
        { temperature: 0.1, max_tokens: 100 }
      )

      if (result.success) {
        return result.data.effort
      } else {
        console.warn(
          `[TaskServer] Could not determine effort using structured output: ${result.error}. Defaulting to medium.`
        )
        return 'medium'
      }
    } else {
      // Use Gemini with structured output
      const result = await aiService.callGeminiWithSchema(
        GEMINI_MODEL,
        prompt,
        EffortEstimationSchema,
        { temperature: 0.1, maxOutputTokens: 100 }
      )

      if (result.success) {
        return result.data.effort
      } else {
        console.warn(
          `[TaskServer] Could not determine effort using structured output: ${result.error}. Defaulting to medium.`
        )
        return 'medium'
      }
    }
  } catch (error) {
    console.error('[TaskServer] Error determining task effort:', error)
    return 'medium' // Default to medium on error
  }
}

/**
 * Breaks down a high-effort task into subtasks using an LLM.
 * Uses structured JSON output for consistent results.
 * Works with both OpenRouter and Gemini models.
 */
export async function breakDownHighEffortTask(
  taskDescription: string,
  parentId: string,
  model: GenerativeModel | OpenAI | null,
  options: BreakdownOptions = {}
): Promise<string[]> {
  if (!model) {
    console.error('[TaskServer] Cannot break down task: No model provided.')
    return []
  }

  // Use provided options or defaults
  const {
    minSubtasks = 2,
    maxSubtasks = 5,
    preferredEffort = 'medium',
    maxRetries = 3,
  } = options

  // Enhanced prompt with clearer instructions for JSON output
  const breakdownPrompt = `
I need to break down this high-effort coding task into smaller, actionable subtasks:

Task: "${taskDescription}"

Guidelines:
1. Create ${minSubtasks}-${maxSubtasks} subtasks.
2. Each subtask should ideally be '${preferredEffort}' effort, focusing on a specific part of the implementation.
3. Make each subtask a concrete coding action (e.g., "Create function X", "Refactor module Y", "Add field Z to interface").
4. The subtasks should represent a logical sequence for implementation.
5. Only include coding tasks, not testing, documentation, or deployment steps.

IMPORTANT RESPONSE FORMAT INSTRUCTIONS:
- Return ONLY a valid JSON object
- The JSON object MUST have a single key named "subtasks"
- "subtasks" MUST be an array of objects with exactly two fields each:
  - "description": string - The subtask description
  - "effort": string - MUST be either "low" or "medium"
- No other text before or after the JSON object
- No markdown formatting, code blocks, or comments

Example of EXACTLY how your response should be formatted:
{
  "subtasks": [
    {
      "description": "Create the database schema for user profiles",
      "effort": "medium"
    },
    {
      "description": "Implement the user profile repository class",
      "effort": "medium"
    }
  ]
}
`

  // Function to handle the actual API call with retry logic
  async function attemptBreakdown(attempt: number): Promise<string[]> {
    try {
      // Use structured response with schema validation
      if (model instanceof OpenAI) {
        // Use OpenRouter with structured output
        const result = await aiService.callOpenRouterWithSchema(
          OPENROUTER_MODEL,
          [{ role: 'user', content: breakdownPrompt }],
          TaskBreakdownResponseSchema,
          { temperature: 0.2 } // Lower temperature for more consistent output
        )

        if (result.success) {
          // Extract the descriptions from the structured response
          return result.data.subtasks.map(
            (subtask) => `[${subtask.effort}] ${subtask.description}`
          )
        } else {
          console.warn(
            `[TaskServer] Could not break down task using structured output (attempt ${attempt}): ${result.error}`
          )

          // Retry if attempts remain
          if (attempt < maxRetries) {
            logToFile(
              `[TaskServer] Retrying task breakdown (attempt ${attempt + 1})`
            )
            return attemptBreakdown(attempt + 1)
          }
          return []
        }
      } else {
        // Use Gemini with structured output
        const result = await aiService.callGeminiWithSchema(
          GEMINI_MODEL,
          breakdownPrompt,
          TaskBreakdownResponseSchema,
          { temperature: 0.2 } // Lower temperature for more consistent output
        )

        if (result.success) {
          // Extract the descriptions from the structured response
          return result.data.subtasks.map(
            (subtask) => `[${subtask.effort}] ${subtask.description}`
          )
        } else {
          console.warn(
            `[TaskServer] Could not break down task using structured output (attempt ${attempt}): ${result.error}`
          )

          // Retry if attempts remain
          if (attempt < maxRetries) {
            logToFile(
              `[TaskServer] Retrying task breakdown (attempt ${attempt + 1})`
            )
            return attemptBreakdown(attempt + 1)
          }
          return []
        }
      }
    } catch (error) {
      console.error(
        `[TaskServer] Error breaking down high-effort task (attempt ${attempt}):`,
        error
      )

      // Retry if attempts remain
      if (attempt < maxRetries) {
        logToFile(
          `[TaskServer] Retrying task breakdown after error (attempt ${
            attempt + 1
          })`
        )
        return attemptBreakdown(attempt + 1)
      }
      return []
    }
  }

  // Start the breakdown process with first attempt
  return attemptBreakdown(1)
}

/**
 * Extracts parent task ID from a task description if present.
 * @param taskDescription The task description to check
 * @returns An object with the cleaned description and parentTaskId if found
 */
export function extractParentTaskId(taskDescription: string): {
  description: string
  parentTaskId?: string
} {
  const parentTaskMatch = taskDescription.match(/\[parentTask:([a-f0-9-]+)\]$/i)

  if (parentTaskMatch) {
    // Extract the parent task ID
    const parentTaskId = parentTaskMatch[1]
    // Remove the parent task tag from the description
    const description = taskDescription.replace(
      /\s*\[parentTask:[a-f0-9-]+\]$/i,
      ''
    )
    return { description, parentTaskId }
  }

  return { description: taskDescription }
}

/**
 * Extracts effort rating from a task description.
 * @param taskDescription The task description to check
 * @returns An object with the cleaned description and effort
 */
export function extractEffort(taskDescription: string): {
  description: string
  effort: 'low' | 'medium' | 'high'
} {
  const effortMatch = taskDescription.match(/^\[(low|medium|high)\]/i)

  if (effortMatch) {
    const effort = effortMatch[1].toLowerCase() as 'low' | 'medium' | 'high'
    // Remove the effort tag from the description
    const description = taskDescription.replace(
      /^\[(low|medium|high)\]\s*/i,
      ''
    )
    return { description, effort }
  }

  // Default to medium if no effort found
  return { description: taskDescription, effort: 'medium' }
}

/**
 * A more robust approach to parsing LLM-generated JSON that might be malformed due to newlines
 * or other common issues in AI responses.
 */
function robustJsonParse(text: string): any {
  // First attempt: Try with standard JSON.parse
  try {
    return JSON.parse(text)
  } catch (error: any) {
    // If standard parsing fails, try more aggressive fixing
    logToFile(
      `[robustJsonParse] Standard parsing failed, attempting recovery: ${error}`
    )

    try {
      // Detect the main expected structure type (tasks vs subtasks)
      const isTasksArray = text.includes('"tasks"')
      const isSubtasksArray = text.includes('"subtasks"')
      const hasDescription = text.includes('"description"')
      const hasEffort = text.includes('"effort"')

      // Special handling for common OpenRouter/AI model response patterns
      if ((isTasksArray || isSubtasksArray) && hasDescription && hasEffort) {
        const arrayKey = isSubtasksArray ? 'subtasks' : 'tasks'

        // 1. Enhanced regex that works for both tasks and subtasks arrays
        const taskRegex =
          /"description"\s*:\s*"((?:[^"\\]|\\"|\\|[\s\S])*?)"\s*,\s*"effort"\s*:\s*"(low|medium|high)"/g
        const tasks = []
        let match

        while ((match = taskRegex.exec(text)) !== null) {
          try {
            if (match[1] && match[2]) {
              tasks.push({
                description: match[1].replace(/\\"/g, '"'),
                effort: match[2],
              })
            }
          } catch (innerError) {
            logToFile(`[robustJsonParse] Error extracting task: ${innerError}`)
          }
        }

        if (tasks.length > 0) {
          logToFile(
            `[robustJsonParse] Successfully extracted ${tasks.length} ${arrayKey} with regex`
          )
          return { [arrayKey]: tasks }
        }

        // 2. If regex extraction fails, try extracting JSON objects directly
        if (tasks.length === 0) {
          try {
            const objectsExtracted = extractJSONObjects(text)
            if (objectsExtracted.length > 0) {
              // Filter valid task objects
              const validTasks = objectsExtracted.filter(
                (obj) =>
                  obj &&
                  typeof obj === 'object' &&
                  obj.description &&
                  obj.effort &&
                  typeof obj.description === 'string' &&
                  typeof obj.effort === 'string'
              )

              if (validTasks.length > 0) {
                logToFile(
                  `[robustJsonParse] Successfully extracted ${validTasks.length} ${arrayKey} with object extraction`
                )
                return { [arrayKey]: validTasks }
              }
            }
          } catch (objExtractionError) {
            logToFile(
              `[robustJsonParse] Object extraction failed: ${objExtractionError}`
            )
          }
        }
      }

      // 3. Fall back to manual line-by-line parsing for JSON objects
      const lines = text.split('\n')
      let cleanJson = ''
      let inString = false

      for (const line of lines) {
        let processedLine = line

        // Count quote marks to track if we're inside a string
        for (let i = 0; i < line.length; i++) {
          if (line[i] === '"' && (i === 0 || line[i - 1] !== '\\')) {
            inString = !inString
          }
        }

        // Add a space instead of newline if we're in the middle of a string
        cleanJson += inString ? ' ' + processedLine : processedLine
      }

      // 4. Balance braces and brackets if needed
      cleanJson = balanceBracesAndBrackets(cleanJson)

      // Final attempt to parse the cleaned JSON
      return JSON.parse(cleanJson)
    } catch (recoveryError) {
      logToFile(
        `[robustJsonParse] All recovery attempts failed: ${recoveryError}`
      )
      throw new Error(`Failed to parse JSON: ${error.message}`)
    }
  }
}

/**
 * Extracts valid JSON objects from a potentially malformed string.
 * Helps recover objects from truncated or malformed JSON.
 */
function extractJSONObjects(text: string): any[] {
  const objects: any[] = []

  // First try to find array boundaries
  const arrayStartIndex = text.indexOf('[')
  const arrayEndIndex = text.lastIndexOf(']')

  if (arrayStartIndex !== -1 && arrayEndIndex > arrayStartIndex) {
    // Extract array content
    const arrayContent = text.substring(arrayStartIndex + 1, arrayEndIndex)

    // Split by potential object boundaries, respecting nested objects
    let depth = 0
    let currentObject = ''
    let inString = false

    for (let i = 0; i < arrayContent.length; i++) {
      const char = arrayContent[i]

      // Track string boundaries
      if (char === '"' && (i === 0 || arrayContent[i - 1] !== '\\')) {
        inString = !inString
      }

      // Only track structure when not in a string
      if (!inString) {
        if (char === '{') {
          depth++
          if (depth === 1) {
            // Start of a new object
            currentObject = '{'
            continue
          }
        } else if (char === '}') {
          depth--
          if (depth === 0) {
            // End of an object, try to parse it
            currentObject += '}'
            try {
              const obj = JSON.parse(currentObject)
              objects.push(obj)
            } catch (e) {
              // If this object can't be parsed, just continue
            }
            currentObject = ''
            continue
          }
        } else if (char === ',' && depth === 0) {
          // Skip commas between objects
          continue
        }
      }

      // Add character to current object if we're inside one
      if (depth > 0) {
        currentObject += char
      }
    }
  }

  return objects
}

/**
 * Balances braces and brackets in a JSON string to make it valid
 */
function balanceBracesAndBrackets(text: string): string {
  let result = text

  // Count opening and closing braces/brackets
  const openBraces = (result.match(/\{/g) || []).length
  const closeBraces = (result.match(/\}/g) || []).length
  const openBrackets = (result.match(/\[/g) || []).length
  const closeBrackets = (result.match(/\]/g) || []).length

  // Add missing closing braces/brackets
  if (openBraces > closeBraces) {
    result += '}'.repeat(openBraces - closeBraces)
  }

  if (openBrackets > closeBrackets) {
    result += ']'.repeat(openBrackets - closeBrackets)
  }

  return result
}

/**
 * Parses and validates a JSON response string against a provided Zod schema.
 *
 * @param responseText - The raw JSON string from the LLM response
 * @param schema - The Zod schema to validate against
 * @returns An object containing either the validated data or error information
 */
export function parseAndValidateJsonResponse<T extends z.ZodType>(
  responseText: string | null | undefined,
  schema: T
):
  | { success: true; data: z.infer<T> }
  | { success: false; error: string; rawData: any | null } {
  // Handle null or empty responses
  if (!responseText) {
    return {
      success: false,
      error: 'Response text is empty or null',
      rawData: null,
    }
  }

  // Enhanced logging for debugging
  try {
    logToFile(
      `[parseAndValidateJsonResponse] Raw response text: ${responseText?.substring(
        0,
        1000
      )}`
    )
  } catch (logError) {
    // Ignore logging errors
  }

  // Extract JSON from the response if it's wrapped in markdown or other text
  let jsonString = responseText

  // Look for JSON in markdown code blocks
  const jsonBlockMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)\s*```/)
  if (jsonBlockMatch && jsonBlockMatch[1]) {
    jsonString = jsonBlockMatch[1]
  }

  // --- Additional cleaning: extract first valid JSON object from text ---
  function extractJsonFromText(text: string): string {
    // Remove markdown code fences
    text = text.replace(/```(?:json)?/gi, '').replace(/```/g, '')
    // Find the first { and last }
    const firstBrace = text.indexOf('{')
    const lastBrace = text.lastIndexOf('}')
    if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
      return text.substring(firstBrace, lastBrace + 1)
    }
    return text.trim()
  }
  jsonString = extractJsonFromText(jsonString)

  // Try to identify expected content type for better recovery
  const expectsSubtasks =
    responseText.includes('"subtasks"') || responseText.includes('subtasks')

  const expectsTasks =
    responseText.includes('"tasks"') || responseText.includes('tasks')

  // --- Auto-fix common JSON issues (trailing commas, comments) ---
  function fixCommonJsonIssues(text: string): string {
    // Remove JavaScript-style comments
    text = text.replace(/\/\/.*$/gm, '')
    text = text.replace(/\/\*[\s\S]*?\*\//g, '')
    // Remove trailing commas in objects and arrays
    text = text.replace(/,\s*([}\]])/g, '$1')

    // Fix broken newlines in the middle of strings
    text = text.replace(/([^\\])"\s*\n\s*"/g, '$1')

    // Normalize string values that got broken across lines
    text = text.replace(/([^\\])"\s*\n\s*([^"])/g, '$1", "$2')

    // Fix incomplete JSON objects
    const openBraces = (text.match(/\{/g) || []).length
    const closeBraces = (text.match(/\}/g) || []).length
    if (openBraces > closeBraces) {
      text = text + '}'.repeat(openBraces - closeBraces)
    }

    // Fix unclosed quotes at end of string
    if ((text.match(/"/g) || []).length % 2 !== 0) {
      // Check if the last quote is an opening quote (likely in the middle of a string)
      const lastQuotePos = text.lastIndexOf('"')
      const endsWithOpenQuote =
        lastQuotePos !== -1 &&
        text.substring(lastQuotePos).split('"').length === 2

      if (endsWithOpenQuote) {
        text = text + '"'
      }
    }

    return text
  }
  jsonString = fixCommonJsonIssues(jsonString)
  // --- End auto-fix ---

  try {
    logToFile(
      `[parseAndValidateJsonResponse] Cleaned JSON string: ${jsonString?.substring(
        0,
        1000
      )}`
    )
  } catch (logError) {
    // Ignore logging errors
  }

  // Attempt to parse the JSON using robust parser
  let parsedData: any
  try {
    parsedData = robustJsonParse(jsonString)
    logToFile(
      `[parseAndValidateJsonResponse] JSON parsed successfully with robust parser`
    )
  } catch (parseError) {
    // If primary parsing failed, try reconstructing specific expected structures
    try {
      // For tasks/subtasks, try to reconstruct using direct object extraction
      if (expectsTasks || expectsSubtasks) {
        const arrayKey = expectsSubtasks ? 'subtasks' : 'tasks'

        // Extract task objects directly from text
        const extractedObjects = extractJSONObjects(jsonString)
        if (extractedObjects.length > 0) {
          // Filter out invalid objects
          const validItems = extractedObjects.filter(
            (obj) =>
              obj &&
              typeof obj === 'object' &&
              obj.description &&
              obj.effort &&
              typeof obj.description === 'string' &&
              typeof obj.effort === 'string'
          )

          if (validItems.length > 0) {
            parsedData = { [arrayKey]: validItems }
            logToFile(
              `[parseAndValidateJsonResponse] Successfully reconstructed ${arrayKey} array with ${validItems.length} items`
            )

            // Validate against schema immediately
            const validationResult = schema.safeParse(parsedData)
            if (validationResult.success) {
              return {
                success: true,
                data: validationResult.data,
              }
            }
          }
        }

        // If we can see where tasks are, try regex extraction
        const regex = new RegExp(
          `"(description|desc|name)"\\s*:\\s*"([^"]*)"[\\s\\S]*?"(effort|difficulty)"\\s*:\\s*"(low|medium|high)"`,
          'gi'
        )

        const items = []
        let match
        while ((match = regex.exec(responseText)) !== null) {
          try {
            items.push({
              description: match[2],
              effort: match[4].toLowerCase(),
            })
          } catch (e) {
            // Skip invalid matches
          }
        }

        if (items.length > 0) {
          parsedData = { [arrayKey]: items }
          logToFile(
            `[parseAndValidateJsonResponse] Successfully extracted ${items.length} ${arrayKey} with regex`
          )

          // Validate against schema
          const validationResult = schema.safeParse(parsedData)
          if (validationResult.success) {
            return {
              success: true,
              data: validationResult.data,
            }
          }
        }
      }
    } catch (reconstructionError) {
      logToFile(
        `[parseAndValidateJsonResponse] Reconstruction error: ${reconstructionError}`
      )
      // Continue to normal error handling
    }

    // All parsing methods have failed
    logToFile(
      `[parseAndValidateJsonResponse] All parsing attempts failed: ${parseError}`
    )
    return {
      success: false,
      error: `Failed to parse JSON: ${(parseError as Error).message}`,
      rawData: responseText,
    }
  }

  // Validate against the schema
  const validationResult = schema.safeParse(parsedData)

  if (validationResult.success) {
    return {
      success: true,
      data: validationResult.data,
    }
  } else {
    logToFile(
      `[parseAndValidateJsonResponse] Schema validation failed. Errors: ${JSON.stringify(
        validationResult.error.errors
      )}`
    )

    // Attempt to recover partial valid data
    const recoveredData = attemptPartialResponseRecovery(parsedData, schema)
    if (recoveredData) {
      logToFile(
        `[parseAndValidateJsonResponse] Successfully recovered partial response`
      )
      return {
        success: true,
        data: recoveredData,
      }
    }

    return {
      success: false,
      error: `Schema validation failed: ${validationResult.error.message}`,
      rawData: parsedData,
    }
  }
}

/**
 * Attempts to recover partial valid data from a failed schema validation.
 * Particularly useful for array of tasks or subtasks where some items might be valid.
 */
function attemptPartialResponseRecovery(
  parsedData: any,
  schema: z.ZodType
): any | null {
  try {
    logToFile(
      `[attemptPartialResponseRecovery] Attempting to recover partial valid response`
    )

    // Handle common case: tasks array with valid and invalid items
    if (
      parsedData &&
      ((parsedData.tasks && Array.isArray(parsedData.tasks)) ||
        (parsedData.subtasks && Array.isArray(parsedData.subtasks)))
    ) {
      const isSubtasksArray =
        parsedData.subtasks && Array.isArray(parsedData.subtasks)
      const arrayKey = isSubtasksArray ? 'subtasks' : 'tasks'
      const items = isSubtasksArray ? parsedData.subtasks : parsedData.tasks

      // Filter out invalid task items
      const validItems = items.filter(
        (item: any) =>
          item &&
          typeof item === 'object' &&
          item.description &&
          item.effort &&
          typeof item.description === 'string' &&
          typeof item.effort === 'string'
      )

      if (validItems.length > 0) {
        const recoveredData = { ...parsedData, [arrayKey]: validItems }
        const validationResult = schema.safeParse(recoveredData)

        if (validationResult.success) {
          logToFile(
            `[attemptPartialResponseRecovery] Recovery successful, found ${validItems.length} valid ${arrayKey}`
          )
          return validationResult.data
        }
      }
    }

    return null
  } catch (error) {
    logToFile(
      `[attemptPartialResponseRecovery] Recovery attempt failed: ${error}`
    )
    return null
  }
}

/**
 * Ensures all task descriptions have an effort rating prefix.
 * Determines effort using an LLM if missing.
 */
export async function ensureEffortRatings(
  taskDescriptions: string[],
  model: GenerativeModel | OpenAI | null
): Promise<string[]> {
  const effortRatedTasks: string[] = []
  for (const taskDesc of taskDescriptions) {
    const effortMatch = taskDesc.match(/^\[(low|medium|high)\]/i)
    if (effortMatch) {
      // Ensure consistent casing
      const effort = effortMatch[1].toLowerCase() as 'low' | 'medium' | 'high'
      const cleanDesc = taskDesc.replace(/^\[(low|medium|high)\]\s*/i, '')
      effortRatedTasks.push(`[${effort}] ${cleanDesc}`)
    } else {
      let effort: 'low' | 'medium' | 'high' = 'medium' // Default effort
      try {
        if (model) {
          // Only call if model is available
          effort = await determineTaskEffort(taskDesc, model)
        }
      } catch (error) {
        console.error(
          `[TaskServer] Error determining effort for task "${taskDesc.substring(
            0,
            40
          )}...". Defaulting to medium:`,
          error
        )
      }
      effortRatedTasks.push(`[${effort}] ${taskDesc}`)
    }
  }
  return effortRatedTasks
}

/**
 * Processes tasks: breaks down high-effort ones, ensures effort, and creates Task objects.
 */
export async function processAndBreakdownTasks(
  initialTasksWithEffort: string[],
  model: GenerativeModel | OpenAI | null,
  featureId: string,
  fromReviewContext: boolean
): Promise<{ finalTasks: Task[]; complexTaskMap: Map<string, string> }> {
  const finalProcessedSteps: string[] = []
  const complexTaskMap = new Map<string, string>()
  let breakdownSuccesses = 0
  let breakdownFailures = 0

  for (const step of initialTasksWithEffort) {
    const effortMatch = step.match(/^\[(low|medium|high)\]/i)
    const isHighEffort = effortMatch && effortMatch[1].toLowerCase() === 'high'

    if (isHighEffort) {
      const taskDescription = step.replace(/^\[high\]\s*/i, '')
      const parentId = crypto.randomUUID()
      complexTaskMap.set(taskDescription, parentId) // Map original description to ID

      try {
        await addHistoryEntry(featureId, 'model', {
          step: 'task_breakdown_attempt',
          task: step,
          parentId,
        })

        const subtasks = await breakDownHighEffortTask(
          taskDescription,
          parentId,
          model,
          { minSubtasks: 2, maxSubtasks: 5, preferredEffort: 'medium' }
        )

        if (subtasks.length > 0) {
          // Add parent container task (marked completed later)
          finalProcessedSteps.push(`${step} [parentContainer]`) // Add marker

          // Process and add subtasks immediately after parent
          // Ensure subtasks also have effort ratings
          const subtasksWithEffort = await ensureEffortRatings(subtasks, model)
          const subtasksWithParentId = subtasksWithEffort.map((subtaskDesc) => {
            const { description: cleanSubDesc } = extractEffort(subtaskDesc) // Already has effort
            return `${subtaskDesc} [parentTask:${parentId}]`
          })

          finalProcessedSteps.push(...subtasksWithParentId)

          await addHistoryEntry(featureId, 'model', {
            step: 'task_breakdown_success',
            task: step,
            parentId,
            subtasks: subtasksWithParentId,
          })
          breakdownSuccesses++
        } else {
          // Breakdown failed, keep original high-effort task
          finalProcessedSteps.push(step)
          await addHistoryEntry(featureId, 'model', {
            step: 'task_breakdown_failure',
            task: step,
          })
          breakdownFailures++
        }
      } catch (breakdownError) {
        console.error(
          `[TaskServer] Error during breakdown for task "${taskDescription.substring(
            0,
            40
          )}...":`,
          breakdownError
        )
        finalProcessedSteps.push(step) // Keep original task on error
        await addHistoryEntry(featureId, 'model', {
          step: 'task_breakdown_error',
          task: step,
          error:
            breakdownError instanceof Error
              ? breakdownError.message
              : String(breakdownError),
        })
        breakdownFailures++
      }
    } else {
      // Keep low/medium effort tasks as is
      finalProcessedSteps.push(step)
    }
  }

  await logToFile(
    `[TaskServer] Breakdown processing complete: ${breakdownSuccesses} successes, ${breakdownFailures} failures.`
  )

  // --- Create Task Objects ---
  const finalTasks: Task[] = []
  const taskCreationErrors: string[] = []

  for (const step of finalProcessedSteps) {
    try {
      const isParentContainer = step.includes('[parentContainer]')
      const descriptionWithTags = step.replace('[parentContainer]', '').trim()

      const { description: descWithoutParent, parentTaskId } =
        extractParentTaskId(descriptionWithTags)
      const { description: cleanDescription, effort } =
        extractEffort(descWithoutParent)

      // Validate effort extracted or default
      const validatedEffort = ['low', 'medium', 'high'].includes(effort)
        ? effort
        : 'medium'

      // Get the predetermined ID for parent containers, otherwise generate new
      const originalHighEffortDesc = isParentContainer ? cleanDescription : null

      const taskId =
        (originalHighEffortDesc &&
          complexTaskMap.get(originalHighEffortDesc)) ||
        crypto.randomUUID()

      // If it's a parent container, set status to 'decomposed', otherwise 'pending'
      const status = isParentContainer ? 'decomposed' : 'pending'

      const taskDataToValidate: Omit<
        Task,
        'title' | 'subTasks' | 'dependencies' | 'history' | 'isManual'
      > = {
        id: taskId,
        feature_id: featureId,
        status,
        description: cleanDescription,
        effort: validatedEffort,
        completed: false, // All new tasks/subtasks start as not completed.
        ...(parentTaskId && { parentTaskId }),
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        ...(fromReviewContext && { fromReview: true }), // Set fromReview if in review context
      }

      // --- Enhanced Logging ---
      logToFile(
        `[processAndBreakdownTasks] Preparing ${
          isParentContainer ? 'Parent' : parentTaskId ? 'Subtask' : 'Task'
        } for validation: ID=${taskId}, Status=${status}, Parent=${
          parentTaskId || 'N/A'
        }, Desc="${cleanDescription.substring(0, 50)}..."`
      )
      logToFile(
        `[processAndBreakdownTasks] Task data before validation: ${JSON.stringify(
          taskDataToValidate
        )}`
      )
      // --- End Enhanced Logging ---

      // Validate against the Task schema before pushing
      const validationResult = TaskSchema.safeParse(taskDataToValidate)
      if (validationResult.success) {
        // --- Enhanced Logging ---
        logToFile(
          `[processAndBreakdownTasks] Validation successful for Task ID: ${taskId}`
        )
        // --- End Enhanced Logging ---
        finalTasks.push(validationResult.data)
      } else {
        // --- Enhanced Logging ---
        const errorMsg = `Task "${cleanDescription.substring(
          0,
          30
        )}..." (ID: ${taskId}) failed validation: ${
          validationResult.error.message
        }`
        logToFile(`[processAndBreakdownTasks] ${errorMsg}`)
        // --- End Enhanced Logging ---
        taskCreationErrors.push(errorMsg)
        console.warn(
          `[TaskServer] Task validation failed for "${cleanDescription.substring(
            0,
            30
          )}..." (ID: ${taskId}):`,
          validationResult.error.flatten()
        )
      }
    } catch (creationError) {
      const errorMsg = `Error creating task object for step "${step.substring(
        0,
        30
      )}...": ${
        creationError instanceof Error
          ? creationError.message
          : String(creationError)
      }`
      // --- Enhanced Logging ---
      logToFile(`[processAndBreakdownTasks] ${errorMsg}`)
      // --- End Enhanced Logging ---
      taskCreationErrors.push(errorMsg)
      console.error(
        `[TaskServer] Error creating task object for step "${step.substring(
          0,
          30
        )}...":`,
        creationError
      )
    }
  }

  if (taskCreationErrors.length > 0) {
    console.error(
      `[TaskServer] ${taskCreationErrors.length} errors occurred during task object creation/validation.`
    )
    await addHistoryEntry(featureId, 'model', {
      step: 'task_creation_errors',
      errors: taskCreationErrors,
    })
    // Decide if we should throw or return partial results. Returning for now.
  }

  return { finalTasks, complexTaskMap }
}

/**
 * Processes raw plan steps, ensures effort ratings are assigned, breaks down high-effort tasks,
 * saves the final task list, and notifies WebSocket clients of the update.
 *
 * @param rawPlanSteps Array of task descriptions (format: "[effort] description").
 * @param model The generative model to use for effort estimation/task breakdown.
 * @param featureId The ID of the feature being planned.
 * @param fromReview Optional flag to set fromReview: true on all saved tasks.
 * @returns The final list of processed Task objects.
 */
export async function processAndFinalizePlan(
  rawPlanSteps: string[],
  model: GenerativeModel | OpenAI | null,
  featureId: string,
  fromReview: boolean = false // Add default value
): Promise<Task[]> {
  logToFile(
    `[TaskServer] Processing and finalizing plan for feature ${featureId}...`
  )
  let existingTasks: Task[] = []
  let finalTasks: Task[] = []
  const complexTaskMap = new Map<string, string>() // To track original description of broken down tasks

  try {
    // 1. Ensure all raw steps have effort ratings.
    // ensureEffortRatings preserves existing [high] prefixes from rawPlanSteps
    // and assigns effort to those without a prefix.
    const initialTasksWithEffort = await ensureEffortRatings(
      rawPlanSteps,
      model
    )

    // Explicitly define the tasks to be sent for breakdown processing.
    // This includes tasks from rawPlanSteps that were marked [high]
    // (as ensureEffortRatings preserves such tags) and will be
    // unconditionally processed by processAndBreakdownTasks.
    const tasksForBreakdownProcessing = initialTasksWithEffort

    // 2. Process tasks: Breakdown high-effort ones.
    // processAndBreakdownTasks will identify and attempt to break down tasks
    // with a "[high]" prefix within tasksForBreakdownProcessing.
    const { finalTasks: processedTasks, complexTaskMap: breakdownMap } =
      await processAndBreakdownTasks(
        tasksForBreakdownProcessing, // Using the explicitly defined variable
        model,
        featureId, // Pass featureId for logging/history
        fromReview // Pass the fromReview context flag
      )

    // Merge complexTaskMap from breakdown
    breakdownMap.forEach((value, key) => complexTaskMap.set(key, value))

    // --- Start Database Operations ---
    await databaseService.connect()
    logToFile(
      `[processAndFinalizePlan] Database connected. Fetching existing tasks...`
    )

    // 3. Fetch existing tasks to compare
    existingTasks = await databaseService.getTasksByFeatureId(featureId)

    const existingTaskMap = new Map(existingTasks.map((t) => [t.id, t]))
    const processedTaskMap = new Map(processedTasks.map((t) => [t.id, t]))
    const tasksToAdd: Task[] = []
    const tasksToUpdate: { id: string; updates: Partial<Task> }[] = []
    const taskIdsToDelete: string[] = []

    // 4. Compare processed tasks with existing tasks
    for (const processedTask of processedTasks) {
      if (existingTaskMap.has(processedTask.id)) {
        // Task exists, check for updates
        const existing = existingTaskMap.get(processedTask.id)!
        // updates object should only contain keys matching DB columns (snake_case)
        const updates: Partial<
          Pick<Task, 'description' | 'effort' | 'fromReview'> & {
            parentTaskId?: string
          }
        > = {}
        if (existing.description !== processedTask.description) {
          updates.description = processedTask.description
        }
        if (existing.effort !== processedTask.effort) {
          updates.effort = processedTask.effort
        }
        // Compare snake_case from DB (existing) with camelCase from processed Task
        if (existing.parentTaskId !== processedTask.parentTaskId) {
          // Add snake_case key to updates object for DB
          updates.parentTaskId = processedTask.parentTaskId
        }
        // Always update the 'fromReview' flag if this process is from review
        if (fromReview && !existing.fromReview) {
          // Use camelCase here as Task type expects it, DB service handles conversion to snake_case
          updates.fromReview = true
          await logToFile(
            `[processAndFinalizePlan] Updating task ${existing.id} to set fromReview = true. Context fromReview: ${fromReview}, existing.fromReview: ${existing.fromReview}`
          )
        }

        // Check if any updates are needed using the keys in the updates object
        if (Object.keys(updates).length > 0) {
          tasksToUpdate.push({ id: processedTask.id, updates })
        }
      } else {
        // New task to add
        tasksToAdd.push(processedTask)
      }
    }

    if (!fromReview) {
      // Identify tasks to delete (exist in DB but not in new plan)
      for (const existingTask of existingTasks) {
        if (!processedTaskMap.has(existingTask.id)) {
          taskIdsToDelete.push(existingTask.id)
        }
      }
    }

    // 5. Apply changes to the database
    logToFile(
      `[processAndFinalizePlan] Applying DB changes: ${tasksToAdd.length} adds, ${tasksToUpdate.length} updates, ${taskIdsToDelete.length} deletes.`
    )
    for (const { id, updates } of tasksToUpdate) {
      // Check if the task being updated was decomposed
      const isDecomposed = complexTaskMap.has(id)
      if (isDecomposed) {
        // If decomposed, mark status as 'decomposed' and completed = true
        await databaseService.updateTaskStatus(id, 'decomposed', true)
        // Only update other details if necessary (rare for decomposed tasks)
        if (Object.keys(updates).length > 0) {
          // Pass updates object (contains snake_case key) to DB service
          await databaseService.updateTaskDetails(id, updates)
        }
      } else {
        // Otherwise, just update details
        // Pass updates object (contains snake_case key) to DB service
        await databaseService.updateTaskDetails(id, updates)
      }
    }

    for (const task of tasksToAdd) {
      // Ensure parent task exists if specified using camelCase from Task type
      if (task.parentTaskId) {
        const parentExistsInDB = existingTaskMap.has(task.parentTaskId)
        const parentExistsInProcessed = processedTaskMap.has(task.parentTaskId)

        if (!parentExistsInDB && !parentExistsInProcessed) {
          logToFile(
            `[processAndFinalizePlan] Warning: Parent task ${task.parentTaskId} for task ${task.id} not found. Setting parent to null.`
          )
          // Use camelCase when modifying the task object
          task.parentTaskId = undefined
        }
      }

      // Prepare object for DB insertion with snake_case keys
      const now = Math.floor(Date.now() / 1000)
      const dbTaskPayload: any = {
        id: task.id,
        title: task.title,
        description: task.description,
        status: task.status,
        completed: task.completed ? 1 : 0,
        effort: task.effort,
        feature_id: featureId,
        created_at:
          task.createdAt && typeof task.createdAt === 'number'
            ? Math.floor(new Date(task.createdAt * 1000).getTime() / 1000)
            : now, // Map and convert
        updated_at:
          task.updatedAt && typeof task.updatedAt === 'number'
            ? Math.floor(new Date(task.updatedAt * 1000).getTime() / 1000)
            : now, // Map and convert
        // Use camelCase 'fromReview' to align with the Task interface expected by addTask
        fromReview: fromReview || task.fromReview || false,
      }
      await logToFile(
        `[processAndFinalizePlan] Adding task ${task.id}. Context fromReview: ${fromReview}, task.fromReview property: ${task.fromReview}, dbTaskPayload.fromReview value: ${dbTaskPayload.fromReview}`,
        'debug'
      )

      try {
        // Ensure that the object passed to addTask conforms to the Task interface
        await databaseService.addTask({
          id: dbTaskPayload.id,
          title: dbTaskPayload.title,
          description: dbTaskPayload.description,
          status: dbTaskPayload.status,
          completed: dbTaskPayload.completed === 1, // Ensure boolean
          effort: dbTaskPayload.effort,
          feature_id: dbTaskPayload.feature_id,
          created_at: dbTaskPayload.created_at,
          updated_at: dbTaskPayload.updated_at,
          fromReview: dbTaskPayload.fromReview, // This is now correctly camelCased
        })
      } catch (dbError) {
        logToFile(
          `[processAndFinalizePlan] Error adding task to database: ${dbError}`
        )
        console.error(`[TaskServer] Error adding task to database:`, dbError)
        throw dbError
      }
    }

    for (const taskId of taskIdsToDelete) {
      await databaseService.deleteTask(taskId)
    }

    // 6. Fetch the final list of tasks after all modifications
    finalTasks = await databaseService.getTasksByFeatureId(featureId)

    logToFile(
      `[processAndFinalizePlan] Final task count for feature ${featureId}: ${finalTasks.length}`
    )
    // --- End Database Operations ---
  } catch (error) {
    logToFile(
      `[processAndFinalizePlan] Error during plan finalization for feature ${featureId}: ${error}`
    )
    console.error(`[TaskServer] Error during plan finalization:`, error)
    // Re-throw the error to be handled by the caller (e.g., tool handler)
    throw error
  } finally {
    // Ensure database connection is closed, even if errors occurred
    try {
      await databaseService.close()
      logToFile(`[processAndFinalizePlan] Database connection closed.`)
    } catch (closeError) {
      logToFile(
        `[processAndFinalizePlan] Error closing database connection: ${closeError}`
      )
      console.error(`[TaskServer] Error closing database:`, closeError)
    }
  }

  // 7. Notify UI about the updated tasks (outside the main try/catch for DB ops)
  try {
    // Add detailed logging to debug
    if (finalTasks.length > 0) {
      await logToFile(
        `[processAndFinalizePlan] Sample of final task from DB (finalTasks[0]): ${JSON.stringify(
          finalTasks[0],
          null,
          2
        )}`
      )
    } else {
      await logToFile(
        `[processAndFinalizePlan] No final tasks to log from DB sample.`
      )
    }

    const formattedTasks = finalTasks.map((task: any) => {
      // Create a clean task object for the WebSocket
      return {
        id: task.id,
        description: task.description,
        status: task.status,
        effort: task.effort,
        parentTaskId: task.parentTaskId,
        completed: task.completed,
        title: task.title,
        fromReview: task.fromReview || task.from_review === 1, // Handle both camelCase and snake_case
        createdAt:
          typeof task.createdAt === 'number'
            ? new Date(task.createdAt * 1000).toISOString()
            : undefined,
        updatedAt:
          typeof task.updatedAt === 'number'
            ? new Date(task.updatedAt * 1000).toISOString()
            : undefined,
      }
    })

    // Log the first formatted task
    if (formattedTasks.length > 0) {
      await logToFile(
        `[processAndFinalizePlan] First formatted task for WebSocket (formattedTasks[0]): ${JSON.stringify(
          formattedTasks[0],
          null,
          2
        )}`,
        'debug'
      )
    } else {
      await logToFile(
        `[processAndFinalizePlan] No formatted tasks to log for WebSocket sample.`,
        'debug'
      )
    }

    webSocketService.notifyTasksUpdated(featureId, formattedTasks)
    logToFile(`[processAndFinalizePlan] WebSocket notification sent.`)
  } catch (wsError) {
    logToFile(
      `[processAndFinalizePlan] Error sending WebSocket notification: ${wsError}`
    )
    console.error(`[TaskServer] Error sending WebSocket update:`, wsError)
    // Do not re-throw WS errors, as the main operation succeeded
  }

  return finalTasks
}

/**
 * Detects if the LLM response contains a clarification request.
 * This function searches for both JSON-formatted clarification requests and
 * special prefix format like [CLARIFICATION_NEEDED].
 *
 * @param responseText The raw response from the LLM
 * @returns An object with success flag and either the parsed clarification request or error message
 */
export function detectClarificationRequest(
  responseText: string | null | undefined
):
  | {
      detected: true
      clarificationRequest: z.infer<typeof LLMClarificationRequestSchema>
      rawResponse: string
    }
  | { detected: false; rawResponse: string | null } {
  if (!responseText) {
    return { detected: false, rawResponse: null }
  }

  // Check for [CLARIFICATION_NEEDED] format
  const prefixMatch = responseText.match(
    /\[CLARIFICATION_NEEDED\](.*?)(\[END_CLARIFICATION\]|$)/s
  )
  if (prefixMatch) {
    const questionText = prefixMatch[1].trim()

    // Parse out options if they exist
    const optionsMatch = questionText.match(/Options:\s*\[(.*?)\]/)
    const options = optionsMatch
      ? optionsMatch[1].split(',').map((o) => o.trim())
      : undefined

    // Check if text input is allowed
    const allowsText = !questionText.includes('MULTIPLE_CHOICE_ONLY')

    // Create a clarification request object
    return {
      detected: true,
      clarificationRequest: {
        type: 'clarification_needed',
        question: questionText
          .replace(/Options:\s*\[.*?\]/, '')
          .replace('MULTIPLE_CHOICE_ONLY', '')
          .trim(),
        options,
        allowsText,
      },
      rawResponse: responseText,
    }
  }

  // Try to parse as JSON
  try {
    // Check if we have a JSON object in the response
    const jsonMatch = responseText.match(/\{[\s\S]*\}/)
    if (jsonMatch) {
      const jsonStr = jsonMatch[0]
      const parsedJson = JSON.parse(jsonStr)

      // Check if it's a clarification request
      if (
        parsedJson.type === 'clarification_needed' ||
        parsedJson.clarification_needed ||
        parsedJson.needs_clarification
      ) {
        // Attempt to validate against our schema
        const result = LLMClarificationRequestSchema.safeParse({
          type: 'clarification_needed',
          question: parsedJson.question || parsedJson.message || '',
          options: parsedJson.options || undefined,
          allowsText: parsedJson.allowsText !== false,
        })

        if (result.success) {
          return {
            detected: true,
            clarificationRequest: result.data,
            rawResponse: responseText,
          }
        }
      }
    }

    return { detected: false, rawResponse: responseText }
  } catch (error) {
    // If JSON parsing fails, it's not a JSON-formatted clarification request
    return { detected: false, rawResponse: responseText }
  }
}

```
Page 2/2FirstPrevNextLast