665 lines
19 KiB
PowerShell
665 lines
19 KiB
PowerShell
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")
|
|
}
|