# One tick of the Grok side of the agent loop (Windows, PowerShell 5.1+). # Picks at most one issue labelled agent/ + agent/, runs Grok on # the assigned codex/* branch, validates the result and hands over to Claude. # This file is ASCII only: Windows PowerShell 5.1 reads BOM-less scripts as # ANSI, so every Chinese string lives in grok-strings.json (read as UTF-8). $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2 $Bin = Split-Path -Parent $MyInvocation.MyCommand.Path $Cfg = Get-Content -Raw -Encoding UTF8 (Join-Path $Bin 'grok-config.json') | ConvertFrom-Json $S = [IO.File]::ReadAllText((Join-Path $Bin 'grok-strings.json'), [Text.Encoding]::UTF8) | ConvertFrom-Json $ConfDir = Join-Path $HOME '.config\jyotisha-agent' $StateDir = Join-Path $HOME '.jyotisha-agent\state' $Api = "$($Cfg.gitea_url)/api/v1/repos/$($Cfg.gitea_repo)" $Utf8 = New-Object Text.UTF8Encoding $false New-Item -ItemType Directory -Force (Join-Path $StateDir 'runs') | Out-Null $LogFile = Join-Path $StateDir 'grok-tick.log' function Log($m) { [IO.File]::AppendAllText($LogFile, ("{0} {1}`r`n" -f (Get-Date).ToUniversalTime().ToString('s'), $m), $Utf8) } if (Test-Path (Join-Path $ConfDir 'STOP')) { Log 'STOP file present, skipping'; exit 0 } $mutex = New-Object Threading.Mutex($false, 'Local\JyotishaAgentGrokTick') if (-not $mutex.WaitOne(0)) { Log 'another tick is running, skipping'; exit 0 } $Token = ([IO.File]::ReadAllText((Join-Path $ConfDir 'gitea-token'))).Trim() function Gitea([string]$Method, [string]$Path, $Body = $null) { $p = @{ Uri = "$Api$Path"; Method = $Method; UseBasicParsing = $true Headers = @{ Authorization = "token $Token" } } if ($null -ne $Body) { $p.Body = $Utf8.GetBytes(($Body | ConvertTo-Json -Depth 10 -Compress)) $p.ContentType = 'application/json; charset=utf-8' } $r = Invoke-WebRequest @p $text = $Utf8.GetString($r.RawContentStream.ToArray()) if ($text) { return $text | ConvertFrom-Json } else { return $null } } function Enc($s) { [uri]::EscapeDataString($s) } function Issues($labels) { @(Gitea GET "/issues?state=open&type=issues&limit=50&labels=$(Enc ($labels -join ','))") | Sort-Object created_at } function LabelNames($n) { @(Gitea GET "/issues/$n/labels" | ForEach-Object { $_.name }) } function LabelId($name) { (@(Gitea GET '/labels?limit=100') | Where-Object { $_.name -eq $name } | Select-Object -First 1).id } function AddLabel($n, $name) { Gitea POST "/issues/$n/labels" @{ labels = @($name) } | Out-Null } function RemoveLabel($n, $name) { if ((LabelNames $n) -contains $name) { Gitea DELETE "/issues/$n/labels/$(LabelId $name)" | Out-Null } } function SetState($n, $new) { foreach ($st in $S.states) { if ($st -ne $new) { RemoveLabel $n $st } } AddLabel $n $new } $Owner = (Invoke-RestMethod -UseBasicParsing -Uri "$($Cfg.gitea_url)/api/v1/user" -Headers @{ Authorization = "token $Token" }).login function OwnerComments($n) { @(Gitea GET "/issues/$n/comments" | Where-Object { $_.user.login -eq $Owner }) } function Markers($n) { @(OwnerComments $n | ForEach-Object { ($_.body -split "`n")[0] } | Where-Object { $_ -like '' ($S.msg_error -f $reason) $e = ErrorsSinceProgress $n if ($e -ge 3) { SetState $n $S.label_decision Post $n '' ($S.msg_escalate -f $Owner, $e, $reason, $LogFile) } Log "issue #${n}: error: $reason" } function PrivacyOk($text) { $pat = '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY|(api[_-]?key|secret|password|passwd|token)\s*[:=]\s*[^\s<]{8,}' return -not ($text -match $pat) } $RunId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') try { # 1. Stale lock: the mutex proves no other tick runs, so a lock label is a crash leftover. foreach ($i in Issues @($S.label_lock, $S.label_scope)) { RemoveLabel $i.number $S.label_lock FailRun $i.number $S.msg_stale } # 2. Pick the oldest todo issue. $todo = @(Issues @($S.label_todo, $S.label_scope) | Where-Object { @($_.labels | ForEach-Object { $_.name }) -notcontains $S.label_lock }) if ($todo.Count -eq 0) { Log 'nothing to do'; exit 0 } $n = $todo[0].number $plan = @(Markers $n | Where-Object { $_ -like '" $res.comment_markdown SetState $n $S.label_review } elseif ($res.next -eq $S.next_decision) { Post $n '' $res.comment_markdown SetState $n $S.label_decision } else { RemoveLabel $n $S.label_lock; FailRun $n ($S.msg_bad_next -f $res.next); exit 0 } RemoveLabel $n $S.label_lock Log "issue #${n}: done -> $($res.next)" } catch { Log "tick crashed: $_" throw } finally { $mutex.ReleaseMutex() }