workflows/default/systems/mcp/tools/task-get-next/script.ps1
|
# Import task index module $indexModule = Join-Path $global:DotbotProjectRoot ".bot\systems\mcp\modules\TaskIndexCache.psm1" if (-not (Get-Module TaskIndexCache)) { Import-Module $indexModule -Force } # Import task store (for Move-TaskState when skipping condition-unmet tasks) $taskStoreModule = Join-Path $global:DotbotProjectRoot ".bot\systems\mcp\modules\TaskStore.psm1" if (-not (Get-Module TaskStore)) { Import-Module $taskStoreModule -Force } # Import ManifestCondition module for Test-ManifestCondition $manifestConditionModule = Join-Path $global:DotbotProjectRoot ".bot\systems\runtime\modules\ManifestCondition.psm1" if (-not (Get-Module ManifestCondition)) { Import-Module $manifestConditionModule -Force } # Fail loud if still missing — silent fallback would resurrect #226. Stderr (not Write-BotLog) # because tool discovery may run before DotBotLog is initialized. if (-not (Get-Command Test-ManifestCondition -ErrorAction SilentlyContinue)) { [Console]::Error.WriteLine("WARN: [task-get-next] Test-ManifestCondition unavailable - runtime condition checks DISABLED. Re-run 'pwsh install.ps1' or 'dotbot init'.") } # Initialize index on first use $tasksBaseDir = Join-Path $global:DotbotProjectRoot ".bot\workspace\tasks" Initialize-TaskIndex -TasksBaseDir $tasksBaseDir function Invoke-TaskGetNext { param( [hashtable]$Arguments ) $verbose = $Arguments['verbose'] -eq $true $preferAnalysed = $Arguments['prefer_analysed'] $workflowFilter = $Arguments['workflow_filter'] # Default to preferring analysed tasks (can be overridden) if ($null -eq $preferAnalysed) { $preferAnalysed = $true } Write-BotLog -Level Debug -Message "[task-get-next] Using cached task index (prefer_analysed: $preferAnalysed)" $nextTask = $null $taskStatus = 'todo' $blockedCount = 0 $conditionSkipCount = 0 $moveFailures = @() # Re-evaluate `condition` per candidate (issue #226). Loop so we can skip # condition-unmet tasks and pick the next eligible one. Priority: analysed → todo. # Bound = current candidate pool size (+ small buffer) so we can't return # "no task" while eligible candidates remain behind skipped ones. $initialIndex = Get-TaskIndex $candidatePoolSize = $initialIndex.Todo.Count + $initialIndex.Analysed.Count $maxIterations = [Math]::Max(50, $candidatePoolSize + 10) # Track IDs we've already considered in this invocation. Acts as a safety net # against re-picking a task whose Move-TaskState failed (so the index still # lists it as todo/analysed on subsequent Update-TaskIndex calls). $seenIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $iter = 0 for (; $iter -lt $maxIterations; $iter++) { $candidate = $null $candidateStatus = 'todo' if ($preferAnalysed) { $analysedResult = Get-NextAnalysedTask -WorkflowFilter $workflowFilter # Track max so the "no tasks available" message stays accurate after skips. if ($analysedResult.BlockedCount -gt $blockedCount) { $blockedCount = $analysedResult.BlockedCount } if ($analysedResult.Task -and -not $seenIds.Contains($analysedResult.Task.id)) { $candidate = $analysedResult.Task $candidateStatus = 'analysed' Write-BotLog -Level Debug -Message "[task-get-next] Found analysed task: $($candidate.id) ($($analysedResult.BlockedCount) blocked by dependencies)" } elseif ($analysedResult.BlockedCount -gt 0) { Write-BotLog -Level Debug -Message "[task-get-next] All $($analysedResult.BlockedCount) analysed task(s) blocked by unmet dependencies" } } # Fallback to todo (or todo-only when prefer_analysed=false, used by analysis phase) if (-not $candidate) { $todoCandidate = Get-NextTask -WorkflowFilter $workflowFilter if ($todoCandidate -and -not $seenIds.Contains($todoCandidate.id)) { $candidate = $todoCandidate $candidateStatus = 'todo' } } if (-not $candidate) { break } [void]$seenIds.Add($candidate.id) # If Test-ManifestCondition is missing here we deliberately let PS raise (see load-time check). if ($candidate.condition) { $conditionMet = Test-ManifestCondition -ProjectRoot $global:DotbotProjectRoot -Condition $candidate.condition if (-not $conditionMet) { $conditionText = if ($candidate.condition -is [array]) { ($candidate.condition -join ', ') } else { "$($candidate.condition)" } Write-BotLog -Level Info -Message "[task-get-next] Skipped task '$($candidate.name)' ($($candidate.id)): condition not met ($conditionText)" try { Move-TaskState -TaskId $candidate.id ` -FromStates @($candidateStatus) ` -ToState 'skipped' ` -Updates @{ skip_reason = 'condition-not-met' skip_detail = "Condition not met at runtime: $conditionText" } | Out-Null $conditionSkipCount++ # TODO: incrementalise — full rescan per skip is O(N·skips). Update-TaskIndex } catch { # Surface the failure but keep looking — one bad task shouldn't stall # the whole pipeline. $seenIds prevents re-picking this candidate. Write-BotLog -Level Warn -Message "[task-get-next] Failed to move task $($candidate.id) to skipped; continuing with other candidates" -Exception $_ $moveFailures += "$($candidate.id) ($($candidate.name))" } continue } } $nextTask = $candidate $taskStatus = $candidateStatus break } if ($iter -ge $maxIterations) { Write-BotLog -Level Warn -Message "[task-get-next] Hit maxIterations cap ($maxIterations) — possible stuck task; inspect .bot/workspace/tasks/ for orphans." } $index = Get-TaskIndex if (-not $nextTask) { # Check if there are tasks in other states that might explain why nothing is available $analysingCount = $index.Analysing.Count $needsInputCount = $index.NeedsInput.Count $statusMessage = "No pending tasks available." if ($blockedCount -gt 0) { $statusMessage += " $blockedCount analysed task(s) blocked by unmet dependencies." } if ($analysingCount -gt 0) { $statusMessage += " $analysingCount task(s) being analysed." } if ($needsInputCount -gt 0) { $statusMessage += " $needsInputCount task(s) waiting for input." } if ($conditionSkipCount -gt 0) { $statusMessage += " $conditionSkipCount task(s) skipped (condition not met)." } if ($moveFailures.Count -gt 0) { $statusMessage += " WARNING: $($moveFailures.Count) task(s) stuck (Move-TaskState failed): $($moveFailures -join ', '). Inspect logs and .bot/workspace/tasks/." } Write-BotLog -Level Debug -Message "[task-get-next] No eligible tasks found" return @{ success = $true task = $null message = $statusMessage analysing_count = $analysingCount needs_input_count = $needsInputCount blocked_count = $blockedCount condition_skip_count = $conditionSkipCount move_failures = $moveFailures } } Write-BotLog -Level Debug -Message "[task-get-next] Selected task: $($nextTask.id) - $($nextTask.name) (Priority: $($nextTask.priority), Status: $taskStatus)" # Return the highest priority task if ($verbose) { $taskObj = @{ id = $nextTask.id name = $nextTask.name status = $taskStatus priority = $nextTask.priority effort = $nextTask.effort category = $nextTask.category description = $nextTask.description dependencies = $nextTask.dependencies acceptance_criteria = $nextTask.acceptance_criteria steps = $nextTask.steps applicable_agents = $nextTask.applicable_agents applicable_standards = $nextTask.applicable_standards file_path = $nextTask.file_path needs_interview = $nextTask.needs_interview questions_resolved = $nextTask.questions_resolved working_dir = $nextTask.working_dir external_repo = $nextTask.external_repo research_prompt = $nextTask.research_prompt type = $nextTask.type script_path = $nextTask.script_path prompt = $nextTask.prompt mcp_tool = $nextTask.mcp_tool mcp_args = $nextTask.mcp_args skip_analysis = $nextTask.skip_analysis skip_worktree = $nextTask.skip_worktree workflow = $nextTask.workflow model = $nextTask.model } } else { $taskObj = @{ id = $nextTask.id name = $nextTask.name status = $taskStatus priority = $nextTask.priority effort = $nextTask.effort category = $nextTask.category type = $nextTask.type script_path = $nextTask.script_path prompt = $nextTask.prompt mcp_tool = $nextTask.mcp_tool mcp_args = $nextTask.mcp_args workflow = $nextTask.workflow model = $nextTask.model } } $sourceLabel = if ($taskStatus -eq 'analysed') { 'analysed (ready)' } else { 'todo (needs analysis)' } return @{ success = $true task = $taskObj message = "Next task to work on: $($nextTask.name) (Priority: $($nextTask.priority), Effort: $($nextTask.effort), Source: $sourceLabel)" } } |