param( [string]$SourceRoot = (Split-Path -Parent $MyInvocation.MyCommand.Path), [string]$AgentRoot = "C:\Wooo\Agent99", [string]$SourceRevision = "", [switch]$RegisterTasks, [switch]$ValidateOnly ) $ErrorActionPreference = "Stop" $stamp = Get-Date -Format "yyyyMMdd-HHmmss" $BinDir = Join-Path $AgentRoot "bin" $ConfigPath = Join-Path $AgentRoot "config\agent99.config.json" $EvidenceDir = Join-Path $AgentRoot "evidence" $StateDir = Join-Path $AgentRoot "state" $DeployRoot = Join-Path $AgentRoot "deploy" $StageDir = Join-Path $DeployRoot "staging-$stamp" $BackupDir = Join-Path $DeployRoot "backup-$stamp" $ManifestPath = Join-Path $StateDir "runtime-manifest.json" $EvidencePath = Join-Path $EvidenceDir "agent99-deploy-$stamp.json" $script:ConfigMigrations = @() $script:RuntimeCopyRetries = @() $runtimeFiles = @( "agent99-bootstrap.ps1", "agent99-contract-check.ps1", "agent99-control-plane.ps1", "agent99-deploy.ps1", "agent99-register-tasks.ps1", "agent99-sre-alert-inbox.ps1", "agent99-sre-alert-relay.ps1", "agent99-submit-request.ps1", "agent99-synthetic-tests.ps1", "agent99-telegram-inbox.ps1", "agent99-windows-update-policy.ps1", "host-reboot-scorecard.ps1", "agent99.config.example.json", "agent99.config.99.example.json" ) function Ensure-Utf8Bom { param([string]$Path) $bytes = [IO.File]::ReadAllBytes($Path) $text = [Text.Encoding]::UTF8.GetString($bytes) if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } [IO.File]::WriteAllText($Path, $text, (New-Object System.Text.UTF8Encoding($true))) } function Add-DefaultProperty { param([object]$Object, [string]$Name, [object]$Value) if (-not $Object.PSObject.Properties[$Name] -or $null -eq $Object.PSObject.Properties[$Name].Value) { if ($Object.PSObject.Properties[$Name]) { $Object.PSObject.Properties[$Name].Value = $Value } else { $Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value } } } function Set-AgentProperty { param([object]$Object, [string]$Name, [object]$Value) if ($Object.PSObject.Properties[$Name]) { $Object.PSObject.Properties[$Name].Value = $Value } else { $Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value } } function Get-AgentHost112CanonicalVm { param([string]$PolicyPath) if (-not (Test-Path -LiteralPath $PolicyPath -PathType Leaf)) { throw "Agent99 Host112 canonical policy config is unavailable: $PolicyPath" } $policy = Get-Content -LiteralPath $PolicyPath -Raw | ConvertFrom-Json $candidates = @($policy.vms | Where-Object { [string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali" }) if ($candidates.Count -ne 1) { throw "Agent99 Host112 canonical policy must contain exactly one Host112 VM row; found $($candidates.Count)." } $candidate = $candidates[0] $candidateVmx = ([string]$candidate.vmx).Trim() if ( [string]$candidate.name -ne "host112-kali" -or [string]$candidate.host -ne "192.168.0.112" -or -not $candidateVmx ) { throw "Agent99 Host112 canonical policy identity is invalid." } return ($candidate | ConvertTo-Json -Depth 10 | ConvertFrom-Json) } function Copy-AgentRuntimeFileWithRetry { param( [string]$Source, [string]$Destination, [int]$MaxAttempts = 20, [int]$DelayMilliseconds = 500 ) for ($attempt = 1; $attempt -le $MaxAttempts; $attempt += 1) { try { Copy-Item -Force $Source $Destination return } catch [System.IO.IOException] { if ($attempt -ge $MaxAttempts) { throw } $script:RuntimeCopyRetries += [pscustomobject]@{ file = Split-Path -Leaf $Destination attempt = $attempt reason = "file_in_use" } Start-Sleep -Milliseconds $DelayMilliseconds } } } function Write-DeployEvidence { param([bool]$Ok, [string]$Status, [string]$ErrorText, [object[]]$Files, [object]$TaskResult) New-Item -ItemType Directory -Force -Path $EvidenceDir | Out-Null [pscustomobject]@{ schemaVersion = "agent99_deploy_receipt_v2" timestamp = (Get-Date -Format o) ok = $Ok status = $Status sourceRoot = $SourceRoot sourceRevision = $SourceRevision agentRoot = $AgentRoot stageDir = $StageDir backupDir = $BackupDir runtimeManifest = $ManifestPath validateOnly = [bool]$ValidateOnly registerTasks = [bool]$RegisterTasks error = $ErrorText configMigrations = @($script:ConfigMigrations) runtimeCopyRetryCount = @($script:RuntimeCopyRetries).Count runtimeCopyRetries = @($script:RuntimeCopyRetries) files = $Files taskRegistration = $TaskResult } | ConvertTo-Json -Depth 10 | Set-Content -Path $EvidencePath -Encoding UTF8 } function Restore-Agent99Deployment { param([object[]]$Rows, [bool]$ConfigBackedUp, [bool]$ManifestBackedUp) foreach ($item in $Rows) { $backupPath = Join-Path $BackupDir $item.name if ($item.existed -and (Test-Path $backupPath)) { Copy-AgentRuntimeFileWithRetry $backupPath $item.livePath } elseif (-not $item.existed -and (Test-Path $item.livePath)) { Remove-Item -Force $item.livePath } } if ($ConfigBackedUp) { Copy-Item -Force (Join-Path $BackupDir "agent99.config.json") $ConfigPath } if ($ManifestBackedUp) { Copy-Item -Force (Join-Path $BackupDir "runtime-manifest.json") $ManifestPath } elseif (Test-Path $ManifestPath) { Remove-Item -Force $ManifestPath } } if (-not $SourceRevision) { try { $revision = & git -C $SourceRoot rev-parse HEAD 2>$null if ($LASTEXITCODE -eq 0 -and $revision) { $SourceRevision = ([string]$revision).Trim() } } catch { $SourceRevision = "" } } if (-not $SourceRevision) { $SourceRevision = "unknown" } New-Item -ItemType Directory -Force -Path $StageDir, $EvidenceDir, $StateDir | Out-Null foreach ($name in $runtimeFiles) { $source = Join-Path $SourceRoot $name if (-not (Test-Path $source)) { throw "Required deployment file missing: $source" } $target = Join-Path $StageDir $name Copy-Item -Force $source $target if ($name.EndsWith(".ps1")) { Ensure-Utf8Bom $target } } & (Join-Path $StageDir "agent99-contract-check.ps1") -SourceRoot $StageDir | Out-Null if ($LASTEXITCODE -ne 0) { Write-DeployEvidence $false "preflight_failed" "contract_check_failed" @() $null throw "Agent99 staged contract check failed." } & (Join-Path $StageDir "agent99-synthetic-tests.ps1") -SourceRoot $StageDir -WorkRoot (Join-Path $StageDir "synthetic-work") | Out-Null if ($LASTEXITCODE -ne 0) { Write-DeployEvidence $false "preflight_failed" "synthetic_tests_failed" @() $null throw "Agent99 staged synthetic tests failed." } $stagedFiles = @($runtimeFiles | ForEach-Object { $path = Join-Path $StageDir $_ [pscustomobject]@{ name = $_; sha256 = (Get-FileHash -Algorithm SHA256 -Path $path).Hash.ToLowerInvariant() } }) $policyConfigPath = Join-Path $StageDir "agent99.config.99.example.json" $policyConfig = Get-Content -LiteralPath $policyConfigPath -Raw | ConvertFrom-Json $canonicalHost112Vm = Get-AgentHost112CanonicalVm $policyConfigPath $canonicalHost112Vmx = [string]$canonicalHost112Vm.vmx if ($ValidateOnly) { Write-DeployEvidence $true "validated" $null $stagedFiles $null [pscustomobject]@{ ok = $true; status = "validated"; sourceRevision = $SourceRevision; evidence = $EvidencePath; files = $stagedFiles } | ConvertTo-Json -Depth 8 exit 0 } if (-not (Test-Path -LiteralPath $canonicalHost112Vmx -PathType Leaf)) { throw "Host112 canonical VMX path from staged policy is unavailable: $canonicalHost112Vmx" } New-Item -ItemType Directory -Force -Path $BinDir, $BackupDir | Out-Null $promotionRows = @() $configBackedUp = $false $manifestBackedUp = $false try { if (-not (Test-Path $ConfigPath)) { throw "Live Agent99 config not found: $ConfigPath" } Copy-Item -Force $ConfigPath (Join-Path $BackupDir "agent99.config.json") $configBackedUp = $true if (Test-Path $ManifestPath) { Copy-Item -Force $ManifestPath (Join-Path $BackupDir "runtime-manifest.json") $manifestBackedUp = $true } foreach ($name in $runtimeFiles) { $livePath = Join-Path $BinDir $name $existed = Test-Path $livePath if ($existed) { Copy-Item -Force $livePath (Join-Path $BackupDir $name) } $promotionRows += [pscustomobject]@{ name = $name; livePath = $livePath; existed = [bool]$existed } Copy-AgentRuntimeFileWithRetry (Join-Path $StageDir $name) $livePath } $config = Get-Content $ConfigPath -Raw | ConvertFrom-Json Add-DefaultProperty $config "sshIdentityFile" (Join-Path $AgentRoot "keys\agent99_ed25519") Add-DefaultProperty $config "sshUsers" ([pscustomobject]@{}) $previousHost111User = if ($config.sshUsers.PSObject.Properties["192.168.0.111"]) { [string]$config.sshUsers.PSObject.Properties["192.168.0.111"].Value } else { "" } if ($previousHost111User -ne "ooo") { Set-AgentProperty $config.sshUsers "192.168.0.111" "ooo" $script:ConfigMigrations += [pscustomobject]@{ name = "host111_canonical_ssh_user" from = if ($previousHost111User) { $previousHost111User } else { "missing" } to = "ooo" } } $previousHost112User = if ($config.sshUsers.PSObject.Properties["192.168.0.112"]) { [string]$config.sshUsers.PSObject.Properties["192.168.0.112"].Value } else { "" } if ($previousHost112User -ne "kali") { Set-AgentProperty $config.sshUsers "192.168.0.112" "kali" $script:ConfigMigrations += [pscustomobject]@{ name = "host112_canonical_ssh_user" from = if ($previousHost112User) { $previousHost112User } else { "missing" } to = "kali" } } Add-DefaultProperty $config "sshTransport" ([pscustomobject]@{}) Add-DefaultProperty $config.sshTransport "serialized" $true Add-DefaultProperty $config.sshTransport "lockTimeoutSeconds" 90 Add-DefaultProperty $config.sshTransport "lockPollMilliseconds" 200 Add-DefaultProperty $config "sshProcessGuard" ([pscustomobject]@{}) Add-DefaultProperty $config.sshProcessGuard "enabled" $true Add-DefaultProperty $config.sshProcessGuard "staleMinutes" 15 Add-DefaultProperty $config.sshProcessGuard "hardStaleMinutes" 60 Add-DefaultProperty $config "hosts" @("192.168.0.110", "192.168.0.111", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188") $configuredHosts = @($config.hosts | ForEach-Object { [string]$_ } | Where-Object { $_ }) if ("192.168.0.111" -notin $configuredHosts) { Set-AgentProperty $config "hosts" @($configuredHosts + "192.168.0.111" | Select-Object -Unique) $script:ConfigMigrations += [pscustomobject]@{ name = "host111_canonical_recovery_target" from = ($configuredHosts -join ",") to = ((@($configuredHosts + "192.168.0.111") | Select-Object -Unique) -join ",") } } Add-DefaultProperty $config "externalHosts" @() $externalHosts = @($config.externalHosts) $host111External = $externalHosts | Where-Object { [string]$_.host -eq "192.168.0.111" } | Select-Object -First 1 if (-not $host111External) { $host111External = [pscustomobject]@{ name = "host111-workstation" host = "192.168.0.111" recoveryAction = "wake_on_lan" macAddresses = @("00:E0:4C:AF:10:0A", "DA:64:76:98:F1:0E") broadcastAddresses = @("255.255.255.255", "192.168.0.255") ports = @(7, 9) packetBursts = 5 verifyTimeoutSeconds = 180 verifyIntervalSeconds = 5 verifyTcpPorts = @(22) enabled = $true required = $true } Set-AgentProperty $config "externalHosts" @($externalHosts + $host111External) $script:ConfigMigrations += [pscustomobject]@{ name = "host111_wol_executor" from = "missing" to = "wake_on_lan" } } else { Add-DefaultProperty $host111External "name" "host111-workstation" Add-DefaultProperty $host111External "recoveryAction" "wake_on_lan" Add-DefaultProperty $host111External "macAddresses" @("00:E0:4C:AF:10:0A", "DA:64:76:98:F1:0E") Add-DefaultProperty $host111External "broadcastAddresses" @("255.255.255.255", "192.168.0.255") Add-DefaultProperty $host111External "ports" @(7, 9) Add-DefaultProperty $host111External "packetBursts" 5 Add-DefaultProperty $host111External "verifyTimeoutSeconds" 180 Add-DefaultProperty $host111External "verifyIntervalSeconds" 5 Add-DefaultProperty $host111External "verifyTcpPorts" @(22) Add-DefaultProperty $host111External "enabled" $true Add-DefaultProperty $host111External "required" $true } Add-DefaultProperty $config "k3s" ([pscustomobject]@{}) Add-DefaultProperty $config.k3s "jumpHost" "192.168.0.110" Add-DefaultProperty $config.k3s "preferJumpHost" $true Add-DefaultProperty $config.k3s "directHosts" @("192.168.0.110", "192.168.0.112") $directHosts = @($config.k3s.directHosts | ForEach-Object { [string]$_ } | Where-Object { $_ }) if ("192.168.0.112" -notin $directHosts) { Set-AgentProperty $config.k3s "directHosts" @($directHosts + "192.168.0.112" | Select-Object -Unique) $script:ConfigMigrations += [pscustomobject]@{ name = "host112_canonical_direct_route" from = ($directHosts -join ",") to = ((@($directHosts + "192.168.0.112") | Select-Object -Unique) -join ",") } } Add-DefaultProperty $config "vms" @() $host112Vm = @($config.vms | Where-Object { [string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali" }) $nonHost112Vms = @($config.vms | Where-Object { -not ([string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali") }) $canonicalHost112Json = $canonicalHost112Vm | ConvertTo-Json -Depth 10 -Compress $currentHost112Json = if ($host112Vm.Count -eq 1) { $host112Vm[0] | ConvertTo-Json -Depth 10 -Compress } else { "" } $host112MigrationRequired = [bool]( $host112Vm.Count -ne 1 -or $currentHost112Json -cne $canonicalHost112Json ) if ($host112MigrationRequired) { $previousHost112Identity = if ($host112Vm.Count -eq 0) { "missing" } else { (@($host112Vm | ForEach-Object { "$([string]$_.name)|$([string]$_.host)|$([string]$_.vmx)" }) -join ";") } $script:ConfigMigrations += [pscustomobject]@{ name = "host112_canonical_vmx_path" from = $previousHost112Identity to = $canonicalHost112Vmx } } Set-AgentProperty $config "vms" @(@($nonHost112Vms) + @($canonicalHost112Vm)) Add-DefaultProperty $config "autoRecovery" ([pscustomobject]@{}) Add-DefaultProperty $config.autoRecovery "enabled" $true Add-DefaultProperty $config.autoRecovery "triggerOnHostUnreachable" $true Add-DefaultProperty $config.autoRecovery "minimumUnreachableHosts" 1 Add-DefaultProperty $config.autoRecovery "cooldownMinutes" 10 Add-DefaultProperty $config "recoverySlo" ([pscustomobject]@{}) Add-DefaultProperty $config.recoverySlo "targetMinutes" 10 Add-DefaultProperty $config.recoverySlo "hostTimeoutSeconds" 90 Add-DefaultProperty $config.recoverySlo "harborTimeoutSeconds" 120 Add-DefaultProperty $config.recoverySlo "workloadTimeoutSeconds" 180 Add-DefaultProperty $config "recoveryCoordinator" ([pscustomobject]@{}) Add-DefaultProperty $config.recoveryCoordinator "enabled" $true Add-DefaultProperty $config.recoveryCoordinator "controllerHost" "192.168.0.110" Add-DefaultProperty $config.recoveryCoordinator "readbackTimeoutSeconds" 15 Add-DefaultProperty $config.recoveryCoordinator "pollIntervalSeconds" 10 Add-DefaultProperty $config.recoveryCoordinator "freshWaitTimeoutSeconds" 420 Add-DefaultProperty $config.recoveryCoordinator "maxArtifactAgeSeconds" 600 Add-DefaultProperty $config.recoveryCoordinator "artifactClockSkewSeconds" 90 Add-DefaultProperty $config.recoveryCoordinator "requiredHostAliases" @("99", "110", "111", "112", "120", "121", "188") Add-DefaultProperty $config "selfHealth" ([pscustomobject]@{}) Add-DefaultProperty $config.selfHealth "requiredHosts" @("192.168.0.110", "192.168.0.111", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188") $selfHealthHosts = @($config.selfHealth.requiredHosts | ForEach-Object { [string]$_ } | Where-Object { $_ }) if ("192.168.0.111" -notin $selfHealthHosts) { Set-AgentProperty $config.selfHealth "requiredHosts" @($selfHealthHosts + "192.168.0.111" | Select-Object -Unique) $script:ConfigMigrations += [pscustomobject]@{ name = "host111_self_health_coverage" from = ($selfHealthHosts -join ",") to = ((@($selfHealthHosts + "192.168.0.111") | Select-Object -Unique) -join ",") } } Add-DefaultProperty $config "completionCallback" ([pscustomobject]@{}) Add-DefaultProperty $config.completionCallback "enabled" $true Add-DefaultProperty $config.completionCallback "url" "https://awoooi.wooo.work/api/v1/agents/agent99/completion-callback" Add-DefaultProperty $config.completionCallback "tokenEnv" "AGENT99_SRE_RELAY_TOKEN" Add-DefaultProperty $config.completionCallback "timeoutSeconds" 15 Add-DefaultProperty $config.completionCallback "maxAttempts" 2 Add-DefaultProperty $config.completionCallback "batchLimit" 5 Add-DefaultProperty $config.completionCallback "retryAfterMinutes" 5 Add-DefaultProperty $config "telegram" ([pscustomobject]@{}) Add-DefaultProperty $config.telegram "repeatAfterMinutes" 120 Add-DefaultProperty $config "performance" ([pscustomobject]@{}) Add-DefaultProperty $config.performance "processCpuHostPercentWarning" 25 Add-DefaultProperty $config.performance "processCpuHostPercentCritical" 50 foreach ($sourceAction in @($policyConfig.performance.loadShedding.actions | Where-Object { $_.PSObject.Properties["policyVersion"] })) { $liveAction = $config.performance.loadShedding.actions | Where-Object { $_.name -eq $sourceAction.name } | Select-Object -First 1 if (-not $liveAction) { continue } $sourceVersion = [int]$sourceAction.policyVersion $liveVersion = if ($liveAction.PSObject.Properties["policyVersion"]) { [int]$liveAction.policyVersion } else { 0 } if ($sourceVersion -le $liveVersion) { continue } foreach ($propertyName in @("policyVersion", "host", "command", "timeoutSeconds", "postVerifyCommand", "postVerifyMaxDiskUsedPercent", "postVerifyOkRegex", "rollback", "cooldownMinutes", "failureBackoffMinutes", "minSeverity", "reasonMatches", "enabled")) { if ($sourceAction.PSObject.Properties[$propertyName]) { Set-AgentProperty $liveAction $propertyName $sourceAction.PSObject.Properties[$propertyName].Value } } $script:ConfigMigrations += [pscustomobject]@{ name = [string]$sourceAction.name fromPolicyVersion = $liveVersion toPolicyVersion = $sourceVersion } } $config | ConvertTo-Json -Depth 20 | Set-Content -Path $ConfigPath -Encoding UTF8 $persistedConfig = Get-Content $ConfigPath -Raw | ConvertFrom-Json $persistedHost111User = if ($persistedConfig.sshUsers -and $persistedConfig.sshUsers.PSObject.Properties["192.168.0.111"]) { [string]$persistedConfig.sshUsers.PSObject.Properties["192.168.0.111"].Value } else { "" } $persistedHost112User = if ($persistedConfig.sshUsers -and $persistedConfig.sshUsers.PSObject.Properties["192.168.0.112"]) { [string]$persistedConfig.sshUsers.PSObject.Properties["192.168.0.112"].Value } else { "" } $persistedDirectHosts = if ($persistedConfig.k3s -and $persistedConfig.k3s.PSObject.Properties["directHosts"]) { @($persistedConfig.k3s.directHosts | ForEach-Object { [string]$_ }) } else { @() } $persistedHost112Vm = @($persistedConfig.vms | Where-Object { [string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali" }) $persistedHost112Vmx = if ($persistedHost112Vm.Count -eq 1) { [string]$persistedHost112Vm[0].vmx } else { "" } $persistedHosts = @($persistedConfig.hosts | ForEach-Object { [string]$_ }) $persistedSelfHealthHosts = @($persistedConfig.selfHealth.requiredHosts | ForEach-Object { [string]$_ }) $persistedHost111External = @($persistedConfig.externalHosts | Where-Object { [string]$_.host -eq "192.168.0.111" }) $host111ExternalReady = [bool]( $persistedHost111External.Count -eq 1 -and [string]$persistedHost111External[0].recoveryAction -eq "wake_on_lan" -and [bool]$persistedHost111External[0].enabled -and [bool]$persistedHost111External[0].required -and @($persistedHost111External[0].macAddresses).Count -ge 1 -and 22 -in @($persistedHost111External[0].verifyTcpPorts | ForEach-Object { [int]$_ }) ) if ( $persistedHost111User -ne "ooo" -or $persistedHost112User -ne "kali" -or "192.168.0.112" -notin $persistedDirectHosts -or "192.168.0.111" -notin $persistedHosts -or "192.168.0.111" -notin $persistedSelfHealthHosts -or -not $host111ExternalReady -or $persistedHost112Vm.Count -ne 1 -or [string]$persistedHost112Vm[0].name -ne [string]$canonicalHost112Vm.name -or [string]$persistedHost112Vm[0].host -ne [string]$canonicalHost112Vm.host -or $persistedHost112Vmx -ne $canonicalHost112Vmx -or -not (Test-Path -LiteralPath $persistedHost112Vmx -PathType Leaf) ) { throw "Canonical Host111 SSH/recovery and Host112 direct/SSD config post-verifier failed." } $manifestRows = @() foreach ($staged in $stagedFiles) { $livePath = Join-Path $BinDir $staged.name $runtimeSha = if (Test-Path $livePath) { (Get-FileHash -Algorithm SHA256 -Path $livePath).Hash.ToLowerInvariant() } else { "" } $matched = [bool]($runtimeSha -and $runtimeSha -eq $staged.sha256) $manifestRows += [pscustomobject]@{ name = $staged.name sourceExists = $true runtimeExists = [bool](Test-Path $livePath) sourceSha256 = $staged.sha256 runtimeSha256 = $runtimeSha contentMatched = $matched matched = $matched } } $runtimeMatched = [bool](@($manifestRows | Where-Object { -not $_.matched }).Count -eq 0) [pscustomobject]@{ schemaVersion = "agent99_runtime_manifest_v1" generatedAt = (Get-Date -Format o) sourceRevision = $SourceRevision runtimeMatched = $runtimeMatched fileCount = $manifestRows.Count mismatchCount = @($manifestRows | Where-Object { -not $_.matched }).Count files = $manifestRows } | ConvertTo-Json -Depth 10 | Set-Content -Path $ManifestPath -Encoding UTF8 if (-not $runtimeMatched) { throw "Promoted Agent99 runtime does not match staged source." } & (Join-Path $BinDir "agent99-contract-check.ps1") -SourceRoot $BinDir | Out-Null if ($LASTEXITCODE -ne 0) { throw "Post-promotion Agent99 contract check failed." } & (Join-Path $BinDir "agent99-synthetic-tests.ps1") -SourceRoot $BinDir -WorkRoot (Join-Path $StageDir "live-synthetic-work") | Out-Null if ($LASTEXITCODE -ne 0) { throw "Post-promotion Agent99 synthetic tests failed." } } catch { Restore-Agent99Deployment $promotionRows $configBackedUp $manifestBackedUp Write-DeployEvidence $false "rolled_back" $_.Exception.Message $stagedFiles $null throw } $taskResult = $null if ($RegisterTasks) { try { & (Join-Path $BinDir "agent99-register-tasks.ps1") -AgentRoot $AgentRoot | Out-Null $taskResult = [pscustomobject]@{ ok = $true; status = "registered" } } catch { $taskResult = [pscustomobject]@{ ok = $false; status = "registration_failed"; error = $_.Exception.Message } } } $deployOk = [bool](-not $taskResult -or $taskResult.ok) $deployStatus = if ($deployOk) { "deployed" } else { "deployed_task_registration_failed" } Write-DeployEvidence $deployOk $deployStatus $(if ($taskResult -and -not $taskResult.ok) { $taskResult.error } else { $null }) $stagedFiles $taskResult [pscustomobject]@{ ok = $deployOk status = $deployStatus sourceRevision = $SourceRevision evidence = $EvidencePath backupDir = $BackupDir runtimeManifest = $ManifestPath taskRegistration = $taskResult files = $stagedFiles } | ConvertTo-Json -Depth 10 if (-not $deployOk) { exit 2 } exit 0