293 lines
10 KiB
PowerShell
293 lines
10 KiB
PowerShell
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
|
|
}
|