workflows/default/systems/runtime/modules/ProcessTypes/Invoke-AnalysisProcess.ps1

<#
.SYNOPSIS
    Analysis process type: todo -> analysing -> analysed task loop.
.DESCRIPTION
    Runs a continuous loop picking up todo tasks, running analysis via Claude,
    and marking them as analysed/needs-input/skipped. No worktrees.
    Extracted from launch-process.ps1 as part of v4 Phase 03 (#92).
#>


param(
    [Parameter(Mandatory)]
    [hashtable]$Context
)

$botRoot = $Context.BotRoot
$procId = $Context.ProcId
$processData = $Context.ProcessData
$claudeModelName = $Context.ModelName
$claudeSessionId = $Context.SessionId
$ShowDebug = $Context.ShowDebug
$ShowVerbose = $Context.ShowVerbose
$projectRoot = $Context.ProjectRoot
$processesDir = $Context.ProcessesDir
$settings = $Context.Settings
$Model = $Context.Model
$sessionId = $Context.BatchSessionId
$instanceId = $Context.InstanceId
$Continue = $Context.Continue
$NoWait = $Context.NoWait
$MaxTasks = $Context.MaxTasks
$TaskId = $Context.TaskId
$permissionMode = $Context.PermissionMode

# Load prompt template
$templateFile = Join-Path $botRoot "recipes\prompts\98-analyse-task.md"
$promptTemplate = Get-Content $templateFile -Raw
$processData.workflow = "98-analyse-task.md"

# Task reset
. (Join-Path $botRoot "systems\runtime\modules\task-reset.ps1")
$tasksBaseDir = Join-Path $botRoot "workspace\tasks"
Reset-AnalysingTasks -TasksBaseDir $tasksBaseDir -ProcessesDir $processesDir | Out-Null

# Clean up orphan worktrees from previous runs
Remove-OrphanWorktrees -ProjectRoot $projectRoot -BotRoot $botRoot

# Initialize task index for analysis
Initialize-TaskIndex -TasksBaseDir $tasksBaseDir

$tasksProcessed = 0
$maxRetriesPerTask = 2
$consecutiveFailureThreshold = 3

# Update process status to running
$processData.status = 'running'
Write-ProcessFile -Id $procId -Data $processData

try {
    while ($true) {
        # Check max tasks
        if ($MaxTasks -gt 0 -and $tasksProcessed -ge $MaxTasks) {
            Write-Status "Reached maximum task limit ($MaxTasks)" -Type Warn
            break
        }

        # Check stop signal
        if (Test-ProcessStopSignal -Id $procId) {
            Write-Status "Stop signal received" -Type Error
            $processData.status = 'stopped'
            $processData.failed_at = (Get-Date).ToUniversalTime().ToString("o")
            Write-ProcessFile -Id $procId -Data $processData
            Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Process stopped by user"
            break
        }

        # Get next task
        Write-Status "Fetching next task..." -Type Process
        Reset-TaskIndex

        # Wait for any active execution worktrees to merge first
        $waitingLogged = $false
        while ($true) {
            Initialize-WorktreeMap -BotRoot $botRoot
            $map = Read-WorktreeMap
            $hasActiveExecutionWt = $false

            if ($map.Count -gt 0) {
                $index = Get-TaskIndex
                foreach ($taskId in @($map.Keys)) {
                    if ($index.InProgress.ContainsKey($taskId) -or
                        $index.Done.ContainsKey($taskId)) {
                        $entry = $map[$taskId]
                        if ($entry.worktree_path -and (Test-Path $entry.worktree_path)) {
                            $hasActiveExecutionWt = $true
                            break
                        }
                    }
                }
            }

            if (-not $hasActiveExecutionWt) { break }

            if (-not $waitingLogged) {
                Write-Status "Waiting for execution merge before next analysis..." -Type Info
                Write-ProcessActivity -Id $procId -ActivityType "text" `
                    -Message "Waiting for execution to merge before starting next analysis"
                $processData.heartbeat_status = "Waiting for execution merge"
                Write-ProcessFile -Id $procId -Data $processData
                $waitingLogged = $true
            }

            Start-Sleep -Seconds 5
            if (Test-ProcessStopSignal -Id $procId) { break }
        }

        # For analysis: check resumed tasks (answered questions) first, then todo
        $taskResult = Get-NextTodoTask -Verbose

        # Immediately claim task to prevent execution from picking it up
        if ($taskResult.task) {
            # Auto-promote non-prompt tasks that skip analysis
            $taskSkipAnalysis = $taskResult.task.skip_analysis
            $taskTypeVal = if ($taskResult.task.type) { $taskResult.task.type } else { 'prompt' }
            if ($taskSkipAnalysis -or $taskTypeVal -notin @('prompt', 'prompt_template')) {
                Write-Status "Auto-promoting task (type=$taskTypeVal, skip_analysis): $($taskResult.task.name)" -Type Info
                Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Auto-promoted $($taskResult.task.name) (type=$taskTypeVal)"
                Invoke-TaskMarkAnalysing -Arguments @{ task_id = $taskResult.task.id } | Out-Null
                Invoke-TaskMarkAnalysed -Arguments @{
                    task_id = $taskResult.task.id
                    analysis = @{
                        summary = "Auto-promoted: task type '$taskTypeVal' skips LLM analysis"
                        auto_promoted = $true
                    }
                } | Out-Null
                $tasksProcessed++
                continue
            }
            Invoke-TaskMarkAnalysing -Arguments @{ task_id = $taskResult.task.id } | Out-Null
        }

        if (-not $taskResult.success) {
            Write-Status "Error fetching task: $($taskResult.message)" -Type Error
            break
        }

        if (-not $taskResult.task) {
            if ($Continue -and -not $NoWait) {
                $waitReason = if ($taskResult.message) { $taskResult.message } else { "No eligible tasks." }
                Write-Status "No tasks available - waiting... ($waitReason)" -Type Info
                Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Waiting for new tasks..."

                $foundTask = $false
                while ($true) {
                    Start-Sleep -Seconds 5
                    if (Test-ProcessStopSignal -Id $procId) { break }
                    $processData.last_heartbeat = (Get-Date).ToUniversalTime().ToString("o")
                    Write-ProcessFile -Id $procId -Data $processData
                    Reset-TaskIndex
                    $taskResult = Get-NextTodoTask -Verbose
                    if ($taskResult.task) { $foundTask = $true; break }
                    if (Test-DependencyDeadlock -ProcessId $procId) { break }
                }
                if (-not $foundTask) { break }
            } else {
                Write-Status "No tasks available" -Type Info
                break
            }
        }

        $task = $taskResult.task
        $processData.task_id = $task.id
        $processData.task_name = $task.name
        $processData.heartbeat_status = "Working on: $($task.name)"
        Write-ProcessFile -Id $procId -Data $processData

        $env:DOTBOT_CURRENT_TASK_ID = $task.id
        $taskTypeForHeader = if ($task.type) { $task.type } else { 'prompt' }
        Write-TaskHeader -TaskName $task.name -TaskType $taskTypeForHeader -Model $Model -ProcessId $procId
        Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Started task: $($task.name)"

        # Generate new provider session ID per task
        $claudeSessionId = New-ProviderSession
        $env:CLAUDE_SESSION_ID = $claudeSessionId
        $processData.claude_session_id = $claudeSessionId
        Write-ProcessFile -Id $procId -Data $processData

        # Build analysis prompt
        $prompt = $promptTemplate
        $prompt = $prompt -replace '\{\{SESSION_ID\}\}', $sessionId
        $prompt = $prompt -replace '\{\{TASK_ID\}\}', $task.id
        $prompt = $prompt -replace '\{\{TASK_NAME\}\}', $task.name
        $prompt = $prompt -replace '\{\{TASK_CATEGORY\}\}', $task.category
        $prompt = $prompt -replace '\{\{TASK_PRIORITY\}\}', $task.priority
        $prompt = $prompt -replace '\{\{TASK_EFFORT\}\}', $task.effort
        $prompt = $prompt -replace '\{\{TASK_DESCRIPTION\}\}', $task.description
        $niValue = if ("$($task.needs_interview)" -eq 'true') { 'true' } else { 'false' }
        Write-Status "needs_interview raw=$($task.needs_interview) resolved=$niValue" -Type Info
        $prompt = $prompt -replace '\{\{NEEDS_INTERVIEW\}\}', $niValue
        $acceptanceCriteria = if ($task.acceptance_criteria) { ($task.acceptance_criteria | ForEach-Object { "- $_" }) -join "`n" } else { "No specific acceptance criteria defined." }
        $prompt = $prompt -replace '\{\{ACCEPTANCE_CRITERIA\}\}', $acceptanceCriteria
        $steps = if ($task.steps) { ($task.steps | ForEach-Object { "- $_" }) -join "`n" } else { "No specific steps defined." }
        $prompt = $prompt -replace '\{\{TASK_STEPS\}\}', $steps
        $splitThreshold = if ($settings.analysis.split_threshold_effort) { $settings.analysis.split_threshold_effort } else { 'XL' }
        $prompt = $prompt -replace '\{\{SPLIT_THRESHOLD_EFFORT\}\}', $splitThreshold
        $prompt = $prompt -replace '\{\{BRANCH_NAME\}\}', "main"

        # Build resolved questions context for resumed tasks
        $isResumedTask = $task.status -eq 'analysing'
        $resolvedQuestionsContext = ""
        $taskQR = if ($task.PSObject.Properties['questions_resolved']) { $task.questions_resolved } else { $null }
        if ($isResumedTask -and $taskQR) {
            $resolvedQuestionsContext = "`n## Previously Resolved Questions`n`n"
            $resolvedQuestionsContext += "This task was previously paused for human input. The following questions have been answered:`n`n"
            foreach ($q in $taskQR) {
                $resolvedQuestionsContext += "**Q:** $($q.question)`n"
                $resolvedQuestionsContext += "**A:** $($q.answer)`n`n"
            }
            $resolvedQuestionsContext += "Use these answers to guide your analysis. The task is already in ``analysing`` status - do NOT call ``task_mark_analysing`` again.`n"
        }

        $fullPrompt = @"
$prompt
$resolvedQuestionsContext
## Process Context

- **Process ID:** $procId
- **Instance Type:** analysis

Use the Process ID when calling ``steering_heartbeat`` (pass it as ``process_id``).

## Completion Goal

Analyse task $($task.id) completely. When analysis is finished:
- If all context is gathered: Call task_mark_analysed with the full analysis object
- If you need human input: Call task_mark_needs_input with a question or split_proposal
- If blocked by issues: Call task_mark_skipped with a reason

Do NOT implement the task. Your job is research and preparation only.
"@


        # Invoke Claude with retries
        $attemptNumber = 0
        $taskSuccess = $false

        try {
        while ($attemptNumber -le $maxRetriesPerTask) {
            $attemptNumber++

            if ($attemptNumber -gt 1) {
                Write-Status "Retry attempt $attemptNumber of $maxRetriesPerTask" -Type Warn
            }

            if (Test-ProcessStopSignal -Id $procId) {
                $processData.status = 'stopped'
                $processData.failed_at = (Get-Date).ToUniversalTime().ToString("o")
                Write-ProcessFile -Id $procId -Data $processData
                break
            }

            Write-Header "Claude Session"
            try {
                $streamArgs = @{
                    Prompt = $fullPrompt
                    Model = $claudeModelName
                    SessionId = $claudeSessionId
                    PersistSession = $false
                }
                if ($ShowDebug) { $streamArgs['ShowDebugJson'] = $true }
                if ($ShowVerbose) { $streamArgs['ShowVerbose'] = $true }
                if ($permissionMode) { $streamArgs['PermissionMode'] = $permissionMode }

                Invoke-ProviderStream @streamArgs
                $exitCode = 0
            } catch {
                Write-Status "Error: $($_.Exception.Message)" -Type Error
                $exitCode = 1
            }

            # Update heartbeat
            $processData.last_heartbeat = (Get-Date).ToUniversalTime().ToString("o")
            Write-ProcessFile -Id $procId -Data $processData

            # Check rate limit
            $rateLimitMsg = Get-LastProviderRateLimitInfo
            if ($rateLimitMsg) {
                Write-Status "Rate limit detected!" -Type Warn
                $rateLimitInfo = Get-RateLimitResetTime -Message $rateLimitMsg
                if ($rateLimitInfo) {
                    $processData.heartbeat_status = "Rate limited - waiting..."
                    Write-ProcessFile -Id $procId -Data $processData
                    Write-ProcessActivity -Id $procId -ActivityType "rate_limit" -Message $rateLimitMsg

                    $waitSeconds = $rateLimitInfo.wait_seconds
                    if (-not $waitSeconds -or $waitSeconds -lt 30) { $waitSeconds = 60 }
                    for ($w = 0; $w -lt $waitSeconds; $w++) {
                        Start-Sleep -Seconds 1
                        if (Test-ProcessStopSignal -Id $procId) { break }
                    }

                    $attemptNumber--
                    continue
                }
            }

            # Check if task moved to analysed/needs-input/skipped
            $taskDirs = @('analysed', 'needs-input', 'skipped', 'in-progress', 'done')
            $taskFound = $false
            foreach ($dir in $taskDirs) {
                $checkDir = Join-Path $botRoot "workspace\tasks\$dir"
                if (Test-Path $checkDir) {
                    $files = Get-ChildItem -Path $checkDir -Filter "*.json" -File
                    foreach ($f in $files) {
                        try {
                            $content = Get-Content -Path $f.FullName -Raw | ConvertFrom-Json
                            if ($content.id -eq $task.id) {
                                $taskFound = $true
                                $taskSuccess = $true
                                Write-Status "Analysis complete (status: $dir)" -Type Complete
                                break
                            }
                        } catch { Write-BotLog -Level Debug -Message "Failed to parse data" -Exception $_ }
                    }
                    if ($taskFound) { break }
                }
            }
            if ($taskSuccess) { break }

            if ($attemptNumber -ge $maxRetriesPerTask) {
                Write-Status "Max retries exhausted" -Type Error
                break
            }
        }
        } finally { }

        # Update process data
        $env:DOTBOT_CURRENT_TASK_ID = $null
        $env:CLAUDE_SESSION_ID = $null

        if ($taskSuccess) {
            $tasksProcessed++
            $processData.tasks_completed = $tasksProcessed
            $processData.heartbeat_status = "Completed: $($task.name)"
            Write-ProcessFile -Id $procId -Data $processData
            Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Task completed: $($task.name)"

            try { Remove-ProviderSession -SessionId $claudeSessionId -ProjectRoot $projectRoot | Out-Null } catch { Write-BotLog -Level Debug -Message "Session operation failed" -Exception $_ }
        } else {
            Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Task failed: $($task.name)"
        }

        # Continue to next task?
        if (-not $Continue) { break }

        $TaskId = $null
        $processData.task_id = $null
        $processData.task_name = $null

        # Delay between tasks
        Write-Status "Waiting 3s before next task..." -Type Info
        for ($i = 0; $i -lt 3; $i++) {
            Start-Sleep -Seconds 1
            if (Test-ProcessStopSignal -Id $procId) { break }
        }

        if (Test-ProcessStopSignal -Id $procId) {
            $processData.status = 'stopped'
            $processData.failed_at = (Get-Date).ToUniversalTime().ToString("o")
            Write-ProcessFile -Id $procId -Data $processData
            break
        }
    }
} finally {
    if ($processData.status -eq 'running') {
        $processData.status = 'completed'
        $processData.completed_at = (Get-Date).ToUniversalTime().ToString("o")
    }
    Write-ProcessFile -Id $procId -Data $processData
    Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Process $procId finished ($($processData.status))"
}