Sync: Complete project state with all MEGA SPRINT V1-V3 features and Codex stubs
This commit is contained in:
664
ralph/scripts/Common.ps1
Normal file
664
ralph/scripts/Common.ps1
Normal file
@@ -0,0 +1,664 @@
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-RalphRoot {
|
||||
return (Split-Path -Parent $PSScriptRoot)
|
||||
}
|
||||
|
||||
function Get-RepoRoot {
|
||||
return (Split-Path -Parent (Get-RalphRoot))
|
||||
}
|
||||
|
||||
function Ensure-Directory {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Read-JsonFile {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if (-not (Test-Path $Path)) {
|
||||
throw "JSON file not found: $Path"
|
||||
}
|
||||
|
||||
return Get-Content -Raw -Path $Path | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-ProvidersConfig {
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$localPath = Join-Path $ralphRoot "config\providers.local.json"
|
||||
$examplePath = Join-Path $ralphRoot "config\providers.example.json"
|
||||
|
||||
if (Test-Path $localPath) {
|
||||
return Read-JsonFile -Path $localPath
|
||||
}
|
||||
|
||||
return Read-JsonFile -Path $examplePath
|
||||
}
|
||||
|
||||
function Get-ProviderConfig {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
|
||||
$config = Get-ProvidersConfig
|
||||
if (-not $config.providers.PSObject.Properties.Name.Contains($Name)) {
|
||||
throw "Provider '$Name' not found in providers config."
|
||||
}
|
||||
|
||||
return $config.providers.$Name
|
||||
}
|
||||
|
||||
function Get-DefaultImplementer {
|
||||
$config = Get-ProvidersConfig
|
||||
return [string]$config.default_implementer
|
||||
}
|
||||
|
||||
function Get-DefaultReviewers {
|
||||
$config = Get-ProvidersConfig
|
||||
return @($config.default_reviewers)
|
||||
}
|
||||
|
||||
function Get-CodexConfig {
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$localPath = Join-Path $ralphRoot "config\codex.local.json"
|
||||
$examplePath = Join-Path $ralphRoot "config\codex.example.json"
|
||||
|
||||
if (Test-Path $localPath) {
|
||||
return Read-JsonFile -Path $localPath
|
||||
}
|
||||
|
||||
return Read-JsonFile -Path $examplePath
|
||||
}
|
||||
|
||||
function Get-RalphAutomationConfig {
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$localPath = Join-Path $ralphRoot "config\automation.local.json"
|
||||
$examplePath = Join-Path $ralphRoot "config\automation.example.json"
|
||||
|
||||
if (Test-Path $localPath) {
|
||||
return Read-JsonFile -Path $localPath
|
||||
}
|
||||
|
||||
if (Test-Path $examplePath) {
|
||||
return Read-JsonFile -Path $examplePath
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-TelegramConfig {
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$localPath = Join-Path $ralphRoot "config\telegram.local.json"
|
||||
$examplePath = Join-Path $ralphRoot "config\telegram.example.json"
|
||||
|
||||
if (Test-Path $localPath) {
|
||||
return Read-JsonFile -Path $localPath
|
||||
}
|
||||
|
||||
if (Test-Path $examplePath) {
|
||||
return Read-JsonFile -Path $examplePath
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-TelegramChatIds {
|
||||
param($Config)
|
||||
|
||||
if ($null -eq $Config) {
|
||||
return @()
|
||||
}
|
||||
|
||||
if ($Config.PSObject.Properties.Name -contains "chat_ids") {
|
||||
return @($Config.chat_ids | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { [string]$_ })
|
||||
}
|
||||
|
||||
if ($Config.PSObject.Properties.Name -contains "chat_id" -and -not [string]::IsNullOrWhiteSpace([string]$Config.chat_id)) {
|
||||
return @([string]$Config.chat_id)
|
||||
}
|
||||
|
||||
return @()
|
||||
}
|
||||
|
||||
function Test-TelegramEventEnabled {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]$Config,
|
||||
[Parameter(Mandatory = $true)][string]$EventName
|
||||
)
|
||||
|
||||
$enabled = $false
|
||||
try { $enabled = [bool]$Config.enabled } catch { $enabled = $false }
|
||||
if (-not $enabled) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($Config.PSObject.Properties.Name -contains "events" -and $null -ne $Config.events) {
|
||||
$eventProperty = $Config.events.PSObject.Properties[$EventName]
|
||||
if ($null -ne $eventProperty) {
|
||||
try { return [bool]$eventProperty.Value } catch { return $false }
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function Send-TelegramNotification {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$EventName,
|
||||
[Parameter(Mandatory = $true)][string]$Title,
|
||||
[Parameter(Mandatory = $true)][string]$Message,
|
||||
[string]$RunId = "",
|
||||
[string]$Stage = "",
|
||||
[string]$Status = ""
|
||||
)
|
||||
|
||||
try {
|
||||
$config = Get-TelegramConfig
|
||||
}
|
||||
catch {
|
||||
return [ordered]@{
|
||||
sent = $false
|
||||
reason = $_.Exception.Message
|
||||
delivered = 0
|
||||
errors = @()
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $config) {
|
||||
return [ordered]@{
|
||||
sent = $false
|
||||
reason = "telegram config missing"
|
||||
delivered = 0
|
||||
errors = @()
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-TelegramEventEnabled -Config $config -EventName $EventName)) {
|
||||
return [ordered]@{
|
||||
sent = $false
|
||||
reason = "event disabled"
|
||||
delivered = 0
|
||||
errors = @()
|
||||
}
|
||||
}
|
||||
|
||||
$botToken = ""
|
||||
if ($config.PSObject.Properties.Name -contains "bot_token") {
|
||||
$botToken = [string]$config.bot_token
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($botToken)) {
|
||||
return [ordered]@{
|
||||
sent = $false
|
||||
reason = "bot token missing"
|
||||
delivered = 0
|
||||
errors = @()
|
||||
}
|
||||
}
|
||||
|
||||
$chatIds = @(Get-TelegramChatIds -Config $config)
|
||||
if ($chatIds.Count -eq 0) {
|
||||
return [ordered]@{
|
||||
sent = $false
|
||||
reason = "chat ids missing"
|
||||
delivered = 0
|
||||
errors = @()
|
||||
}
|
||||
}
|
||||
|
||||
$timeoutSeconds = 8
|
||||
if ($config.PSObject.Properties.Name -contains "timeout_seconds") {
|
||||
try {
|
||||
$timeoutSeconds = [int]$config.timeout_seconds
|
||||
}
|
||||
catch {
|
||||
$timeoutSeconds = 8
|
||||
}
|
||||
}
|
||||
|
||||
$prefix = "Ralph"
|
||||
if ($config.PSObject.Properties.Name -contains "prefix" -and -not [string]::IsNullOrWhiteSpace([string]$config.prefix)) {
|
||||
$prefix = [string]$config.prefix
|
||||
}
|
||||
|
||||
$lines = @(
|
||||
("{0} | {1}" -f $prefix, $Title.Trim())
|
||||
$Message.Trim()
|
||||
)
|
||||
if (-not [string]::IsNullOrWhiteSpace($RunId)) {
|
||||
$lines += ("run: " + $RunId)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($Stage) -or -not [string]::IsNullOrWhiteSpace($Status)) {
|
||||
$statusLine = @()
|
||||
if (-not [string]::IsNullOrWhiteSpace($Stage)) {
|
||||
$statusLine += ("stage=" + $Stage)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($Status)) {
|
||||
$statusLine += ("status=" + $Status)
|
||||
}
|
||||
if ($statusLine.Count -gt 0) {
|
||||
$lines += ($statusLine -join " ")
|
||||
}
|
||||
}
|
||||
$text = ($lines -join "`n").Trim()
|
||||
|
||||
$uri = "https://api.telegram.org/bot{0}/sendMessage" -f $botToken
|
||||
$delivered = 0
|
||||
$errors = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($chatId in $chatIds) {
|
||||
try {
|
||||
$body = @{
|
||||
chat_id = $chatId
|
||||
text = $text
|
||||
disable_web_page_preview = $true
|
||||
}
|
||||
Invoke-RestMethod -Method Post -Uri $uri -Body $body -ContentType "application/x-www-form-urlencoded" -TimeoutSec $timeoutSeconds | Out-Null
|
||||
$delivered += 1
|
||||
}
|
||||
catch {
|
||||
$errors.Add(("{0}: {1}" -f $chatId, $_.Exception.Message))
|
||||
}
|
||||
}
|
||||
|
||||
return [ordered]@{
|
||||
sent = ($delivered -gt 0)
|
||||
reason = $(if ($delivered -gt 0) { "ok" } else { "delivery failed" })
|
||||
delivered = $delivered
|
||||
errors = @($errors)
|
||||
}
|
||||
}
|
||||
|
||||
function New-RunId {
|
||||
param([string]$Label = "autopilot")
|
||||
return "{0}-{1}" -f (Get-Date -Format "yyyyMMdd-HHmmss"), $Label
|
||||
}
|
||||
|
||||
function Get-TaskFiles {
|
||||
param([string]$TaskDirectory = $(Join-Path (Get-RalphRoot) "tasks\current"))
|
||||
|
||||
return @{
|
||||
Task = Join-Path $TaskDirectory "TASK.md"
|
||||
Acceptance = Join-Path $TaskDirectory "ACCEPTANCE.md"
|
||||
Context = Join-Path $TaskDirectory "CONTEXT.md"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RalphInboxDirectory {
|
||||
return (Join-Path (Get-RalphRoot) "tasks\inbox")
|
||||
}
|
||||
|
||||
function Get-RalphProcessingDirectory {
|
||||
return (Join-Path (Get-RalphRoot) "tasks\processing")
|
||||
}
|
||||
|
||||
function Get-RalphArchiveDirectory {
|
||||
return (Join-Path (Get-RalphRoot) "tasks\completed")
|
||||
}
|
||||
|
||||
function Get-RalphFailedDirectory {
|
||||
return (Join-Path (Get-RalphRoot) "tasks\failed")
|
||||
}
|
||||
|
||||
function Read-TaskPack {
|
||||
param([string]$TaskDirectory = $(Join-Path (Get-RalphRoot) "tasks\current"))
|
||||
|
||||
$files = Get-TaskFiles -TaskDirectory $TaskDirectory
|
||||
foreach ($entry in $files.GetEnumerator()) {
|
||||
if (-not (Test-Path $entry.Value)) {
|
||||
throw "Task pack file missing: $($entry.Value)"
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
Files = $files
|
||||
Task = Get-Content -Raw -Path $files.Task
|
||||
Acceptance = Get-Content -Raw -Path $files.Acceptance
|
||||
Context = Get-Content -Raw -Path $files.Context
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-MarkdownToRalphTaskPack {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
throw "Task markdown not found: $Path"
|
||||
}
|
||||
|
||||
$raw = Get-Content -Raw -Path $Path
|
||||
$lines = $raw -split "`r?`n"
|
||||
$taskLines = New-Object System.Collections.Generic.List[string]
|
||||
$acceptanceLines = New-Object System.Collections.Generic.List[string]
|
||||
$contextLines = New-Object System.Collections.Generic.List[string]
|
||||
$current = "task"
|
||||
$title = ""
|
||||
|
||||
foreach ($line in $lines) {
|
||||
if ($line -match '^\s*#{1,3}\s+(.*\S)\s*$') {
|
||||
$heading = $matches[1].Trim()
|
||||
if (-not $title -and $line -match '^\s*#\s+') {
|
||||
$title = $heading
|
||||
}
|
||||
|
||||
$headingLower = $heading.ToLowerInvariant()
|
||||
if ($headingLower -match '^(acceptance|acceptance criteria|criteria|done|definition of done)\b') {
|
||||
$current = "acceptance"
|
||||
}
|
||||
elseif ($headingLower -match '^(context|background|notes|references)\b') {
|
||||
$current = "context"
|
||||
}
|
||||
else {
|
||||
$current = "task"
|
||||
$taskLines.Add($line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch ($current) {
|
||||
"acceptance" { $acceptanceLines.Add($line) }
|
||||
"context" { $contextLines.Add($line) }
|
||||
default { $taskLines.Add($line) }
|
||||
}
|
||||
}
|
||||
|
||||
$taskText = (($taskLines -join "`n").Trim())
|
||||
$acceptanceText = (($acceptanceLines -join "`n").Trim())
|
||||
$contextText = (($contextLines -join "`n").Trim())
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($taskText)) {
|
||||
$taskText = $raw.Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($acceptanceText)) {
|
||||
$acceptanceText = Get-Content -Raw -Path (Join-Path (Get-RalphRoot) "templates\ACCEPTANCE.md")
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($contextText)) {
|
||||
$contextText = Get-Content -Raw -Path (Join-Path (Get-RalphRoot) "templates\CONTEXT.md")
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($title)) {
|
||||
$title = [IO.Path]::GetFileNameWithoutExtension($Path)
|
||||
}
|
||||
|
||||
return [ordered]@{
|
||||
Title = $title
|
||||
SourcePath = $Path
|
||||
RawContent = $raw
|
||||
Task = $taskText
|
||||
Acceptance = $acceptanceText
|
||||
Context = $contextText
|
||||
}
|
||||
}
|
||||
|
||||
function Write-RalphTaskPackDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][hashtable]$TaskPack,
|
||||
[Parameter(Mandatory = $true)][string]$DestinationDirectory
|
||||
)
|
||||
|
||||
Ensure-Directory -Path $DestinationDirectory
|
||||
Write-Utf8File -Path (Join-Path $DestinationDirectory "TASK.md") -Content (($TaskPack.Task.Trim()) + "`n")
|
||||
Write-Utf8File -Path (Join-Path $DestinationDirectory "ACCEPTANCE.md") -Content (($TaskPack.Acceptance.Trim()) + "`n")
|
||||
Write-Utf8File -Path (Join-Path $DestinationDirectory "CONTEXT.md") -Content (($TaskPack.Context.Trim()) + "`n")
|
||||
|
||||
if ($TaskPack.ContainsKey("SourcePath") -and (Test-Path $TaskPack.SourcePath)) {
|
||||
Copy-Item -Path $TaskPack.SourcePath -Destination (Join-Path $DestinationDirectory "SOURCE.md") -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Write-Utf8File {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content
|
||||
)
|
||||
|
||||
$encoding = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
function Write-JsonFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)]$Object
|
||||
)
|
||||
|
||||
$json = $Object | ConvertTo-Json -Depth 100
|
||||
Write-Utf8File -Path $Path -Content ($json + "`n")
|
||||
}
|
||||
|
||||
function Get-RalphStateFile {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
return Join-Path (Get-RalphRoot) ("state\" + $Name)
|
||||
}
|
||||
|
||||
function Get-RalphTaskRoots {
|
||||
$ralphRoot = Get-RalphRoot
|
||||
return [ordered]@{
|
||||
TasksRoot = Join-Path $ralphRoot "tasks"
|
||||
Inbox = Join-Path $ralphRoot "tasks\inbox"
|
||||
Processing = Join-Path $ralphRoot "tasks\processing"
|
||||
Completed = Join-Path $ralphRoot "tasks\completed"
|
||||
Failed = Join-Path $ralphRoot "tasks\failed"
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-ToRalphSlug {
|
||||
param([Parameter(Mandatory = $true)][string]$Text)
|
||||
|
||||
$slug = [string]$Text
|
||||
$slug = $slug.Trim().ToLowerInvariant()
|
||||
if ([string]::IsNullOrWhiteSpace($slug)) {
|
||||
return "task"
|
||||
}
|
||||
|
||||
$slug = [regex]::Replace($slug, "[^a-z0-9]+", "-")
|
||||
$slug = $slug.Trim("-")
|
||||
if ([string]::IsNullOrWhiteSpace($slug)) {
|
||||
return "task"
|
||||
}
|
||||
if ($slug.Length -gt 48) {
|
||||
$slug = $slug.Substring(0, 48).Trim("-")
|
||||
}
|
||||
return $slug
|
||||
}
|
||||
|
||||
function Get-RalphTaskTitleFromMarkdown {
|
||||
param(
|
||||
[string]$Markdown = "",
|
||||
[string]$Fallback = "task"
|
||||
)
|
||||
|
||||
$lines = @($Markdown -split "`r?`n")
|
||||
foreach ($line in $lines) {
|
||||
$trimmed = [string]$line
|
||||
$trimmed = $trimmed.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
||||
continue
|
||||
}
|
||||
if ($trimmed.StartsWith("#")) {
|
||||
return ($trimmed.TrimStart("#").Trim())
|
||||
}
|
||||
return $trimmed
|
||||
}
|
||||
return $Fallback
|
||||
}
|
||||
|
||||
function New-RalphTaskPackFromMarkdown {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$MarkdownPath,
|
||||
[string]$TargetDirectory = "",
|
||||
[string]$TaskId = "",
|
||||
[string]$Title = "",
|
||||
[hashtable]$AdditionalMetadata = @{}
|
||||
)
|
||||
|
||||
if (-not (Test-Path $MarkdownPath)) {
|
||||
throw "Markdown source not found: $MarkdownPath"
|
||||
}
|
||||
|
||||
$roots = Get-RalphTaskRoots
|
||||
foreach ($path in $roots.Values) {
|
||||
Ensure-Directory -Path $path
|
||||
}
|
||||
|
||||
$parsedTaskPack = Convert-MarkdownToRalphTaskPack -Path $MarkdownPath
|
||||
$sourceText = [string]$parsedTaskPack.RawContent
|
||||
$sourceName = [System.IO.Path]::GetFileNameWithoutExtension($MarkdownPath)
|
||||
if ([string]::IsNullOrWhiteSpace($Title)) {
|
||||
$Title = [string]$parsedTaskPack.Title
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($TaskId)) {
|
||||
$TaskId = "{0}-{1}" -f (Get-Date -Format "yyyyMMdd-HHmmss"), (Convert-ToRalphSlug -Text $Title)
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($TargetDirectory)) {
|
||||
$TargetDirectory = Join-Path $roots.Inbox $TaskId
|
||||
}
|
||||
|
||||
Ensure-Directory -Path $TargetDirectory
|
||||
|
||||
$taskPath = Join-Path $TargetDirectory "TASK.md"
|
||||
$acceptancePath = Join-Path $TargetDirectory "ACCEPTANCE.md"
|
||||
$contextPath = Join-Path $TargetDirectory "CONTEXT.md"
|
||||
$metadataPath = Join-Path $TargetDirectory "submission.json"
|
||||
$sourceCopyPath = Join-Path $TargetDirectory "SOURCE.md"
|
||||
|
||||
$acceptance = [string]$parsedTaskPack.Acceptance
|
||||
$context = [string]$parsedTaskPack.Context
|
||||
$taskBody = [string]$parsedTaskPack.Task
|
||||
|
||||
Write-Utf8File -Path $taskPath -Content ($taskBody.Trim() + "`n")
|
||||
Write-Utf8File -Path $sourceCopyPath -Content ($sourceText.Trim() + "`n")
|
||||
Write-Utf8File -Path $acceptancePath -Content ($acceptance + "`n")
|
||||
Write-Utf8File -Path $contextPath -Content ($context + "`n")
|
||||
$metadata = [ordered]@{
|
||||
id = $TaskId
|
||||
title = $Title
|
||||
submitted_at = (Get-Date).ToString("o")
|
||||
source_path = (Resolve-Path $MarkdownPath).Path
|
||||
task_directory = $TargetDirectory
|
||||
state = "queued"
|
||||
}
|
||||
foreach ($key in $AdditionalMetadata.Keys) {
|
||||
$metadata[$key] = $AdditionalMetadata[$key]
|
||||
}
|
||||
Write-JsonFile -Path $metadataPath -Object $metadata
|
||||
|
||||
return [ordered]@{
|
||||
id = $TaskId
|
||||
title = $Title
|
||||
task_directory = $TargetDirectory
|
||||
task_file = $taskPath
|
||||
acceptance_file = $acceptancePath
|
||||
context_file = $contextPath
|
||||
metadata_file = $metadataPath
|
||||
source_copy = $sourceCopyPath
|
||||
}
|
||||
}
|
||||
|
||||
function Read-CodexReviewVerdict {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
throw "Codex review file not found: $Path"
|
||||
}
|
||||
|
||||
$raw = (Get-Content -Raw -Path $Path).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||
throw "Codex review file is empty: $Path"
|
||||
}
|
||||
|
||||
$jsonText = $raw
|
||||
if ($raw -match '(?s)```json\s*(\{.*?\})\s*```') {
|
||||
$jsonText = $matches[1]
|
||||
}
|
||||
elseif ($raw -match '(?s)(\{.*\})') {
|
||||
$jsonText = $matches[1]
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = $jsonText | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
throw "Codex review output is not valid JSON: $Path"
|
||||
}
|
||||
|
||||
$verdict = [string]$parsed.verdict
|
||||
if ([string]::IsNullOrWhiteSpace($verdict)) {
|
||||
throw "Codex review JSON missing 'verdict': $Path"
|
||||
}
|
||||
|
||||
$normalizedVerdict = $verdict.Trim().ToLowerInvariant()
|
||||
if ($normalizedVerdict -notin @("pass", "needs_fix", "fail")) {
|
||||
throw "Codex review verdict '$verdict' is invalid. Expected: pass, needs_fix, fail."
|
||||
}
|
||||
|
||||
$acceptancePassed = $false
|
||||
try {
|
||||
$acceptancePassed = [bool]$parsed.acceptance_passed
|
||||
}
|
||||
catch {
|
||||
$acceptancePassed = ($normalizedVerdict -eq "pass")
|
||||
}
|
||||
|
||||
return [ordered]@{
|
||||
verdict = $normalizedVerdict
|
||||
acceptance_passed = $acceptancePassed
|
||||
fix_required = [bool]$parsed.fix_required
|
||||
summary = [string]$parsed.summary
|
||||
next_sprint_needed = [bool]$parsed.next_sprint_needed
|
||||
next_sprint_brief = [string]$parsed.next_sprint_brief
|
||||
highest_risk_issues = @($parsed.highest_risk_issues)
|
||||
raw = $parsed
|
||||
}
|
||||
}
|
||||
|
||||
function Add-RalphEvent {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$RunId,
|
||||
[Parameter(Mandatory = $true)][string]$Stage,
|
||||
[Parameter(Mandatory = $true)][string]$Status,
|
||||
[Parameter(Mandatory = $true)][string]$Message,
|
||||
[string]$Actor = "system",
|
||||
[hashtable]$Data = @{}
|
||||
)
|
||||
|
||||
$eventsPath = Get-RalphStateFile -Name "events.jsonl"
|
||||
Ensure-Directory -Path (Split-Path -Parent $eventsPath)
|
||||
|
||||
$event = [ordered]@{
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
run_id = $RunId
|
||||
actor = $Actor
|
||||
stage = $Stage
|
||||
status = $Status
|
||||
message = $Message
|
||||
data = $Data
|
||||
}
|
||||
|
||||
$encoding = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::AppendAllText($eventsPath, (($event | ConvertTo-Json -Depth 20 -Compress) + "`n"), $encoding)
|
||||
}
|
||||
|
||||
function Set-RalphCurrentRunState {
|
||||
param([Parameter(Mandatory = $true)][hashtable]$State)
|
||||
$statePath = Get-RalphStateFile -Name "current_run.json"
|
||||
Write-JsonFile -Path $statePath -Object $State
|
||||
}
|
||||
|
||||
function New-PromptDocument {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$TemplatePath,
|
||||
[Parameter(Mandatory = $true)][hashtable]$TaskPack,
|
||||
[Parameter(Mandatory = $true)][string]$OutputPath,
|
||||
[string[]]$ExtraSections = @()
|
||||
)
|
||||
|
||||
$template = Get-Content -Raw -Path $TemplatePath
|
||||
$sections = @(
|
||||
$template,
|
||||
"## TASK`n$($TaskPack.Task)",
|
||||
"## ACCEPTANCE`n$($TaskPack.Acceptance)",
|
||||
"## CONTEXT`n$($TaskPack.Context)"
|
||||
) + $ExtraSections
|
||||
|
||||
Write-Utf8File -Path $OutputPath -Content (($sections -join "`n`n").Trim() + "`n")
|
||||
}
|
||||
63
ralph/scripts/Get-RalphStatus.ps1
Normal file
63
ralph/scripts/Get-RalphStatus.ps1
Normal file
@@ -0,0 +1,63 @@
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$roots = Get-RalphTaskRoots
|
||||
$lockFile = Get-RalphStateFile -Name "inbox_daemon.lock.json"
|
||||
$daemonStateFile = Get-RalphStateFile -Name "inbox_daemon_state.json"
|
||||
$currentRunFile = Get-RalphStateFile -Name "current_run.json"
|
||||
$backgroundFile = Join-Path (Get-RalphRoot) "state\last_inbox_background.json"
|
||||
|
||||
function Get-ItemCount {
|
||||
param([object[]]$Items)
|
||||
return [int](($Items | Measure-Object).Count)
|
||||
}
|
||||
|
||||
$result = [ordered]@{
|
||||
inbox = [ordered]@{
|
||||
queued_taskpacks = Get-ItemCount -Items @(Get-ChildItem -LiteralPath $roots.Inbox -Directory -ErrorAction SilentlyContinue | Where-Object { Test-Path (Join-Path $_.FullName "TASK.md") })
|
||||
queued_markdown_files = Get-ItemCount -Items @(Get-ChildItem -LiteralPath $roots.Inbox -File -Filter *.md -ErrorAction SilentlyContinue)
|
||||
processing = Get-ItemCount -Items @(Get-ChildItem -LiteralPath $roots.Processing -Directory -ErrorAction SilentlyContinue)
|
||||
completed = Get-ItemCount -Items @(Get-ChildItem -LiteralPath $roots.Completed -Directory -ErrorAction SilentlyContinue)
|
||||
failed = Get-ItemCount -Items @(Get-ChildItem -LiteralPath $roots.Failed -Directory -ErrorAction SilentlyContinue)
|
||||
}
|
||||
daemon = $null
|
||||
current_run = $null
|
||||
background = $null
|
||||
}
|
||||
|
||||
if (Test-Path $lockFile) {
|
||||
try {
|
||||
$result.daemon = Read-JsonFile -Path $lockFile
|
||||
}
|
||||
catch {
|
||||
$result.daemon = @{ error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $daemonStateFile) {
|
||||
try {
|
||||
$result.daemon_state = Read-JsonFile -Path $daemonStateFile
|
||||
}
|
||||
catch {
|
||||
$result.daemon_state = @{ error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $currentRunFile) {
|
||||
try {
|
||||
$result.current_run = Read-JsonFile -Path $currentRunFile
|
||||
}
|
||||
catch {
|
||||
$result.current_run = @{ error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $backgroundFile) {
|
||||
try {
|
||||
$result.background = Read-JsonFile -Path $backgroundFile
|
||||
}
|
||||
catch {
|
||||
$result.background = @{ error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
($result | ConvertTo-Json -Depth 100)
|
||||
62
ralph/scripts/Install-RalphScheduledTask.ps1
Normal file
62
ralph/scripts/Install-RalphScheduledTask.ps1
Normal file
@@ -0,0 +1,62 @@
|
||||
param(
|
||||
[string]$TaskName = "RalphInboxDaemon",
|
||||
[int]$PollSeconds = 15,
|
||||
[string]$Implementer = "",
|
||||
[string[]]$Reviewers = @(),
|
||||
[switch]$DisableCodexMaster,
|
||||
[switch]$DisableAutoFix,
|
||||
[switch]$DryRun,
|
||||
[switch]$AtStartup,
|
||||
[switch]$AtLogon = $true
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$daemonScript = Join-Path $PSScriptRoot "Start-RalphInboxDaemon.ps1"
|
||||
$argParts = @(
|
||||
"-NoProfile",
|
||||
"-WindowStyle Hidden",
|
||||
"-ExecutionPolicy Bypass",
|
||||
('-File "{0}"' -f $daemonScript),
|
||||
('-PollSeconds {0}' -f $PollSeconds)
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($Implementer)) {
|
||||
$argParts += ('-Implementer "{0}"' -f $Implementer)
|
||||
}
|
||||
foreach ($reviewer in $Reviewers) {
|
||||
$argParts += ('-Reviewers "{0}"' -f $reviewer)
|
||||
}
|
||||
if ($DisableCodexMaster) {
|
||||
$argParts += "-DisableCodexMaster"
|
||||
}
|
||||
if ($DisableAutoFix) {
|
||||
$argParts += "-DisableAutoFix"
|
||||
}
|
||||
if ($DryRun) {
|
||||
$argParts += "-DryRun"
|
||||
}
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument ($argParts -join " ")
|
||||
$triggers = @()
|
||||
if ($AtStartup) {
|
||||
$triggers += New-ScheduledTaskTrigger -AtStartup
|
||||
}
|
||||
if ($AtLogon) {
|
||||
$triggers += New-ScheduledTaskTrigger -AtLogOn
|
||||
}
|
||||
if ($triggers.Count -eq 0) {
|
||||
throw "At least one trigger must be enabled."
|
||||
}
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest
|
||||
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $triggers -Settings $settings -Principal $principal -Force | Out-Null
|
||||
|
||||
(@{
|
||||
task_name = $TaskName
|
||||
installed = $true
|
||||
poll_seconds = $PollSeconds
|
||||
dry_run = [bool]$DryRun
|
||||
} | ConvertTo-Json -Depth 20)
|
||||
137
ralph/scripts/Invoke-ClaudeProvider.ps1
Normal file
137
ralph/scripts/Invoke-ClaudeProvider.ps1
Normal file
@@ -0,0 +1,137 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ProviderName,
|
||||
[Parameter(Mandatory = $true)][string]$PromptFile,
|
||||
[Parameter(Mandatory = $true)][string]$OutputFile,
|
||||
[string]$WorkingDirectory = "",
|
||||
[string[]]$AddDirectories = @(),
|
||||
[ValidateSet("json", "text")][string]$OutputFormat = "json"
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) {
|
||||
$WorkingDirectory = Get-RepoRoot
|
||||
}
|
||||
|
||||
$provider = Get-ProviderConfig -Name $ProviderName
|
||||
$runner = [string]$provider.runner
|
||||
$promptText = Get-Content -Raw -Path $PromptFile
|
||||
Ensure-Directory -Path (Split-Path -Parent $OutputFile)
|
||||
|
||||
if ($runner -eq "opencode") {
|
||||
$opencodeCommand = Get-Command opencode -ErrorAction SilentlyContinue
|
||||
if ($null -eq $opencodeCommand) {
|
||||
throw "opencode executable not found in PATH."
|
||||
}
|
||||
|
||||
$arguments = @(
|
||||
"run",
|
||||
"--dir", $WorkingDirectory,
|
||||
"--model", [string]$provider.model,
|
||||
"--format", "json"
|
||||
)
|
||||
$agentProp = $provider.PSObject.Properties["agent"]
|
||||
if ($null -ne $agentProp -and -not [string]::IsNullOrWhiteSpace([string]$agentProp.Value)) {
|
||||
$arguments += @("--agent", [string]$agentProp.Value)
|
||||
}
|
||||
$variantProp = $provider.PSObject.Properties["variant"]
|
||||
if ($null -ne $variantProp -and -not [string]::IsNullOrWhiteSpace([string]$variantProp.Value)) {
|
||||
$arguments += @("--variant", [string]$variantProp.Value)
|
||||
}
|
||||
$arguments += @($promptText)
|
||||
|
||||
$output = @(& $opencodeCommand.Source @arguments 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
$result = [ordered]@{
|
||||
provider = $ProviderName
|
||||
runner = "opencode"
|
||||
model = [string]$provider.model
|
||||
working_directory = $WorkingDirectory
|
||||
prompt_file = $PromptFile
|
||||
output_format = "json"
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
exit_code = $exitCode
|
||||
output = ($output -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
Write-Utf8File -Path $OutputFile -Content (($result | ConvertTo-Json -Depth 100) + "`n")
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
throw "Provider '$ProviderName' failed with exit code $exitCode. See $OutputFile"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
$envNames = @(
|
||||
"ANTHROPIC_BASE_URL",
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"API_TIMEOUT_MS",
|
||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
|
||||
"ANTHROPIC_MODEL",
|
||||
"ANTHROPIC_SMALL_FAST_MODEL",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"
|
||||
)
|
||||
|
||||
$previous = @{}
|
||||
foreach ($name in $envNames) {
|
||||
$previous[$name] = [Environment]::GetEnvironmentVariable($name, "Process")
|
||||
}
|
||||
|
||||
try {
|
||||
[Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", [string]$provider.base_url, "Process")
|
||||
[Environment]::SetEnvironmentVariable("ANTHROPIC_AUTH_TOKEN", [string]$provider.auth_token, "Process")
|
||||
[Environment]::SetEnvironmentVariable("API_TIMEOUT_MS", [string]$provider.timeout_ms, "Process")
|
||||
[Environment]::SetEnvironmentVariable("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1", "Process")
|
||||
[Environment]::SetEnvironmentVariable("ANTHROPIC_MODEL", [string]$provider.model, "Process")
|
||||
[Environment]::SetEnvironmentVariable("ANTHROPIC_SMALL_FAST_MODEL", [string]$provider.model, "Process")
|
||||
[Environment]::SetEnvironmentVariable("ANTHROPIC_DEFAULT_HAIKU_MODEL", [string]$provider.model, "Process")
|
||||
[Environment]::SetEnvironmentVariable("ANTHROPIC_DEFAULT_SONNET_MODEL", [string]$provider.model, "Process")
|
||||
[Environment]::SetEnvironmentVariable("ANTHROPIC_DEFAULT_OPUS_MODEL", [string]$provider.model, "Process")
|
||||
[Environment]::SetEnvironmentVariable(
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
|
||||
$(if ($provider.experimental_agent_teams) { "1" } else { "0" }),
|
||||
"Process"
|
||||
)
|
||||
|
||||
$claudeArgs = @(
|
||||
"-p",
|
||||
"--output-format", $OutputFormat,
|
||||
"--dangerously-skip-permissions",
|
||||
"--model", [string]$provider.model
|
||||
)
|
||||
|
||||
foreach ($path in @($WorkingDirectory) + $AddDirectories) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($path)) {
|
||||
$claudeArgs += @("--add-dir", $path)
|
||||
}
|
||||
}
|
||||
|
||||
$output = $promptText | & claude @claudeArgs "-" 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
foreach ($name in $envNames) {
|
||||
[Environment]::SetEnvironmentVariable($name, $previous[$name], "Process")
|
||||
}
|
||||
}
|
||||
|
||||
$result = [ordered]@{
|
||||
provider = $ProviderName
|
||||
model = [string]$provider.model
|
||||
working_directory = $WorkingDirectory
|
||||
prompt_file = $PromptFile
|
||||
output_format = $OutputFormat
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
exit_code = $exitCode
|
||||
output = ($output -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
Write-Utf8File -Path $OutputFile -Content (($result | ConvertTo-Json -Depth 100) + "`n")
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
throw "Provider '$ProviderName' failed with exit code $exitCode. See $OutputFile"
|
||||
}
|
||||
122
ralph/scripts/Invoke-CodexMaster.ps1
Normal file
122
ralph/scripts/Invoke-CodexMaster.ps1
Normal file
@@ -0,0 +1,122 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PromptFile,
|
||||
[Parameter(Mandatory = $true)][string]$OutputFile,
|
||||
[string]$WorkingDirectory = "",
|
||||
[switch]$SkipVerdictParse
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$codexConfig = Get-CodexConfig
|
||||
if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) {
|
||||
$WorkingDirectory = [string]$codexConfig.working_directory
|
||||
}
|
||||
|
||||
Ensure-Directory -Path (Split-Path -Parent $OutputFile)
|
||||
|
||||
$promptText = Get-Content -Raw -Path $PromptFile
|
||||
$preferredCmd = Join-Path $env:APPDATA "npm\codex.cmd"
|
||||
$preferredPs1 = Join-Path $env:APPDATA "npm\codex.ps1"
|
||||
if (Test-Path $preferredCmd) {
|
||||
$codexPath = $preferredCmd
|
||||
}
|
||||
elseif (Test-Path $preferredPs1) {
|
||||
$codexPath = $preferredPs1
|
||||
}
|
||||
else {
|
||||
$codexCommand = Get-Command codex -ErrorAction SilentlyContinue
|
||||
if ($null -eq $codexCommand) {
|
||||
throw "codex executable not found in PATH."
|
||||
}
|
||||
$codexPath = $codexCommand.Source
|
||||
}
|
||||
|
||||
Write-Utf8File -Path $PromptFile -Content $promptText
|
||||
|
||||
$stdoutLog = $OutputFile + ".stdout.log"
|
||||
$stderrLog = $OutputFile + ".stderr.log"
|
||||
$model = [string]$codexConfig.model
|
||||
$sessionId = [string]$codexConfig.session_id
|
||||
if ([string]::IsNullOrWhiteSpace($sessionId)) {
|
||||
throw "codex.local.json is missing session_id"
|
||||
}
|
||||
|
||||
$arguments = @(
|
||||
"-C", $WorkingDirectory,
|
||||
"exec", "resume", $sessionId,
|
||||
"--dangerously-bypass-approvals-and-sandbox"
|
||||
)
|
||||
if (-not [string]::IsNullOrWhiteSpace($model)) {
|
||||
$arguments += @("--model", $model)
|
||||
}
|
||||
$arguments += @("--output-last-message", $OutputFile, "-")
|
||||
|
||||
$stdout = @()
|
||||
$stderr = @()
|
||||
try {
|
||||
$mergedOutput = @($promptText | & $codexPath @arguments 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
foreach ($entry in $mergedOutput) {
|
||||
if ($entry -is [System.Management.Automation.ErrorRecord]) {
|
||||
$stderr += $entry.ToString()
|
||||
}
|
||||
else {
|
||||
$stdout += [string]$entry
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$exitCode = 1
|
||||
$stderr = @($_.Exception.Message)
|
||||
}
|
||||
|
||||
if ($stdout) {
|
||||
Write-Utf8File -Path $stdoutLog -Content ((@($stdout) -join [Environment]::NewLine) + [Environment]::NewLine)
|
||||
}
|
||||
elseif (-not (Test-Path $stdoutLog)) {
|
||||
Write-Utf8File -Path $stdoutLog -Content ""
|
||||
}
|
||||
|
||||
if ($stderr) {
|
||||
Write-Utf8File -Path $stderrLog -Content ((@($stderr) -join [Environment]::NewLine) + [Environment]::NewLine)
|
||||
}
|
||||
elseif (-not (Test-Path $stderrLog)) {
|
||||
Write-Utf8File -Path $stderrLog -Content ""
|
||||
}
|
||||
|
||||
if (-not $SkipVerdictParse) {
|
||||
if (Test-Path $OutputFile) {
|
||||
try {
|
||||
$parsedVerdict = Read-CodexReviewVerdict -Path $OutputFile
|
||||
if ($exitCode -ne 0) {
|
||||
$stderr += "Codex returned exit code $exitCode but produced a valid review output. Accepting the review output."
|
||||
Write-Utf8File -Path $stderrLog -Content ((@($stderr) -join [Environment]::NewLine) + [Environment]::NewLine)
|
||||
}
|
||||
return $parsedVerdict
|
||||
}
|
||||
catch {
|
||||
if ($exitCode -eq 0) {
|
||||
throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (Test-Path $OutputFile) {
|
||||
$rawOutput = (Get-Content -Raw -Path $OutputFile).Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($rawOutput)) {
|
||||
if ($exitCode -ne 0) {
|
||||
$stderr += "Codex returned exit code $exitCode but produced non-empty output. Accepting the generated output."
|
||||
Write-Utf8File -Path $stderrLog -Content ((@($stderr) -join [Environment]::NewLine) + [Environment]::NewLine)
|
||||
}
|
||||
return $rawOutput
|
||||
}
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
$errorExcerpt = ""
|
||||
$combined = ((@($stdout) + @($stderr)) -join " ").Trim()
|
||||
if ($combined -match "usage limit") {
|
||||
$errorExcerpt = " Codex local CLI is currently over its usage limit."
|
||||
}
|
||||
throw "Codex master review failed with exit code $exitCode.$errorExcerpt See $stdoutLog and $stderrLog"
|
||||
}
|
||||
44
ralph/scripts/Invoke-CodexNextTask.ps1
Normal file
44
ralph/scripts/Invoke-CodexNextTask.ps1
Normal file
@@ -0,0 +1,44 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$TaskDirectory,
|
||||
[Parameter(Mandatory = $true)][string]$RunDirectory,
|
||||
[Parameter(Mandatory = $true)][string]$OutputFile,
|
||||
[Parameter(Mandatory = $true)][string]$RunStatus,
|
||||
[string]$WorkingDirectory = "",
|
||||
[string[]]$ExtraSections = @()
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) {
|
||||
$WorkingDirectory = Get-RepoRoot
|
||||
}
|
||||
|
||||
$taskPack = Read-TaskPack -TaskDirectory $TaskDirectory
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$promptPath = Join-Path $RunDirectory "prompts\codex_next_task.md"
|
||||
|
||||
$sections = @(
|
||||
"## PREVIOUS RUN DIRECTORY`n$RunDirectory",
|
||||
"## PREVIOUS RUN STATUS`n$RunStatus"
|
||||
)
|
||||
foreach ($section in $ExtraSections) {
|
||||
$sections += $section
|
||||
}
|
||||
|
||||
New-PromptDocument `
|
||||
-TemplatePath (Join-Path $ralphRoot "templates\CODEX_NEXT_TASK_PROMPT.md") `
|
||||
-TaskPack $taskPack `
|
||||
-OutputPath $promptPath `
|
||||
-ExtraSections $sections
|
||||
|
||||
& (Join-Path $PSScriptRoot "Invoke-CodexMaster.ps1") `
|
||||
-PromptFile $promptPath `
|
||||
-OutputFile $OutputFile `
|
||||
-WorkingDirectory $WorkingDirectory `
|
||||
-SkipVerdictParse | Out-Null
|
||||
|
||||
if (-not (Test-Path $OutputFile)) {
|
||||
throw "Codex next-task generation did not produce output: $OutputFile"
|
||||
}
|
||||
|
||||
Get-Content -Raw -Path $OutputFile
|
||||
152
ralph/scripts/Queue-RalphFollowupFromCurrentRun.ps1
Normal file
152
ralph/scripts/Queue-RalphFollowupFromCurrentRun.ps1
Normal file
@@ -0,0 +1,152 @@
|
||||
param(
|
||||
[string]$RunId = "",
|
||||
[string]$RunStatus = "",
|
||||
[switch]$Enqueue
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$repoRoot = Get-RepoRoot
|
||||
$automationConfig = Get-RalphAutomationConfig
|
||||
if ($null -eq $automationConfig -or -not ($automationConfig.PSObject.Properties.Name -contains "auto_followup")) {
|
||||
throw "automation config missing auto_followup block"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RunId)) {
|
||||
$current = Read-JsonFile -Path (Get-RalphStateFile -Name "current_run.json")
|
||||
}
|
||||
else {
|
||||
$runDir = Join-Path $ralphRoot ("runs\" + $RunId)
|
||||
if (-not (Test-Path $runDir)) {
|
||||
throw "Run directory not found: $runDir"
|
||||
}
|
||||
$currentRunFile = Get-RalphStateFile -Name "current_run.json"
|
||||
$current = Read-JsonFile -Path $currentRunFile
|
||||
if ([string]$current.run_id -ne $RunId) {
|
||||
$summaryPath = Join-Path $runDir "SUMMARY.md"
|
||||
$taskPath = Join-Path $runDir "TASK.md"
|
||||
$acceptancePath = Join-Path $runDir "ACCEPTANCE.md"
|
||||
$contextPath = Join-Path $runDir "CONTEXT.md"
|
||||
if (-not (Test-Path $taskPath)) {
|
||||
throw "Run does not contain TASK.md: $runDir"
|
||||
}
|
||||
$current = [ordered]@{
|
||||
run_id = $RunId
|
||||
run_dir = $runDir
|
||||
task_directory = ""
|
||||
status = "unknown"
|
||||
latest_message = $(if (Test-Path $summaryPath) { (Get-Content -Raw $summaryPath) } else { "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RunStatus)) {
|
||||
$RunStatus = [string]$current.status
|
||||
}
|
||||
|
||||
$runDirResolved = [string]$current.run_dir
|
||||
if ([string]::IsNullOrWhiteSpace($runDirResolved)) {
|
||||
throw "current run state is missing run_dir"
|
||||
}
|
||||
|
||||
$taskDirectory = [string]$current.task_directory
|
||||
if ([string]::IsNullOrWhiteSpace($taskDirectory) -or -not (Test-Path $taskDirectory)) {
|
||||
$taskLeaf = ""
|
||||
try {
|
||||
$taskLeaf = Split-Path -Leaf ([string]$current.task_directory)
|
||||
}
|
||||
catch {
|
||||
$taskLeaf = ""
|
||||
}
|
||||
$searchRoots = @(
|
||||
(Join-Path $ralphRoot "tasks\\processing"),
|
||||
(Join-Path $ralphRoot "tasks\\failed"),
|
||||
(Join-Path $ralphRoot "tasks\\completed")
|
||||
)
|
||||
foreach ($root in $searchRoots) {
|
||||
if (-not (Test-Path $root)) {
|
||||
continue
|
||||
}
|
||||
$candidate = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($taskLeaf)) {
|
||||
$candidate = Join-Path $root $taskLeaf
|
||||
if (Test-Path $candidate) {
|
||||
$taskDirectory = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$matches = @(Get-ChildItem -Path $root -Directory -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -like ("*" + [string]$current.run_id + "*") -or $_.Name -like ("*" + $taskLeaf + "*")
|
||||
} | Select-Object -First 1)
|
||||
if ($matches.Count -gt 0) {
|
||||
$taskDirectory = $matches[0].FullName
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($taskDirectory) -or -not (Test-Path $taskDirectory)) {
|
||||
throw "could not resolve task directory for followup generation"
|
||||
}
|
||||
|
||||
$targetRelative = "docs\\autopilot"
|
||||
if ($automationConfig.auto_followup.PSObject.Properties.Name -contains "target_directory" -and -not [string]::IsNullOrWhiteSpace([string]$automationConfig.auto_followup.target_directory)) {
|
||||
$targetRelative = [string]$automationConfig.auto_followup.target_directory
|
||||
}
|
||||
$targetDirectory = Join-Path $repoRoot $targetRelative
|
||||
Ensure-Directory -Path $targetDirectory
|
||||
|
||||
$outputFile = Join-Path $runDirResolved "NEXT_TASK_MANUAL.md"
|
||||
$extraSections = @(
|
||||
"## PREVIOUS OUTCOME SUMMARY`n$([string]$current.latest_message)",
|
||||
"## RUN SUMMARY FILE`n$(Join-Path $runDirResolved 'SUMMARY.md')",
|
||||
"## RUN OUTPUTS DIRECTORY`n$(Join-Path $runDirResolved 'outputs')",
|
||||
"## RUN REVIEWS DIRECTORY`n$(Join-Path $runDirResolved 'reviews')"
|
||||
)
|
||||
|
||||
& (Join-Path $PSScriptRoot "Invoke-CodexNextTask.ps1") `
|
||||
-TaskDirectory $taskDirectory `
|
||||
-RunDirectory $runDirResolved `
|
||||
-OutputFile $outputFile `
|
||||
-RunStatus $RunStatus `
|
||||
-WorkingDirectory $repoRoot `
|
||||
-ExtraSections $extraSections | Out-Null
|
||||
|
||||
$slugBase = Convert-ToRalphSlug -Text ([IO.Path]::GetFileNameWithoutExtension($outputFile))
|
||||
$finalMdPath = Join-Path $targetDirectory ((Get-Date -Format "yyyyMMdd-HHmmss") + "-" + $slugBase + ".md")
|
||||
Copy-Item -Path $outputFile -Destination $finalMdPath -Force
|
||||
|
||||
$result = [ordered]@{
|
||||
generated = $true
|
||||
source_markdown = $finalMdPath
|
||||
queued = $false
|
||||
}
|
||||
|
||||
if ($Enqueue) {
|
||||
$taskInfo = New-RalphTaskPackFromMarkdown `
|
||||
-MarkdownPath $finalMdPath `
|
||||
-AdditionalMetadata @{
|
||||
auto_generated = $true
|
||||
parent_run_id = [string]$current.run_id
|
||||
parent_task_directory = $taskDirectory
|
||||
followup_generation = 1
|
||||
source_run_status = $RunStatus
|
||||
}
|
||||
$result.queued = $true
|
||||
$result.task_directory = $taskInfo.task_directory
|
||||
$result.title = $taskInfo.title
|
||||
Add-RalphEvent -RunId ([string]$current.run_id) -Stage "auto_followup" -Status "queued" -Actor "codex_master" -Message ("Manual backfill followup queued: " + $taskInfo.title) -Data @{
|
||||
task_directory = $taskInfo.task_directory
|
||||
source_markdown = $finalMdPath
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "task_queued" `
|
||||
-Title "Auto-followup queued" `
|
||||
-Message ($taskInfo.title + "`n" + $taskInfo.task_directory) `
|
||||
-RunId ([string]$current.run_id) `
|
||||
-Stage "auto_followup" `
|
||||
-Status "queued" | Out-Null
|
||||
}
|
||||
|
||||
($result | ConvertTo-Json -Depth 20)
|
||||
714
ralph/scripts/Start-RalphAutopilot.ps1
Normal file
714
ralph/scripts/Start-RalphAutopilot.ps1
Normal file
@@ -0,0 +1,714 @@
|
||||
param(
|
||||
[string]$TaskDirectory = "",
|
||||
[string]$RunLabel = "autopilot",
|
||||
[string]$Implementer = "",
|
||||
[string[]]$Reviewers = @(),
|
||||
[switch]$UseCodexMaster = $true,
|
||||
[switch]$AutoFix = $true,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$repoRoot = Get-RepoRoot
|
||||
$roots = Get-RalphTaskRoots
|
||||
$automationConfig = Get-RalphAutomationConfig
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($TaskDirectory)) {
|
||||
$TaskDirectory = Join-Path $ralphRoot "tasks\current"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Implementer)) {
|
||||
$Implementer = Get-DefaultImplementer
|
||||
}
|
||||
|
||||
if ($Reviewers.Count -eq 0) {
|
||||
$Reviewers = Get-DefaultReviewers
|
||||
}
|
||||
|
||||
$taskPack = Read-TaskPack -TaskDirectory $TaskDirectory
|
||||
$submissionPath = Join-Path $TaskDirectory "submission.json"
|
||||
$submissionMetadata = $null
|
||||
if (Test-Path $submissionPath) {
|
||||
try {
|
||||
$submissionMetadata = Read-JsonFile -Path $submissionPath
|
||||
}
|
||||
catch {
|
||||
$submissionMetadata = $null
|
||||
}
|
||||
}
|
||||
$runId = New-RunId -Label $RunLabel
|
||||
$runDir = Join-Path $ralphRoot ("runs\" + $runId)
|
||||
$worktreePath = Join-Path $ralphRoot ("worktrees\" + $runId + "-" + $Implementer)
|
||||
|
||||
Ensure-Directory -Path $runDir
|
||||
Ensure-Directory -Path (Join-Path $runDir "prompts")
|
||||
Ensure-Directory -Path (Join-Path $runDir "outputs")
|
||||
Ensure-Directory -Path (Join-Path $runDir "reviews")
|
||||
|
||||
Copy-Item -Path $taskPack.Files.Task -Destination (Join-Path $runDir "TASK.md") -Force
|
||||
Copy-Item -Path $taskPack.Files.Acceptance -Destination (Join-Path $runDir "ACCEPTANCE.md") -Force
|
||||
Copy-Item -Path $taskPack.Files.Context -Destination (Join-Path $runDir "CONTEXT.md") -Force
|
||||
|
||||
$script:runState = [ordered]@{
|
||||
run_id = $runId
|
||||
status = "initializing"
|
||||
stage = "initializing"
|
||||
started_at = (Get-Date).ToString("o")
|
||||
finished_at = $null
|
||||
task_directory = $TaskDirectory
|
||||
run_dir = $runDir
|
||||
worktree = $worktreePath
|
||||
implementer = [ordered]@{
|
||||
name = $Implementer
|
||||
status = "pending"
|
||||
started_at = $null
|
||||
finished_at = $null
|
||||
output_file = ""
|
||||
}
|
||||
reviewers = @()
|
||||
codex_master = [ordered]@{
|
||||
enabled = [bool]$UseCodexMaster
|
||||
status = $(if ($UseCodexMaster) { "pending" } else { "disabled" })
|
||||
started_at = $null
|
||||
finished_at = $null
|
||||
output_file = ""
|
||||
verdict = ""
|
||||
acceptance_passed = $false
|
||||
}
|
||||
fix_pass = [ordered]@{
|
||||
enabled = [bool]$AutoFix
|
||||
status = $(if ($AutoFix) { "pending" } else { "disabled" })
|
||||
started_at = $null
|
||||
finished_at = $null
|
||||
output_file = ""
|
||||
}
|
||||
latest_message = "Run initialized"
|
||||
errors = @()
|
||||
}
|
||||
|
||||
foreach ($reviewer in $Reviewers) {
|
||||
$script:runState.reviewers += [ordered]@{
|
||||
name = $reviewer
|
||||
status = "pending"
|
||||
started_at = $null
|
||||
finished_at = $null
|
||||
output_file = ""
|
||||
}
|
||||
}
|
||||
|
||||
function Get-FollowupGeneration {
|
||||
if ($null -eq $submissionMetadata) {
|
||||
return 0
|
||||
}
|
||||
if ($submissionMetadata.PSObject.Properties.Name -contains "followup_generation") {
|
||||
try {
|
||||
return [int]$submissionMetadata.followup_generation
|
||||
}
|
||||
catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function Save-RunState {
|
||||
Set-RalphCurrentRunState -State $script:runState
|
||||
}
|
||||
|
||||
function Set-RunPhase {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Stage,
|
||||
[Parameter(Mandatory = $true)][string]$Status,
|
||||
[Parameter(Mandatory = $true)][string]$Message
|
||||
)
|
||||
|
||||
$script:runState.stage = $Stage
|
||||
$script:runState.status = $Status
|
||||
$script:runState.latest_message = $Message
|
||||
Save-RunState
|
||||
Add-RalphEvent -RunId $script:runState.run_id -Stage $Stage -Status $Status -Message $Message
|
||||
}
|
||||
|
||||
function Set-ActorState {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][ValidateSet("implementer", "reviewer", "codex_master", "fix_pass")][string]$ActorType,
|
||||
[Parameter(Mandatory = $true)][string]$Status,
|
||||
[Parameter(Mandatory = $true)][string]$Message,
|
||||
[string]$ActorName = "",
|
||||
[string]$OutputFile = ""
|
||||
)
|
||||
|
||||
$timestamp = (Get-Date).ToString("o")
|
||||
$target = $null
|
||||
$actorLabel = $ActorName
|
||||
|
||||
switch ($ActorType) {
|
||||
"implementer" {
|
||||
$target = $script:runState.implementer
|
||||
$actorLabel = $script:runState.implementer.name
|
||||
}
|
||||
"reviewer" {
|
||||
$target = $script:runState.reviewers | Where-Object { $_.name -eq $ActorName } | Select-Object -First 1
|
||||
$actorLabel = $ActorName
|
||||
}
|
||||
"codex_master" {
|
||||
$target = $script:runState.codex_master
|
||||
$actorLabel = "codex_master"
|
||||
}
|
||||
"fix_pass" {
|
||||
$target = $script:runState.fix_pass
|
||||
$actorLabel = $script:runState.implementer.name
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $target) {
|
||||
$target.status = $Status
|
||||
if ($Status -eq "running" -and -not $target.started_at) {
|
||||
$target.started_at = $timestamp
|
||||
}
|
||||
if ($Status -in @("completed", "failed", "skipped")) {
|
||||
$target.finished_at = $timestamp
|
||||
}
|
||||
if ($OutputFile) {
|
||||
$target.output_file = $OutputFile
|
||||
}
|
||||
}
|
||||
|
||||
$script:runState.latest_message = $Message
|
||||
Save-RunState
|
||||
Add-RalphEvent -RunId $script:runState.run_id -Stage $ActorType -Status $Status -Actor $actorLabel -Message $Message -Data @{
|
||||
output_file = $OutputFile
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-CodexReviewPass {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$StageName,
|
||||
[Parameter(Mandatory = $true)][string]$PromptFileName,
|
||||
[Parameter(Mandatory = $true)][string]$OutputFileName,
|
||||
[Parameter(Mandatory = $true)][string]$RunningMessage,
|
||||
[Parameter(Mandatory = $true)][string]$CompletedMessage,
|
||||
[string[]]$ExtraSections = @()
|
||||
)
|
||||
|
||||
$promptPath = Join-Path $runDir ("prompts\" + $PromptFileName)
|
||||
$outputPath = Join-Path $runDir ("reviews\" + $OutputFileName)
|
||||
|
||||
New-PromptDocument `
|
||||
-TemplatePath (Join-Path $ralphRoot "templates\CODEX_REVIEW_PROMPT.md") `
|
||||
-TaskPack $taskPack `
|
||||
-OutputPath $promptPath `
|
||||
-ExtraSections $ExtraSections
|
||||
|
||||
Set-RunPhase -Stage $StageName -Status "running" -Message $RunningMessage
|
||||
Set-ActorState -ActorType "codex_master" -Status "running" -Message $RunningMessage -OutputFile $outputPath
|
||||
$verdict = & (Join-Path $PSScriptRoot "Invoke-CodexMaster.ps1") `
|
||||
-PromptFile $promptPath `
|
||||
-OutputFile $outputPath `
|
||||
-WorkingDirectory $worktreePath
|
||||
|
||||
$script:runState.codex_master.verdict = [string]$verdict.verdict
|
||||
$script:runState.codex_master.acceptance_passed = [bool]$verdict.acceptance_passed
|
||||
Set-ActorState -ActorType "codex_master" -Status "completed" -Message ($CompletedMessage + " (verdict: " + $verdict.verdict + ")") -OutputFile $outputPath
|
||||
Set-RunPhase -Stage $StageName -Status "completed" -Message ($CompletedMessage + " (verdict: " + $verdict.verdict + ")")
|
||||
Save-RunState
|
||||
return $verdict
|
||||
}
|
||||
|
||||
function Invoke-AutoFollowupTask {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$OutcomeStatus,
|
||||
[Parameter(Mandatory = $true)][string]$OutcomeSummary
|
||||
)
|
||||
|
||||
if ($null -eq $automationConfig) {
|
||||
return $null
|
||||
}
|
||||
if (-not ($automationConfig.PSObject.Properties.Name -contains "auto_followup")) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$autoFollowup = $automationConfig.auto_followup
|
||||
$enabled = $false
|
||||
try { $enabled = [bool]$autoFollowup.enabled } catch { $enabled = $false }
|
||||
if (-not $enabled) {
|
||||
return $null
|
||||
}
|
||||
if (-not $UseCodexMaster) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$trigger = $false
|
||||
if ($OutcomeStatus -eq "failed") {
|
||||
try { $trigger = [bool]$autoFollowup.trigger_on_failure } catch { $trigger = $false }
|
||||
}
|
||||
elseif ($OutcomeStatus -eq "completed") {
|
||||
try { $trigger = [bool]$autoFollowup.trigger_on_success } catch { $trigger = $false }
|
||||
}
|
||||
if (-not $trigger) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$maxDepth = 0
|
||||
try { $maxDepth = [int]$autoFollowup.max_chain_depth } catch { $maxDepth = 0 }
|
||||
$currentDepth = Get-FollowupGeneration
|
||||
if ($maxDepth -gt 0 -and $currentDepth -ge $maxDepth) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$targetRelative = "docs\autopilot"
|
||||
if ($autoFollowup.PSObject.Properties.Name -contains "target_directory" -and -not [string]::IsNullOrWhiteSpace([string]$autoFollowup.target_directory)) {
|
||||
$targetRelative = [string]$autoFollowup.target_directory
|
||||
}
|
||||
$targetDirectory = Join-Path $repoRoot $targetRelative
|
||||
Ensure-Directory -Path $targetDirectory
|
||||
|
||||
$prefix = "AUTOFOLLOWUP"
|
||||
if ($autoFollowup.PSObject.Properties.Name -contains "title_prefix" -and -not [string]::IsNullOrWhiteSpace([string]$autoFollowup.title_prefix)) {
|
||||
$prefix = [string]$autoFollowup.title_prefix
|
||||
}
|
||||
|
||||
$nextPromptOutput = Join-Path $runDir "NEXT_TASK.md"
|
||||
$extraSections = @(
|
||||
"## PREVIOUS OUTCOME SUMMARY`n$OutcomeSummary",
|
||||
"## RUN SUMMARY FILE`n$(Join-Path $runDir 'SUMMARY.md')",
|
||||
"## RUN OUTPUTS DIRECTORY`n$(Join-Path $runDir 'outputs')",
|
||||
"## RUN REVIEWS DIRECTORY`n$(Join-Path $runDir 'reviews')"
|
||||
)
|
||||
|
||||
$null = & (Join-Path $PSScriptRoot "Invoke-CodexNextTask.ps1") `
|
||||
-TaskDirectory $TaskDirectory `
|
||||
-RunDirectory $runDir `
|
||||
-OutputFile $nextPromptOutput `
|
||||
-RunStatus $OutcomeStatus `
|
||||
-WorkingDirectory $worktreePath `
|
||||
-ExtraSections $extraSections
|
||||
|
||||
$dateLabel = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$fileName = "{0}-{1}.md" -f $dateLabel, (Convert-ToRalphSlug -Text ($prefix + "-" + $taskPack.Task.Substring(0, [Math]::Min($taskPack.Task.Length, 32))))
|
||||
$finalMdPath = Join-Path $targetDirectory $fileName
|
||||
Copy-Item -Path $nextPromptOutput -Destination $finalMdPath -Force
|
||||
|
||||
$taskInfo = New-RalphTaskPackFromMarkdown `
|
||||
-MarkdownPath $finalMdPath `
|
||||
-AdditionalMetadata @{
|
||||
auto_generated = $true
|
||||
parent_run_id = $runId
|
||||
parent_task_directory = $TaskDirectory
|
||||
followup_generation = ($currentDepth + 1)
|
||||
source_run_status = $OutcomeStatus
|
||||
}
|
||||
|
||||
Add-RalphEvent -RunId $runId -Stage "auto_followup" -Status "queued" -Actor "codex_master" -Message ("Auto-followup task queued: " + $taskInfo.title) -Data @{
|
||||
task_directory = $taskInfo.task_directory
|
||||
source_markdown = $finalMdPath
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "task_queued" `
|
||||
-Title "Auto-followup queued" `
|
||||
-Message ($taskInfo.title + "`n" + $taskInfo.task_directory) `
|
||||
-RunId $runId `
|
||||
-Stage "auto_followup" `
|
||||
-Status "queued" | Out-Null
|
||||
|
||||
return [ordered]@{
|
||||
title = $taskInfo.title
|
||||
task_directory = $taskInfo.task_directory
|
||||
source_markdown = $finalMdPath
|
||||
followup_generation = ($currentDepth + 1)
|
||||
}
|
||||
}
|
||||
|
||||
Save-RunState
|
||||
Add-RalphEvent -RunId $runId -Stage "initializing" -Status "started" -Message "Ralph run initialized" -Data @{
|
||||
implementer = $Implementer
|
||||
reviewers = $Reviewers
|
||||
worktree = $worktreePath
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "run_started" `
|
||||
-Title "Run started" `
|
||||
-Message ("Implementer: " + $Implementer + "`nReviewers: " + ($Reviewers -join ", ")) `
|
||||
-RunId $runId `
|
||||
-Stage "initializing" `
|
||||
-Status "started" | Out-Null
|
||||
|
||||
$gitStatus = & git -C $repoRoot status --short
|
||||
Write-Utf8File -Path (Join-Path $runDir "repo_status_before.txt") -Content (($gitStatus -join "`n") + "`n")
|
||||
|
||||
$gitStat = & git -C $repoRoot diff --stat
|
||||
Write-Utf8File -Path (Join-Path $runDir "repo_diff_stat_before.txt") -Content (($gitStat -join "`n") + "`n")
|
||||
|
||||
$implementerPrompt = Join-Path $runDir "prompts\implementer.md"
|
||||
New-PromptDocument `
|
||||
-TemplatePath (Join-Path $ralphRoot "templates\IMPLEMENTER_PROMPT.md") `
|
||||
-TaskPack $taskPack `
|
||||
-OutputPath $implementerPrompt `
|
||||
-ExtraSections @(
|
||||
"## WORKTREE`nEdit only inside this worktree:`n`n$worktreePath",
|
||||
"## REQUIRED RUN OUTPUT`nWrite a file named CHANGES.md in this run directory:`n`n$runDir"
|
||||
)
|
||||
|
||||
$summaryLines = @(
|
||||
"# Ralph Run",
|
||||
"",
|
||||
"- Run ID: $runId",
|
||||
"- Implementer: $Implementer",
|
||||
"- Reviewers: $(($Reviewers -join ', '))",
|
||||
"- Worktree: $worktreePath",
|
||||
"- Codex master review: $(if ($UseCodexMaster) { 'enabled' } else { 'disabled' })",
|
||||
"- Auto fix pass: $(if ($AutoFix) { 'enabled' } else { 'disabled' })",
|
||||
"- Dry run: $(if ($DryRun) { 'yes' } else { 'no' })"
|
||||
)
|
||||
|
||||
if ($DryRun) {
|
||||
Set-RunPhase -Stage "dry_run" -Status "completed" -Message "Dry run prepared successfully"
|
||||
$script:runState.finished_at = (Get-Date).ToString("o")
|
||||
Save-RunState
|
||||
Write-Utf8File -Path (Join-Path $runDir "SUMMARY.md") -Content (($summaryLines -join "`n") + "`n")
|
||||
Get-Content -Raw -Path (Join-Path $runDir "SUMMARY.md")
|
||||
return
|
||||
}
|
||||
|
||||
Set-RunPhase -Stage "worktree" -Status "running" -Message "Creating isolated worktree"
|
||||
& git -C $repoRoot worktree add --detach $worktreePath HEAD | Out-Null
|
||||
Set-RunPhase -Stage "worktree" -Status "completed" -Message "Worktree ready"
|
||||
|
||||
$reviewOutputs = @()
|
||||
$codexFinalVerdict = $null
|
||||
$autoFollowupResult = $null
|
||||
$script:heartbeatJob = $null
|
||||
$script:heartbeatSignalPath = ""
|
||||
$codexReviewSections = @(
|
||||
"## IMPLEMENTER WORKTREE`n$worktreePath",
|
||||
"## IMPLEMENTER DIFF FILE`n$(Join-Path $runDir 'implementer.patch')",
|
||||
"## IMPLEMENTER STATUS FILE`n$(Join-Path $runDir 'implementer_status.txt')"
|
||||
)
|
||||
|
||||
function Start-HeartbeatMonitor {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Stage,
|
||||
[Parameter(Mandatory = $true)][string]$Title,
|
||||
[Parameter(Mandatory = $true)][string]$Message,
|
||||
[int]$IntervalSeconds = 300
|
||||
)
|
||||
|
||||
Stop-HeartbeatMonitor
|
||||
|
||||
$signalPath = Join-Path $runDir ("heartbeat-" + $Stage + ".lock")
|
||||
Write-Utf8File -Path $signalPath -Content ((Get-Date).ToString("o"))
|
||||
$commonPath = Join-Path $PSScriptRoot "Common.ps1"
|
||||
|
||||
$script:heartbeatSignalPath = $signalPath
|
||||
$script:heartbeatJob = Start-Job -ArgumentList $commonPath, $signalPath, $Stage, $Title, $Message, $runId, $IntervalSeconds -ScriptBlock {
|
||||
param($CommonPath, $SignalPath, $StageName, $TitleText, $MessageText, $RunIdValue, $IntervalValue)
|
||||
. $CommonPath
|
||||
while (Test-Path $SignalPath) {
|
||||
Start-Sleep -Seconds $IntervalValue
|
||||
if (-not (Test-Path $SignalPath)) {
|
||||
break
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "run_heartbeat" `
|
||||
-Title $TitleText `
|
||||
-Message $MessageText `
|
||||
-RunId $RunIdValue `
|
||||
-Stage $StageName `
|
||||
-Status "running" | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-HeartbeatMonitor {
|
||||
if (-not [string]::IsNullOrWhiteSpace($script:heartbeatSignalPath) -and (Test-Path $script:heartbeatSignalPath)) {
|
||||
Remove-Item -LiteralPath $script:heartbeatSignalPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
$script:heartbeatSignalPath = ""
|
||||
|
||||
if ($null -ne $script:heartbeatJob) {
|
||||
Wait-Job -Job $script:heartbeatJob -Timeout 1 | Out-Null
|
||||
if ($script:heartbeatJob.State -eq "Running") {
|
||||
Stop-Job -Job $script:heartbeatJob -Force | Out-Null
|
||||
}
|
||||
Remove-Job -Job $script:heartbeatJob -Force -ErrorAction SilentlyContinue
|
||||
$script:heartbeatJob = $null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$implementerOutput = Join-Path $runDir "outputs\implementer.json"
|
||||
Set-RunPhase -Stage "implementer" -Status "running" -Message ("Implementer " + $Implementer + " started")
|
||||
Set-ActorState -ActorType "implementer" -Status "running" -Message ("Implementer " + $Implementer + " received task pack") -OutputFile $implementerOutput
|
||||
Start-HeartbeatMonitor -Stage "implementer" -Title "Run heartbeat" -Message ("Implementer running: " + $Implementer)
|
||||
& (Join-Path $PSScriptRoot "Invoke-ClaudeProvider.ps1") `
|
||||
-ProviderName $Implementer `
|
||||
-PromptFile $implementerPrompt `
|
||||
-OutputFile $implementerOutput `
|
||||
-WorkingDirectory $worktreePath `
|
||||
-AddDirectories @($runDir, $repoRoot)
|
||||
Stop-HeartbeatMonitor
|
||||
Set-ActorState -ActorType "implementer" -Status "completed" -Message ("Implementer " + $Implementer + " finished first pass") -OutputFile $implementerOutput
|
||||
Send-TelegramNotification `
|
||||
-EventName "implementer_completed" `
|
||||
-Title "Implementer finished" `
|
||||
-Message ("Implementer: " + $Implementer) `
|
||||
-RunId $runId `
|
||||
-Stage "implementer" `
|
||||
-Status "completed" | Out-Null
|
||||
|
||||
$worktreeStatus = & git -C $worktreePath status --short
|
||||
Write-Utf8File -Path (Join-Path $runDir "implementer_status.txt") -Content (($worktreeStatus -join "`n") + "`n")
|
||||
|
||||
$worktreeDiff = & git -C $worktreePath diff --no-ext-diff
|
||||
Write-Utf8File -Path (Join-Path $runDir "implementer.patch") -Content (($worktreeDiff -join "`n") + "`n")
|
||||
|
||||
foreach ($reviewer in $Reviewers) {
|
||||
if ($reviewer -eq $Reviewers[0]) {
|
||||
Send-TelegramNotification `
|
||||
-EventName "reviewers_started" `
|
||||
-Title "Reviewers started" `
|
||||
-Message (($Reviewers -join ", ")) `
|
||||
-RunId $runId `
|
||||
-Stage "review" `
|
||||
-Status "running" | Out-Null
|
||||
}
|
||||
$promptPath = Join-Path $runDir ("prompts\review-" + $reviewer + ".md")
|
||||
New-PromptDocument `
|
||||
-TemplatePath (Join-Path $ralphRoot "templates\REVIEWER_PROMPT.md") `
|
||||
-TaskPack $taskPack `
|
||||
-OutputPath $promptPath `
|
||||
-ExtraSections @(
|
||||
"## IMPLEMENTER WORKTREE`n$worktreePath",
|
||||
"## IMPLEMENTER DIFF FILE`n$(Join-Path $runDir 'implementer.patch')",
|
||||
"## IMPLEMENTER STATUS FILE`n$(Join-Path $runDir 'implementer_status.txt')"
|
||||
)
|
||||
|
||||
$reviewOutput = Join-Path $runDir ("reviews\" + $reviewer + ".json")
|
||||
Set-RunPhase -Stage "review" -Status "running" -Message ("Reviewer " + $reviewer + " started")
|
||||
Set-ActorState -ActorType "reviewer" -ActorName $reviewer -Status "running" -Message ("Reviewer " + $reviewer + " analyzing diff") -OutputFile $reviewOutput
|
||||
& (Join-Path $PSScriptRoot "Invoke-ClaudeProvider.ps1") `
|
||||
-ProviderName $reviewer `
|
||||
-PromptFile $promptPath `
|
||||
-OutputFile $reviewOutput `
|
||||
-WorkingDirectory $worktreePath `
|
||||
-AddDirectories @($runDir, $repoRoot)
|
||||
Set-ActorState -ActorType "reviewer" -ActorName $reviewer -Status "completed" -Message ("Reviewer " + $reviewer + " completed review") -OutputFile $reviewOutput
|
||||
$reviewOutputs += $reviewOutput
|
||||
}
|
||||
Set-RunPhase -Stage "review" -Status "completed" -Message "All provider reviews completed"
|
||||
|
||||
$codexReviewSections += "## REVIEW FILES`n$($reviewOutputs -join "`n")"
|
||||
|
||||
if ($UseCodexMaster -and $AutoFix) {
|
||||
$null = Invoke-CodexReviewPass `
|
||||
-StageName "codex_review_pre_fix" `
|
||||
-PromptFileName "codex_master_review_pre_fix.md" `
|
||||
-OutputFileName "codex_master_pre_fix.json" `
|
||||
-RunningMessage "Codex master pre-fix review started" `
|
||||
-CompletedMessage "Codex master pre-fix review completed" `
|
||||
-ExtraSections $codexReviewSections
|
||||
}
|
||||
|
||||
if ($AutoFix) {
|
||||
$fixPrompt = Join-Path $runDir "prompts\fix_pass.md"
|
||||
$fixExtraSections = @(
|
||||
"## FIX PASS`nRead the reviewer outputs and fix the highest-signal issues only.",
|
||||
"## REVIEW FILES`n$($reviewOutputs -join "`n")"
|
||||
)
|
||||
if ($UseCodexMaster) {
|
||||
$fixExtraSections += "## CODEX REVIEW FILE`n$(Join-Path $runDir 'reviews\codex_master_pre_fix.json')"
|
||||
}
|
||||
else {
|
||||
$fixExtraSections += "## CODEX REVIEW FILE`nCodex review disabled for this run."
|
||||
}
|
||||
$fixExtraSections += "## REQUIRED RUN OUTPUT`nUpdate CHANGES.md in this run directory after the fix pass:`n`n$runDir"
|
||||
|
||||
New-PromptDocument `
|
||||
-TemplatePath (Join-Path $ralphRoot "templates\IMPLEMENTER_PROMPT.md") `
|
||||
-TaskPack $taskPack `
|
||||
-OutputPath $fixPrompt `
|
||||
-ExtraSections $fixExtraSections
|
||||
|
||||
Set-RunPhase -Stage "fix_pass" -Status "running" -Message ("Fix pass started with " + $Implementer)
|
||||
Set-ActorState -ActorType "fix_pass" -Status "running" -Message ("Implementer " + $Implementer + " applying review fixes") -OutputFile (Join-Path $runDir "outputs\fix_pass.json")
|
||||
Send-TelegramNotification `
|
||||
-EventName "fix_pass_started" `
|
||||
-Title "Fix pass started" `
|
||||
-Message ("Implementer: " + $Implementer) `
|
||||
-RunId $runId `
|
||||
-Stage "fix_pass" `
|
||||
-Status "running" | Out-Null
|
||||
Start-HeartbeatMonitor -Stage "fix_pass" -Title "Run heartbeat" -Message ("Fix pass running: " + $Implementer)
|
||||
& (Join-Path $PSScriptRoot "Invoke-ClaudeProvider.ps1") `
|
||||
-ProviderName $Implementer `
|
||||
-PromptFile $fixPrompt `
|
||||
-OutputFile (Join-Path $runDir "outputs\fix_pass.json") `
|
||||
-WorkingDirectory $worktreePath `
|
||||
-AddDirectories @($runDir, $repoRoot)
|
||||
Stop-HeartbeatMonitor
|
||||
Set-ActorState -ActorType "fix_pass" -Status "completed" -Message ("Implementer " + $Implementer + " finished fix pass") -OutputFile (Join-Path $runDir "outputs\fix_pass.json")
|
||||
Set-RunPhase -Stage "fix_pass" -Status "completed" -Message "Fix pass completed"
|
||||
|
||||
$finalDiff = & git -C $worktreePath diff --no-ext-diff
|
||||
Write-Utf8File -Path (Join-Path $runDir "final.patch") -Content (($finalDiff -join "`n") + "`n")
|
||||
}
|
||||
|
||||
$finalStatus = & git -C $worktreePath status --short
|
||||
Write-Utf8File -Path (Join-Path $runDir "final_status.txt") -Content (($finalStatus -join "`n") + "`n")
|
||||
|
||||
if ($UseCodexMaster) {
|
||||
$finalDiffPath = Join-Path $runDir "final.patch"
|
||||
if (-not (Test-Path $finalDiffPath)) {
|
||||
$finalDiffPath = Join-Path $runDir "implementer.patch"
|
||||
}
|
||||
$codexFinalSections = @(
|
||||
"## IMPLEMENTER WORKTREE`n$worktreePath",
|
||||
"## FINAL DIFF FILE`n$finalDiffPath",
|
||||
"## FINAL STATUS FILE`n$(Join-Path $runDir 'final_status.txt')",
|
||||
"## REVIEW FILES`n$($reviewOutputs -join "`n")"
|
||||
)
|
||||
if ($AutoFix) {
|
||||
$codexFinalSections += "## FIX PASS OUTPUT`n$(Join-Path $runDir 'outputs\\fix_pass.json')"
|
||||
$codexFinalSections += "## PRE-FIX CODEX REVIEW`n$(Join-Path $runDir 'reviews\\codex_master_pre_fix.json')"
|
||||
}
|
||||
|
||||
$codexFinalVerdict = Invoke-CodexReviewPass `
|
||||
-StageName "codex_review_final" `
|
||||
-PromptFileName "codex_master_review_final.md" `
|
||||
-OutputFileName "codex_master_final.json" `
|
||||
-RunningMessage "Codex master final review started" `
|
||||
-CompletedMessage "Codex master final review completed" `
|
||||
-ExtraSections $codexFinalSections
|
||||
|
||||
if (([string]$codexFinalVerdict.verdict) -ne "pass" -or -not [bool]$codexFinalVerdict.acceptance_passed) {
|
||||
Send-TelegramNotification `
|
||||
-EventName "codex_failed" `
|
||||
-Title "Codex gate rejected run" `
|
||||
-Message ([string]$codexFinalVerdict.summary) `
|
||||
-RunId $runId `
|
||||
-Stage "codex_review_final" `
|
||||
-Status "failed" | Out-Null
|
||||
throw ("Codex master rejected the run with verdict '{0}': {1}" -f $codexFinalVerdict.verdict, $codexFinalVerdict.summary)
|
||||
}
|
||||
}
|
||||
|
||||
$summaryLines += @(
|
||||
"",
|
||||
"## Outputs",
|
||||
"",
|
||||
"- Run directory: $runDir",
|
||||
"- Worktree: $worktreePath",
|
||||
"- Implementer output: $(Join-Path $runDir 'outputs\implementer.json')",
|
||||
"- Final status: $(Join-Path $runDir 'final_status.txt')",
|
||||
"- Codex final verdict: $(if ($UseCodexMaster -and $null -ne $codexFinalVerdict) { [string]$codexFinalVerdict.verdict } else { 'not-run' })"
|
||||
)
|
||||
|
||||
try {
|
||||
$autoFollowupResult = Invoke-AutoFollowupTask -OutcomeStatus "completed" -OutcomeSummary "Run completed successfully"
|
||||
}
|
||||
catch {
|
||||
$script:runState.errors += ("Auto-followup generation error: " + $_.Exception.Message)
|
||||
Save-RunState
|
||||
Add-RalphEvent -RunId $runId -Stage "auto_followup" -Status "failed" -Actor "codex_master" -Message $_.Exception.Message
|
||||
}
|
||||
|
||||
if ($null -ne $autoFollowupResult) {
|
||||
$summaryLines += @(
|
||||
"",
|
||||
"## Auto Followup",
|
||||
"",
|
||||
"- Title: $($autoFollowupResult.title)",
|
||||
"- Task directory: $($autoFollowupResult.task_directory)",
|
||||
"- Source markdown: $($autoFollowupResult.source_markdown)"
|
||||
)
|
||||
}
|
||||
|
||||
Write-Utf8File -Path (Join-Path $runDir "SUMMARY.md") -Content (($summaryLines -join "`n") + "`n")
|
||||
$script:runState.status = "completed"
|
||||
$script:runState.stage = "completed"
|
||||
$script:runState.finished_at = (Get-Date).ToString("o")
|
||||
$script:runState.latest_message = "Run completed successfully"
|
||||
Save-RunState
|
||||
Add-RalphEvent -RunId $runId -Stage "completed" -Status "completed" -Message "Ralph run completed successfully" -Data @{
|
||||
run_dir = $runDir
|
||||
worktree = $worktreePath
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "run_completed" `
|
||||
-Title "Run completed" `
|
||||
-Message ("Implementer: " + $Implementer + "`nCodex verdict: " + $(if ($UseCodexMaster -and $null -ne $codexFinalVerdict) { [string]$codexFinalVerdict.verdict } else { "not-run" })) `
|
||||
-RunId $runId `
|
||||
-Stage "completed" `
|
||||
-Status "completed" | Out-Null
|
||||
Get-Content -Raw -Path (Join-Path $runDir "SUMMARY.md")
|
||||
}
|
||||
catch {
|
||||
Stop-HeartbeatMonitor
|
||||
$failureMessage = $_.Exception.Message
|
||||
if ($UseCodexMaster -and $null -eq $codexFinalVerdict) {
|
||||
try {
|
||||
$failureSections = @(
|
||||
"## FAILURE CONTEXT`nThe autopilot run failed before final acceptance.",
|
||||
"## FAILURE MESSAGE`n$failureMessage",
|
||||
"## IMPLEMENTER WORKTREE`n$worktreePath",
|
||||
"## IMPLEMENTER DIFF FILE`n$(Join-Path $runDir 'implementer.patch')",
|
||||
"## IMPLEMENTER STATUS FILE`n$(Join-Path $runDir 'implementer_status.txt')",
|
||||
"## REVIEW FILES`n$($reviewOutputs -join "`n")"
|
||||
)
|
||||
$null = Invoke-CodexReviewPass `
|
||||
-StageName "codex_review_failure" `
|
||||
-PromptFileName "codex_master_review_failure.md" `
|
||||
-OutputFileName "codex_master_failure.json" `
|
||||
-RunningMessage "Codex master failure review started" `
|
||||
-CompletedMessage "Codex master failure review completed" `
|
||||
-ExtraSections $failureSections
|
||||
}
|
||||
catch {
|
||||
$script:runState.errors += ("Codex failure review error: " + $_.Exception.Message)
|
||||
Add-RalphEvent -RunId $runId -Stage "codex_review_failure" -Status "failed" -Actor "codex_master" -Message $_.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
$script:runState.status = "failed"
|
||||
$script:runState.stage = "failed"
|
||||
$script:runState.finished_at = (Get-Date).ToString("o")
|
||||
$script:runState.latest_message = $failureMessage
|
||||
$script:runState.errors += $failureMessage
|
||||
Save-RunState
|
||||
Add-RalphEvent -RunId $runId -Stage "failed" -Status "failed" -Message $failureMessage
|
||||
Send-TelegramNotification `
|
||||
-EventName "run_failed" `
|
||||
-Title "Run failed" `
|
||||
-Message $failureMessage `
|
||||
-RunId $runId `
|
||||
-Stage "failed" `
|
||||
-Status "failed" | Out-Null
|
||||
try {
|
||||
$autoFollowupResult = Invoke-AutoFollowupTask -OutcomeStatus "failed" -OutcomeSummary $failureMessage
|
||||
}
|
||||
catch {
|
||||
$script:runState.errors += ("Auto-followup generation error: " + $_.Exception.Message)
|
||||
Save-RunState
|
||||
Add-RalphEvent -RunId $runId -Stage "auto_followup" -Status "failed" -Actor "codex_master" -Message $_.Exception.Message
|
||||
}
|
||||
$summaryLines += @(
|
||||
"",
|
||||
"## Failure",
|
||||
"",
|
||||
$failureMessage
|
||||
)
|
||||
if ($null -ne $autoFollowupResult) {
|
||||
$summaryLines += @(
|
||||
"",
|
||||
"## Auto Followup",
|
||||
"",
|
||||
"- Title: $($autoFollowupResult.title)",
|
||||
"- Task directory: $($autoFollowupResult.task_directory)",
|
||||
"- Source markdown: $($autoFollowupResult.source_markdown)"
|
||||
)
|
||||
}
|
||||
Write-Utf8File -Path (Join-Path $runDir "SUMMARY.md") -Content (($summaryLines -join "`n") + "`n")
|
||||
throw
|
||||
}
|
||||
68
ralph/scripts/Start-RalphBackground.ps1
Normal file
68
ralph/scripts/Start-RalphBackground.ps1
Normal file
@@ -0,0 +1,68 @@
|
||||
param(
|
||||
[string]$TaskDirectory = "",
|
||||
[string]$RunLabel = "background",
|
||||
[string]$Implementer = "",
|
||||
[string[]]$Reviewers = @(),
|
||||
[switch]$DisableCodexMaster,
|
||||
[switch]$DisableAutoFix
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$stdoutLog = Join-Path $ralphRoot ("logs\ralph-" + $timestamp + ".out.log")
|
||||
$stderrLog = Join-Path $ralphRoot ("logs\ralph-" + $timestamp + ".err.log")
|
||||
$stateFile = Join-Path $ralphRoot "state\last_background_run.json"
|
||||
|
||||
$autopilotScript = Join-Path $PSScriptRoot "Start-RalphAutopilot.ps1"
|
||||
$argumentParts = @(
|
||||
"-ExecutionPolicy Bypass",
|
||||
('-File "{0}"' -f $autopilotScript),
|
||||
('-RunLabel "{0}"' -f $RunLabel)
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($TaskDirectory)) {
|
||||
$argumentParts += ('-TaskDirectory "{0}"' -f $TaskDirectory)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($Implementer)) {
|
||||
$argumentParts += ('-Implementer "{0}"' -f $Implementer)
|
||||
}
|
||||
foreach ($reviewer in $Reviewers) {
|
||||
$argumentParts += ('-Reviewers "{0}"' -f $reviewer)
|
||||
}
|
||||
if ($DisableCodexMaster) {
|
||||
$argumentParts += '-UseCodexMaster:$false'
|
||||
}
|
||||
if ($DisableAutoFix) {
|
||||
$argumentParts += '-AutoFix:$false'
|
||||
}
|
||||
|
||||
$argumentList = $argumentParts -join ' '
|
||||
|
||||
$process = Start-Process `
|
||||
-FilePath "powershell.exe" `
|
||||
-ArgumentList $argumentList `
|
||||
-WorkingDirectory (Get-RepoRoot) `
|
||||
-WindowStyle Hidden `
|
||||
-RedirectStandardOutput $stdoutLog `
|
||||
-RedirectStandardError $stderrLog `
|
||||
-PassThru
|
||||
|
||||
$state = [ordered]@{
|
||||
pid = $process.Id
|
||||
started_at = (Get-Date).ToString("o")
|
||||
stdout_log = $stdoutLog
|
||||
stderr_log = $stderrLog
|
||||
run_label = $RunLabel
|
||||
status = "started"
|
||||
}
|
||||
|
||||
Write-JsonFile -Path $stateFile -Object $state
|
||||
Add-RalphEvent -RunId ("background-" + $timestamp) -Stage "background" -Status "started" -Actor "launcher" -Message "Background Ralph process launched" -Data @{
|
||||
pid = $process.Id
|
||||
run_label = $RunLabel
|
||||
stdout_log = $stdoutLog
|
||||
stderr_log = $stderrLog
|
||||
}
|
||||
Get-Content -Raw -Path $stateFile
|
||||
13
ralph/scripts/Start-RalphDashboard.ps1
Normal file
13
ralph/scripts/Start-RalphDashboard.ps1
Normal file
@@ -0,0 +1,13 @@
|
||||
param(
|
||||
[string]$Host = "127.0.0.1",
|
||||
[int]$Port = 8765
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$dashboardScript = Join-Path (Get-RalphRoot) "gui\app.py"
|
||||
if (-not (Test-Path $dashboardScript)) {
|
||||
throw "Dashboard script not found: $dashboardScript"
|
||||
}
|
||||
|
||||
& python $dashboardScript --host $Host --port $Port
|
||||
69
ralph/scripts/Start-RalphInboxBackground.ps1
Normal file
69
ralph/scripts/Start-RalphInboxBackground.ps1
Normal file
@@ -0,0 +1,69 @@
|
||||
param(
|
||||
[int]$PollSeconds = 15,
|
||||
[string]$Implementer = "",
|
||||
[string[]]$Reviewers = @(),
|
||||
[switch]$DisableCodexMaster,
|
||||
[switch]$DisableAutoFix,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$ralphRoot = Get-RalphRoot
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$stdoutLog = Join-Path $ralphRoot ("logs\ralph-inbox-" + $timestamp + ".out.log")
|
||||
$stderrLog = Join-Path $ralphRoot ("logs\ralph-inbox-" + $timestamp + ".err.log")
|
||||
$stateFile = Join-Path $ralphRoot "state\last_inbox_background.json"
|
||||
$daemonScript = Join-Path $PSScriptRoot "Start-RalphInboxDaemon.ps1"
|
||||
|
||||
$argumentParts = @(
|
||||
"-ExecutionPolicy Bypass",
|
||||
('-File "{0}"' -f $daemonScript),
|
||||
('-PollSeconds {0}' -f $PollSeconds)
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($Implementer)) {
|
||||
$argumentParts += ('-Implementer "{0}"' -f $Implementer)
|
||||
}
|
||||
foreach ($reviewer in $Reviewers) {
|
||||
$argumentParts += ('-Reviewers "{0}"' -f $reviewer)
|
||||
}
|
||||
if ($DisableCodexMaster) {
|
||||
$argumentParts += '-DisableCodexMaster'
|
||||
}
|
||||
if ($DisableAutoFix) {
|
||||
$argumentParts += '-DisableAutoFix'
|
||||
}
|
||||
if ($DryRun) {
|
||||
$argumentParts += '-DryRun'
|
||||
}
|
||||
|
||||
$argumentList = $argumentParts -join ' '
|
||||
|
||||
$process = Start-Process `
|
||||
-FilePath "powershell.exe" `
|
||||
-ArgumentList $argumentList `
|
||||
-WorkingDirectory (Get-RepoRoot) `
|
||||
-WindowStyle Hidden `
|
||||
-RedirectStandardOutput $stdoutLog `
|
||||
-RedirectStandardError $stderrLog `
|
||||
-PassThru
|
||||
|
||||
$state = [ordered]@{
|
||||
pid = $process.Id
|
||||
started_at = (Get-Date).ToString("o")
|
||||
stdout_log = $stdoutLog
|
||||
stderr_log = $stderrLog
|
||||
poll_seconds = $PollSeconds
|
||||
dry_run = [bool]$DryRun
|
||||
status = "started"
|
||||
}
|
||||
|
||||
Write-JsonFile -Path $stateFile -Object $state
|
||||
Add-RalphEvent -RunId ("inbox-background-" + $timestamp) -Stage "background" -Status "started" -Actor "launcher" -Message "Background Ralph inbox daemon launched" -Data @{
|
||||
pid = $process.Id
|
||||
stdout_log = $stdoutLog
|
||||
stderr_log = $stderrLog
|
||||
}
|
||||
|
||||
Get-Content -Raw -Path $stateFile
|
||||
292
ralph/scripts/Start-RalphInboxDaemon.ps1
Normal file
292
ralph/scripts/Start-RalphInboxDaemon.ps1
Normal file
@@ -0,0 +1,292 @@
|
||||
param(
|
||||
[int]$PollSeconds = 15,
|
||||
[string]$Implementer = "",
|
||||
[string[]]$Reviewers = @(),
|
||||
[switch]$DisableCodexMaster,
|
||||
[switch]$DisableAutoFix,
|
||||
[switch]$DryRun,
|
||||
[switch]$Once,
|
||||
[int]$MaxTasks = 0,
|
||||
[string]$InboxDirectory = ""
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$roots = Get-RalphTaskRoots
|
||||
foreach ($path in $roots.Values) {
|
||||
Ensure-Directory -Path $path
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($InboxDirectory)) {
|
||||
$InboxDirectory = $roots.Inbox
|
||||
}
|
||||
Ensure-Directory -Path $InboxDirectory
|
||||
|
||||
$lockFile = Get-RalphStateFile -Name "inbox_daemon.lock.json"
|
||||
$daemonStateFile = Get-RalphStateFile -Name "inbox_daemon_state.json"
|
||||
$script:daemonRunId = "daemon-" + (Get-Date -Format "yyyyMMdd-HHmmss")
|
||||
$script:processedCount = 0
|
||||
|
||||
function Save-DaemonState {
|
||||
param(
|
||||
[string]$Status,
|
||||
[string]$Message,
|
||||
[hashtable]$Extra = @{}
|
||||
)
|
||||
|
||||
$state = [ordered]@{
|
||||
pid = $PID
|
||||
status = $Status
|
||||
message = $Message
|
||||
updated_at = (Get-Date).ToString("o")
|
||||
poll_seconds = $PollSeconds
|
||||
dry_run = [bool]$DryRun
|
||||
inbox_directory = $InboxDirectory
|
||||
processed_count = $script:processedCount
|
||||
}
|
||||
foreach ($key in $Extra.Keys) {
|
||||
$state[$key] = $Extra[$key]
|
||||
}
|
||||
Write-JsonFile -Path $daemonStateFile -Object $state
|
||||
}
|
||||
|
||||
function Acquire-DaemonLock {
|
||||
if (Test-Path $lockFile) {
|
||||
try {
|
||||
$existing = Read-JsonFile -Path $lockFile
|
||||
$existingPid = 0
|
||||
try { $existingPid = [int]$existing.pid } catch { $existingPid = 0 }
|
||||
if ($existingPid -gt 0) {
|
||||
$proc = Get-Process -Id $existingPid -ErrorAction SilentlyContinue
|
||||
if ($null -ne $proc) {
|
||||
throw "Ralph inbox daemon already running with PID $existingPid."
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# stale or malformed lock; overwrite it
|
||||
}
|
||||
}
|
||||
|
||||
Write-JsonFile -Path $lockFile -Object ([ordered]@{
|
||||
pid = $PID
|
||||
started_at = (Get-Date).ToString("o")
|
||||
inbox_directory = $InboxDirectory
|
||||
})
|
||||
}
|
||||
|
||||
function Release-DaemonLock {
|
||||
if (Test-Path $lockFile) {
|
||||
Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NextInboxItem {
|
||||
$directories = @(Get-ChildItem -LiteralPath $InboxDirectory -Directory -ErrorAction SilentlyContinue | Sort-Object LastWriteTime, Name)
|
||||
foreach ($dir in $directories) {
|
||||
if (Test-Path (Join-Path $dir.FullName "TASK.md")) {
|
||||
return [ordered]@{
|
||||
kind = "taskpack"
|
||||
path = $dir.FullName
|
||||
name = $dir.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $InboxDirectory -File -Filter *.md -ErrorAction SilentlyContinue | Sort-Object LastWriteTime, Name)
|
||||
foreach ($file in $files) {
|
||||
return [ordered]@{
|
||||
kind = "markdown"
|
||||
path = $file.FullName
|
||||
name = $file.Name
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Move-InboxItemToProcessing {
|
||||
param([hashtable]$Item)
|
||||
|
||||
$id = "{0}-{1}" -f (Get-Date -Format "yyyyMMdd-HHmmss"), (Convert-ToRalphSlug -Text ([System.IO.Path]::GetFileNameWithoutExtension($Item.name)))
|
||||
$targetDir = Join-Path $roots.Processing $id
|
||||
Ensure-Directory -Path $targetDir
|
||||
|
||||
if ($Item.kind -eq "taskpack") {
|
||||
$targetParent = Split-Path -Parent $targetDir
|
||||
if (-not (Test-Path $targetParent)) {
|
||||
Ensure-Directory -Path $targetParent
|
||||
}
|
||||
Remove-Item -LiteralPath $targetDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Move-Item -LiteralPath $Item.path -Destination $targetDir
|
||||
return [ordered]@{
|
||||
id = $id
|
||||
task_directory = $targetDir
|
||||
title = $Item.name
|
||||
source = $Item.path
|
||||
}
|
||||
}
|
||||
|
||||
$taskInfo = New-RalphTaskPackFromMarkdown -MarkdownPath $Item.path -TaskId $id -TargetDirectory $targetDir
|
||||
Remove-Item -LiteralPath $Item.path -Force
|
||||
return [ordered]@{
|
||||
id = $taskInfo.id
|
||||
task_directory = $taskInfo.task_directory
|
||||
title = $taskInfo.title
|
||||
source = $Item.path
|
||||
}
|
||||
}
|
||||
|
||||
function Finalize-ProcessedItem {
|
||||
param(
|
||||
[hashtable]$Processed,
|
||||
[string]$DestinationRoot,
|
||||
[string]$State,
|
||||
[string]$RunId = "",
|
||||
[string]$Summary = ""
|
||||
)
|
||||
|
||||
Ensure-Directory -Path $DestinationRoot
|
||||
$destination = Join-Path $DestinationRoot ([System.IO.Path]::GetFileName($Processed.task_directory))
|
||||
if (Test-Path $destination) {
|
||||
Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Move-Item -LiteralPath $Processed.task_directory -Destination $destination
|
||||
|
||||
$submissionFile = Join-Path $destination "submission.json"
|
||||
$submission = [ordered]@{
|
||||
id = $Processed.id
|
||||
title = $Processed.title
|
||||
state = $State
|
||||
finished_at = (Get-Date).ToString("o")
|
||||
source = $Processed.source
|
||||
run_id = $RunId
|
||||
summary = $Summary
|
||||
}
|
||||
Write-JsonFile -Path $submissionFile -Object $submission
|
||||
return $destination
|
||||
}
|
||||
|
||||
Acquire-DaemonLock
|
||||
Save-DaemonState -Status "running" -Message "Inbox daemon started"
|
||||
Add-RalphEvent -RunId $script:daemonRunId -Stage "daemon" -Status "running" -Actor "daemon" -Message "Ralph inbox daemon started" -Data @{
|
||||
inbox_directory = $InboxDirectory
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "daemon_started" `
|
||||
-Title "Ralph daemon started" `
|
||||
-Message ("Inbox: " + $InboxDirectory) `
|
||||
-RunId $script:daemonRunId `
|
||||
-Stage "daemon" `
|
||||
-Status "running" | Out-Null
|
||||
|
||||
try {
|
||||
while ($true) {
|
||||
$item = Get-NextInboxItem
|
||||
if ($null -eq $item) {
|
||||
if ($Once) {
|
||||
Save-DaemonState -Status "idle" -Message "No tasks in inbox; exiting because -Once was used"
|
||||
break
|
||||
}
|
||||
Save-DaemonState -Status "idle" -Message "Waiting for inbox tasks"
|
||||
Start-Sleep -Seconds $PollSeconds
|
||||
continue
|
||||
}
|
||||
|
||||
$processed = Move-InboxItemToProcessing -Item $item
|
||||
$script:processedCount += 1
|
||||
Save-DaemonState -Status "running" -Message ("Processing task " + $processed.id) -Extra @{
|
||||
current_task = $processed.id
|
||||
current_task_directory = $processed.task_directory
|
||||
}
|
||||
Add-RalphEvent -RunId $script:daemonRunId -Stage "queue" -Status "started" -Actor "daemon" -Message ("Dequeued task " + $processed.id) -Data @{
|
||||
task_directory = $processed.task_directory
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "task_processing" `
|
||||
-Title "Task processing" `
|
||||
-Message ($processed.title + "`n" + $processed.task_directory) `
|
||||
-RunId $processed.id `
|
||||
-Stage "queue" `
|
||||
-Status "started" | Out-Null
|
||||
|
||||
$runId = ""
|
||||
$summary = ""
|
||||
try {
|
||||
$args = @(
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Join-Path $PSScriptRoot "Start-RalphAutopilot.ps1"),
|
||||
"-TaskDirectory", $processed.task_directory,
|
||||
"-RunLabel", "queue"
|
||||
)
|
||||
if ($DryRun) {
|
||||
$args += "-DryRun"
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($Implementer)) {
|
||||
$args += @("-Implementer", $Implementer)
|
||||
}
|
||||
foreach ($reviewer in $Reviewers) {
|
||||
$args += @("-Reviewers", $reviewer)
|
||||
}
|
||||
if ($DisableCodexMaster) {
|
||||
$args += "-UseCodexMaster:$false"
|
||||
}
|
||||
if ($DisableAutoFix) {
|
||||
$args += "-AutoFix:$false"
|
||||
}
|
||||
|
||||
& powershell.exe @args
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Start-RalphAutopilot.ps1 failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$currentRunState = Read-JsonFile -Path (Get-RalphStateFile -Name "current_run.json")
|
||||
$runId = [string]$currentRunState.run_id
|
||||
$summary = [string]$currentRunState.latest_message
|
||||
$finalPath = Finalize-ProcessedItem -Processed $processed -DestinationRoot $roots.Completed -State "completed" -RunId $runId -Summary $summary
|
||||
Add-RalphEvent -RunId $script:daemonRunId -Stage "queue" -Status "completed" -Actor "daemon" -Message ("Completed task " + $processed.id) -Data @{
|
||||
task_directory = $finalPath
|
||||
run_id = $runId
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "task_completed" `
|
||||
-Title "Task completed" `
|
||||
-Message ($processed.title + "`n" + $summary) `
|
||||
-RunId $runId `
|
||||
-Stage "queue" `
|
||||
-Status "completed" | Out-Null
|
||||
}
|
||||
catch {
|
||||
$summary = $_.Exception.Message
|
||||
$failedPath = Finalize-ProcessedItem -Processed $processed -DestinationRoot $roots.Failed -State "failed" -RunId $runId -Summary $summary
|
||||
Add-RalphEvent -RunId $script:daemonRunId -Stage "queue" -Status "failed" -Actor "daemon" -Message ("Failed task " + $processed.id + ": " + $summary) -Data @{
|
||||
task_directory = $failedPath
|
||||
run_id = $runId
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "task_failed" `
|
||||
-Title "Task failed" `
|
||||
-Message ($processed.title + "`n" + $summary) `
|
||||
-RunId $runId `
|
||||
-Stage "queue" `
|
||||
-Status "failed" | Out-Null
|
||||
}
|
||||
|
||||
if ($MaxTasks -gt 0 -and $script:processedCount -ge $MaxTasks) {
|
||||
Save-DaemonState -Status "completed" -Message "MaxTasks limit reached"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Save-DaemonState -Status "stopped" -Message "Inbox daemon stopped"
|
||||
Add-RalphEvent -RunId $script:daemonRunId -Stage "daemon" -Status "stopped" -Actor "daemon" -Message "Ralph inbox daemon stopped"
|
||||
Send-TelegramNotification `
|
||||
-EventName "daemon_stopped" `
|
||||
-Title "Ralph daemon stopped" `
|
||||
-Message "Inbox daemon stopped" `
|
||||
-RunId $script:daemonRunId `
|
||||
-Stage "daemon" `
|
||||
-Status "stopped" | Out-Null
|
||||
Release-DaemonLock
|
||||
}
|
||||
54
ralph/scripts/Stop-RalphInboxDaemon.ps1
Normal file
54
ralph/scripts/Stop-RalphInboxDaemon.ps1
Normal file
@@ -0,0 +1,54 @@
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$lockFile = Get-RalphStateFile -Name "inbox_daemon.lock.json"
|
||||
$daemonStateFile = Get-RalphStateFile -Name "inbox_daemon_state.json"
|
||||
|
||||
if (-not (Test-Path $lockFile)) {
|
||||
@{
|
||||
stopped = $false
|
||||
message = "No inbox daemon lock file found."
|
||||
} | ConvertTo-Json -Depth 20
|
||||
return
|
||||
}
|
||||
|
||||
$lock = Read-JsonFile -Path $lockFile
|
||||
$daemonPid = 0
|
||||
try { $daemonPid = [int]$lock.pid } catch { $daemonPid = 0 }
|
||||
|
||||
if ($daemonPid -le 0) {
|
||||
Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue
|
||||
@{
|
||||
stopped = $false
|
||||
message = "Lock file was invalid and has been removed."
|
||||
} | ConvertTo-Json -Depth 20
|
||||
return
|
||||
}
|
||||
|
||||
$proc = Get-Process -Id $daemonPid -ErrorAction SilentlyContinue
|
||||
if ($null -ne $proc) {
|
||||
Stop-Process -Id $daemonPid -Force
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue
|
||||
Write-JsonFile -Path $daemonStateFile -Object ([ordered]@{
|
||||
pid = $daemonPid
|
||||
status = "stopped"
|
||||
message = "Inbox daemon stopped manually."
|
||||
updated_at = (Get-Date).ToString("o")
|
||||
})
|
||||
|
||||
Add-RalphEvent -RunId ("inbox-daemon-stop-" + (Get-Date -Format "yyyyMMdd-HHmmss")) -Stage "daemon" -Status "stopped" -Actor "stop" -Message "Inbox daemon stopped manually" -Data @{
|
||||
pid = $daemonPid
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "daemon_stopped" `
|
||||
-Title "Ralph daemon stopped manually" `
|
||||
-Message ("PID " + $daemonPid) `
|
||||
-RunId ("inbox-daemon-stop-" + (Get-Date -Format "yyyyMMdd-HHmmss")) `
|
||||
-Stage "daemon" `
|
||||
-Status "stopped" | Out-Null
|
||||
|
||||
@{
|
||||
stopped = $true
|
||||
pid = $daemonPid
|
||||
} | ConvertTo-Json -Depth 20
|
||||
61
ralph/scripts/Submit-RalphTask.ps1
Normal file
61
ralph/scripts/Submit-RalphTask.ps1
Normal file
@@ -0,0 +1,61 @@
|
||||
param(
|
||||
[string]$SourceFile = "",
|
||||
[string]$Text = "",
|
||||
[string]$Title = "",
|
||||
[switch]$PassThru
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SourceFile) -and [string]::IsNullOrWhiteSpace($Text)) {
|
||||
throw "Provide either -SourceFile or -Text."
|
||||
}
|
||||
|
||||
$roots = Get-RalphTaskRoots
|
||||
foreach ($path in $roots.Values) {
|
||||
Ensure-Directory -Path $path
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($SourceFile)) {
|
||||
$taskInfo = New-RalphTaskPackFromMarkdown -MarkdownPath $SourceFile -Title $Title
|
||||
}
|
||||
else {
|
||||
$tempName = "{0}.md" -f ([guid]::NewGuid().ToString("N"))
|
||||
$tempPath = Join-Path ([System.IO.Path]::GetTempPath()) $tempName
|
||||
try {
|
||||
Write-Utf8File -Path $tempPath -Content ($Text.Trim() + "`n")
|
||||
$taskInfo = New-RalphTaskPackFromMarkdown -MarkdownPath $tempPath -Title $Title
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $tempPath) {
|
||||
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Add-RalphEvent -RunId $taskInfo.id -Stage "submit" -Status "queued" -Actor "submit" -Message ("Task queued: " + $taskInfo.title) -Data @{
|
||||
task_directory = $taskInfo.task_directory
|
||||
}
|
||||
Send-TelegramNotification `
|
||||
-EventName "task_queued" `
|
||||
-Title "Task queued" `
|
||||
-Message ($taskInfo.title + "`n" + $taskInfo.task_directory) `
|
||||
-RunId $taskInfo.id `
|
||||
-Stage "submit" `
|
||||
-Status "queued" | Out-Null
|
||||
|
||||
$result = [ordered]@{
|
||||
id = $taskInfo.id
|
||||
title = $taskInfo.title
|
||||
state = "queued"
|
||||
task_directory = $taskInfo.task_directory
|
||||
task_file = $taskInfo.task_file
|
||||
}
|
||||
|
||||
$json = ($result | ConvertTo-Json -Depth 20)
|
||||
if ($PassThru) {
|
||||
$result
|
||||
}
|
||||
else {
|
||||
$json
|
||||
}
|
||||
80
ralph/scripts/Test-RalphCodex.ps1
Normal file
80
ralph/scripts/Test-RalphCodex.ps1
Normal file
@@ -0,0 +1,80 @@
|
||||
param(
|
||||
[switch]$Full
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$codexConfig = Get-CodexConfig
|
||||
$preferredCmd = Join-Path $env:APPDATA "npm\codex.cmd"
|
||||
$preferredPs1 = Join-Path $env:APPDATA "npm\codex.ps1"
|
||||
$codexPath = $null
|
||||
if (Test-Path $preferredCmd) {
|
||||
$codexPath = $preferredCmd
|
||||
}
|
||||
elseif (Test-Path $preferredPs1) {
|
||||
$codexPath = $preferredPs1
|
||||
}
|
||||
else {
|
||||
$codexCommand = Get-Command codex -ErrorAction SilentlyContinue
|
||||
if ($null -ne $codexCommand) {
|
||||
$codexPath = $codexCommand.Source
|
||||
}
|
||||
}
|
||||
|
||||
$result = [ordered]@{
|
||||
ok = $false
|
||||
binary_found = $false
|
||||
binary_path = $codexPath
|
||||
session_id = [string]$codexConfig.session_id
|
||||
model = [string]$codexConfig.model
|
||||
help_ok = $false
|
||||
full_ok = $false
|
||||
output = ""
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($codexPath)) {
|
||||
$result.output = "codex executable not found"
|
||||
}
|
||||
else {
|
||||
$result.binary_found = $true
|
||||
$helpOutput = & $codexPath exec resume --help 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$result.help_ok = $true
|
||||
}
|
||||
$result.output = (@($helpOutput) -join [Environment]::NewLine)
|
||||
|
||||
if ($Full) {
|
||||
$tempPromptPath = Join-Path ([System.IO.Path]::GetTempPath()) ("ralph-codex-smoke-" + [guid]::NewGuid().ToString("N") + ".md")
|
||||
$tempOutput = Join-Path ([System.IO.Path]::GetTempPath()) ("ralph-codex-smoke-" + [guid]::NewGuid().ToString("N") + ".json")
|
||||
$tempPrompt = @'
|
||||
You are Codex.
|
||||
Return exactly one JSON object and nothing else.
|
||||
Use this exact payload:
|
||||
{"verdict":"pass","acceptance_passed":true,"fix_required":false,"summary":"smoke","highest_risk_issues":[],"next_sprint_needed":false,"next_sprint_brief":""}
|
||||
'@
|
||||
try {
|
||||
Write-Utf8File -Path $tempPromptPath -Content ($tempPrompt.Trim() + "`n")
|
||||
$verdict = & (Join-Path $PSScriptRoot "Invoke-CodexMaster.ps1") -PromptFile $tempPromptPath -OutputFile $tempOutput
|
||||
$result.output = ($result.output + [Environment]::NewLine + ($verdict | ConvertTo-Json -Depth 20)).Trim()
|
||||
if ($null -ne $verdict -and [string]$verdict.verdict -eq "pass" -and [bool]$verdict.acceptance_passed) {
|
||||
$result.full_ok = $true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$result.output = ($result.output + [Environment]::NewLine + $_.Exception.Message).Trim()
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $tempPromptPath) {
|
||||
Remove-Item -LiteralPath $tempPromptPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (Test-Path $tempOutput) {
|
||||
Remove-Item -LiteralPath $tempOutput -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result.ok = [bool]($result.binary_found -and $result.help_ok -and ((-not $Full) -or $result.full_ok))
|
||||
$outputPath = Join-Path (Get-RalphRoot) "state\codex_smoke.json"
|
||||
Write-Utf8File -Path $outputPath -Content ((@($result) | ConvertTo-Json -Depth 20) + "`n")
|
||||
Get-Content -Raw -Path $outputPath
|
||||
105
ralph/scripts/Test-RalphProviders.ps1
Normal file
105
ralph/scripts/Test-RalphProviders.ps1
Normal file
@@ -0,0 +1,105 @@
|
||||
param(
|
||||
[string[]]$ProviderNames = @()
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
function Get-ResponseText {
|
||||
param($Response)
|
||||
|
||||
if ($null -eq $Response) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if ($Response.content) {
|
||||
$textBlocks = @($Response.content | Where-Object { $_.type -eq "text" -and $_.text })
|
||||
if ($textBlocks.Count -gt 0) {
|
||||
return (($textBlocks | ForEach-Object { [string]$_.text }) -join " ").Trim()
|
||||
}
|
||||
|
||||
$fallbackBlocks = @($Response.content | Where-Object { $_.thinking -or $_.text })
|
||||
if ($fallbackBlocks.Count -gt 0) {
|
||||
$parts = foreach ($block in $fallbackBlocks) {
|
||||
if ($block.text) { [string]$block.text }
|
||||
elseif ($block.thinking) { [string]$block.thinking }
|
||||
}
|
||||
return (($parts | Where-Object { $_ }) -join " ").Trim()
|
||||
}
|
||||
}
|
||||
|
||||
return (($Response | ConvertTo-Json -Depth 20) -replace "\s+", " ").Trim()
|
||||
}
|
||||
|
||||
$config = Get-ProvidersConfig
|
||||
if ($ProviderNames.Count -eq 0) {
|
||||
$ProviderNames = @($config.providers.PSObject.Properties.Name)
|
||||
}
|
||||
else {
|
||||
$expanded = @()
|
||||
foreach ($entry in $ProviderNames) {
|
||||
if ($null -ne $entry) {
|
||||
$expanded += ([string]$entry).Split(",") | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
}
|
||||
}
|
||||
$ProviderNames = $expanded
|
||||
}
|
||||
|
||||
$results = @()
|
||||
|
||||
foreach ($name in $ProviderNames) {
|
||||
$provider = Get-ProviderConfig -Name $name
|
||||
$endpoint = ([string]$provider.base_url).TrimEnd("/") + "/v1/messages"
|
||||
|
||||
$headers = @{
|
||||
"x-api-key" = [string]$provider.auth_token
|
||||
"anthropic-version" = "2023-06-01"
|
||||
"content-type" = "application/json"
|
||||
}
|
||||
|
||||
$body = @{
|
||||
model = [string]$provider.model
|
||||
max_tokens = 16
|
||||
messages = @(
|
||||
@{
|
||||
role = "user"
|
||||
content = "Respond with exactly OK and nothing else."
|
||||
}
|
||||
)
|
||||
} | ConvertTo-Json -Depth 10
|
||||
|
||||
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
try {
|
||||
$response = Invoke-RestMethod -Method Post -Uri $endpoint -Headers $headers -Body $body -TimeoutSec 120
|
||||
$stopwatch.Stop()
|
||||
$text = Get-ResponseText -Response $response
|
||||
$semanticOk = ($text.Trim() -eq "OK")
|
||||
|
||||
$results += [ordered]@{
|
||||
provider = $name
|
||||
model = [string]$provider.model
|
||||
endpoint = $endpoint
|
||||
http_ok = $true
|
||||
semantic_ok = $semanticOk
|
||||
ok = $semanticOk
|
||||
latency_ms = $stopwatch.ElapsedMilliseconds
|
||||
response = $text.Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$stopwatch.Stop()
|
||||
$results += [ordered]@{
|
||||
provider = $name
|
||||
model = [string]$provider.model
|
||||
endpoint = $endpoint
|
||||
http_ok = $false
|
||||
semantic_ok = $false
|
||||
ok = $false
|
||||
latency_ms = $stopwatch.ElapsedMilliseconds
|
||||
response = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$outputPath = Join-Path (Get-RalphRoot) "state\provider_smoke.json"
|
||||
Write-Utf8File -Path $outputPath -Content ((@($results) | ConvertTo-Json -Depth 10) + "`n")
|
||||
Get-Content -Raw -Path $outputPath
|
||||
16
ralph/scripts/Test-RalphTelegram.ps1
Normal file
16
ralph/scripts/Test-RalphTelegram.ps1
Normal file
@@ -0,0 +1,16 @@
|
||||
param(
|
||||
[string]$Title = "Ralph Telegram test",
|
||||
[string]$Message = "Telegram notifications are enabled."
|
||||
)
|
||||
|
||||
. (Join-Path $PSScriptRoot "Common.ps1")
|
||||
|
||||
$result = Send-TelegramNotification `
|
||||
-EventName "run_started" `
|
||||
-Title $Title `
|
||||
-Message $Message `
|
||||
-RunId ("telegram-test-" + (Get-Date -Format "yyyyMMdd-HHmmss")) `
|
||||
-Stage "test" `
|
||||
-Status "manual"
|
||||
|
||||
($result | ConvertTo-Json -Depth 20)
|
||||
18
ralph/scripts/Uninstall-RalphScheduledTask.ps1
Normal file
18
ralph/scripts/Uninstall-RalphScheduledTask.ps1
Normal file
@@ -0,0 +1,18 @@
|
||||
param(
|
||||
[string]$TaskName = "RalphInboxDaemon"
|
||||
)
|
||||
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
||||
@{
|
||||
task_name = $TaskName
|
||||
removed = $true
|
||||
} | ConvertTo-Json -Depth 20
|
||||
}
|
||||
catch {
|
||||
@{
|
||||
task_name = $TaskName
|
||||
removed = $false
|
||||
error = $_.Exception.Message
|
||||
} | ConvertTo-Json -Depth 20
|
||||
}
|
||||
Reference in New Issue
Block a user