Files
openclaw-control-plane/sync/sync-agent.ps1
2026-05-15 16:32:46 +08:00

214 lines
5.2 KiB
PowerShell

param(
[int]$IntervalSeconds = 30,
[int]$HeartbeatEverySeconds = 60,
[switch]$Once
)
$ErrorActionPreference = "Stop"
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$LocalStateDir = Join-Path $RepoRoot ".sync-state"
$PublicStateDir = Join-Path $RepoRoot "sync-state"
$HeartbeatFile = Join-Path $PublicStateDir "heartbeat-pc.json"
$ErrorFile = Join-Path $PublicStateDir "error-pc.md"
$AllowedPaths = @(
"AGENT_BOARD.md",
"README.md",
"USER_STATUS.md",
".cursor/rules",
"handoff",
"evidence",
"rollback",
"tasks",
"sync",
"sync-state",
".gitattributes",
".gitignore"
)
function Ensure-StateDir {
foreach ($path in @($LocalStateDir, $PublicStateDir)) {
if (-not (Test-Path $path)) {
New-Item -ItemType Directory -Path $path | Out-Null
}
}
}
function Write-SyncError {
param([string]$Message)
Ensure-StateDir
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz"
@"
# PC Sync Error
- Time: $timestamp
- Repository: $RepoRoot
- Message: $Message
Manual action required. Do not continue automatic sync until the repository is clean.
"@ | Set-Content -Path $ErrorFile -Encoding UTF8
}
function Get-GitOutput {
param([string[]]$GitArgs)
$output = & git @GitArgs
if ($LASTEXITCODE -ne 0) {
throw "git $($GitArgs -join ' ') failed with exit code $LASTEXITCODE"
}
return $output
}
function Invoke-Git {
param([string[]]$GitArgs)
& git @GitArgs
if ($LASTEXITCODE -ne 0) {
throw "git $($GitArgs -join ' ') failed with exit code $LASTEXITCODE"
}
}
function Remove-StaleSyncError {
if (Test-Path $ErrorFile) {
Remove-Item $ErrorFile -Force
}
}
function Push-WithRetry {
& git push
if ($LASTEXITCODE -eq 0) {
return
}
# Another agent may have pushed between our pull and push. Rebase once and retry.
Invoke-Git @("pull", "--rebase", "--autostash")
if (Test-ConflictMarkers) {
throw "merge or rebase conflicts detected after push retry"
}
Invoke-Git @("push")
}
function Get-AllowedChanges {
$changes = & git -C $RepoRoot status --porcelain
if ($LASTEXITCODE -ne 0) {
throw "git status failed with exit code $LASTEXITCODE"
}
return $changes | Where-Object {
$line = $_
if ([string]::IsNullOrWhiteSpace($line)) {
return $false
}
$path = $line.Substring(3).Trim()
foreach ($allowed in $AllowedPaths) {
if ($path -eq $allowed -or $path.StartsWith("$allowed/") -or $path.StartsWith("$allowed\")) {
return $true
}
}
return $false
}
}
function Test-ConflictMarkers {
$conflicts = & git -C $RepoRoot diff --name-only --diff-filter=U
if ($LASTEXITCODE -ne 0) {
throw "git diff conflict check failed with exit code $LASTEXITCODE"
}
return -not [string]::IsNullOrWhiteSpace(($conflicts -join ""))
}
function Get-RepoCleanStatus {
$changes = & git -C $RepoRoot status --porcelain
if ($LASTEXITCODE -ne 0) {
throw "git status failed with exit code $LASTEXITCODE"
}
if ([string]::IsNullOrWhiteSpace(($changes -join ""))) {
return "clean"
}
return "dirty"
}
function Test-HeartbeatDue {
if ($Once -or -not (Test-Path $HeartbeatFile)) {
return $true
}
$age = (Get-Date) - (Get-Item $HeartbeatFile).LastWriteTime
return $age.TotalSeconds -ge $HeartbeatEverySeconds
}
function Write-Heartbeat {
Ensure-StateDir
if (-not (Test-HeartbeatDue)) {
return
}
$head = (Get-GitOutput @("-C", $RepoRoot, "rev-parse", "--short", "HEAD")) -join ""
$branch = (Get-GitOutput @("-C", $RepoRoot, "branch", "--show-current")) -join ""
$status = Get-RepoCleanStatus
$timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:sszzz"
[ordered]@{
host = "pc"
time = $timestamp
branch = $branch
head = $head
status = $status
intervalSeconds = $IntervalSeconds
heartbeatEverySeconds = $HeartbeatEverySeconds
} | ConvertTo-Json | Set-Content -Path $HeartbeatFile -Encoding UTF8
}
function Sync-Once {
Set-Location $RepoRoot
Ensure-StateDir
Invoke-Git @("pull", "--rebase", "--autostash")
if (Test-ConflictMarkers) {
throw "merge or rebase conflicts detected"
}
Remove-StaleSyncError
Write-Heartbeat
$allowedChanges = @(Get-AllowedChanges)
if ($allowedChanges.Count -eq 0) {
return
}
foreach ($path in $AllowedPaths) {
if (Test-Path (Join-Path $RepoRoot $path)) {
Invoke-Git @("add", "--", $path)
}
}
$staged = & git -C $RepoRoot diff --cached --name-only
if ($LASTEXITCODE -ne 0) {
throw "git diff --cached failed with exit code $LASTEXITCODE"
}
if ([string]::IsNullOrWhiteSpace(($staged -join ""))) {
return
}
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Invoke-Git @("commit", "-m", "Sync agent state from PC at $timestamp")
Push-WithRetry
}
do {
try {
Sync-Once
}
catch {
Write-SyncError $_.Exception.Message
Write-Error $_
break
}
if ($Once) {
break
}
Start-Sleep -Seconds $IntervalSeconds
} while ($true)