import { parseArgs } from "node:util"; import { api } from "@code/backend/convex/_generated/api"; import { parseSmokeEnv } from "@code/env/smoke"; import type { SmokeEnv } from "@code/env/smoke"; import { advanceSmokeLoop, decodeSmokeChatText, decodeSmokeModelCatalog, decodeSmokePlan, decodeSmokeReview, decodeSmokeWorkerReport, initialSmokeLoopState, redactSmokeText, selectSmokeModelRoles, validateSmokeCapabilities, } from "@code/primitives/smoke"; import type { SmokeCapabilityCheck, SmokeLoopState, SmokeModelRoles, } from "@code/primitives/smoke"; import { createFlueClient } from "@flue/sdk"; import { ConvexHttpClient } from "convex/browser"; import { Effect } from "effect"; const DEFAULT_TIMEOUT_MS = 120_000; const REQUIRED_ARTIFACTS = [ "agent.md", "work.md", "steps.md", "artifacts.md", "signals.md", "agent-manager.md", "context.md", "card.md", ] as const; type SmokeStatus = | "preflight-passed" | "completed" | "contract-blocked" | "failed"; interface SmokeReport { readonly checks: readonly SmokeCapabilityCheck[]; readonly featureRequest: string; readonly issueId?: string; readonly loop: SmokeLoopState; readonly mode: "preflight" | "run"; readonly roles?: Pick; readonly review?: { readonly approved: boolean; readonly findingsCount: number; }; readonly status: SmokeStatus; readonly worker?: { readonly status: "completed" | "blocked" | "failed"; }; } interface ChatMessage { readonly content: string; readonly role: "system" | "user"; } const { values, positionals } = parseArgs({ allowPositionals: true, args: process.argv.slice(2), options: { help: { default: false, short: "h", type: "boolean" }, report: { type: "string" }, run: { default: false, type: "boolean" }, "timeout-ms": { default: String(DEFAULT_TIMEOUT_MS), type: "string" }, }, strict: true, }); const usage = `Usage: bun run smoke:zopu [--run] [--report ] [--timeout-ms ] Without --run, the command probes the control-plane (web/Convex/Flue/AgentOS/CPA) and the Orb execution-plane (Docker/OpenCode/Gitea/repository) contracts. With --run, it creates one issue in the explicit smoke project, drives the worker, and sends the result to the GLM planner/reviewer. Required environment: ZOPU_SMOKE_ACCESS_TOKEN Better Auth/Convex bearer token ZOPU_SMOKE_PROJECT_ID disposable/test project id ZOPU_SMOKE_CPA_API_KEY CPA key (or AGENT_MODEL_API_KEY) ZOPU_SMOKE_CPA_BASE_URL CPA OpenAI-compatible /v1 URL (or AGENT_MODEL_BASE_URL) ZOPU_SMOKE_CONVEX_URL Convex URL (or CONVEX_URL) ZOPU_SMOKE_DAEMON_ID online local daemon id (or DAEMON_ID) Optional environment: ZOPU_SMOKE_FEATURE_REQUEST override the tiny feature request ZOPU_SMOKE_WEB_URL default: SITE_URL or http://localhost:5173 ZOPU_SMOKE_FLUE_URL default: VITE_FLUE_URL or http://localhost:3583 Orb execution-plane environment (probed and reported as BLOCKED when absent): GITEA_URL self-hosted Gitea base URL GITEA_TOKEN Gitea API token for PR creation ZOPU_SMOKE_REPOSITORY_PATH owner/repo slug for the PR-creation probe ORB_DOCKER_IMAGE optional Docker image override ORB_DOCKER_WORKSPACE optional Orb workspace root (default /tmp/orb-*) `; if (values.help || positionals.length > 0) { console.log(usage); process.exit(positionals.length > 0 ? 1 : 0); } const runEffect = (effect: Effect.Effect): A => Effect.runSync(effect); const errorMessage = (error: unknown, secrets: readonly string[]): string => { const message = error instanceof Error ? error.message : String(error); return redactSmokeText(message, secrets).replaceAll(/\s+/gu, " ").trim(); }; const withTimeout = (timeoutMs: number): AbortSignal => AbortSignal.timeout(timeoutMs); const normalizeBaseUrl = (value: string): URL => { const url = new URL(value); if (!url.pathname.endsWith("/")) { url.pathname = `${url.pathname}/`; } return url; }; const endpoint = (baseUrl: URL, path: string): string => new URL(path, baseUrl).toString(); const addCheck = ( checks: SmokeCapabilityCheck[], id: string, passed: boolean, message: string ) => { checks.push({ id, message, passed }); }; const probeHttp = async ( checks: SmokeCapabilityCheck[], id: string, label: string, url: string, timeoutMs: number ): Promise => { try { const response = await fetch(url, { redirect: "manual", signal: withTimeout(timeoutMs), }); const passed = response.status >= 200 && response.status < 400; addCheck( checks, id, passed, passed ? `${label} responded with HTTP ${response.status}` : `${label} returned HTTP ${response.status}` ); } catch (error) { addCheck( checks, id, false, `${label} is unreachable: ${errorMessage(error, [])}` ); } }; const callOpenAiChat = async (input: { readonly apiKey: string; readonly baseUrl: URL; readonly messages: readonly ChatMessage[]; readonly model: string; readonly timeoutMs: number; }): Promise => { const response = await fetch(endpoint(input.baseUrl, "chat/completions"), { body: JSON.stringify({ max_tokens: 512, messages: input.messages, model: input.model, temperature: 0, }), headers: { Authorization: `Bearer ${input.apiKey}`, "Content-Type": "application/json", }, method: "POST", signal: withTimeout(input.timeoutMs), }); if (!response.ok) { throw new Error( `CPA ${input.model} completion returned HTTP ${response.status}` ); } const body = (await response.json()) as unknown; return await Effect.runPromise(decodeSmokeChatText(body)); }; const parseJsonObject = (text: string): unknown => { const withoutFence = text .trim() .replace(/^```(?:json)?\s*/iu, "") .replace(/\s*```$/u, ""); const start = withoutFence.indexOf("{"); const end = withoutFence.lastIndexOf("}"); if (start === -1 || end <= start) { throw new Error("model response did not contain a JSON object"); } return JSON.parse(withoutFence.slice(start, end + 1)) as unknown; }; const buildPlanPrompt = (featureRequest: string): readonly ChatMessage[] => [ { content: "You are the planner/reviewer for a disposable Zopu web-project smoke. Return JSON only with summary, steps, acceptanceCriteria, and nonGoals. Keep the plan tiny and testable.", role: "system", }, { content: featureRequest, role: "user" }, ]; const buildWorkerPrompt = (input: { readonly featureRequest: string; readonly plan: { readonly acceptanceCriteria: readonly string[]; readonly nonGoals: readonly string[]; readonly steps: readonly string[]; readonly summary: string; }; }): string => `Execute this one disposable web-project smoke issue. Feature request: ${input.featureRequest} Plan: ${JSON.stringify(input.plan)} Rules: - Work only in the bound project workspace. - Use one tool call at a time; never issue parallel tool calls because this worker is minimax-m3. - Inspect the bound repository before editing. - Run the narrowest relevant test or check. - Do not claim completion without observed changed files and command evidence. - Return JSON only: {"status":"completed"|"blocked"|"failed","summary":"..."}`; const buildReviewPrompt = (input: { readonly featureRequest: string; readonly plan: unknown; readonly workerReport: unknown; }): readonly ChatMessage[] => [ { content: "You are the independent GLM-5.2 planner/reviewer. Review the worker report against the feature request and plan. Return JSON only with approved, summary, and findings (an array of concise strings). Reject missing evidence or a non-completed worker.", role: "system", }, { content: JSON.stringify({ featureRequest: input.featureRequest, plan: input.plan, workerReport: input.workerReport, }), role: "user", }, ]; const printReport = (report: SmokeReport): void => { console.log(`ZOPU_SMOKE_${report.status.toUpperCase().replaceAll("-", "_")}`); console.log(JSON.stringify(report, null, 2)); }; const writeReport = async ( path: string | undefined, report: SmokeReport ): Promise => { if (path !== undefined) { await Bun.write(path, `${JSON.stringify(report, null, 2)}\n`); } }; const waitForIssueStatus = async ( convex: ConvexHttpClient, projectId: string, issueId: string, timeoutMs: number ): Promise => { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { // oxlint-disable-next-line no-await-in-loop -- polling intentionally waits between durable reads. const issues = await convex.query(api.projectIssues.list, { projectId: projectId as never, }); const issue = issues.find((candidate) => candidate._id === issueId); if (issue?.status === "completed" || issue?.status === "failed") { return issue.status; } // oxlint-disable-next-line no-await-in-loop -- polling intentionally waits between durable reads. await Bun.sleep(500); } return null; }; const probeModelRoles = ( env: SmokeEnv, cpaBaseUrl: URL, checks: SmokeCapabilityCheck[], secrets: readonly string[] ): SmokeModelRoles | undefined => { try { const roles = runEffect( selectSmokeModelRoles({ plannerReviewerModel: "glm-5.2", plannerReviewerProvider: "cheaptricks", workerModel: env.agentModelName ?? "", workerProvider: env.agentModelProvider ?? "cheaptricks", workerToolCallMode: "serial", }) ); addCheck( checks, "model-roles", true, "worker=minimax-m3; planner-reviewer=glm-5.2" ); const workerGatewayMatches = env.agentModelBaseUrl !== undefined && normalizeBaseUrl(env.agentModelBaseUrl).toString() === cpaBaseUrl.toString(); addCheck( checks, "worker-gateway-config", workerGatewayMatches, workerGatewayMatches ? "the local worker points at the configured CPA base URL" : "AGENT_MODEL_BASE_URL must match the CPA base URL; the harness will not rewrite it" ); return roles; } catch (error) { addCheck(checks, "model-roles", false, errorMessage(error, secrets)); addCheck( checks, "worker-gateway-config", false, "worker gateway configuration could not be checked because the model role contract failed" ); return undefined; } }; const probeControlPlane = async (input: { readonly checks: SmokeCapabilityCheck[]; readonly convex: ConvexHttpClient; readonly daemonId: string; readonly projectId: string | undefined; readonly secrets: readonly string[]; }): Promise => { const { checks, convex, daemonId, projectId, secrets } = input; try { const health = await convex.query(api.healthCheck.get, {}); addCheck( checks, "convex", health === "OK", health === "OK" ? "Convex health query returned OK" : "Convex health query returned an unexpected value" ); } catch (error) { addCheck( checks, "convex", false, `Convex health query failed: ${errorMessage(error, secrets)}` ); } try { const organization = await convex.query(api.organizations.getCurrent, {}); addCheck( checks, "organization", organization !== null, organization ? "authenticated personal organization is available" : "no personal organization; sign in through the web app before running the smoke" ); } catch (error) { addCheck( checks, "organization", false, `organization query failed: ${errorMessage(error, secrets)}` ); } if (projectId === undefined) { addCheck( checks, "project-contract", false, "ZOPU_SMOKE_PROJECT_ID is required and must point at a disposable/test project" ); } else { // eslint-disable-next-line no-use-before-define -- the project probe is a focused helper below the control-plane flow. await probeProjectContract({ checks, convex, projectId, secrets }); } try { const daemon = await convex.query(api.daemons.get, { daemonId }); const online = daemon.presence?.status === "online"; addCheck( checks, "agentos-daemon", online, online ? `daemon ${daemonId} is online` : `daemon ${daemonId} is not online; start the local AgentOS daemon without changing CPA` ); } catch (error) { addCheck( checks, "agentos-daemon", false, `daemon probe failed: ${errorMessage(error, secrets)}` ); } }; const probeProjectContract = async (input: { readonly checks: SmokeCapabilityCheck[]; readonly convex: ConvexHttpClient; readonly projectId: string; readonly secrets: readonly string[]; }): Promise => { const { checks, convex, projectId, secrets } = input; try { const project = await convex.query(api.projects.get, { projectId: projectId as never, }); const hasSource = (project?.sources.length ?? 0) > 0; let projectMessage = "project has no connected repository source; land the project backend lane first"; if (project === null) { projectMessage = "project was not found or is not accessible to the supplied token"; } else if (hasSource) { projectMessage = "project source is available"; } addCheck( checks, "project-contract", project !== null && hasSource, projectMessage ); const artifacts = await convex.query(api.projectArtifacts.list, { projectId: projectId as never, }); const paths = new Set(artifacts.map((artifact) => artifact.path)); const missing = REQUIRED_ARTIFACTS.filter((path) => !paths.has(path)); addCheck( checks, "agentos-workspace-contract", missing.length === 0, missing.length === 0 ? "issue-scoped AgentOS artifact contract is present" : `missing AgentOS artifacts: ${missing.join(", ")}; land the project loop lane first` ); } catch (error) { addCheck( checks, "project-contract", false, `project query failed: ${errorMessage(error, secrets)}` ); addCheck( checks, "agentos-workspace-contract", false, "project artifacts could not be inspected" ); } }; const probeFlue = async (input: { readonly accessToken: string; readonly checks: SmokeCapabilityCheck[]; readonly flueUrl: string; readonly secrets: readonly string[]; }): Promise | undefined> => { const { accessToken, checks, flueUrl, secrets } = input; const flueClient = createFlueClient({ baseUrl: flueUrl, headers: { Authorization: `Bearer ${accessToken}` }, }); try { await flueClient.agents.history( "project-manager", "zopu-smoke-capability-probe" ); addCheck( checks, "flue", true, "project-manager history endpoint is reachable" ); return flueClient; } catch (error) { const message = errorMessage(error, secrets); const streamMissing = message.includes("[stream_not_found]"); addCheck( checks, "flue", streamMissing, streamMissing ? "project-manager history route is reachable; the probe stream is absent as expected" : `Flue project-manager endpoint failed: ${message}` ); return streamMissing ? flueClient : undefined; } }; const probeGateway = async (input: { readonly apiKey: string; readonly baseUrl: URL; readonly checks: SmokeCapabilityCheck[]; readonly secrets: readonly string[]; readonly timeoutMs: number; }): Promise => { const { apiKey, baseUrl, checks, secrets, timeoutMs } = input; try { const response = await fetch(endpoint(baseUrl, "models"), { headers: { Authorization: `Bearer ${apiKey}` }, signal: withTimeout(timeoutMs), }); if (!response.ok) { throw new Error(`CPA models returned HTTP ${response.status}`); } const catalog = await Effect.runPromise( decodeSmokeModelCatalog(await response.json()) ); const modelIds = new Set(catalog.data.map((entry) => entry.id)); const missing = ["minimax-m3", "glm-5.2"].filter( (model) => !modelIds.has(model) ); addCheck( checks, "cpa-model-catalog", missing.length === 0, missing.length === 0 ? "CPA exposes minimax-m3 and glm-5.2" : `CPA is missing canonical models: ${missing.join(", ")}` ); } catch (error) { addCheck( checks, "cpa-model-catalog", false, `CPA model probe failed: ${errorMessage(error, secrets)}` ); } for (const model of ["minimax-m3", "glm-5.2"] as const) { try { // oxlint-disable-next-line no-await-in-loop -- M3 must never be probed in parallel with another worker call. const text = await callOpenAiChat({ apiKey, baseUrl, messages: [{ content: "Reply with exactly OK.", role: "user" }], model, timeoutMs, }); addCheck( checks, `cpa-completion-${model}`, text.trim().length > 0, `${model} accepted a no-tools OpenAI-compatible completion` ); } catch (error) { addCheck( checks, `cpa-completion-${model}`, false, errorMessage(error, secrets) ); } } }; // --------------------------------------------------------------------------- // Orb-path preflight probes — Docker, OpenCode, Git/Gitea, repository writable // // These probes check the Orb execution-plane dependencies that the // OrbProjectManager relies on. They are intentionally separate from the // control-plane/gateway probes: a missing Docker daemon or Gitea token // reports as a BLOCKED preflight item, never a fake success. // --------------------------------------------------------------------------- const probeDocker = async (checks: SmokeCapabilityCheck[]): Promise => { try { const proc = Bun.spawn( ["docker", "version", "--format", "{{.Server.Version}}"], { stderr: "pipe", stdout: "pipe" } ); const stdout = await new Response(proc.stdout).text(); const exitCode = await proc.exited; const version = stdout.trim(); if (exitCode === 0 && version.length > 0) { addCheck( checks, "docker-daemon", true, `Docker server version ${version}` ); } else { addCheck( checks, "docker-daemon", false, "Docker daemon is not running; start Docker Desktop or dockerd" ); } } catch (error) { addCheck( checks, "docker-daemon", false, `Docker CLI unavailable: ${errorMessage(error, [])}` ); } }; const probeOpenCodePackage = async ( checks: SmokeCapabilityCheck[] ): Promise => { try { await import("@agentos-software/opencode"); addCheck( checks, "opencode-package", true, "@agentos-software/opencode is resolvable" ); } catch { addCheck( checks, "opencode-package", false, "@agentos-software/opencode is not installed; Orb AgentOS VM cannot link OpenCode" ); } }; const probeGitea = async (input: { readonly checks: SmokeCapabilityCheck[]; readonly giteaToken: string | undefined; readonly giteaUrl: string | undefined; readonly repositoryPath: string | undefined; readonly secrets: readonly string[]; readonly timeoutMs: number; }): Promise => { const { checks, giteaToken, giteaUrl, repositoryPath, secrets, timeoutMs } = input; if (giteaUrl === undefined) { addCheck( checks, "gitea-reachable", false, "GITEA_URL is not set; set it to the self-hosted Gitea base URL for the Orb Git lifecycle" ); } else { try { const response = await fetch( `${giteaUrl.replace(/\/$/u, "")}/api/v1/version`, { headers: giteaToken ? { Authorization: `token ${giteaToken}` } : {}, signal: withTimeout(timeoutMs), } ); addCheck( checks, "gitea-reachable", response.ok, response.ok ? "Gitea API version endpoint is reachable" : `Gitea API returned HTTP ${response.status}` ); } catch (error) { addCheck( checks, "gitea-reachable", false, `Gitea API is unreachable: ${errorMessage(error, secrets)}` ); } } if (giteaUrl === undefined || giteaToken === undefined) { addCheck( checks, "gitea-creds-valid", false, giteaToken === undefined ? "GITEA_TOKEN is not set; the Orb Git lifecycle needs it to create pull requests" : "Gitea URL is not configured; cannot validate token" ); } else { try { const response = await fetch( `${giteaUrl.replace(/\/$/u, "")}/api/v1/user`, { headers: { Authorization: `token ${giteaToken}` }, signal: withTimeout(timeoutMs), } ); addCheck( checks, "gitea-creds-valid", response.ok, response.ok ? "Gitea token is valid" : `Gitea token returned HTTP ${response.status}` ); } catch (error) { addCheck( checks, "gitea-creds-valid", false, `Gitea credential probe failed: ${errorMessage(error, secrets)}` ); } } if ( repositoryPath === undefined || giteaToken === undefined || giteaUrl === undefined ) { addCheck( checks, "gitea-pr-creation", false, repositoryPath === undefined ? "ZOPU_SMOKE_REPOSITORY_PATH (owner/repo) is not set; cannot probe PR creation" : "Gitea URL or token is missing; cannot probe PR creation" ); } else { try { const path = repositoryPath .split("/") .map((segment) => encodeURIComponent(segment)) .join("/"); const response = await fetch( `${giteaUrl.replace(/\/$/u, "")}/api/v1/repos/${path}`, { headers: { Authorization: `token ${giteaToken}` }, signal: withTimeout(timeoutMs), } ); addCheck( checks, "gitea-pr-creation", response.ok, response.ok ? `Repository ${repositoryPath} is accessible for PR creation` : `Repository ${repositoryPath} returned HTTP ${response.status}` ); } catch (error) { addCheck( checks, "gitea-pr-creation", false, `Gitea repository probe failed: ${errorMessage(error, secrets)}` ); } } }; const probeRepositoryWritable = async ( checks: SmokeCapabilityCheck[] ): Promise => { const { mkdir, rm, writeFile } = await import("node:fs/promises"); const workspace = process.env.ORB_DOCKER_WORKSPACE ?? "/tmp/orb-smoke-workspace-probe"; try { await mkdir(workspace, { recursive: true }); const probe = `${workspace}/.orb-write-probe`; await writeFile(probe, "ok"); await rm(probe); addCheck( checks, "repository-writable", true, `Orb workspace root ${workspace} is writable` ); } catch (error) { addCheck( checks, "repository-writable", false, `Orb workspace root is not writable: ${errorMessage(error, [])}` ); } }; const probeOrbCapabilities = async (input: { readonly checks: SmokeCapabilityCheck[]; readonly env: SmokeEnv; readonly secrets: readonly string[]; readonly timeoutMs: number; }): Promise => { const { checks, env, secrets, timeoutMs } = input; await probeDocker(checks); await probeOpenCodePackage(checks); await probeGitea({ checks, giteaToken: env.giteaToken, giteaUrl: env.giteaUrl, repositoryPath: env.repositoryPath, secrets, timeoutMs, }); await probeRepositoryWritable(checks); }; const runSmoke = async (input: { readonly baseReport: Omit; readonly convex: ConvexHttpClient; readonly cpaBaseUrl: URL; readonly env: SmokeEnv; readonly flueClient: ReturnType; readonly loop: SmokeLoopState; readonly roles: SmokeModelRoles; readonly secrets: readonly string[]; readonly timeoutMs: number; }): Promise => { const { baseReport, convex, cpaBaseUrl, env, flueClient, roles, secrets, timeoutMs, } = input; const { projectId } = env; if (projectId === undefined) { throw new Error("run requested without ZOPU_SMOKE_PROJECT_ID"); } let { loop } = input; let issueId: string | undefined; let worker: SmokeReport["worker"]; let review: SmokeReport["review"]; try { const planText = await callOpenAiChat({ apiKey: env.cpaApiKey, baseUrl: cpaBaseUrl, messages: buildPlanPrompt(env.featureRequest), model: roles.plannerReviewer.model, timeoutMs, }); const plan = await Effect.runPromise( decodeSmokePlan(parseJsonObject(planText)) ); loop = runEffect(advanceSmokeLoop(loop, "plan-validated")); issueId = await convex.mutation(api.projectIssues.create, { body: `${env.featureRequest}\n\nSmoke plan:\n${JSON.stringify(plan)}`, projectId: projectId as never, title: "Zopu smoke: tiny web feature", }); await convex.mutation(api.projectIssues.begin, { issueId: issueId as never, }); const workerResult = await flueClient.agents.prompt( "project-manager", issueId, { message: buildWorkerPrompt({ featureRequest: env.featureRequest, plan, }), signal: withTimeout(timeoutMs), } ); const workerReport = await Effect.runPromise( decodeSmokeWorkerReport(parseJsonObject(workerResult.result.text)) ); worker = { status: workerReport.status }; if (workerReport.status !== "completed") { throw new Error( `worker returned ${workerReport.status}: ${workerReport.summary}` ); } loop = runEffect(advanceSmokeLoop(loop, "worker-succeeded")); const issueStatus = await waitForIssueStatus( convex, projectId, issueId, timeoutMs ); if (issueStatus !== "completed") { throw new Error( `worker reported completed but durable issue status is ${issueStatus ?? "missing"}` ); } const reviewText = await callOpenAiChat({ apiKey: env.cpaApiKey, baseUrl: cpaBaseUrl, messages: buildReviewPrompt({ featureRequest: env.featureRequest, plan, workerReport, }), model: roles.plannerReviewer.model, timeoutMs, }); const reviewReport = await Effect.runPromise( decodeSmokeReview(parseJsonObject(reviewText)) ); review = { approved: reviewReport.approved, findingsCount: reviewReport.findings.length, }; if (!reviewReport.approved) { throw new Error( `GLM review rejected the worker result: ${reviewReport.summary}` ); } loop = runEffect(advanceSmokeLoop(loop, "review-approved")); } catch (error) { const message = errorMessage(error, secrets); if (issueId !== undefined) { await convex.mutation(api.projectIssues.markDispatchFailed, { error: message, issueId: issueId as never, }); } loop = runEffect(advanceSmokeLoop(loop, "runtime-failed", message)); const report: SmokeReport = { ...baseReport, issueId, loop, status: "failed", ...(review === undefined ? {} : { review }), ...(worker === undefined ? {} : { worker }), }; await writeReport(values.report, report); printReport(report); return 1; } const report: SmokeReport = { ...baseReport, issueId, loop, review, status: "completed", worker, }; await writeReport(values.report, report); printReport(report); return 0; }; const main = async (): Promise => { const timeoutMs = Number(values["timeout-ms"] ?? DEFAULT_TIMEOUT_MS); if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) { console.error( "ZOPU_SMOKE_CONTRACT_BLOCKED: --timeout-ms must be a positive integer" ); return 2; } let env: SmokeEnv; try { env = parseSmokeEnv(process.env); } catch (error) { console.error( `ZOPU_SMOKE_CONTRACT_BLOCKED: environment contract failed: ${errorMessage(error, [])}` ); return 2; } const secrets = [env.accessToken, env.cpaApiKey]; const checks: SmokeCapabilityCheck[] = []; const convex = new ConvexHttpClient(env.convexUrl); convex.setAuth(env.accessToken); const cpaBaseUrl = normalizeBaseUrl(env.cpaBaseUrl); const roles = probeModelRoles(env, cpaBaseUrl, checks, secrets); await probeHttp(checks, "web", "web app", env.webUrl, timeoutMs); await probeControlPlane({ checks, convex, daemonId: env.daemonId, projectId: env.projectId, secrets, }); const flueClient = await probeFlue({ accessToken: env.accessToken, checks, flueUrl: env.flueUrl, secrets, }); await probeGateway({ apiKey: env.cpaApiKey, baseUrl: cpaBaseUrl, checks, secrets, timeoutMs, }); await probeOrbCapabilities({ checks, env, secrets, timeoutMs, }); const initialLoop = initialSmokeLoopState(); let loop: SmokeLoopState; try { runEffect(validateSmokeCapabilities(checks)); loop = runEffect(advanceSmokeLoop(initialLoop, "preflight-passed")); } catch (error) { loop = runEffect( advanceSmokeLoop( initialLoop, "contract-failed", errorMessage(error, secrets) ) ); } const baseReport = { checks, featureRequest: redactSmokeText(env.featureRequest, secrets), loop, mode: values.run ? ("run" as const) : ("preflight" as const), ...(roles === undefined ? {} : { roles }), }; if (loop.phase === "failed") { const report = { ...baseReport, status: "contract-blocked" as const }; await writeReport(values.report, report); printReport(report); return 2; } if (!values.run) { const report = { ...baseReport, status: "preflight-passed" as const }; await writeReport(values.report, report); printReport(report); return 0; } if (roles === undefined || flueClient === undefined) { throw new Error("run prerequisites were not retained after preflight"); } return await runSmoke({ baseReport, convex, cpaBaseUrl, env, flueClient, loop, roles, secrets, timeoutMs, }); }; const exitCode = await main().catch((error: unknown) => { console.error(`ZOPU_SMOKE_FAILED: ${errorMessage(error, [])}`); return 1; }); process.exitCode = exitCode;