workflows/default/systems/runtime/modules/post-script-runner.ps1
|
<# .SYNOPSIS Post-script execution helper shared between the task-runner and kickstart engines. .DESCRIPTION Resolves a `post_script` path (relative to the bot root) and invokes it with the standard parameter set used by both the task-runner (Invoke-WorkflowProcess) and the kickstart engine (Invoke-KickstartProcess). Raises on non-zero exit code so callers can decide how to handle failure. Path resolution rules: - "scripts/..." -> resolved relative to $BotRoot - anything else -> resolved relative to $BotRoot/systems/runtime/ Forward- or back-slashes in the raw path are normalised so the resolved path is valid on both Windows and Unix. #> function Invoke-PostScript { [CmdletBinding()] param( [Parameter(Mandatory)][string]$BotRoot, [Parameter(Mandatory)][string]$ProductDir, [Parameter(Mandatory)]$Settings, [Parameter(Mandatory)][AllowEmptyString()][string]$Model, [Parameter(Mandatory)][string]$ProcessId, [Parameter(Mandatory)][string]$RawPostScript ) # NOTE: post_script is trusted manifest input (developer-authored, checked in). # Normalise backslashes to forward slashes so Join-Path produces a valid # path on both Windows and Unix (Windows accepts either separator). $normalized = $RawPostScript -replace '\\', '/' $postPath = if ($normalized -match '^scripts/') { Join-Path $BotRoot $normalized } else { Join-Path $BotRoot "systems/runtime/$normalized" } if (-not (Test-Path $postPath)) { throw "post_script not found: $postPath" } Write-Status "Running post-script: $RawPostScript" -Type Process Write-ProcessActivity -Id $ProcessId -ActivityType "text" -Message "Executing post_script: $RawPostScript" $global:LASTEXITCODE = 0 & $postPath -BotRoot $BotRoot -ProductDir $ProductDir -Settings $Settings -Model $Model -ProcessId $ProcessId if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { throw "post_script exited with code $LASTEXITCODE" } } <# .SYNOPSIS Escalate a post_script failure by moving a task from done/ → needs-input/. .DESCRIPTION Used by the Claude-executed branch in Invoke-WorkflowProcess.ps1 when a post_script fails after `task_mark_done` has already moved the task JSON into `workspace\tasks\done\`. Rather than destroy the worktree and increment failure counters, we move the task to `workspace\tasks\needs-input\` with a `pending_question` so the operator can inspect the worktree, fix the post_script (or the artefacts it consumes), and retry manually. Mirrors the merge-conflict escalation pattern already used when the squash-merge step fails. Returns $true if the task was moved, $false otherwise (e.g. the task JSON was not found in done/). #> function Invoke-PostScriptFailureEscalation { [CmdletBinding()] param( [Parameter(Mandatory)]$Task, [Parameter(Mandatory)][string]$TasksBaseDir, [Parameter(Mandatory)][string]$PostScriptError, [AllowEmptyString()][string]$WorktreePath = "" ) $doneDir = Join-Path $TasksBaseDir "done" $needsInputDir = Join-Path $TasksBaseDir "needs-input" if (-not (Test-Path $needsInputDir)) { New-Item -ItemType Directory -Force -Path $needsInputDir | Out-Null } $taskFile = Get-ChildItem -Path $doneDir -Filter "*.json" -File -ErrorAction SilentlyContinue | Where-Object { try { $c = Get-Content $_.FullName -Raw | ConvertFrom-Json $c.id -eq $Task.id } catch { $false } } | Select-Object -First 1 if (-not $taskFile) { return $false } $taskContent = Get-Content $taskFile.FullName -Raw | ConvertFrom-Json $nowIso = (Get-Date).ToUniversalTime().ToString("o") # Use Add-Member -Force so the helper works on task JSON that may or may not # already have status / updated_at / pending_question properties. $taskContent | Add-Member -NotePropertyName 'status' -NotePropertyValue 'needs-input' -Force $taskContent | Add-Member -NotePropertyName 'updated_at' -NotePropertyValue $nowIso -Force if (-not $taskContent.PSObject.Properties['pending_question']) { $taskContent | Add-Member -NotePropertyName 'pending_question' -NotePropertyValue $null -Force } $contextText = if ($WorktreePath) { "Error: $PostScriptError. Worktree preserved at: $WorktreePath" } else { "Error: $PostScriptError" } $taskContent.pending_question = @{ id = "post-script-failure" question = "post_script failed during task completion" context = $contextText options = @( @{ key = "A"; label = "Fix the post_script and retry manually"; rationale = "Inspect the worktree, repair the post_script, then retry the task" } @{ key = "B"; label = "Discard task changes"; rationale = "Remove worktree and abandon this task's changes" } ) recommendation = "A" asked_at = (Get-Date).ToUniversalTime().ToString("o") } $newPath = Join-Path $needsInputDir $taskFile.Name $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $newPath -Encoding UTF8 Remove-Item -Path $taskFile.FullName -Force -ErrorAction SilentlyContinue return $true } <# .SYNOPSIS Task-runner wrapper that invokes a task's post_script (if any) and reports failure. .DESCRIPTION Used by both task-runner code paths in Invoke-WorkflowProcess.ps1 to avoid duplicating the guard + try/catch + logging block. Returns $null on success or when the task has no post_script; returns a string error message on failure, leaving it to the caller to flip any success flag. #> function Invoke-TaskPostScriptIfPresent { [CmdletBinding()] param( [Parameter(Mandatory)]$Task, [Parameter(Mandatory)][string]$BotRoot, [Parameter(Mandatory)][string]$ProductDir, [Parameter(Mandatory)]$Settings, [Parameter(Mandatory)][AllowEmptyString()][string]$Model, [Parameter(Mandatory)][string]$ProcessId ) if (-not $Task.post_script) { return $null } try { Invoke-PostScript -BotRoot $BotRoot -ProductDir $ProductDir -Settings $Settings ` -Model $Model -ProcessId $ProcessId -RawPostScript $Task.post_script return $null } catch { $msg = "post_script failed: $($_.Exception.Message)" Write-Status $msg -Type Error Write-ProcessActivity -Id $ProcessId -ActivityType "error" -Message "$($Task.name): $msg" return $msg } } |