Sync: Complete project state with all MEGA SPRINT V1-V3 features and Codex stubs

This commit is contained in:
renato97
2026-04-08 17:58:47 -03:00
parent c9d3528900
commit 6d080d43b3
372 changed files with 189715 additions and 8590 deletions

664
ralph/scripts/Common.ps1 Normal file
View 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")
}

View 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)

View 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)

View 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"
}

View 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"
}

View 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

View 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)

View 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
}

View 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

View 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

View 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

View 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
}

View 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

View 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
}

View 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

View 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

View 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)

View 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
}