feat: ship agent-first mission control v0.2.57
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s

This commit is contained in:
AzuTear
2026-07-31 22:39:47 +02:00
parent 3bc7622977
commit f5552218bc
535 changed files with 95242 additions and 8791 deletions
@@ -0,0 +1,149 @@
param(
[string]$AuditDate = '2026-07-28'
)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$auditRoot = Join-Path $repoRoot "docs\audits\$AuditDate\openclaw-core-integration"
$screenshotRoot = Join-Path $auditRoot 'screenshots'
function New-LabelledComparison {
param(
[Parameter(Mandatory)][string]$LeftPath,
[Parameter(Mandatory)][string]$LeftLabel,
[Parameter(Mandatory)][string]$RightPath,
[Parameter(Mandatory)][string]$RightLabel,
[Parameter(Mandatory)][string]$OutputPath
)
$left = [System.Drawing.Image]::FromFile($LeftPath)
$right = [System.Drawing.Image]::FromFile($RightPath)
$canvas = New-Object System.Drawing.Bitmap 1440, 490
$graphics = [System.Drawing.Graphics]::FromImage($canvas)
$font = New-Object System.Drawing.Font 'Segoe UI', 13, ([System.Drawing.FontStyle]::Bold)
$labelBrush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(232, 234, 240))
$backgroundBrush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(8, 7, 24))
try {
$graphics.FillRectangle($backgroundBrush, 0, 0, $canvas.Width, $canvas.Height)
$graphics.DrawString($LeftLabel, $font, $labelBrush, 12, 10)
$graphics.DrawString($RightLabel, $font, $labelBrush, 732, 10)
$graphics.DrawImage($left, 0, 40, 720, 450)
$graphics.DrawImage($right, 720, 40, 720, 450)
$canvas.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$backgroundBrush.Dispose()
$labelBrush.Dispose()
$font.Dispose()
$graphics.Dispose()
$canvas.Dispose()
$right.Dispose()
$left.Dispose()
}
}
function New-ContactSheet {
param(
[Parameter(Mandatory)][array]$Items,
[Parameter(Mandatory)][int]$Columns,
[Parameter(Mandatory)][int]$CellWidth,
[Parameter(Mandatory)][int]$CellHeight,
[Parameter(Mandatory)][string]$OutputPath
)
$labelHeight = 28
$rows = [Math]::Ceiling($Items.Count / $Columns)
$canvas = New-Object System.Drawing.Bitmap ($Columns * $CellWidth), ($rows * ($CellHeight + $labelHeight))
$graphics = [System.Drawing.Graphics]::FromImage($canvas)
$font = New-Object System.Drawing.Font 'Segoe UI', 10, ([System.Drawing.FontStyle]::Bold)
$labelBrush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(224, 226, 238))
$backgroundBrush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(8, 7, 24))
try {
$graphics.FillRectangle($backgroundBrush, 0, 0, $canvas.Width, $canvas.Height)
for ($index = 0; $index -lt $Items.Count; $index++) {
$item = $Items[$index]
$column = $index % $Columns
$row = [Math]::Floor($index / $Columns)
$x = $column * $CellWidth
$y = $row * ($CellHeight + $labelHeight)
$image = [System.Drawing.Image]::FromFile($item.Path)
try {
$graphics.DrawString($item.Label, $font, $labelBrush, $x + 8, $y + 6)
$graphics.DrawImage($image, $x, $y + $labelHeight, $CellWidth, $CellHeight)
}
finally {
$image.Dispose()
}
}
$canvas.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$backgroundBrush.Dispose()
$labelBrush.Dispose()
$font.Dispose()
$graphics.Dispose()
$canvas.Dispose()
}
}
New-LabelledComparison `
-LeftPath (Join-Path $repoRoot 'docs\audits\2026-07-26\screenshots\00-dashboard-reference-1440.png') `
-LeftLabel 'Reference - 1440 px' `
-RightPath (Join-Path $screenshotRoot '02-dashboard-1440.png') `
-RightLabel 'Current build - 1440 px' `
-OutputPath (Join-Path $auditRoot 'dashboard-reference-vs-current.png')
New-LabelledComparison `
-LeftPath (Join-Path $repoRoot 'docs\audits\2026-07-28\sites-mission-control\02-run-control.png') `
-LeftLabel 'Reference mockup' `
-RightPath (Join-Path $screenshotRoot '03-run-control-1440.png') `
-RightLabel 'Current functional build' `
-OutputPath (Join-Path $auditRoot 'run-control-mock-vs-current.png')
$coreItems = @(
@{ Label = 'Dashboard'; Path = Join-Path $screenshotRoot '02-dashboard-1440.png' },
@{ Label = 'Run Control'; Path = Join-Path $screenshotRoot '03-run-control-1440.png' },
@{ Label = 'Agents'; Path = Join-Path $screenshotRoot '04-agents-1440.png' },
@{ Label = 'Agent detail'; Path = Join-Path $screenshotRoot '05-agent-detail-1440.png' },
@{ Label = 'Projects'; Path = Join-Path $screenshotRoot '06-projects-1440.png' },
@{ Label = 'Project detail'; Path = Join-Path $screenshotRoot '07-project-detail-1440.png' },
@{ Label = 'Task Board'; Path = Join-Path $screenshotRoot '08-task-board-1440.png' },
@{ Label = 'Task detail'; Path = Join-Path $screenshotRoot '09-task-detail-1440.png' },
@{ Label = 'Memory'; Path = Join-Path $screenshotRoot '10-memory-1440.png' },
@{ Label = 'Docs'; Path = Join-Path $screenshotRoot '11-docs-1440.png' },
@{ Label = 'Models'; Path = Join-Path $screenshotRoot '12-models-1440.png' },
@{ Label = 'Activity'; Path = Join-Path $screenshotRoot '13-activity-1440.png' },
@{ Label = 'Calendar'; Path = Join-Path $screenshotRoot '14-calendar-1440.png' },
@{ Label = 'Security'; Path = Join-Path $screenshotRoot '15-security-1440.png' },
@{ Label = 'Incidents'; Path = Join-Path $screenshotRoot '16-incidents-1440.png' },
@{ Label = 'Notifications'; Path = Join-Path $screenshotRoot '17-notifications-1440.png' },
@{ Label = 'Settings'; Path = Join-Path $screenshotRoot '18-settings-1440.png' }
)
New-ContactSheet `
-Items $coreItems `
-Columns 4 `
-CellWidth 360 `
-CellHeight 225 `
-OutputPath (Join-Path $auditRoot 'core-pages-contact-sheet.png')
$responsiveItems = @(
@{ Label = 'Run Control - 375 px'; Path = Join-Path $screenshotRoot '19-run-control-375.png' },
@{ Label = 'Run Control - 768 px'; Path = Join-Path $screenshotRoot '23-run-control-768.png' },
@{ Label = 'Run Control - 1024 px'; Path = Join-Path $screenshotRoot '24-run-control-1024.png' },
@{ Label = 'Run Control - 1920 px'; Path = Join-Path $screenshotRoot '25-run-control-1920.png' }
)
New-ContactSheet `
-Items $responsiveItems `
-Columns 2 `
-CellWidth 720 `
-CellHeight 450 `
-OutputPath (Join-Path $auditRoot 'run-control-responsive-contact-sheet.png')
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
[CmdletBinding()]
param(
[string]$Config = "evals/promptfoo/promptfooconfig.yaml",
[switch]$ValidateOnly,
[ValidateRange(1, 100)]
[int]$Repeat = 1
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$configPath = if ([System.IO.Path]::IsPathRooted($Config)) {
$Config
} else {
Join-Path $repoRoot $Config
}
if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
throw "Promptfoo config not found: $configPath"
}
$providerPath = Join-Path (Split-Path -Parent $configPath) "nexus-provider.cjs"
if (-not (Test-Path -LiteralPath $providerPath -PathType Leaf)) {
throw "Promptfoo provider not found: $providerPath"
}
& node --check $providerPath
if ($LASTEXITCODE -ne 0) {
throw "Promptfoo provider syntax validation failed."
}
if (-not $ValidateOnly) {
if ($env:NEXUS_EVAL_ALLOW_PROPOSALS -ne "1") {
throw "Set NEXUS_EVAL_ALLOW_PROPOSALS=1 after selecting an isolated test database."
}
if ([string]::IsNullOrWhiteSpace($env:NEXUS_EVAL_BEARER_TOKEN)) {
throw "Set NEXUS_EVAL_BEARER_TOKEN to a fresh owner JWT. The script never prints it."
}
}
Push-Location $repoRoot
try {
if ($ValidateOnly) {
& npx --yes "promptfoo@0.121.19" validate --config $configPath
} else {
& npx --yes "promptfoo@0.121.19" eval `
--config $configPath `
--max-concurrency 1 `
--repeat $Repeat `
--no-cache
}
if ($LASTEXITCODE -ne 0) {
$operation = if ($ValidateOnly) { "validation" } else { "evaluation" }
throw "Promptfoo $operation failed with exit code $LASTEXITCODE."
}
} finally {
Pop-Location
}
+173
View File
@@ -0,0 +1,173 @@
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate, Trend } from 'k6/metrics'
const BASE_URL = (__ENV.NEXUS_BASE_URL || 'http://127.0.0.1:18880').replace(/\/+$/, '')
const BEARER_TOKEN = (__ENV.NEXUS_BEARER_TOKEN || '').trim()
const API_KEY = (__ENV.NEXUS_API_KEY || '').trim()
const IS_SMOKE = (__ENV.NEXUS_K6_SMOKE || '').trim() === '1'
const TARGET = new URL(BASE_URL)
const IS_LOOPBACK = ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(TARGET.hostname)
const INSECURE_TLS = (__ENV.NEXUS_INSECURE_TLS || '').trim() === '1'
const ALLOW_REMOTE = (__ENV.NEXUS_K6_ALLOW_REMOTE || '').trim() === '1'
const REQUIRE_DONE_CURSOR = (__ENV.NEXUS_K6_REQUIRE_DONE_CURSOR || '').trim()
? (__ENV.NEXUS_K6_REQUIRE_DONE_CURSOR || '').trim() === '1'
: !IS_SMOKE
if (!BEARER_TOKEN && !API_KEY) {
throw new Error(
'Set NEXUS_BEARER_TOKEN or NEXUS_API_KEY. Credentials are read from the environment and are never printed.',
)
}
if (!['http:', 'https:'].includes(TARGET.protocol)) {
throw new Error('NEXUS_BASE_URL must use HTTP or HTTPS.')
}
if (
TARGET.username
|| TARGET.password
|| TARGET.search
|| TARGET.hash
|| !['', '/'].includes(TARGET.pathname)
) {
throw new Error(
'NEXUS_BASE_URL must be an origin without embedded credentials, path, query, or fragment.',
)
}
if (!IS_LOOPBACK && !ALLOW_REMOTE) {
throw new Error(
'Remote k6 targets are blocked. Set NEXUS_K6_ALLOW_REMOTE=1 only for an isolated non-production environment.',
)
}
if (!IS_LOOPBACK && TARGET.protocol !== 'https:') {
throw new Error(
'Remote k6 targets must use HTTPS because the test sends an authenticated credential.',
)
}
if (!IS_LOOPBACK && INSECURE_TLS) {
throw new Error('NEXUS_INSECURE_TLS is allowed only for loopback diagnostics.')
}
const boardRequestDuration = new Trend('nexus_board_request_duration', true)
const boardInitialDuration = new Trend('nexus_board_initial_duration', true)
const boardDoneDuration = new Trend('nexus_board_done_duration', true)
const boardFailures = new Rate('nexus_board_failures')
const boardContractFailures = new Rate('nexus_board_contract_failures')
const boardDatasetFailures = new Rate('nexus_board_dataset_failures')
export const options = {
scenarios: {
task_board: IS_SMOKE
? {
executor: 'constant-vus',
vus: 1,
duration: '10s',
gracefulStop: '5s',
}
: {
executor: 'constant-vus',
vus: 10,
duration: '2m',
gracefulStop: '15s',
},
},
thresholds: IS_SMOKE
? {
nexus_board_failures: ['rate<0.05'],
nexus_board_contract_failures: ['rate<0.05'],
nexus_board_dataset_failures: ['rate<0.05'],
}
: {
'http_req_duration{endpoint:task-board,page:initial}': ['p(95)<500'],
'http_req_duration{endpoint:task-board,page:done}': ['p(95)<300'],
'http_req_failed{endpoint:task-board,page:initial}': ['rate<0.01'],
'http_req_failed{endpoint:task-board,page:done}': ['rate<0.01'],
nexus_board_initial_duration: ['p(95)<500'],
nexus_board_done_duration: ['p(95)<300'],
nexus_board_failures: ['rate<0.01'],
nexus_board_contract_failures: ['rate<0.01'],
nexus_board_dataset_failures: ['rate<0.01'],
checks: ['rate>0.99'],
},
userAgent: 'nexus-task-board-k6/1.0',
insecureSkipTLSVerify: INSECURE_TLS,
noConnectionReuse: false,
}
function authHeaders() {
const headers = {
Accept: 'application/json',
}
if (BEARER_TOKEN) headers.Authorization = `Bearer ${BEARER_TOKEN}`
if (API_KEY) headers['X-Nexus-Api-Key'] = API_KEY
return headers
}
function readBoardPage(url, page) {
const response = http.get(url, {
headers: authHeaders(),
tags: {
endpoint: 'task-board',
page,
},
timeout: '10s',
})
boardRequestDuration.add(response.timings.duration, { page })
if (page === 'initial') boardInitialDuration.add(response.timings.duration)
if (page === 'done') boardDoneDuration.add(response.timings.duration)
const httpOk = response.status === 200
boardFailures.add(!httpOk, { page })
let body = null
if (httpOk) {
try {
body = response.json()
} catch {
body = null
}
}
const contractOk = Boolean(
body
&& typeof body.revision === 'string'
&& Array.isArray(body.offen)
&& Array.isArray(body.inProgress)
&& Array.isArray(body.review)
&& Array.isArray(body.blocked)
&& Array.isArray(body.done)
&& typeof body.hasMoreDone === 'boolean',
)
boardContractFailures.add(!contractOk, { page })
check(response, {
[`${page}: returns 200`]: () => httpOk,
[`${page}: preserves the board contract`]: () => contractOk,
[`${page}: emits Server-Timing`]: res => Boolean(res.headers['Server-Timing']),
})
return contractOk ? body : null
}
export default function () {
const firstPage = readBoardPage(
`${BASE_URL}/api/v1/tasks/board?doneLimit=50`,
'initial',
)
const hasDoneCursor = Boolean(firstPage?.hasMoreDone && firstPage.nextDoneCursor)
const datasetOk = !REQUIRE_DONE_CURSOR || hasDoneCursor
boardDatasetFailures.add(!datasetOk)
check(firstPage, {
'acceptance dataset exposes a Done continuation cursor': () => datasetOk,
})
if (hasDoneCursor) {
readBoardPage(
`${BASE_URL}/api/v1/tasks/board?doneLimit=50&doneCursor=${encodeURIComponent(firstPage.nextDoneCursor)}`,
'done',
)
}
sleep(0.75 + Math.random() * 0.5)
}
+240
View File
@@ -0,0 +1,240 @@
#requires -Version 7.0
[CmdletBinding()]
param(
[ValidateSet("Pin", "Baseline", "Candidate")]
[string]$Mode = "Pin",
[string]$CandidateVersion = "2.0.0",
[switch]$KeepTemporaryCopy
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$stableVersion = "1.4.1"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$projectPath = Join-Path $repoRoot "backend\Nexus.Api.csproj"
$testProjectPath = Join-Path $repoRoot "backend-tests\Nexus.Api.Tests.csproj"
$projectHashBefore = (Get-FileHash -LiteralPath $projectPath -Algorithm SHA256).Hash
$temporaryRoot = $null
$dotnetExe = $null
function Get-McpPackageVersion {
param([Parameter(Mandatory)][string]$Path)
[xml]$project = Get-Content -LiteralPath $Path -Raw
$reference = @(
@($project.Project.ItemGroup.PackageReference) |
Where-Object { $_.Include -eq "ModelContextProtocol.AspNetCore" }
)
if ($reference.Count -ne 1) {
throw "Expected exactly one ModelContextProtocol.AspNetCore reference in $Path."
}
return $reference[0].GetAttribute("Version")
}
function Assert-StaticGate {
$version = Get-McpPackageVersion -Path $projectPath
if ($version -ne $stableVersion) {
throw "MCP production pin must remain $stableVersion; found $version."
}
$testFiles = @(
"backend-tests\McpServerConfigurationTests.cs",
"backend-tests\McpToolsTests.cs",
"backend-tests\AgentProposalServiceTests.cs"
)
foreach ($relativePath in $testFiles) {
$path = Join-Path $repoRoot $relativePath
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
throw "Required MCP compatibility coverage is missing: $relativePath"
}
}
$toolSource = Get-Content -LiteralPath (Join-Path $repoRoot "backend\Services\NexusMcpTools.cs") -Raw
foreach ($marker in @(
"nexus_propose_agent",
"nexus_get_agent_proposal",
"UseStructuredContent = true",
"OutputSchemaType = typeof(AgentProposalToolResult)",
"Idempotent = true",
"OpenWorld = false"
)) {
if (-not $toolSource.Contains($marker, [StringComparison]::Ordinal)) {
throw "Required MCP contract marker is missing: $marker"
}
}
}
function Resolve-DotNet10 {
$candidates = @()
$pathDotnet = Get-Command dotnet -ErrorAction SilentlyContinue
if ($pathDotnet) {
$candidates += $pathDotnet.Source
}
if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
$candidates += (Join-Path $env:USERPROFILE ".dotnet\dotnet.exe")
}
foreach ($candidate in $candidates | Select-Object -Unique) {
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
continue
}
$sdks = & $candidate --list-sdks
if ($LASTEXITCODE -eq 0 -and ($sdks | Where-Object { $_ -match "^10\." })) {
return (Resolve-Path -LiteralPath $candidate).Path
}
}
throw ".NET SDK 10 is required for Baseline and Candidate modes. Add it to PATH or install it under USERPROFILE\\.dotnet."
}
function Invoke-DotNetTest {
param(
[Parameter(Mandatory)][string]$Project,
[Parameter(Mandatory)][string]$WorkingDirectory
)
Push-Location $WorkingDirectory
try {
& $script:dotnetExe test $Project `
--configuration Release `
--verbosity minimal
if ($LASTEXITCODE -ne 0) {
throw "dotnet test failed for $Project."
}
} finally {
Pop-Location
}
}
function Copy-SourceTree {
param(
[Parameter(Mandatory)][string]$Source,
[Parameter(Mandatory)][string]$Destination
)
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
$sourceRoot = [System.IO.Path]::GetFullPath($Source)
Get-ChildItem -LiteralPath $sourceRoot -Recurse -File |
Where-Object {
$relative = [System.IO.Path]::GetRelativePath($sourceRoot, $_.FullName)
$segments = $relative -split "[\\/]"
-not ($segments -contains "bin" -or $segments -contains "obj")
} |
ForEach-Object {
$relative = [System.IO.Path]::GetRelativePath($sourceRoot, $_.FullName)
$target = Join-Path $Destination $relative
$targetDirectory = Split-Path -Parent $target
New-Item -ItemType Directory -Path $targetDirectory -Force | Out-Null
Copy-Item -LiteralPath $_.FullName -Destination $target -Force
}
}
function Set-CandidatePackageVersion {
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Version
)
[xml]$project = Get-Content -LiteralPath $Path -Raw
$reference = @(
@($project.Project.ItemGroup.PackageReference) |
Where-Object { $_.Include -eq "ModelContextProtocol.AspNetCore" }
)
if ($reference.Count -ne 1) {
throw "Candidate copy does not contain exactly one MCP package reference."
}
$reference[0].SetAttribute("Version", $Version)
$project.Save($Path)
}
try {
Assert-StaticGate
Write-Host "PASS: MCP production dependency is pinned to $stableVersion and required source/test markers are present."
Write-Host "Static Pin mode did not execute tests or a live MCP/OpenClaw negotiation."
if ($Mode -eq "Pin") {
return
}
$dotnetExe = Resolve-DotNet10
# MSBuild launches helper applications (for example the OpenAPI document
# generator) through `dotnet`, not through the absolute executable used by
# this script. Keep those child processes on the same .NET 10 runtime.
$dotnetRoot = Split-Path -Parent $dotnetExe
$env:DOTNET_ROOT = $dotnetRoot
$pathSeparator = [System.IO.Path]::PathSeparator
if (-not (($env:PATH -split [Regex]::Escape([string]$pathSeparator)) -contains $dotnetRoot)) {
$env:PATH = "$dotnetRoot$pathSeparator$env:PATH"
}
Invoke-DotNetTest -Project $testProjectPath -WorkingDirectory $repoRoot
Write-Host "PASS: MCP $stableVersion baseline command completed successfully."
Write-Warning "Environment-gated Docker, Toxiproxy, or live OpenClaw tests may still be skipped; inspect the test summary before treating this as full compatibility evidence."
if ($Mode -eq "Baseline") {
return
}
if ($CandidateVersion -notmatch "^2\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$") {
throw "CandidateVersion must be an explicit MCP 2.x package version."
}
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) "nexus-mcp2-gate-$([Guid]::NewGuid().ToString('N'))"
New-Item -ItemType Directory -Path $temporaryRoot -Force | Out-Null
Copy-SourceTree -Source (Join-Path $repoRoot "backend") -Destination (Join-Path $temporaryRoot "backend")
Copy-SourceTree -Source (Join-Path $repoRoot "backend-tests") -Destination (Join-Path $temporaryRoot "backend-tests")
$candidateProject = Join-Path $temporaryRoot "backend\Nexus.Api.csproj"
$candidateTests = Join-Path $temporaryRoot "backend-tests\Nexus.Api.Tests.csproj"
Set-CandidatePackageVersion -Path $candidateProject -Version $CandidateVersion
& $dotnetExe restore $candidateTests --force-evaluate
if ($LASTEXITCODE -ne 0) {
throw "MCP $CandidateVersion restore failed in the isolated copy."
}
$assetsPath = Join-Path $temporaryRoot "backend\obj\project.assets.json"
if (-not (Test-Path -LiteralPath $assetsPath -PathType Leaf)) {
throw "Candidate restore did not create project.assets.json."
}
$resolvedMarker = "`"ModelContextProtocol.AspNetCore/$CandidateVersion`""
$assets = Get-Content -LiteralPath $assetsPath -Raw
if (-not $assets.Contains($resolvedMarker, [StringComparison]::OrdinalIgnoreCase)) {
throw "Restore did not resolve the exact requested candidate $CandidateVersion."
}
Invoke-DotNetTest -Project $candidateTests -WorkingDirectory $temporaryRoot
if ($CandidateVersion.Contains("-", [StringComparison]::Ordinal)) {
Write-Warning "COMPATIBILITY PROBE PASS: $CandidateVersion compiled and passed all backend tests."
Write-Warning "PROMOTION GATE CLOSED: prerelease packages are not approved for production. The repository remains on $stableVersion."
} else {
Write-Host "SDK COMPATIBILITY PROBE PASS: stable candidate $CandidateVersion compiled and passed the backend suite."
Write-Warning "LIVE OPENCLAW GATE CLOSED: this probe does not test OpenClaw tools/list, client identity, Streamable HTTP, or protocol down-level negotiation."
Write-Host "The repository was not modified; live isolated OpenClaw compatibility evidence and a separate reviewed dependency change are still required."
}
} finally {
$projectHashAfter = (Get-FileHash -LiteralPath $projectPath -Algorithm SHA256).Hash
if ($projectHashAfter -ne $projectHashBefore) {
throw "Safety invariant violated: $projectPath changed during the compatibility gate."
}
if ($temporaryRoot -and (Test-Path -LiteralPath $temporaryRoot)) {
if ($KeepTemporaryCopy) {
Write-Host "Temporary candidate copy retained at $temporaryRoot"
} else {
$resolvedTemporaryRoot = [System.IO.Path]::GetFullPath($temporaryRoot)
$systemTemporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
if (
-not $resolvedTemporaryRoot.StartsWith($systemTemporaryRoot, [StringComparison]::OrdinalIgnoreCase) -or
-not ([System.IO.Path]::GetFileName($resolvedTemporaryRoot)).StartsWith(
"nexus-mcp2-gate-",
[StringComparison]::Ordinal)
) {
throw "Refusing to remove an unverified temporary path: $resolvedTemporaryRoot"
}
Remove-Item -LiteralPath $resolvedTemporaryRoot -Recurse -Force
}
}
}