import { spawn } from "node:child_process"; import { appendFileSync, createWriteStream, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { agentOutputHasExpectedOkMarker, buildCrossOsReleaseAgentSessionId, buildReleaseAgentTurnArgs, maybeBuildOptionalAgentTurnSkipResult, shouldRetryCrossOsAgentTurnError, } from "./agent.ts"; import type { AgentTurnResult, CommandOptions, GatewayHandle, LaneState, ProviderConfig, } from "./config.ts"; import { CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS, CROSS_OS_GATEWAY_STATUS_COMMAND_TIMEOUT_MS, CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS, CROSS_OS_RELEASE_SMOKE_TOOLS_PROFILE, buildCrossOsReleaseSmokeMemorySlotConfigArgs, buildCrossOsReleaseSmokePluginAllowlist, buildReleaseProviderConfigOverride, gatewayReadyDeadlineMs, installTimeoutMs, looksLikeCommitSha, resolveExplicitBaselineVersion, resolveExpectedDevUpdateRef, shouldSkipInstallerDaemonHealthCheck, } from "./config.ts"; import { installedEntryPath, normalizeWindowsInstalledCliPath, npmCommand, resolveInstalledPrefixDirFromCliPath, } from "./install.ts"; import { readLogFileSize, readLogTextSince } from "./logs.ts"; import { canConnectToLoopbackPort, hasChildExited, resolveCommandSpawnInvocation, runCommand, runCommandInvocation, waitForGatewayWithStartupMigrationRestart, withAllocatedGatewayPort, } from "./process.ts"; import { formatError, shellEscapeForSh, sleep } from "./shared.ts"; const INSTALLER_CONNECT_TIMEOUT_SECONDS = 10; const INSTALLER_REQUEST_TIMEOUT_SECONDS = 120; export async function resolveInstallerTargetVersion(params: { baselineSpec: string; logsDir: string; suiteName: string; }) { const resolvedVersion = resolveExplicitBaselineVersion(params.baselineSpec); if (resolvedVersion) { return resolvedVersion; } const latestResult = await runCommand(npmCommand(), ["view", "openclaw@latest", "version"], { logPath: join(params.logsDir, `${params.suiteName}-latest-version.log`), timeoutMs: 2 * 60 * 1000, }); const latestVersion = latestResult.stdout.trim(); if (!latestVersion) { throw new Error("npm view openclaw@latest version did not return a version."); } return latestVersion; } function powerShellSingleQuote(value: string) { return value.replace(/'/gu, "''"); } function parseMarkerLine(output: string, marker: string) { return output .split(/\r?\n/gu) .find((line) => line.startsWith(marker)) ?.slice(marker.length) .trim(); } export function resolveInstalledCliInvocation( cliPath: string, args: string[] = [], options: { platform?: NodeJS.Platform; comSpec?: string; env?: NodeJS.ProcessEnv } = { platform: process.platform, }, ) { const platform = options.platform ?? process.platform; if (platform !== "win32") { return { command: cliPath, args, shell: false }; } const normalizedCliPath = normalizeWindowsInstalledCliPath(cliPath); if (!/\.cmd$/iu.test(normalizedCliPath)) { return { command: normalizedCliPath, args, shell: false }; } const entryPath = installedEntryPath( resolveInstalledPrefixDirFromCliPath(normalizedCliPath, platform), ); if (existsSync(entryPath)) { return { command: process.execPath, args: [entryPath, ...args], shell: false, }; } return resolveCommandSpawnInvocation(normalizedCliPath, args, { comSpec: options.comSpec, env: options.env, platform, }); } async function runPosixShellScript(script: string, options: CommandOptions) { return runCommand("/bin/bash", ["-lc", script], options); } async function runPowerShellScript(script: string, options: CommandOptions) { return runCommand( "powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], options, ); } export function buildInstallerSmokeScript( params: { installerUrl: string; installTarget: string; platform?: NodeJS.Platform; }, options: { connectTimeoutSeconds?: number; requestTimeoutSeconds?: number; } = {}, ) { const connectTimeoutSeconds = options.connectTimeoutSeconds ?? INSTALLER_CONNECT_TIMEOUT_SECONDS; const requestTimeoutSeconds = options.requestTimeoutSeconds ?? INSTALLER_REQUEST_TIMEOUT_SECONDS; if ((params.platform ?? process.platform) === "win32") { return ` $installerPath = Join-Path ([System.IO.Path]::GetTempPath()) ("openclaw-installer-" + [guid]::NewGuid().ToString("N") + ".ps1") try { & curl.exe -fsSL --connect-timeout ${connectTimeoutSeconds} --max-time ${requestTimeoutSeconds} -o $installerPath '${powerShellSingleQuote(params.installerUrl)}' if ($LASTEXITCODE -ne 0) { throw "curl.exe failed to download the OpenClaw installer (exit $LASTEXITCODE)" } $content = [System.IO.File]::ReadAllText($installerPath, [System.Text.Encoding]::UTF8) & ([scriptblock]::Create($content)) -Tag '${powerShellSingleQuote(params.installTarget)}' -NoOnboard } finally { Remove-Item -LiteralPath $installerPath -Force -ErrorAction SilentlyContinue } `; } // Execute only a complete installer: a timed-out response may still contain an executable prefix. return [ "set -euo pipefail", 'installer_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-installer-XXXXXX")"', "trap 'rm -f \"$installer_path\"' EXIT", `curl -fsSL --connect-timeout ${connectTimeoutSeconds} --max-time ${requestTimeoutSeconds} -o "$installer_path" '${shellEscapeForSh(params.installerUrl)}'`, `bash -- "$installer_path" --version '${shellEscapeForSh(params.installTarget)}' --no-onboard`, ].join("\n"); } export async function runInstallerSmoke(params: { lane: LaneState; env: NodeJS.ProcessEnv; installerUrl: string; installTarget: string; logPath: string; }) { const script = buildInstallerSmokeScript(params); if (process.platform === "win32") { await runPowerShellScript(script, { cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: installTimeoutMs(), }); return; } await runPosixShellScript(script, { cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: installTimeoutMs(), }); } export function buildWindowsPathBootstrapScript( options: { includeCurrentProcessPath?: boolean } = {}, ) { const includeCurrentProcessPath = options.includeCurrentProcessPath !== false; // setup-node provisions the supported runtime in the current process PATH. Keep it ahead of // stale runner image entries while still merging newly persisted user and machine paths. const pathCandidates = includeCurrentProcessPath ? "@($env:Path, $userPath, $machinePath)" : "@($userPath, $machinePath)"; return ` $machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $segments = New-Object System.Collections.Generic.List[string] foreach ($candidate in ${pathCandidates}) { foreach ($segment in ($candidate -split ';')) { if ([string]::IsNullOrWhiteSpace($segment)) { continue } if (-not $segments.Contains($segment)) { $segments.Add($segment) } } } $env:Path = [string]::Join(';', $segments) `.trim(); } export function buildWindowsFreshShellVersionCheckScript(params: { expectedNeedle?: string } = {}) { const expectedNeedle = powerShellSingleQuote(params.expectedNeedle ?? ""); return ` ${buildWindowsPathBootstrapScript()} $commandPath = $null $npmCommand = Get-Command npm.cmd -ErrorAction SilentlyContinue if ($null -eq $npmCommand) { $npmCommand = Get-Command npm -ErrorAction SilentlyContinue } if ($null -ne $npmCommand) { $npmPrefix = (& $npmCommand.Source config get prefix 2>$null | Out-String).Trim() if (-not [string]::IsNullOrWhiteSpace($npmPrefix)) { $env:Path = "$npmPrefix;$env:Path" foreach ($candidate in @( (Join-Path $npmPrefix 'openclaw.cmd'), (Join-Path $npmPrefix 'openclaw.ps1') )) { if (Test-Path -LiteralPath $candidate) { $commandPath = $candidate break } } } } if ([string]::IsNullOrWhiteSpace($commandPath)) { $cmd = Get-Command openclaw -ErrorAction Stop $commandPath = $cmd.Source } if ($commandPath -match '(?i)\\.ps1$') { $cmdPath = [System.IO.Path]::ChangeExtension($commandPath, '.cmd') if (Test-Path -LiteralPath $cmdPath) { $commandPath = $cmdPath } } $version = (& $commandPath --version 2>&1 | Out-String).Trim() Write-Output "__OPENCLAW_PATH__=$commandPath" Write-Output $version if ('${expectedNeedle}'.Length -gt 0 -and $version -notmatch [regex]::Escape('${expectedNeedle}')) { throw "version mismatch: expected substring ${expectedNeedle}" } `.trim(); } export function buildWindowsDevUpdateToolchainCheckScript() { return ` ${buildWindowsPathBootstrapScript()} function Resolve-CommandPath([string]$Name) { $command = Get-Command $Name -ErrorAction SilentlyContinue if ($null -eq $command) { return $null } $commandPath = $command.Source if ($commandPath -match '(?i)\\.ps1$') { $cmdPath = [System.IO.Path]::ChangeExtension($commandPath, '.cmd') if (Test-Path -LiteralPath $cmdPath) { $commandPath = $cmdPath } } return $commandPath } $pnpmPath = Resolve-CommandPath 'pnpm' if ($null -ne $pnpmPath) { Write-Output "__UPDATE_TOOL__=pnpm" Write-Output "__UPDATE_TOOL_PATH__=$pnpmPath" & $pnpmPath --version return } $corepackPath = Resolve-CommandPath 'corepack' if ($null -ne $corepackPath) { Write-Output "__UPDATE_TOOL__=corepack" Write-Output "__UPDATE_TOOL_PATH__=$corepackPath" & $corepackPath --version return } $npmPath = Resolve-CommandPath 'npm' if ($null -ne $npmPath) { Write-Output "__UPDATE_TOOL__=npm" Write-Output "__UPDATE_TOOL_PATH__=$npmPath" & $npmPath --version return } throw 'Neither pnpm, corepack, nor npm is discoverable from the reconstructed Windows PATH.' `.trim(); } export async function verifyFreshShellCommand(params: { lane: LaneState; env: NodeJS.ProcessEnv; expectedNeedle: string; logPath: string; }) { if (process.platform === "win32") { const script = buildWindowsFreshShellVersionCheckScript({ expectedNeedle: params.expectedNeedle, }); const result = await runPowerShellScript(script, { cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); const cliPath = normalizeWindowsInstalledCliPath( parseMarkerLine(result.stdout, "__OPENCLAW_PATH__=") ?? "", ); if (!cliPath) { throw new Error("Failed to resolve installed openclaw path from fresh Windows shell."); } return { cliPath, versionOutput: `${result.stdout}\n${result.stderr}`.trim(), }; } const script = [ "set -euo pipefail", 'if [ -f "$HOME/.bashrc" ]; then . "$HOME/.bashrc"; fi', "command -v openclaw >/dev/null 2>&1", 'printf "__OPENCLAW_PATH__=%s\\n" "$(command -v openclaw)"', "openclaw --version", ].join("\n"); const result = await runPosixShellScript(script, { cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); const cliPath = parseMarkerLine(result.stdout, "__OPENCLAW_PATH__="); const versionOutput = `${result.stdout}\n${result.stderr}`.trim(); if (!cliPath) { throw new Error("Failed to resolve installed openclaw path from fresh POSIX shell."); } if (params.expectedNeedle && !versionOutput.includes(params.expectedNeedle)) { throw new Error( `Installed CLI version did not contain expected substring ${params.expectedNeedle}.`, ); } return { cliPath, versionOutput }; } export async function runInstalledCli(params: { cliPath: string; args: string[]; cwd: string; env: NodeJS.ProcessEnv; logPath: string; timeoutMs?: number; check?: boolean; }) { const invocation = resolveInstalledCliInvocation(params.cliPath, params.args, { env: params.env, platform: process.platform, }); return runCommandInvocation(invocation, { cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: params.timeoutMs, check: params.check ?? true, }); } export async function resolveInstalledGatewayStopArgs(params: { cliPath: string; cwd: string; env: NodeJS.ProcessEnv; logPath: string; }) { const help = await runInstalledCli({ cliPath: params.cliPath, args: ["gateway", "stop", "--help"], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 15_000, }); return buildGatewayStopArgsFromHelpText(`${help.stdout}\n${help.stderr}`); } async function readInstalledUpdateStatus(params: { cliPath: string; cwd: string; env: NodeJS.ProcessEnv; logPath: string; }) { return runInstalledCli({ cliPath: params.cliPath, args: ["update", "status", "--json"], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); } export async function ensureDevUpdateGitInstall(params: { lane: LaneState; env: NodeJS.ProcessEnv; cliPath: string; logsDir: string; requestedRef: string; }) { const updateStatus = await readInstalledUpdateStatus({ cliPath: params.cliPath, cwd: params.lane.homeDir, env: params.env, logPath: join(params.logsDir, "dev-update-status.log"), }); // The dev-update lane must prove that `openclaw update --channel dev` landed on // the expected git checkout. Falling back to a manual repair here would hide // updater regressions and turn the suite into a false green. verifyDevUpdateStatus(updateStatus.stdout, { ref: params.requestedRef }); return { cliPath: params.cliPath }; } export async function runOnboardWithInstalledCli(params: { lane: LaneState; cliPath: string; env: NodeJS.ProcessEnv; providerConfig: ProviderConfig; installDaemon: boolean; logPath: string; allocateGatewayPort?: boolean; }) { const runOnboard = async () => { const args = buildReleaseOnboardArgs({ authChoice: params.providerConfig.authChoice, gatewayPort: params.lane.gatewayPort, installDaemon: params.installDaemon, skipHealth: !params.installDaemon || shouldSkipInstallerDaemonHealthCheck(), }); await runInstalledCli({ cliPath: params.cliPath, args, cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: 10 * 60 * 1000, }); }; if (params.allocateGatewayPort === false) { if (params.lane.gatewayPort <= 0) { throw new Error("Installed onboarding requires a reserved gateway port."); } await runOnboard(); return; } await withAllocatedGatewayPort(params.lane, runOnboard); } export function buildReleaseOnboardArgs(params: { authChoice: string; gatewayPort: number; installDaemon?: boolean; skipHealth?: boolean; }) { const args: string[] = [ "onboard", "--non-interactive", "--mode", "local", "--auth-choice", params.authChoice, "--secret-input-mode", "ref", "--gateway-port", String(params.gatewayPort), "--gateway-bind", "loopback", "--skip-skills", "--skip-bootstrap", "--accept-risk", "--json", ]; if (params.installDaemon) { args.push("--install-daemon"); } if (params.skipHealth) { args.push("--skip-health"); } return args; } export async function startManualGatewayFromInstalledCli(params: { lane: LaneState; cliPath: string; env: NodeJS.ProcessEnv; logPath: string; }): Promise { mkdirSync(dirname(params.logPath), { recursive: true }); const launchLogOffset = readLogFileSize(params.logPath); const gatewayLog = createWriteStream(params.logPath, { flags: "a" }); const invocation = resolveInstalledCliInvocation( params.cliPath, ["gateway", "run", "--bind", "loopback", "--port", String(params.lane.gatewayPort), "--force"], { env: params.env, platform: process.platform, }, ); const child = spawn(invocation.command, invocation.args, { cwd: params.lane.homeDir, env: params.env, shell: invocation.shell, stdio: ["ignore", "pipe", "pipe"], windowsVerbatimArguments: invocation.windowsVerbatimArguments, windowsHide: true, }); child.stdout?.on("data", (chunk) => { gatewayLog.write(chunk); }); child.stderr?.on("data", (chunk) => { gatewayLog.write(chunk); }); let resolveChildClose: () => void; const childClosePromise = new Promise((resolvePromise) => { resolveChildClose = resolvePromise; }); let closeLogPromise: Promise | undefined; const closeLog = () => { closeLogPromise ??= new Promise((resolvePromise) => { gatewayLog.once("error", () => resolvePromise()); gatewayLog.end(() => resolvePromise()); }); return closeLogPromise; }; child.once("close", () => { resolveChildClose(); void closeLog(); }); child.once("error", () => { resolveChildClose(); void closeLog(); }); return { child, closeLog, launchLogOffset, logPath: params.logPath, waitForClose: () => childClosePromise, }; } async function resolveInstalledGatewayStatusArgs(params: { cliPath: string; cwd: string; env: NodeJS.ProcessEnv; logPath: string; requireRpc?: boolean; }) { const requireRpc = params.requireRpc !== false; try { const help = await runInstalledCli({ cliPath: params.cliPath, args: ["gateway", "status", "--help"], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 15_000, check: false, }); return buildGatewayStatusArgsFromHelpText(`${help.stdout}\n${help.stderr}`, { requireRpc }); } catch (error) { appendGatewayStatusHelpProbeFallback(params.logPath, error); return buildGatewayStatusArgsFromHelpText("--require-rpc", { requireRpc }); } } export function buildGatewayStatusArgsFromHelpText( helpText: string, options: { requireRpc?: boolean } = {}, ) { const requireRpc = options.requireRpc !== false; if (requireRpc && helpText.includes("--require-rpc")) { return [ "gateway", "status", "--require-rpc", "--timeout", String(CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS), ]; } return ["gateway", "status"]; } export function buildGatewayStopArgsFromHelpText(helpText: string) { if (helpText.includes("--force")) { return ["gateway", "stop", "--force"]; } return ["gateway", "stop"]; } export function appendGatewayStatusHelpProbeFallback(logPath: string, error: unknown) { appendFileSync( logPath, `${new Date().toISOString()} gateway status help probe failed; assuming current --require-rpc support: ${formatError(error)}\n`, ); } export async function waitForInstalledGateway(params: { lane: LaneState; cliPath: string; env: NodeJS.ProcessEnv; gateway?: GatewayHandle; gatewayHolder?: { current: GatewayHandle | null }; gatewayLogPath?: string; logPath: string; }) { if (params.gatewayHolder) { if (!params.gatewayLogPath) { throw new Error("Gateway restart coordination requires a gateway log path."); } const gatewayLogPath = params.gatewayLogPath; await waitForGatewayWithStartupMigrationRestart({ gatewayHolder: params.gatewayHolder, restartGateway: () => startManualGatewayFromInstalledCli({ lane: params.lane, cliPath: params.cliPath, env: params.env, logPath: gatewayLogPath, }), waitUntilReady: (gateway) => waitForInstalledGateway({ lane: params.lane, cliPath: params.cliPath, env: params.env, gateway, logPath: params.logPath, }), }); return; } const statusArgs = await resolveInstalledGatewayStatusArgs({ cliPath: params.cliPath, cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, }); const deadline = Date.now() + gatewayReadyDeadlineMs(); while (Date.now() < deadline) { if (params.gateway && hasChildExited(params.gateway.child)) { throw new Error(`Gateway exited before becoming ready on port ${params.lane.gatewayPort}.`); } const result = await runInstalledCli({ cliPath: params.cliPath, args: statusArgs, cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: CROSS_OS_GATEWAY_STATUS_COMMAND_TIMEOUT_MS, check: false, }); if (result.exitCode === 0) { return; } if (params.gateway && hasChildExited(params.gateway.child)) { throw new Error(`Gateway exited before becoming ready on port ${params.lane.gatewayPort}.`); } await sleep(2_000); } throw new Error(`Gateway did not become ready on port ${params.lane.gatewayPort}.`); } export async function waitForInstalledGatewayToStop(params: { lane: LaneState; cliPath: string; env: NodeJS.ProcessEnv; logPath: string; }) { const statusArgs = await resolveInstalledGatewayStatusArgs({ cliPath: params.cliPath, cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, requireRpc: false, }); const deadline = Date.now() + gatewayReadyDeadlineMs(); while (Date.now() < deadline) { await runInstalledCli({ cliPath: params.cliPath, args: statusArgs, cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: CROSS_OS_GATEWAY_STATUS_COMMAND_TIMEOUT_MS, check: false, }); const portReachable = await canConnectToLoopbackPort(params.lane.gatewayPort); if (!portReachable) { return; } await sleep(2_000); } throw new Error( `Managed gateway did not stop on port ${params.lane.gatewayPort} before manual fallback.`, ); } export async function ensureManagedGatewayReady(params: { lane: LaneState; cliPath: string; env: NodeJS.ProcessEnv; logPath: string; }) { try { await waitForInstalledGateway(params); return; } catch { await runInstalledCli({ cliPath: params.cliPath, args: ["gateway", "start"], cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, check: false, }); } await waitForInstalledGateway(params); } export async function runInstalledModelsSet(params: { cliPath: string; cwd: string; env: NodeJS.ProcessEnv; providerConfig: ProviderConfig; logPath: string; }) { await runInstalledCli({ cliPath: params.cliPath, args: ["models", "set", params.providerConfig.model], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); const providerConfigOverride = buildReleaseProviderConfigOverride(params.providerConfig); if (providerConfigOverride) { await runInstalledCli({ cliPath: params.cliPath, args: [ "config", "set", `models.providers.${params.providerConfig.extensionId}`, JSON.stringify(providerConfigOverride), "--strict-json", "--merge", ], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); } await runInstalledCli({ cliPath: params.cliPath, args: [ "config", "set", "plugins.allow", JSON.stringify(buildCrossOsReleaseSmokePluginAllowlist(params.providerConfig)), "--strict-json", ], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); await runInstalledCli({ cliPath: params.cliPath, args: buildCrossOsReleaseSmokeMemorySlotConfigArgs(), cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); await runInstalledCli({ cliPath: params.cliPath, args: ["config", "set", "agents.defaults.skipBootstrap", "true", "--strict-json"], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); await runInstalledCli({ cliPath: params.cliPath, args: ["config", "set", "tools.profile", CROSS_OS_RELEASE_SMOKE_TOOLS_PROFILE], cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); } export async function runInstalledAgentTurn(params: { cliPath: string; cwd: string; env: NodeJS.ProcessEnv; label: string; logPath: string; }): Promise { let lastError; for (let attempt = 1; attempt <= 2; attempt += 1) { const sessionId = buildCrossOsReleaseAgentSessionId(params.label, attempt); try { const logOffset = readLogFileSize(params.logPath); const result = await runInstalledCli({ cliPath: params.cliPath, args: buildReleaseAgentTurnArgs(sessionId), cwd: params.cwd, env: params.env, logPath: params.logPath, timeoutMs: (CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS + 60) * 1000, }); const logText = readLogTextSince(params.logPath, logOffset); if (!agentOutputHasExpectedOkMarker(result.stdout, { logText })) { throw new Error("Agent output did not contain the expected OK marker."); } return result; } catch (error) { lastError = error; const skipped = maybeBuildOptionalAgentTurnSkipResult(error, params.logPath, { attempt, maxAttempts: 2, }); if (skipped) { return skipped; } if (attempt >= 2 || !shouldRetryCrossOsAgentTurnError(error)) { throw error; } appendFileSync( params.logPath, `\n[release-checks] retrying installed agent turn after retryable live failure: ${ error instanceof Error ? error.message : String(error) }\n`, ); } } throw lastError; } export function verifyDevUpdateStatus(stdout: string, options: { ref?: string } = {}) { let payload; try { payload = JSON.parse(stdout) as { update?: { installKind?: string; git?: { branch?: string; sha?: string } }; channel?: { value?: string; channel?: string }; installKind?: string; git?: { branch?: string; sha?: string }; }; } catch { payload = null; } const expectedRef = resolveExpectedDevUpdateRef(options.ref); const update = payload?.update ?? payload; const installKind = update?.installKind ?? null; const branch = update?.git?.branch ?? null; const sha = update?.git?.sha ?? null; const channelValue = payload?.channel?.value ?? payload?.channel?.channel ?? null; if (installKind !== "git") { throw new Error( `Dev update did not land on a git install. Found ${installKind ?? ""}.`, ); } if (channelValue !== "dev") { throw new Error( `Dev update status did not report channel=dev. Found ${channelValue ?? ""}.`, ); } if (looksLikeCommitSha(expectedRef)) { const normalizedSha = typeof sha === "string" ? sha.toLowerCase() : ""; const normalizedExpectedRef = expectedRef.toLowerCase(); if (!normalizedSha || !normalizedSha.startsWith(normalizedExpectedRef)) { throw new Error( `Dev update status did not report sha=${expectedRef}. Found ${sha ?? ""}.`, ); } return; } if (branch !== expectedRef) { throw new Error( `Dev update status did not report branch=${expectedRef}. Found ${branch ?? ""}.`, ); } } export async function verifyWindowsDevUpdateToolchain(params: { lane: LaneState; env: NodeJS.ProcessEnv; logPath: string; }) { const script = buildWindowsDevUpdateToolchainCheckScript(); const result = await runPowerShellScript(script, { cwd: params.lane.homeDir, env: params.env, logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); if (!parseMarkerLine(result.stdout, "__UPDATE_TOOL__=")) { throw new Error( "No Windows update bootstrap tool (pnpm, corepack, or npm) was discoverable after the dev update.", ); } }