param( [int]$IntervalSeconds = 30, [switch]$Once ) $ErrorActionPreference = "Stop" $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $StateDir = Join-Path $RepoRoot ".sync-state" $ErrorFile = Join-Path $StateDir "sync-error-pc.md" $AllowedPaths = @( "AGENT_BOARD.md", "README.md", ".cursor/rules", "handoff", "evidence", "rollback", "tasks", "sync", ".gitignore" ) function Ensure-StateDir { if (-not (Test-Path $StateDir)) { New-Item -ItemType Directory -Path $StateDir | 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 Invoke-Git { param([string[]]$GitArgs) & git @GitArgs if ($LASTEXITCODE -ne 0) { throw "git $($GitArgs -join ' ') failed with exit code $LASTEXITCODE" } } 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 Sync-Once { Set-Location $RepoRoot Ensure-StateDir Invoke-Git @("pull", "--rebase", "--autostash") if (Test-ConflictMarkers) { throw "merge or rebase conflicts detected" } $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") Invoke-Git @("push") } do { try { Sync-Once if (Test-Path $ErrorFile) { Remove-Item $ErrorFile -Force } } catch { Write-SyncError $_.Exception.Message Write-Error $_ break } if ($Once) { break } Start-Sleep -Seconds $IntervalSeconds } while ($true)