workflows/default/systems/runtime/modules/ProcessTypes/Invoke-ExecutionProcess.ps1
|
<# .SYNOPSIS Execution process type: analysed -> in-progress -> done task loop. .DESCRIPTION Runs a continuous loop picking up analysed tasks, executing them via Claude in isolated git worktrees, verifying results, and squash-merging to main. 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 $instanceId = $Context.InstanceId $Continue = $Context.Continue $NoWait = $Context.NoWait $MaxTasks = $Context.MaxTasks $TaskId = $Context.TaskId $permissionMode = $Context.PermissionMode # Initialize session $sessionResult = Invoke-SessionInitialize -Arguments @{ session_type = "autonomous" } $sessionId = if ($sessionResult.success) { $sessionResult.session.session_id } else { $Context.BatchSessionId } # Load prompt template $templateFile = Join-Path $botRoot "recipes\prompts\99-autonomous-task.md" $promptTemplate = Get-Content $templateFile -Raw $processData.workflow = "99-autonomous-task.md" # Standards and product context $standardsList = "" $productMission = "" $entityModel = "" $standardsDir = Join-Path $botRoot "recipes\standards\global" if (Test-Path $standardsDir) { $standardsFiles = Get-ChildItem -Path $standardsDir -Filter "*.md" -File | ForEach-Object { ".bot/recipes/standards/global/$($_.Name)" } $standardsList = if ($standardsFiles) { "- " + ($standardsFiles -join "`n- ") } else { "No standards files found." } } $productDir = Join-Path $botRoot "workspace\product" $productMission = if (Test-Path (Join-Path $productDir "mission.md")) { "Read the product mission and context from: .bot/workspace/product/mission.md" } else { "No product mission file found." } $entityModel = if (Test-Path (Join-Path $productDir "entity-model.md")) { "Read the entity model design from: .bot/workspace/product/entity-model.md" } else { "No entity model file found." } # 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 Reset-InProgressTasks -TasksBaseDir $tasksBaseDir | Out-Null Reset-SkippedTasks -TasksBaseDir $tasksBaseDir | Out-Null # Clean up orphan worktrees from previous runs Remove-OrphanWorktrees -ProjectRoot $projectRoot -BotRoot $botRoot $tasksProcessed = 0 $maxRetriesPerTask = 2 $consecutiveFailureThreshold = 3 # Update process status to running $processData.status = 'running' Write-ProcessFile -Id $procId -Data $processData try { while ($true) { if ($MaxTasks -gt 0 -and $tasksProcessed -ge $MaxTasks) { Write-Status "Reached maximum task limit ($MaxTasks)" -Type Warn break } 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 } Write-Status "Fetching next task..." -Type Process $taskResult = Invoke-TaskGetNext -Arguments @{ verbose = $true } 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 = Invoke-TaskGetNext -Arguments @{ verbose = $true } 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)" # Mark execution task immediately Invoke-TaskMarkInProgress -Arguments @{ task_id = $task.id } | Out-Null Invoke-SessionUpdate -Arguments @{ current_task_id = $task.id } | Out-Null # --- Task type dispatch (script / mcp / task_gen bypass Claude) --- $taskTypeExec = if ($task.type) { $task.type } else { 'prompt' } if ($taskTypeExec -notin @('prompt', 'prompt_template')) { $typeSuccess = $false $typeError = $null try { switch ($taskTypeExec) { 'script' { $resolvedScript = Join-Path $botRoot $task.script_path Write-Status "Running script: $($task.script_path)" -Type Process Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Executing script task: $($task.name)" & $resolvedScript -BotRoot $botRoot -ProcessId $procId -Settings $settings $typeSuccess = ($LASTEXITCODE -eq 0 -or $null -eq $LASTEXITCODE) } 'mcp' { $toolFuncParts = $task.mcp_tool -split '_' $capitalParts = foreach ($p in $toolFuncParts) { $p.Substring(0,1).ToUpper() + $p.Substring(1) } $toolFunc = 'Invoke-' + ($capitalParts -join '') $toolArgs = if ($task.mcp_args) { $task.mcp_args } else { @{} } Write-Status "Calling MCP tool: $($task.mcp_tool)" -Type Process Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Executing MCP task: $($task.name)" $mcpResult = & $toolFunc -Arguments $toolArgs $typeSuccess = $true } 'task_gen' { $resolvedScript = Join-Path $botRoot $task.script_path Write-Status "Running task generator: $($task.script_path)" -Type Process Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Generating tasks: $($task.name)" & $resolvedScript -BotRoot $botRoot -ProcessId $procId -Settings $settings $typeSuccess = ($LASTEXITCODE -eq 0 -or $null -eq $LASTEXITCODE) Reset-TaskIndex } } } catch { $typeError = $_.Exception.Message Write-Status "Task type execution failed: $typeError" -Type Error Write-ProcessActivity -Id $procId -ActivityType "error" -Message "$($task.name): $typeError" } if ($typeSuccess) { try { $doneDir = Join-Path $botRoot "workspace\tasks\done" if (-not (Test-Path $doneDir)) { New-Item -Path $doneDir -ItemType Directory -Force | Out-Null } $taskFile = Get-ChildItem (Join-Path $botRoot "workspace\tasks\in-progress") -Filter "*.json" -File | Where-Object { (Get-Content $_.FullName -Raw | ConvertFrom-Json).id -eq $task.id } | Select-Object -First 1 if ($taskFile) { $content = Get-Content $taskFile.FullName -Raw | ConvertFrom-Json $content.status = 'done' $content.completed_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") $content.updated_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") $content | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $doneDir $taskFile.Name) -Encoding UTF8 Remove-Item $taskFile.FullName -Force } } catch { Write-Status "Failed to mark done: $($_.Exception.Message)" -Type Warn } Write-Status "Task completed: $($task.name)" -Type Complete Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Completed $taskTypeExec task: $($task.name)" Invoke-SessionIncrementCompleted -Arguments @{} | Out-Null $tasksProcessed++ } else { Write-Status "Task failed: $($task.name)" -Type Error try { Invoke-TaskMarkSkipped -Arguments @{ task_id = $task.id; skip_reason = "$taskTypeExec execution failed: $typeError" } | Out-Null } catch { Write-BotLog -Level Debug -Message "Session operation failed" -Exception $_ } } continue } # --- Worktree setup --- $worktreePath = $null $branchName = $null $wtInfo = Get-TaskWorktreeInfo -TaskId $task.id -BotRoot $botRoot if ($wtInfo -and (Test-Path $wtInfo.worktree_path)) { $worktreePath = $wtInfo.worktree_path $branchName = $wtInfo.branch_name Write-Status "Using worktree: $worktreePath" -Type Info } else { try { Assert-OnBaseBranch -ProjectRoot $projectRoot | Out-Null } catch { Write-Status "Branch guard warning: $($_.Exception.Message)" -Type Warn } $wtResult = New-TaskWorktree -TaskId $task.id -TaskName $task.name ` -ProjectRoot $projectRoot -BotRoot $botRoot if ($wtResult.success) { $worktreePath = $wtResult.worktree_path $branchName = $wtResult.branch_name Write-Status "Worktree: $worktreePath" -Type Info } else { Write-Status "Worktree failed: $($wtResult.message)" -Type Warn } } # 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 execution prompt $prompt = Build-TaskPrompt ` -PromptTemplate $promptTemplate ` -Task $task ` -SessionId $sessionId ` -ProductMission $productMission ` -EntityModel $entityModel ` -StandardsList $standardsList ` -InstanceId $instanceId $branchForPrompt = if ($branchName) { $branchName } else { "main" } $prompt = $prompt -replace '\{\{BRANCH_NAME\}\}', $branchForPrompt $fullPrompt = @" $prompt ## Process Context - **Process ID:** $procId - **Instance Type:** execution Use the Process ID when calling ``steering_heartbeat`` (pass it as ``process_id``). ## Completion Goal Task $($task.id) is complete: all acceptance criteria met, verification passed, and task marked done. Work on this task autonomously. When complete, ensure you call task_mark_done via MCP. "@ # Invoke Claude with retries $attemptNumber = 0 $taskSuccess = $false if ($worktreePath) { Push-Location $worktreePath } 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 } # Kill any background processes Claude may have spawned in the worktree if ($worktreePath) { $cleanedUp = Stop-WorktreeProcesses -WorktreePath $worktreePath if ($cleanedUp -gt 0) { Write-Diag "Cleaned up $cleanedUp orphan process(es) after execution attempt" Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Cleaned up $cleanedUp background process(es) from worktree" } } # 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 completion $completionCheck = Test-TaskCompletion -TaskId $task.id if ($completionCheck.completed) { Write-Status "Task completed!" -Type Complete Invoke-SessionIncrementCompleted -Arguments @{} | Out-Null $taskSuccess = $true break } # Task not completed - handle failure $failureReason = Get-FailureReason -ExitCode $exitCode -Stdout "" -Stderr "" -TimedOut $false if (-not $failureReason.recoverable) { Write-Status "Non-recoverable failure - skipping" -Type Error try { Invoke-TaskMarkSkipped -Arguments @{ task_id = $task.id; skip_reason = "non-recoverable" } | Out-Null } catch { Write-BotLog -Level Warn -Message "Task operation failed" -Exception $_ } break } if ($attemptNumber -ge $maxRetriesPerTask) { Write-Status "Max retries exhausted" -Type Error try { Invoke-TaskMarkSkipped -Arguments @{ task_id = $task.id; skip_reason = "max-retries" } | Out-Null } catch { Write-BotLog -Level Warn -Message "Task operation failed" -Exception $_ } break } } } finally { if ($worktreePath) { Stop-WorktreeProcesses -WorktreePath $worktreePath | Out-Null Pop-Location } } # Update process data $env:DOTBOT_CURRENT_TASK_ID = $null $env:CLAUDE_SESSION_ID = $null if ($taskSuccess) { # Post-completion: squash-merge task branch to main if ($worktreePath) { Write-Status "Merging task branch to main..." -Type Process $mergeResult = Complete-TaskWorktree -TaskId $task.id -ProjectRoot $projectRoot -BotRoot $botRoot if ($mergeResult.success) { Write-Status "Merged: $($mergeResult.message)" -Type Complete Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Squash-merged to main: $($task.name)" if ($mergeResult.push_result.attempted) { if ($mergeResult.push_result.success) { Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Pushed to remote: $($task.name)" } else { Write-Status "Push failed: $($mergeResult.push_result.error)" -Type Warn Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Push failed after merge: $($mergeResult.push_result.error)" } } } else { Write-Status "Merge failed: $($mergeResult.message)" -Type Error Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Merge failed for $($task.name): $($mergeResult.message)" # Resolve via $PSScriptRoot so the lookup is immune to a null # $global:DotbotProjectRoot and to Join-Path's backslash quirk on Linux. $escalationModule = Join-Path (Split-Path $PSScriptRoot -Parent) 'MergeConflictEscalation.psm1' if (Test-Path $escalationModule) { Import-Module $escalationModule -Force Invoke-MergeConflictEscalation -Task $task -TasksBaseDir $tasksBaseDir -MergeResult $mergeResult -WorktreePath $worktreePath -ProcId $procId -BotRoot $botRoot | Out-Null } else { Write-Status "Merge-conflict escalation helper not found at $escalationModule" -Type Error Write-ProcessActivity -Id $procId -ActivityType "text" -Message "Escalation helper missing for $($task.name); task left in done/" } } } $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)" # Clean up worktree for failed/skipped tasks if ($worktreePath) { Write-Status "Cleaning up worktree for failed task..." -Type Info try { Remove-Junctions -WorktreePath $worktreePath -ErrorOnFailure $false | Out-Null git -C $projectRoot worktree remove $worktreePath --force 2>$null git -C $projectRoot branch -D $branchName 2>$null } finally { Initialize-WorktreeMap -BotRoot $botRoot Invoke-WorktreeMapLocked -Action { $cleanupMap = Read-WorktreeMap $cleanupMap.Remove($task.id) Write-WorktreeMap -Map $cleanupMap } try { Assert-OnBaseBranch -ProjectRoot $projectRoot | Out-Null } catch { Write-BotLog -Level Warn -Message "Task operation failed" -Exception $_ } } } # Update session failure counters try { $state = Invoke-SessionGetState -Arguments @{} $newFailures = $state.state.consecutive_failures + 1 Invoke-SessionUpdate -Arguments @{ consecutive_failures = $newFailures tasks_skipped = $state.state.tasks_skipped + 1 } | Out-Null if ($newFailures -ge $consecutiveFailureThreshold) { Write-Status "$consecutiveFailureThreshold consecutive failures - stopping" -Type Error break } } catch { Write-BotLog -Level Warn -Message "Task operation failed" -Exception $_ } } # 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))" try { Invoke-SessionUpdate -Arguments @{ status = "stopped" } | Out-Null } catch { Write-BotLog -Level Debug -Message "Logging operation failed" -Exception $_ } } |