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