workflows/default/systems/mcp/tools/task-mark-needs-input/script.ps1
|
# Import modules Import-Module (Join-Path $global:DotbotProjectRoot ".bot\systems\mcp\modules\SessionTracking.psm1") -Force Import-Module (Join-Path $global:DotbotProjectRoot ".bot\systems\mcp\modules\TaskStore.psm1") -Force function Invoke-TaskMarkNeedsInput { param( [hashtable]$Arguments ) $taskId = $Arguments['task_id'] $question = $Arguments['question'] $questionsArg = $Arguments['questions'] $splitProposal = $Arguments['split_proposal'] if (-not $taskId) { throw "Task ID is required" } if (-not $question -and -not $questionsArg -and -not $splitProposal) { throw "Either 'questions' array, 'question' object, or 'split_proposal' is required" } if (($question -or $questionsArg) -and $splitProposal) { throw "Cannot provide both questions and split_proposal - use one at a time" } # Pre-read the task to build question data before the transition $found = Find-TaskFileById -TaskId $taskId -SearchStatuses @('analysing', 'in-progress', 'needs-input') if (-not $found) { throw "Task with ID '$taskId' not found in 'analysing', 'in-progress', or 'needs-input' status" } # Guard: refuse to add more questions if all questions are already answered if (($question -or $questionsArg) -and $found.Content.PSObject.Properties['all_questions_answered'] -and $found.Content.all_questions_answered -eq $true) { throw "all_questions_answered is true — all questions have been answered. Proceed to Step 4 (write summary, call task_mark_done). Do NOT call task_mark_needs_input again." } # Build updates $updates = @{} $newPendingQuestions = @() if ($questionsArg) { # Batch questions (preferred path) — store as pending_questions array $questionsResolved = @() if ($found.Content.PSObject.Properties['questions_resolved']) { $questionsResolved = @($found.Content.questions_resolved) } $existingPending = @() if ($found.Content.PSObject.Properties['pending_questions']) { $existingPending = @($found.Content.pending_questions) } # Migrate legacy single pending_question into pending_questions before clearing it if ($found.Content.PSObject.Properties['pending_question'] -and $found.Content.pending_question) { $legacyQ = $found.Content.pending_question $alreadyMigrated = $existingPending | Where-Object { $_.id -eq $legacyQ.id } if (-not $alreadyMigrated) { $existingPending = @($legacyQ) + $existingPending } } $askedAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") $baseCount = $questionsResolved.Count + $existingPending.Count $newPending = @() for ($i = 0; $i -lt @($questionsArg).Count; $i++) { $q = @($questionsArg)[$i] $newPending += @{ id = "q$($baseCount + $i + 1)" question = $q.question context = $q.context options = $q.options recommendation = if ($q.recommendation) { $q.recommendation } else { "A" } asked_at = $askedAt } } $updates['pending_questions'] = $existingPending + $newPending $newPendingQuestions = $newPending $updates['pending_question'] = $null $updates['split_proposal'] = $null $updates['notification'] = $null $updates['questions_resolved'] = $questionsResolved } elseif ($question) { $questionsResolved = @() if ($found.Content.PSObject.Properties['questions_resolved']) { $questionsResolved = @($found.Content.questions_resolved) } $questionId = "q$($questionsResolved.Count + 1)" $pendingQuestion = @{ id = $questionId question = $question.question context = $question.context options = $question.options recommendation = if ($question.recommendation) { $question.recommendation } else { "A" } asked_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") } $updates['pending_question'] = $pendingQuestion $updates['split_proposal'] = $null $updates['questions_resolved'] = $questionsResolved } elseif ($splitProposal) { $updates['split_proposal'] = @{ reason = $splitProposal.reason sub_tasks = $splitProposal.sub_tasks proposed_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") } $updates['pending_question'] = $null } $result = Move-TaskState -TaskId $taskId ` -FromStates @('analysing', 'in-progress', 'needs-input') ` -ToState 'needs-input' ` -Updates $updates $taskContent = $result.task_content # Close current Claude session on actual transition if (-not $result.already_in_state) { $claudeSessionId = $env:CLAUDE_SESSION_ID if ($claudeSessionId) { $sessionPhase = if ($found.Status -eq 'in-progress') { 'execution' } else { 'analysis' } Close-SessionOnTask -TaskContent $taskContent -SessionId $claudeSessionId -Phase $sessionPhase $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $result.file_path -Encoding UTF8 } } # If already in needs-input, still apply the new question/proposal if ($result.already_in_state) { foreach ($key in $updates.Keys) { Set-OrAddProperty -Object $taskContent -Name $key -Value $updates[$key] } Set-OrAddProperty -Object $taskContent -Name 'updated_at' -Value ((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")) $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $result.file_path -Encoding UTF8 } # --- External notification (opt-in) --- try { $notifModule = Join-Path $global:DotbotProjectRoot ".bot\systems\mcp\modules\NotificationClient.psm1" if (Test-Path $notifModule) { Import-Module $notifModule -Force $settings = Get-NotificationSettings if ($settings.enabled -and ($question -or $splitProposal)) { $sendResult = if ($question) { Send-TaskNotification -TaskContent $taskContent -PendingQuestion $taskContent.pending_question -Settings $settings } else { Send-SplitProposalNotification -TaskContent $taskContent -SplitProposal $taskContent.split_proposal -Settings $settings } if ($sendResult.success) { $taskContent | Add-Member -NotePropertyName 'notification' -NotePropertyValue @{ question_id = $sendResult.question_id instance_id = $sendResult.instance_id channel = $sendResult.channel project_id = $sendResult.project_id sent_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") } -Force $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $result.file_path -Encoding UTF8 } } elseif ($settings.enabled -and $newPendingQuestions.Count -gt 0) { $sentAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") $notificationsMap = @{} if ($taskContent.PSObject.Properties['notifications']) { foreach ($prop in $taskContent.notifications.PSObject.Properties) { $notificationsMap[$prop.Name] = $prop.Value } } foreach ($pq in $newPendingQuestions) { $maxAttempts = 3 $sendResult = $null for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { $sendResult = Send-TaskNotification -TaskContent $taskContent -PendingQuestion $pq -Settings $settings if ($sendResult.success) { break } if ($attempt -lt $maxAttempts) { Start-Sleep -Milliseconds 500 } } if ($sendResult -and $sendResult.success) { $notificationsMap[$pq.id] = @{ question_id = $sendResult.question_id instance_id = $sendResult.instance_id channel = $sendResult.channel project_id = $sendResult.project_id sent_at = $sentAt } } } if ($notificationsMap.Count -gt 0) { $taskContent | Add-Member -NotePropertyName 'notifications' -NotePropertyValue $notificationsMap -Force $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $result.file_path -Encoding UTF8 } } } } catch { # Never block the core flow } # Build result $output = @{ success = $true message = if ($questionsArg) { "Task paused for human input - $(@($questionsArg).Count) question(s) pending" } elseif ($question) { "Task paused for human input - question pending" } else { "Task paused for human input - split proposal pending" } task_id = $taskId task_name = $result.task_name old_status = $result.old_status new_status = 'needs-input' file_path = $result.file_path } if ($questionsArg) { $output['pending_questions'] = $taskContent.pending_questions; $output['questions_count'] = @($taskContent.pending_questions).Count } elseif ($question) { $output['pending_question'] = $taskContent.pending_question } elseif ($splitProposal) { $output['split_proposal'] = $taskContent.split_proposal } return $output } |