// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. // Responsibility: Source for the authenticated HWLAB node web-probe runner. export function nodeWebProbeScriptRunnerSource(): string { return String.raw`#!/usr/bin/env node import { createHash } from "node:crypto"; import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; const startedAtMs = Date.now(); const startedAt = new Date(startedAtMs).toISOString(); const baseUrl = normalizeBaseUrl(process.env.HWLAB_WEB_BASE_URL); const username = process.env.HWLAB_WEB_USER || "admin"; const password = process.env.HWLAB_WEB_PASS || ""; const runDir = path.resolve(process.env.UNIDESK_WEB_PROBE_RUN_DIR || ".state/web-probe-script/run-manual"); const userScript = path.resolve(process.env.UNIDESK_WEB_PROBE_USER_SCRIPT || path.join(runDir, "user-script.mjs")); const timeoutMs = positiveInteger(process.env.UNIDESK_WEB_PROBE_TIMEOUT_MS, 30000); const viewport = parseViewport(process.env.UNIDESK_WEB_PROBE_VIEWPORT || "1440x900"); const artifactRecords = []; const readinessRecords = []; const stepRecords = []; let browser; let context; let page; let auth = null; let cookieSecrets = []; let userScriptSha256 = null; try { if (!password) throw new Error("missing HWLAB_WEB_PASS"); await mkdir(runDir, { recursive: true, mode: 0o700 }); userScriptSha256 = await sha256File(userScript).catch(() => null); const launcher = await import(pathToFileURL(path.resolve("scripts/src/browser-launcher.mjs")).href); const { chromium } = await launcher.importPlaywright(); browser = await launcher.launchChromium(chromium); context = await browser.newContext({ viewport }); auth = await authenticate(context); if (!auth.ok) throw new Error("auth-login-failed"); page = await context.newPage(); const mod = await import(pathToFileURL(userScript).href + "?t=" + Date.now()); const fn = typeof mod.default === "function" ? mod.default : typeof mod.run === "function" ? mod.run : typeof mod.probe === "function" ? mod.probe : null; if (fn === null) throw new Error("custom script must export default, run, or probe function"); const scriptResult = await fn(scriptHelpers()); const safeResult = sanitize(scriptResult); const scriptOk = !(safeResult && typeof safeResult === "object" && safeResult.ok === false); const failure = scriptOk ? null : classifiedProbeError(assertionResultError(safeResult)); const lastScreenshot = scriptOk ? null : await captureFailureScreenshot("failure.png"); const lastUrl = currentPageUrl(); await emit({ ok: scriptOk, status: scriptOk ? "pass" : "blocked", command: "web-probe-script", generatedAt: new Date().toISOString(), startedAt, baseUrl, finalUrl: lastUrl, lastUrl, scriptSha256: userScriptSha256, runDir, auth: publicAuth(auth), script: { ok: scriptOk, result: safeResult, steps: publicSteps() }, steps: publicSteps(), failureKind: failure ? failure.failureKind : null, error: failure ? failure.code : null, errorMessage: failure ? sanitize(failure.message) : null, guidance: failure ? failure.guidance : null, lastScreenshot, readiness: publicReadiness(failure ? { error: failure.code, failureKind: failure.failureKind } : {}), artifacts: { runDir, items: artifactRecords }, safety: { valuesRedacted: true, secretValuesPrinted: false } }); process.exitCode = scriptOk ? 0 : 2; } catch (error) { const failure = classifiedProbeError(error); const lastScreenshot = await captureFailureScreenshot("failure.png"); const lastUrl = currentPageUrl(); await emit({ ok: false, status: "blocked", command: "web-probe-script", generatedAt: new Date().toISOString(), startedAt, baseUrl, finalUrl: lastUrl, lastUrl, scriptSha256: userScriptSha256, runDir, auth: auth === null ? null : publicAuth(auth), script: { ok: false, steps: publicSteps() }, steps: publicSteps(), failureKind: failure.failureKind, error: failure.code, errorMessage: sanitize(failure.message), guidance: failure.guidance, lastScreenshot, readiness: publicReadiness({ error: failure.code, failureKind: failure.failureKind }), artifacts: { runDir, items: artifactRecords }, safety: { valuesRedacted: true, secretValuesPrinted: false } }); process.exitCode = 2; } finally { if (browser) await browser.close().catch(() => {}); } async function authenticate(browserContext) { const apiAuth = await authenticateWithApiRetries(browserContext); if (apiAuth.ok) return apiAuth; return authenticateWithFormFallback(browserContext, apiAuth); } async function authenticateWithApiRetries(browserContext) { const loginUrl = new URL("/auth/login", baseUrl).toString(); const attempts = []; const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { const response = await browserContext.request.post(loginUrl, { data: { username, password }, headers: { accept: "application/json", "content-type": "application/json" }, timeout: Math.min(timeoutMs, 12000), }); const summary = await responseSummary(response); const cookieState = await readAuthCookieState(browserContext); const retryable = isRetryableAuthStatus(response.status()); const item = { attempt, method: "api", ...summary, retryable, cookiePresent: cookieState.cookiePresent, cookieNames: cookieState.cookieNames, }; attempts.push(item); if (response.ok() && cookieState.cookiePresent) { return { ok: true, method: "api", loginUrl: new URL("/auth/login", baseUrl).pathname, status: response.status(), statusText: response.statusText(), cookiePresent: true, cookieNames: cookieState.cookieNames, attempts, retryCount: attempt - 1, fallbackUsed: false, valuesRedacted: true, }; } if (!retryable) break; } catch (error) { attempts.push({ attempt, method: "api", status: 0, statusText: "request-error", retryable: true, error: error instanceof Error ? error.message : String(error), cookiePresent: false, }); } if (attempt < maxAttempts) await sleep(300 * attempt); } const cookieState = await readAuthCookieState(browserContext); const last = attempts[attempts.length - 1] ?? null; return { ok: false, method: "api", loginUrl: new URL("/auth/login", baseUrl).pathname, status: typeof last?.status === "number" ? last.status : 0, statusText: typeof last?.statusText === "string" ? last.statusText : "api-login-failed", cookiePresent: cookieState.cookiePresent, cookieNames: cookieState.cookieNames, attempts, retryCount: retryCountFromAttempts(attempts), fallbackUsed: false, retryable: authAttemptsRetryable(attempts), errorSummary: last, valuesRedacted: true, }; } async function authenticateWithFormFallback(browserContext, apiAuth) { const loginPage = await browserContext.newPage(); const fallback = { method: "form", startPath: "/workbench", surface: null, finalPath: null, response: null, outcome: null, error: null, }; try { await loginPage.goto(new URL("/workbench", baseUrl).toString(), { waitUntil: "domcontentloaded", timeout: timeoutMs }); const surface = await waitForAuthSurface(loginPage); fallback.surface = surface; if (surface === "workspace") { const cookieState = await readAuthCookieState(browserContext); fallback.finalPath = new URL(loginPage.url()).pathname; return { ok: cookieState.cookiePresent, method: "form-fallback", loginUrl: new URL("/auth/login", baseUrl).pathname, status: cookieState.cookiePresent ? 200 : 0, statusText: cookieState.cookiePresent ? "already-authenticated" : "workspace-without-session-cookie", cookiePresent: cookieState.cookiePresent, cookieNames: cookieState.cookieNames, attempts: apiAuth.attempts, retryCount: apiAuth.retryCount ?? retryCountFromAttempts(apiAuth.attempts), fallbackUsed: true, retryable: apiAuth.retryable === true, apiErrorSummary: apiAuth.errorSummary, fallback, valuesRedacted: true, }; } if (surface !== "login-card" && surface !== "legacy-id") { throw new Error("login form not visible"); } const usernameInput = loginPage.locator('#login-username, input[autocomplete="username"], .login-card input').first(); const passwordInput = loginPage.locator('#login-password, input[autocomplete="current-password"], .login-card input[type="password"], input[type="password"]').first(); const submitButton = loginPage.locator('#login-submit, .login-card button[type="submit"], button[type="submit"]').first(); await usernameInput.fill(username); await passwordInput.fill(password); await loginPage.waitForFunction((expected) => { const user = document.querySelector('#login-username, input[autocomplete="username"], .login-card input'); const pass = document.querySelector('#login-password, input[autocomplete="current-password"], .login-card input[type="password"], input[type="password"]'); const submit = document.querySelector('#login-submit, .login-card button[type="submit"], button[type="submit"]'); return Boolean(user && pass && user.value === expected.user && pass.value.length === expected.passwordLength && !submit?.disabled); }, { user: username, passwordLength: password.length }, { timeout: Math.min(timeoutMs, 5000) }).catch(() => null); const loginResponsePromise = loginPage.waitForResponse((response) => responsePathMatches(response.url(), "/auth/login"), { timeout: Math.min(timeoutMs, 15000) }).catch(() => null); await Promise.allSettled([ loginPage.waitForFunction(() => document.querySelector("#workspace") || document.querySelector("#command-input"), null, { timeout: Math.min(timeoutMs, 15000) }), submitButton.click(), ]); const loginResponse = await loginResponsePromise; if (loginResponse) fallback.response = await responseSummary(loginResponse); fallback.outcome = await formLoginOutcome(loginPage); fallback.finalPath = new URL(loginPage.url()).pathname; assertNoCredentialLeak(loginPage.url()); const cookieState = await readAuthCookieState(browserContext); return { ok: cookieState.cookiePresent, method: "form-fallback", loginUrl: new URL("/auth/login", baseUrl).pathname, status: fallback.response?.status ?? (cookieState.cookiePresent ? 200 : 0), statusText: fallback.response?.statusText ?? (cookieState.cookiePresent ? "OK" : "form-login-no-cookie"), cookiePresent: cookieState.cookiePresent, cookieNames: cookieState.cookieNames, attempts: apiAuth.attempts, retryCount: apiAuth.retryCount ?? retryCountFromAttempts(apiAuth.attempts), fallbackUsed: true, retryable: apiAuth.retryable === true, apiErrorSummary: apiAuth.errorSummary, fallback, valuesRedacted: true, }; } catch (error) { fallback.error = error instanceof Error ? error.message : String(error); fallback.finalPath = loginPage ? new URL(loginPage.url()).pathname : null; const cookieState = await readAuthCookieState(browserContext).catch(() => ({ cookiePresent: false, cookieNames: [] })); return { ok: false, method: "form-fallback", loginUrl: new URL("/auth/login", baseUrl).pathname, status: fallback.response?.status ?? 0, statusText: fallback.response?.statusText ?? "form-login-failed", cookiePresent: cookieState.cookiePresent, cookieNames: cookieState.cookieNames, attempts: apiAuth.attempts, retryCount: apiAuth.retryCount ?? retryCountFromAttempts(apiAuth.attempts), fallbackUsed: true, retryable: apiAuth.retryable === true, apiErrorSummary: apiAuth.errorSummary, fallback, valuesRedacted: true, }; } finally { await loginPage.close().catch(() => {}); } } async function waitForAuthSurface(targetPage) { const handle = await targetPage.waitForFunction(() => { if (document.querySelector("#workspace") || document.querySelector("#command-input")) return "workspace"; if (document.querySelector("#login-username")) return "legacy-id"; if (document.querySelector('input[autocomplete="username"]') && document.querySelector('input[autocomplete="current-password"]')) return "login-card"; if (document.querySelector(".login-card input")) return "login-card"; return ""; }, null, { timeout: Math.min(timeoutMs, 12000) }).catch(() => null); if (!handle) return null; return handle.jsonValue().catch(() => null); } async function formLoginOutcome(targetPage) { const handle = await targetPage.waitForFunction(() => { if (document.querySelector("#workspace") || document.querySelector("#command-input")) return { kind: "workspace" }; const errorText = document.querySelector(".form-error, .login-error, [role='alert'], .error, .text-red-500, .text-destructive")?.textContent?.trim(); if (errorText) return { kind: "error", errorText }; return null; }, null, { timeout: Math.min(timeoutMs, 5000) }).catch(() => null); if (!handle) return null; return handle.jsonValue().catch(() => null); } function scriptHelpers() { const livePage = createLivePageProxy(); const helpers = { browser, context, page: livePage, baseUrl, runDir, auth: publicAuth(auth), artifactPath, screenshot, screenshotOnError, jsonArtifact, sha256File, wait: sleep, goto, gotoRaw, gotoStable, reloadStable, safeReload, gotoCurrentStable, waitWorkbenchReady, waitForReady, fetchJson, safeFetchJson, fetchApiMatrix, recordStep, collectText, safeEvaluate, summarizeWorkspace, summarizeConversation, getPage: () => page, request: context.request, }; Object.defineProperty(helpers, "currentPage", { enumerable: false, get: () => page, }); return helpers; } function createLivePageProxy() { return new Proxy({}, { get(_target, prop) { if (prop === "then") return undefined; const target = page; if (!target) throw stableProbeError("browser-load-jitter", "browser page is not initialized", publicReadiness({ error: "browser-page-missing" })); const value = target[prop]; return typeof value === "function" ? value.bind(target) : value; }, set(_target, prop, value) { const target = page; if (!target) throw stableProbeError("browser-load-jitter", "browser page is not initialized", publicReadiness({ error: "browser-page-missing" })); target[prop] = value; return true; }, has(_target, prop) { return Boolean(page && prop in page); } }); } function recordStep(name, data = {}, options = {}) { const normalizedName = String(name || "step").replace(/[^A-Za-z0-9_.:-]/gu, "-").slice(0, 80) || "step"; const item = { index: stepRecords.length, name: normalizedName, ok: data && typeof data === "object" && typeof data.ok === "boolean" ? data.ok : null, atMs: Date.now() - startedAtMs, data: sanitize(data), }; stepRecords.push(item); while (stepRecords.length > boundedInteger(options.maxSteps, 80, 1, 200)) stepRecords.shift(); return deepClonePlain(item); } function publicSteps() { return stepRecords.slice(-50).map((item) => deepClonePlain(item)); } async function safeFetchJson(target, options = {}) { try { return await fetchJson(target, { ...normalizeHelperOptions(options), throwOnError: false }); } catch (error) { const failure = classifiedProbeError(error); return { ok: false, path: typeof target === "string" ? urlPath(target) : String(target ?? ""), status: 0, statusText: "helper-error", failureKind: failure.failureKind, error: sanitize(failure.message), guidance: failure.guidance, }; } } async function fetchApiMatrix(paths, options = {}) { options = normalizeHelperOptions(options); const items = normalizeApiMatrixItems(paths); const rows = []; for (const item of items) { const response = await safeFetchJson(item.path, { headers: item.headers ?? options.headers, method: item.method ?? options.method, body: item.body ?? undefined, }); rows.push(compactApiMatrixResponse(item, response)); } const failed = rows.filter((row) => row.ok !== true); const matrix = { ok: failed.length === 0, count: rows.length, okCount: rows.length - failed.length, failedCount: failed.length, items: rows, }; if (options.record !== false) recordStep(typeof options.name === "string" ? options.name : "api-matrix", matrix); return matrix; } function normalizeApiMatrixItems(value) { const list = Array.isArray(value) ? value : [value]; return list.slice(0, 20).map((item, index) => { if (typeof item === "string") return { name: item, path: item, method: "GET" }; if (item && typeof item === "object") { const path = typeof item.path === "string" ? item.path : typeof item.url === "string" ? item.url : "/"; return { name: typeof item.name === "string" ? item.name : path, path, method: typeof item.method === "string" ? item.method : "GET", headers: item.headers, body: item.body, }; } return { name: "item-" + index, path: "/", method: "GET" }; }); } function compactApiMatrixResponse(item, response) { const body = response && typeof response.body === "object" && response.body !== null ? response.body : null; return { name: item.name, path: response?.path ?? item.path, method: item.method ?? "GET", ok: response?.ok === true, status: response?.status ?? null, statusText: response?.statusText ?? null, error: response?.error ?? response?.parseError ?? null, failureKind: response?.failureKind ?? null, bodyKeys: body && !Array.isArray(body) ? Object.keys(body).slice(0, 20) : [], bodyPreview: body ? normalizeTextPreview(JSON.stringify(sanitize(body)), 240) : response?.textPreview ?? null, }; } async function safeEvaluate(fn, arg = undefined, options = {}) { options = normalizeHelperOptions(options); if (typeof fn !== "function") { return { ok: false, failureKind: "script-api-misuse", error: "safeEvaluate requires a function", guidance: evaluateSingleArgGuidance(), }; } try { return { ok: true, value: sanitize(await page.evaluate(fn, arg)) }; } catch (error) { const failure = classifiedProbeError(markEvaluateError(error)); if (options.throwOnError === true) throw markEvaluateError(error); return { ok: false, failureKind: failure.failureKind, error: sanitize(failure.message), guidance: failure.guidance, }; } } async function fetchJson(target, options = {}) { options = normalizeHelperOptions(options); let url; try { url = new URL(String(target || "/"), baseUrl).toString(); } catch (error) { return { ok: false, path: String(target ?? ""), status: 0, statusText: "invalid-url", error: error instanceof Error ? error.message : String(error), }; } const headers = normalizeFetchHeaders(options.headers); const method = typeof options.method === "string" && options.method.length > 0 ? options.method : "GET"; const body = normalizeFetchBody(options.body, headers); const evaluated = await safeEvaluate(async ({ url: requestUrl, method: requestMethod, headers: requestHeaders, body: requestBody }) => { try { const response = await fetch(requestUrl, { method: requestMethod, headers: requestHeaders, body: requestBody === null ? undefined : requestBody, credentials: "include", }); const text = await response.text(); let json = null; let parseError = null; if (text.length > 0) { try { json = JSON.parse(text); } catch (error) { parseError = error instanceof Error ? error.message : String(error); } } return { ok: response.ok, status: response.status, statusText: response.statusText, url: response.url, body: json, textPreview: json === null ? text.slice(0, 1000) : null, parseError, }; } catch (error) { return { ok: false, status: 0, statusText: "fetch-error", error: error instanceof Error ? error.message : String(error), }; } }, { url, method, headers, body }); if (evaluated.ok !== true) { const result = { ok: false, path: urlPath(url), status: 0, statusText: "page-evaluate-failed", failureKind: evaluated.failureKind ?? "script-api-misuse", error: evaluated.error ?? "page.evaluate failed", guidance: evaluated.guidance ?? null, }; if (options.throwOnError === true) throw stableProbeError("api-response-failed", result.error, publicReadiness({ error: "api-response-failed" })); return result; } const response = evaluated.value && typeof evaluated.value === "object" ? evaluated.value : {}; const result = { ok: response.ok === true, path: urlPath(response.url || url), status: Number.isInteger(response.status) ? response.status : 0, statusText: typeof response.statusText === "string" ? response.statusText : null, body: response.body ?? null, textPreview: typeof response.textPreview === "string" ? response.textPreview : null, parseError: typeof response.parseError === "string" ? response.parseError : null, error: typeof response.error === "string" ? response.error : null, }; result.failureKind = result.ok ? null : classifyFetchFailureKind(result); if (options.throwOnError === true && result.ok !== true) { throw stableProbeError(result.failureKind || "api-response-failed", result.error || result.statusText || "fetchJson failed", publicReadiness({ error: result.failureKind || "api-response-failed" })); } return result; } function classifyFetchFailureKind(result) { const text = String(result?.error ?? "") + " " + String(result?.statusText ?? ""); if (Number(result?.status ?? 0) === 0 && /Failed to fetch|fetch-error|network|NetworkError/iu.test(text)) return "same-origin-api-fetch-failed"; if (Number(result?.status ?? 0) === 401 || Number(result?.status ?? 0) === 403) return "auth-or-session-api-failed"; return "api-response-failed"; } async function collectText(selector, options = {}) { options = normalizeHelperOptions(options); const normalizedSelector = String(selector || "").trim(); if (normalizedSelector.length === 0) return { ok: false, selector: normalizedSelector, count: 0, texts: [], error: "selector is required" }; const limit = boundedInteger(options.limit, 20, 1, 200); const textLimit = boundedInteger(options.textLimit, 1000, 80, 6000); const waitTimeoutMs = boundedInteger(options.timeoutMs ?? options.waitTimeoutMs, Math.min(timeoutMs, 5000), 1, Math.max(timeoutMs, 60000)); const state = typeof options.state === "string" ? options.state : "attached"; try { const locator = page.locator(normalizedSelector); if (options.wait !== false) await locator.first().waitFor({ state, timeout: waitTimeoutMs }); const count = await locator.count(); const texts = (await locator.allTextContents()) .slice(0, limit) .map((text) => normalizeTextPreview(text, textLimit)); return { ok: true, selector: normalizedSelector, count, returned: texts.length, texts }; } catch (error) { return { ok: false, selector: normalizedSelector, count: 0, texts: [], error: error instanceof Error ? error.message : String(error), }; } } async function waitWorkbenchReady(options = {}) { options = normalizeHelperOptions(options); const target = typeof options.target === "string" && options.target.length > 0 ? options.target : "/workbench"; const next = { ...options }; delete next.target; if (next.selectors === undefined) next.selectors = ["#workspace", "#command-input"]; const stable = await gotoStable(target, next); const readiness = await ensureWorkbenchComposerReady(next); const result = { ...stable, shellReady: stable.ok === true, sessionReady: readiness.sessionReady, composerReady: readiness.composerReady, composer: readiness.composer, workspace: readiness.workspace, sessionRepair: readiness.sessionRepair }; recordStep("workbench-ready", { ok: result.shellReady && result.sessionReady && result.composerReady, finalUrl: result.finalUrl, shellReady: result.shellReady, sessionReady: result.sessionReady, composerReady: result.composerReady, disabledReason: result.composer?.disabledReason ?? null, sessionRepair: result.sessionRepair }); if (options.throwOnFailure === true && (!result.sessionReady || !result.composerReady)) { const reason = result.composer?.disabledReason || (!result.sessionReady ? "session-not-selected" : "composer-disabled"); throw stableProbeError(reason === "session_required" ? "session-not-selected" : "composer-disabled", "Workbench composer is not ready: " + JSON.stringify({ reason, composer: result.composer, workspace: result.workspace }), publicReadiness({ error: reason })); } return result; } async function ensureWorkbenchComposerReady(options = {}) { const before = await collectWorkbenchReadyState(); let sessionRepair = null; let after = before; if (before.composer.disabledReason === "session_required" && options.ensureSession !== false) { sessionRepair = await createProbeSessionFromUi(options); after = await collectWorkbenchReadyState(); } const timeout = boundedInteger(options.composerTimeoutMs ?? options.readinessTimeoutMs ?? options.timeoutMs, Math.min(timeoutMs, 10000), 1, Math.max(timeoutMs, 60000)); if (!after.composer.ready) { await page.waitForFunction(() => { const input = document.querySelector("#command-input"); const send = document.querySelector("#command-send"); const warning = document.querySelector(".composer-warning")?.textContent?.trim() || ""; const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']"); return Boolean(input && send && activeTab && !input.disabled && !warning); }, null, { timeout }).catch(() => null); after = await collectWorkbenchReadyState(); } return { shellReady: after.shellReady, sessionReady: after.sessionReady, composerReady: after.composer.ready, composer: after.composer, workspace: after.workspace, before, after, sessionRepair }; } async function createProbeSessionFromUi(options = {}) { const create = page.locator("#session-create").first(); if (!(await create.isVisible({ timeout: Math.min(timeoutMs, 5000) }).catch(() => false))) { return { ok: false, method: "ui-click", reason: "session-create-not-visible" }; } const before = await collectWorkbenchReadyState(); await create.click(); const timeout = boundedInteger(options.sessionCreateTimeoutMs ?? options.readinessTimeoutMs ?? options.timeoutMs, Math.min(timeoutMs, 15000), 1, Math.max(timeoutMs, 60000)); await page.waitForFunction((initial) => { const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']"); const sessionId = activeTab?.getAttribute("data-session-id") || ""; const warning = document.querySelector(".composer-warning")?.textContent?.trim() || ""; const input = document.querySelector("#command-input"); return Boolean(activeTab && sessionId && sessionId !== initial.sessionId && input && !input.disabled && !warning); }, { sessionId: before.workspace.activeSessionId }, { timeout }).catch(() => null); const after = await collectWorkbenchReadyState(); return { ok: after.sessionReady && after.composer.ready, method: "ui-click", before: before.workspace, after: after.workspace, composer: after.composer }; } async function collectWorkbenchReadyState() { return page.evaluate(() => { const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']"); const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) || window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u); const input = document.querySelector("#command-input"); const send = document.querySelector("#command-send"); const form = document.querySelector("#command-form"); const warning = document.querySelector(".composer-warning")?.textContent?.trim() || null; const mode = document.querySelector(".composer-mode")?.textContent?.trim() || null; const draft = input && typeof input.value === "string" ? input.value : ""; const sendAction = send?.getAttribute("data-action") || null; const inputVisible = Boolean(input); const sendVisible = Boolean(send); const inputDisabled = input ? Boolean(input.disabled) : null; const sendDisabled = send ? Boolean(send.disabled) : null; const disabledReason = warning || (form?.getAttribute("title") === "session_required" ? "session_required" : null); const sendDisabledReason = disabledReason || (sendDisabled && draft.trim().length === 0 && sendAction !== "cancel" ? "empty_draft" : sendDisabled ? "button_disabled" : null); const sessionId = activeTab?.getAttribute("data-session-id") || null; const conversationId = activeTab?.getAttribute("data-conversation-id") || (routeMatch ? decodeURIComponent(routeMatch[1] || "") : null); const sessionReady = Boolean(sessionId || conversationId); const composerReady = Boolean(inputVisible && sendVisible && inputDisabled === false && !disabledReason && sessionReady); return { shellReady: Boolean(document.querySelector("#workspace") && inputVisible), sessionReady, workspace: { finalUrl: window.location.href, routeConversationId: routeMatch ? decodeURIComponent(routeMatch[1] || "") : null, activeSessionId: sessionId, activeConversationId: conversationId, activeStatus: activeTab?.getAttribute("data-status") || null, tabCount: document.querySelectorAll(".session-tab").length }, composer: { ready: composerReady, inputVisible, inputDisabled, inputLength: draft.length, sendVisible, sendDisabled, sendAction, disabledReason, sendDisabledReason, mode, formTitle: form?.getAttribute("title") || null } }; }); } async function screenshotOnError(name = "failure.png", fn = null, options = {}) { if (typeof name === "function") { options = normalizeHelperOptions(fn); fn = name; name = "failure.png"; } if (typeof fn !== "function") return captureFailureScreenshot(String(name || "failure.png")); try { return await fn(); } catch (error) { const shot = await captureFailureScreenshot(String(name || "failure.png")); if (error && typeof error === "object") error.screenshot = shot; throw error; } } async function summarizeWorkspace(options = {}) { options = normalizeHelperOptions(options); const projectId = typeof options.projectId === "string" && options.projectId.length > 0 ? options.projectId : inferProjectId(); const response = await fetchJson("/v1/workbench/workspace?projectId=" + encodeURIComponent(projectId)); if (response.ok !== true) return { ...response, projectId }; const body = response.body && typeof response.body === "object" ? response.body : {}; const conversations = firstArrayAtPaths(body, ["conversations", "workspace.conversations", "data.conversations"]); return { ok: true, path: response.path, status: response.status, projectId, selectedConversationId: firstStringAtPaths(body, ["selectedConversationId", "workspace.selectedConversationId", "data.selectedConversationId"]), sessionId: firstStringAtPaths(body, ["sessionId", "workspace.sessionId", "data.sessionId"]), conversationCount: conversations ? conversations.length : null, conversationPreview: conversations ? conversations.slice(0, 8).map(summarizeConversationItem) : [], }; } async function summarizeConversation(input = {}) { const options = typeof input === "string" ? { conversationId: input } : normalizeHelperOptions(input); let workspace = null; let conversationId = typeof options.conversationId === "string" && options.conversationId.length > 0 ? options.conversationId : inferConversationIdFromUrl(); if (!conversationId) { workspace = await summarizeWorkspace(options); conversationId = typeof workspace.selectedConversationId === "string" ? workspace.selectedConversationId : null; } if (!conversationId) { return { ok: false, error: "conversationId is required and could not be inferred from the current workbench", workspace }; } const response = await fetchJson("/v1/agent/conversations/" + encodeURIComponent(conversationId)); if (response.ok !== true) return { ...response, conversationId, workspace }; const body = response.body && typeof response.body === "object" ? response.body : {}; const messages = firstArrayAtPaths(body, ["messages", "conversation.messages", "data.messages"]) || []; return { ok: true, path: response.path, status: response.status, conversationId, title: firstStringAtPaths(body, ["title", "conversation.title", "data.title"]), lastTraceId: firstStringAtPaths(body, ["lastTraceId", "conversation.lastTraceId", "data.lastTraceId"]), messageCount: messages.length, lastMessage: messages.length > 0 ? summarizeMessage(messages[messages.length - 1]) : null, }; } async function goto(target = "/workbench", options = {}) { options = normalizeHelperOptions(options); if (options && typeof options === "object" && options.stable === false) return gotoRaw(target, options); const stable = await gotoStable(target, options); return stable.finalUrl; } async function gotoRaw(target = "/workbench", options = {}) { options = normalizeHelperOptions(options); const url = new URL(target, baseUrl).toString(); await page.goto(url, navigationOptions(options)); return page.url(); } async function reloadStable(options = {}) { return stableCurrentPageNavigation("reload-stable", null, normalizeHelperOptions(options)); } async function safeReload(options = {}) { try { return await reloadStable(options); } catch (error) { const failure = classifiedProbeError(error); return { ok: false, failureKind: failure.failureKind, error: failure.code, errorMessage: sanitize(failure.message), guidance: failure.guidance, beforeUrl: error && typeof error === "object" && typeof error.beforeUrl === "string" ? error.beforeUrl : null, finalUrl: currentPageUrl(), readiness: publicReadiness({ error: failure.code, failureKind: failure.failureKind }), }; } } async function gotoCurrentStable(options = {}) { options = normalizeHelperOptions(options); const target = typeof options.url === "string" && options.url.length > 0 ? new URL(options.url, baseUrl).toString() : currentPageUrl(); if (!target) throw stableProbeError("browser-load-jitter", "cannot retry current page navigation because the browser page has no URL", publicReadiness({ error: "browser-page-missing" })); return stableCurrentPageNavigation("goto-current-stable", target, options); } async function stableCurrentPageNavigation(operation, targetUrl, options = {}) { options = normalizeHelperOptions(options); const attempts = boundedInteger(options.attempts, 3, 1, 5); const retryDelayMs = boundedInteger(options.retryDelayMs, 350, 0, 5000); const beforeUrl = currentPageUrl(); let retryUrl = targetUrl; let lastRecord = null; for (let attempt = 1; attempt <= attempts; attempt += 1) { if (!page || page.isClosed()) page = await context.newPage(); const attemptPage = page; const attemptBeforeUrl = currentPageUrl(); if (!retryUrl && !attemptBeforeUrl) { throw stableProbeError("browser-load-jitter", "cannot reload because the browser page has no URL", publicReadiness({ error: "browser-page-missing" })); } const navigationTarget = retryUrl || attemptBeforeUrl; const readinessSelectors = normalizeSelectorList(options.selectors, defaultReadinessSelectors(navigationTarget || baseUrl)); const network = createNetworkTracker(attemptPage, options.apiPattern); const record = { ok: false, attempt, attempts, policy: operation, targetPath: urlPath(navigationTarget), beforePath: urlPath(attemptBeforeUrl), finalPath: null, stage: operation, error: null, message: null, selector: null, selectors: [], apiRequestsSent: false, apiRequestCount: 0, apiResponseFailed: false, apiFailureCount: 0, screenshot: null, durationMs: 0, }; const started = Date.now(); try { if (operation === "reload-stable" && attempt === 1) { await attemptPage.reload(navigationOptions(options)); } else { await attemptPage.goto(navigationTarget, navigationOptions(options)); } retryUrl = attemptPage.url(); record.finalPath = urlPath(attemptPage.url()); const readiness = await waitForReadyInternal(attemptPage, { ...options, selectors: readinessSelectors, url: attemptPage.url(), }, network); applyReadinessToRecord(record, readiness); if (!readiness.ok) { record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options); lastRecord = record; } else { record.ok = true; record.stage = "ready"; lastRecord = record; } } catch (error) { record.finalPath = attemptPage && !attemptPage.isClosed() ? urlPath(attemptPage.url()) : null; record.error = classifyNavigationError(error); record.message = error instanceof Error ? error.message : String(error); record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options); lastRecord = record; } finally { Object.assign(record, network.summary()); record.durationMs = Date.now() - started; network.dispose(); readinessRecords.push(record); } if (record.ok) { const result = { ok: true, operation, attempt, attempts, beforeUrl, finalUrl: page.url(), readiness: record, summary: publicReadiness(), }; recordStep(typeof options.name === "string" ? options.name : operation, { ok: true, operation, attempt, attempts, beforeUrl, finalUrl: result.finalUrl, }); Object.defineProperty(result, "page", { enumerable: false, value: page, }); return result; } if (attempt < attempts && retryDelayMs > 0) await sleep(retryDelayMs); } const code = lastRecord?.error || "browser-load-jitter"; const message = lastRecord?.message || operation + " did not become ready"; recordStep(typeof options.name === "string" ? options.name : operation, { ok: false, operation, attempts, beforeUrl, finalUrl: currentPageUrl(), error: code, message, screenshot: lastRecord?.screenshot ?? null, }); const error = stableProbeError(code, message, publicReadiness({ error: code })); error.beforeUrl = beforeUrl; throw error; } async function gotoStable(target = "/workbench", options = {}) { options = normalizeHelperOptions(options); const url = new URL(target, baseUrl).toString(); const attempts = boundedInteger(options.attempts, 3, 1, 5); const retryDelayMs = boundedInteger(options.retryDelayMs, 350, 0, 5000); const readinessSelectors = normalizeSelectorList(options.selectors, defaultReadinessSelectors(url)); let lastRecord = null; for (let attempt = 1; attempt <= attempts; attempt += 1) { const useFreshPage = attempt > 1 || options.freshPage === true || options.reusePage === false || !page || page.isClosed(); if (useFreshPage) { const previousPage = page; page = await context.newPage(); if (previousPage && previousPage !== page && !previousPage.isClosed()) await previousPage.close().catch(() => {}); } const attemptPage = page; const network = createNetworkTracker(attemptPage, options.apiPattern); const record = { ok: false, attempt, attempts, policy: useFreshPage ? "fresh-page" : "reuse-current-page", targetPath: urlPath(url), finalPath: null, stage: "navigate", error: null, message: null, selector: null, selectors: [], apiRequestsSent: false, apiRequestCount: 0, apiResponseFailed: false, apiFailureCount: 0, screenshot: null, durationMs: 0, }; const started = Date.now(); try { await attemptPage.goto(url, navigationOptions(options)); record.finalPath = urlPath(attemptPage.url()); const readiness = await waitForReadyInternal(attemptPage, { ...options, selectors: readinessSelectors, url, }, network); applyReadinessToRecord(record, readiness); if (!readiness.ok) { record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options); lastRecord = record; } else if (options.expectApiRequest === true && !readiness.apiRequestsSent) { record.ok = false; record.stage = "api"; record.error = "api-not-sent"; record.message = "expected API request was not observed before readiness finished"; record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options); lastRecord = record; } else if (options.failOnApiResponseFailed === true && readiness.apiResponseFailed) { record.ok = false; record.stage = "api"; record.error = "api-response-failed"; record.message = "API request failed during readiness"; record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options); lastRecord = record; } else { record.ok = true; record.stage = "ready"; lastRecord = record; } } catch (error) { record.finalPath = attemptPage && !attemptPage.isClosed() ? urlPath(attemptPage.url()) : null; record.error = classifyNavigationError(error); record.message = error instanceof Error ? error.message : String(error); record.screenshot = await captureReadinessScreenshot(attemptPage, attempt, options); lastRecord = record; } finally { Object.assign(record, network.summary()); record.durationMs = Date.now() - started; network.dispose(); readinessRecords.push(record); } if (record.ok) { const result = { ok: true, attempt, attempts, finalUrl: page.url(), readiness: record, summary: publicReadiness(), }; Object.defineProperty(result, "page", { enumerable: false, value: page, }); return result; } if (attempt < attempts && retryDelayMs > 0) await sleep(retryDelayMs); } const code = lastRecord?.error || "browser-load-jitter"; const message = lastRecord?.message || "page did not become ready"; throw stableProbeError(code, message, publicReadiness({ error: code })); } async function waitForReady(options = {}) { options = normalizeHelperOptions(options); const readiness = await waitForReadyInternal(options.page || page, options, null); readinessRecords.push({ ...readiness, attempt: readinessRecords.length + 1, attempts: 1, policy: "wait-current-page", durationMs: readiness.durationMs, }); if (options.throwOnFailure === true && !readiness.ok) { throw stableProbeError(readiness.error || "selector-timeout", readiness.message || "page did not become ready", publicReadiness({ error: readiness.error || "selector-timeout" })); } return readiness; } async function waitForReadyInternal(targetPage, options = {}, network = null) { const started = Date.now(); const url = typeof options.url === "string" ? options.url : targetPage.url(); const readinessTimeoutMs = boundedInteger(options.readinessTimeoutMs ?? options.timeoutMs, timeoutMs, 1, Math.max(timeoutMs, 60000)); const selectorState = typeof options.selectorState === "string" ? options.selectorState : "visible"; const selectors = normalizeSelectorList(options.selectors, defaultReadinessSelectors(url)); const activeSelector = typeof options.activeSelector === "string" ? options.activeSelector : typeof options.activeTabSelector === "string" ? options.activeTabSelector : null; const result = { ok: false, stage: "load-state", error: null, message: null, targetPath: urlPath(url), finalPath: null, selector: null, selectors: [], apiRequestsSent: false, apiRequestCount: 0, apiResponseFailed: false, apiFailureCount: 0, durationMs: 0, }; try { await targetPage.waitForLoadState(typeof options.loadState === "string" ? options.loadState : "domcontentloaded", { timeout: readinessTimeoutMs }); result.finalPath = urlPath(targetPage.url()); for (const selector of selectors) { result.stage = "selector"; result.selector = selector; await targetPage.locator(selector).first().waitFor({ state: selectorState, timeout: readinessTimeoutMs }); result.selectors.push({ selector, state: selectorState, matched: true }); } if (activeSelector !== null) { result.stage = "active-selector"; result.selector = activeSelector; await targetPage.locator(activeSelector).first().waitFor({ state: "visible", timeout: readinessTimeoutMs }); result.selectors.push({ selector: activeSelector, state: "visible", matched: true, role: "active" }); } result.ok = true; result.stage = "ready"; } catch (error) { result.ok = false; result.error = result.stage === "selector" || result.stage === "active-selector" ? "selector-timeout" : "browser-load-jitter"; result.message = error instanceof Error ? error.message : String(error); } finally { Object.assign(result, network ? network.summary() : emptyNetworkSummary()); result.durationMs = Date.now() - started; } return result; } function applyReadinessToRecord(record, readiness) { record.ok = readiness.ok; record.stage = readiness.stage; record.error = readiness.error; record.message = readiness.message; record.selector = readiness.selector; record.selectors = readiness.selectors; record.finalPath = readiness.finalPath; record.apiRequestsSent = readiness.apiRequestsSent; record.apiRequestCount = readiness.apiRequestCount; record.apiResponseFailed = readiness.apiResponseFailed; record.apiFailureCount = readiness.apiFailureCount; } function createNetworkTracker(targetPage, apiPattern) { const started = Date.now(); const requests = []; const failures = []; const matches = createApiMatcher(apiPattern); const onRequest = (request) => { if (!matches(request.url())) return; requests.push({ method: request.method(), path: urlPath(request.url()), atMs: Date.now() - started, }); }; const onResponse = (response) => { if (!matches(response.url())) return; if (response.status() >= 400) { failures.push({ type: "response", status: response.status(), statusText: response.statusText(), path: urlPath(response.url()), atMs: Date.now() - started, }); } }; const onRequestFailed = (request) => { if (!matches(request.url())) return; failures.push({ type: "requestfailed", method: request.method(), path: urlPath(request.url()), failureText: request.failure()?.errorText ?? null, atMs: Date.now() - started, }); }; targetPage.on("request", onRequest); targetPage.on("response", onResponse); targetPage.on("requestfailed", onRequestFailed); return { summary: () => ({ apiRequestsSent: requests.length > 0, apiRequestCount: requests.length, apiResponseFailed: failures.length > 0, apiFailureCount: failures.length, apiRequests: requests.slice(0, 5), apiFailures: failures.slice(0, 5), }), dispose: () => { targetPage.off("request", onRequest); targetPage.off("response", onResponse); targetPage.off("requestfailed", onRequestFailed); }, }; } function createApiMatcher(pattern) { if (pattern === false) return () => false; if (pattern instanceof RegExp) return (rawUrl) => pattern.test(rawUrl); if (typeof pattern === "function") return (rawUrl) => { try { return pattern(rawUrl) === true; } catch { return false; } }; const normalized = typeof pattern === "string" && pattern.length > 0 ? pattern : "/v1"; return (rawUrl) => { try { const parsed = new URL(rawUrl); if (normalized.startsWith("/")) return parsed.pathname === normalized || parsed.pathname.startsWith(normalized.replace(/\/$/u, "") + "/"); return rawUrl.includes(normalized); } catch { return false; } }; } function emptyNetworkSummary() { return { apiRequestsSent: false, apiRequestCount: 0, apiResponseFailed: false, apiFailureCount: 0, apiRequests: [], apiFailures: [], }; } function defaultReadinessSelectors(rawUrl) { try { const pathname = new URL(rawUrl, baseUrl).pathname; if (pathname === "/" || pathname === "/workbench" || pathname.startsWith("/workbench/")) return ["#workspace", "#command-input"]; } catch { return ["body"]; } return ["body"]; } function normalizeSelectorList(value, fallback) { if (value === false || value === null) return []; if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean).slice(0, 10); if (typeof value === "string" && value.trim().length > 0) return [value.trim()]; return fallback; } function normalizeHelperOptions(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } function navigationOptions(options = {}) { options = normalizeHelperOptions(options); const next = { ...options }; for (const key of [ "activeSelector", "activeTabSelector", "apiPattern", "attempts", "expectApiRequest", "failOnApiResponseFailed", "freshPage", "name", "readinessTimeoutMs", "record", "retryDelayMs", "reusePage", "screenshotOnFailure", "selectorState", "selectors", "stable", "url", ]) { delete next[key]; } return { waitUntil: "domcontentloaded", timeout: timeoutMs, ...next, }; } async function captureReadinessScreenshot(targetPage, attempt, options) { if (options.screenshotOnFailure === false || !targetPage || targetPage.isClosed()) return null; try { return await screenshotPage(targetPage, "readiness-attempt-" + attempt + ".png"); } catch (error) { return { error: error instanceof Error ? error.message : String(error), }; } } async function captureFailureScreenshot(name = "failure.png") { if (!page || page.isClosed()) return null; try { return await screenshotPage(page, name); } catch (error) { return { kind: "screenshot", path: artifactPath(name), error: error instanceof Error ? error.message : String(error), }; } } async function screenshotPage(targetPage, name = "screenshot.png", options = {}) { const file = artifactPath(name); await targetPage.screenshot({ path: file, fullPage: true, ...options }); return recordArtifact(file, "screenshot"); } function publicReadiness(extra = {}) { const attempts = readinessRecords.slice(-10).map((record) => ({ ok: record.ok === true, attempt: record.attempt ?? null, attempts: record.attempts ?? null, policy: record.policy ?? null, targetPath: record.targetPath ?? null, finalPath: record.finalPath ?? null, stage: record.stage ?? null, error: record.error ?? null, message: typeof record.message === "string" ? record.message.slice(0, 500) : null, selector: record.selector ?? null, selectors: clonePlainList(record.selectors, 10), apiRequestsSent: record.apiRequestsSent === true, apiRequestCount: Number.isInteger(record.apiRequestCount) ? record.apiRequestCount : 0, apiResponseFailed: record.apiResponseFailed === true, apiFailureCount: Number.isInteger(record.apiFailureCount) ? record.apiFailureCount : 0, apiRequests: clonePlainList(record.apiRequests, 5), apiFailures: clonePlainList(record.apiFailures, 5), screenshot: clonePlainObject(record.screenshot), durationMs: Number.isInteger(record.durationMs) ? record.durationMs : null, })); return { attemptCount: readinessRecords.length, last: attempts.length > 0 ? deepClonePlain(attempts[attempts.length - 1]) : null, attempts, ...extra, }; } function deepClonePlain(value) { return JSON.parse(JSON.stringify(value)); } function clonePlainList(value, limit) { if (!Array.isArray(value)) return []; return value.slice(0, limit).map((item) => clonePlainObject(item)); } function clonePlainObject(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return value ?? null; return { ...value }; } function classifyNavigationError(error) { const message = error instanceof Error ? error.message : String(error); if (/timeout|navigation|net::|target page|browser has been closed|page has been closed/iu.test(message)) return "browser-load-jitter"; return "browser-load-jitter"; } function classifiedProbeError(error) { const message = error instanceof Error ? error.message : String(error); const code = probeErrorCode(error, message); const failureKind = probeFailureKind(code, message); return { code, failureKind, message, detail: error && typeof error === "object" ? error.readiness ?? null : null, guidance: probeFailureGuidance(failureKind), }; } function stableProbeError(code, message, readiness) { const error = new Error(message); error.code = code; error.readiness = readiness; return error; } function assertionResultError(value) { const message = extractFailureMessage(value) || "script returned ok:false"; const error = new Error(message); error.code = "assertion-failed"; return error; } function extractFailureMessage(value) { if (!value || typeof value !== "object") return null; for (const key of ["errorMessage", "message", "error", "reason", "summary"]) { const nested = value[key]; if (typeof nested === "string" && nested.length > 0) return nested; } return null; } function probeErrorCode(error, message) { const known = new Set([ "assertion-failed", "auth-login-failed", "auth-or-session-api-failed", "auth-redirect-login", "browser-failed", "browser-load-jitter", "composer-disabled", "selector-timeout", "same-origin-api-fetch-failed", "session-not-selected", "api-not-sent", "api-response-failed", "script-api-misuse", "script-failed", ]); const explicit = error && typeof error === "object" && typeof error.code === "string" ? error.code : null; if (explicit && known.has(explicit)) return explicit; if (known.has(message)) return message; if (isEvaluateApiMisuse(message)) return "script-api-misuse"; if (/same-origin-api-fetch-failed|Failed to fetch/iu.test(message)) return "same-origin-api-fetch-failed"; if (/auth-redirect-login|login\?redirect|authState.*login/iu.test(message)) return "auth-redirect-login"; if (/session_required|session-not-selected|session not selected/iu.test(message)) return "session-not-selected"; if (/composer-disabled|command-send|command bar|composer is not ready|button_disabled|disabledReason/iu.test(message)) return "composer-disabled"; if (/AssertionError|ERR_ASSERTION|\bassert(?:ion)?\b|expect\(.*\)|ok:false/iu.test(message)) return "assertion-failed"; if (/auth-login-failed|login form not visible|missing HWLAB_WEB_PASS/iu.test(message)) return "auth-login-failed"; if (/executable doesn't exist|browser executable|failed to launch|browserType\.launch|browser has been closed/iu.test(message)) return "browser-failed"; if (/timeout|navigation|net::|target page|page has been closed|page did not become ready|selector/iu.test(message)) return "browser-load-jitter"; return "script-failed"; } function probeFailureKind(code, message) { if (code === "auth-redirect-login") return "auth-redirect-login"; if (code === "same-origin-api-fetch-failed") return "same-origin-api-fetch-failed"; if (code === "composer-disabled") return "composer-disabled"; if (code === "session-not-selected") return "session-not-selected"; if (code === "auth-or-session-api-failed") return "auth-or-session-api-failed"; if (code === "script-api-misuse") return "script-api-misuse"; if (code === "auth-login-failed") return "auth-failed"; if (code === "browser-failed") return "browser-failed"; if (code === "browser-load-jitter") return "browser-navigation-jitter"; if (code === "selector-timeout" || code === "api-not-sent" || code === "api-response-failed") return "navigation-failed"; if (/browser has been closed|browser executable|failed to launch/iu.test(message)) return "browser-failed"; return "assertion-failed"; } function probeFailureGuidance(failureKind) { if (failureKind === "composer-disabled" || failureKind === "session-not-selected") return "Inspect readiness.composer and readiness.workspace; waitWorkbenchReady can create/select a submit-capable session unless ensureSession:false is set."; if (failureKind === "auth-redirect-login") return "Inspect auth/session bootstrap and finalUrl; the browser returned to /login after CLI-managed auth."; if (failureKind === "same-origin-api-fetch-failed") return "Inspect the failed same-origin API path, current URL, auth state, and browser console/network evidence in the full report."; if (failureKind === "script-api-misuse") return evaluateSingleArgGuidance(); if (failureKind === "browser-navigation-jitter") return "Use reloadStable(), gotoCurrentStable(), or safeReload() to retry the same browser navigation; inspect readiness.last, lastUrl, and screenshots before treating this as an API fetch failure."; if (failureKind === "navigation-failed") return "Inspect probe.readiness for selector/API readiness details and lastScreenshot for the browser state."; if (failureKind === "auth-failed") return "Inspect probe.auth sourceRef/fingerprint/status; do not print or copy the web login secret."; if (failureKind === "browser-failed") return "Inspect Playwright/browser-launcher availability on the target workspace."; return null; } function isEvaluateApiMisuse(message) { return /Too many arguments.*wrap them in an object|page\.evaluate.*one serializable argument|safeEvaluate requires a function/iu.test(String(message)); } function evaluateSingleArgGuidance() { return "Playwright page.evaluate accepts one serializable argument; use page.evaluate(({ a, b }) => ..., { a, b }) or safeEvaluate(fn, { a, b })."; } function markEvaluateError(error) { if (error && typeof error === "object" && isEvaluateApiMisuse(error instanceof Error ? error.message : String(error))) { error.code = "script-api-misuse"; } return error; } function currentPageUrl() { try { return page && !page.isClosed() ? page.url() : null; } catch { return null; } } function urlPath(rawUrl) { try { const parsed = new URL(rawUrl, baseUrl); return parsed.pathname + parsed.search; } catch { return String(rawUrl); } } function boundedInteger(raw, fallback, min, max) { const value = Number(raw ?? fallback); if (!Number.isInteger(value)) return fallback; return Math.max(min, Math.min(max, value)); } function inferProjectId() { try { const current = page && !page.isClosed() ? new URL(page.url()) : null; const fromUrl = current ? current.searchParams.get("projectId") : null; if (fromUrl) return fromUrl; } catch { // Fall through to the Workbench default. } return "prj_hwpod_workbench"; } function inferConversationIdFromUrl() { try { if (!page || page.isClosed()) return null; const pathname = new URL(page.url()).pathname; const match = pathname.match(/\/(?:workbench|workspace)\/sessions\/([^/]+)/u); return match ? decodeURIComponent(match[1]) : null; } catch { return null; } } function firstStringAtPaths(value, paths) { for (const pathSpec of paths) { const nested = valueAtPath(value, pathSpec); if (typeof nested === "string" && nested.length > 0) return nested; } return null; } function firstArrayAtPaths(value, paths) { for (const pathSpec of paths) { const nested = valueAtPath(value, pathSpec); if (Array.isArray(nested)) return nested; } return null; } function valueAtPath(value, pathSpec) { let current = value; for (const key of String(pathSpec).split(".")) { if (!current || typeof current !== "object") return null; current = current[key]; } return current ?? null; } function summarizeConversationItem(value) { if (!value || typeof value !== "object") return value; return { conversationId: firstStringAtPaths(value, ["conversationId", "id"]), title: firstStringAtPaths(value, ["title", "name"]), lastTraceId: firstStringAtPaths(value, ["lastTraceId", "traceId"]), status: firstStringAtPaths(value, ["status", "state"]), }; } function summarizeMessage(value) { if (!value || typeof value !== "object") return value ?? null; return { role: firstStringAtPaths(value, ["role", "type", "sender"]), status: firstStringAtPaths(value, ["status", "state"]), traceId: firstStringAtPaths(value, ["traceId", "runnerTrace.traceId"]), textPreview: normalizeTextPreview(firstStringAtPaths(value, ["content", "text", "message", "finalResponse"]) || "", 500), }; } function normalizeTextPreview(text, limit) { return String(text ?? "").replace(/\s+/gu, " ").trim().slice(0, limit); } function normalizeFetchHeaders(value) { const headers = {}; if (!value || typeof value !== "object" || Array.isArray(value)) return headers; for (const [key, nested] of Object.entries(value)) { if (typeof nested === "string") headers[key] = nested; } return headers; } function normalizeFetchBody(value, headers) { if (value === undefined || value === null) return null; if (typeof value === "string") return value; const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type"); if (!hasContentType) headers["content-type"] = "application/json"; return JSON.stringify(value); } function isRetryableAuthStatus(status) { return status === 0 || status === 429 || status >= 500; } function retryCountFromAttempts(attempts) { return Math.max(0, Array.isArray(attempts) ? attempts.length - 1 : 0); } function authAttemptsRetryable(attempts) { if (!Array.isArray(attempts) || attempts.length === 0) return false; return attempts.some((attempt) => attempt && typeof attempt === "object" && attempt.retryable === true); } async function responseSummary(response) { const status = response.status(); let bodyPreview = null; if (status >= 400) { try { bodyPreview = redactString(await response.text()).replace(/\s+/gu, " ").slice(0, 500); } catch { bodyPreview = null; } } return { status, statusText: response.statusText(), bodyPreview, }; } async function readAuthCookieState(browserContext) { const cookies = await browserContext.cookies(baseUrl); cookieSecrets = cookies.map((cookie) => cookie.value).filter(Boolean); const cookieNames = cookies.map((cookie) => cookie.name).sort(); return { cookiePresent: cookieNames.includes("hwlab_session"), cookieNames, }; } function responsePathMatches(rawUrl, path) { try { return new URL(rawUrl).pathname === path; } catch { return false; } } function assertNoCredentialLeak(value) { const finalUrl = new URL(value); if (finalUrl.searchParams.has("username") || finalUrl.searchParams.has("password")) { throw new Error("login credentials leaked into URL query"); } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function publicAuth(value) { const retryCount = Number.isInteger(value.retryCount) ? value.retryCount : retryCountFromAttempts(value.attempts); const transientObserved = authAttemptsRetryable(value.attempts); const ok = value.ok === true; const retryable = ok ? false : value.retryable === true || transientObserved; return { ok, method: value.method ?? null, origin: new URL(baseUrl).origin, loginPath: value.loginUrl, status: value.status, statusText: value.statusText, cookiePresent: value.cookiePresent, cookieNames: value.cookieNames, attempts: value.attempts ?? null, retryCount, fallbackUsed: value.fallbackUsed === true || value.method === "form-fallback", fallback: value.fallback ?? null, errorSummary: cloneSummary(value.errorSummary ?? value.apiErrorSummary ?? null), degradedReason: ok ? null : "auth-login-failed", retryable, transientObserved, commanderAction: ok ? null : retryable ? "retry same web-probe command after short backoff; inspect target /auth/login and Cloud Web/API rollout if repeated" : "inspect bootstrap admin credential source and target user state", fingerprint: authSummaryFingerprint(value), username, valuesRedacted: true, }; } function authSummaryFingerprint(value) { const payload = JSON.stringify({ origin: new URL(baseUrl).origin, loginPath: value.loginUrl ?? null, method: value.method ?? null, status: value.status ?? null, statusText: value.statusText ?? null, cookiePresent: value.cookiePresent === true, retryCount: Number.isInteger(value.retryCount) ? value.retryCount : retryCountFromAttempts(value.attempts), fallbackUsed: value.fallbackUsed === true || value.method === "form-fallback", }); return "sha256:" + createHash("sha256").update(payload).digest("hex").slice(0, 16); } function cloneSummary(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return value ?? null; return { ...value }; } function normalizeBaseUrl(raw) { if (!raw) throw new Error("missing HWLAB_WEB_BASE_URL"); const parsed = new URL(raw); parsed.hash = ""; parsed.search = ""; return parsed.toString().replace(/\/+$/u, "/"); } function parseViewport(raw) { const match = String(raw).match(/^([0-9]+)x([0-9]+)$/u); if (!match) throw new Error("viewport must look like 1440x900"); return { width: Number(match[1]), height: Number(match[2]) }; } function positiveInteger(raw, fallback) { const value = Number(raw || fallback); if (!Number.isInteger(value) || value <= 0) return fallback; return value; } function artifactPath(name) { const safe = String(name || "artifact") .replace(/[^A-Za-z0-9._-]/gu, "_") .replace(/^\.+/u, "_") .slice(0, 120) || "artifact"; return path.join(runDir, safe); } async function screenshot(name = "screenshot.png", options = {}) { const file = artifactPath(name); await page.screenshot({ path: file, fullPage: true, ...options }); return recordArtifact(file, "screenshot"); } async function jsonArtifact(name, value) { const file = artifactPath(name); await writeFile(file, JSON.stringify(sanitize(value), null, 2) + "\n", "utf8"); return recordArtifact(file, "json"); } async function recordArtifact(file, kind) { const fileStat = await stat(file); const item = { kind, path: file, byteCount: fileStat.size, sha256: await sha256File(file), }; artifactRecords.push({ ...item }); return { ...item }; } async function sha256File(file) { const buffer = await readFile(file); return "sha256:" + createHash("sha256").update(buffer).digest("hex"); } function sanitize(value, depth = 0, seen = new WeakSet()) { if (value === null || value === undefined) return value ?? null; if (typeof value === "string") return redactString(value).slice(0, 6000); if (typeof value === "number" || typeof value === "boolean") return value; if (typeof value === "bigint") return String(value); if (typeof value === "function" || typeof value === "symbol") return "[" + typeof value + "]"; if (depth > 12) return "[max-depth]"; if (Array.isArray(value)) return value.slice(0, 200).map((item) => sanitize(item, depth + 1, seen)); if (typeof value === "object") { if (seen.has(value)) return "[circular]"; seen.add(value); const out = {}; for (const [key, nested] of Object.entries(value).slice(0, 200)) { if (/password|passwd|secret|token|authorization|cookie|api[_-]?key/iu.test(key) && typeof nested === "string") { out[key] = ""; } else { out[key] = sanitize(nested, depth + 1, seen); } } return out; } return String(value); } function redactString(text) { let next = String(text); for (const secret of [password, ...cookieSecrets].filter(Boolean)) { next = next.split(secret).join(""); } return next; } async function emit(payload) { const enriched = { ...payload, steps: publicSteps(), artifacts: { runDir, items: artifactRecords }, }; enriched.summary = scriptIssueSummary(enriched); const reportFile = artifactPath("web-probe-script-report.json"); let reportArtifact = null; try { await writeFile(reportFile, JSON.stringify(sanitize(enriched), null, 2) + "\n", "utf8"); const fileStat = await stat(reportFile); reportArtifact = { kind: "json", path: reportFile, byteCount: fileStat.size, sha256: await sha256File(reportFile), }; artifactRecords.push({ ...reportArtifact }); } catch (error) { reportArtifact = { kind: "json", path: reportFile, error: error instanceof Error ? error.message : String(error), }; } const finalPayload = { ...enriched, reportPath: reportArtifact && typeof reportArtifact.path === "string" ? reportArtifact.path : null, reportSha256: reportArtifact && typeof reportArtifact.sha256 === "string" ? reportArtifact.sha256 : null, artifacts: { runDir, items: artifactRecords }, }; finalPayload.summary = scriptIssueSummary(finalPayload); process.stdout.write(JSON.stringify(sanitize(compactStdoutPayload(finalPayload)), null, 2) + "\n"); } function compactStdoutPayload(payload) { const issueEvidence = compactIssueEvidenceForStdout(payload?.summary?.issueEvidence ?? issueEvidenceFromPayload(payload)); return { ok: payload?.ok === true, status: payload?.status ?? null, command: payload?.command ?? "web-probe-script", generatedAt: payload?.generatedAt ?? null, startedAt: payload?.startedAt ?? null, baseUrl: payload?.baseUrl ?? null, finalUrl: payload?.finalUrl ?? payload?.lastUrl ?? null, lastUrl: payload?.lastUrl ?? null, scriptSha256: payload?.scriptSha256 ?? null, runDir, reportPath: payload?.reportPath ?? null, reportSha256: payload?.reportSha256 ?? null, auth: payload?.auth ?? null, script: compactScriptForStdout(payload?.script), steps: compactStepsForStdout(publicSteps()), failureKind: payload?.failureKind ?? null, error: payload?.error ?? null, errorMessage: typeof payload?.errorMessage === "string" ? compactText(payload.errorMessage, 1200) : payload?.errorMessage ?? null, guidance: typeof payload?.guidance === "string" ? compactText(payload.guidance, 1200) : payload?.guidance ?? null, lastScreenshot: compactArtifactForStdout(payload?.lastScreenshot), readiness: compactJsonForStdout(payload?.readiness), artifacts: compactArtifactsForStdout(payload?.artifacts), issueEvidence, summary: compactSummaryForStdout(payload?.summary), safety: { valuesRedacted: true, secretValuesPrinted: false, stdoutCompacted: true, fullReportInArtifact: true, }, }; } function compactScriptForStdout(script) { const value = script && typeof script === "object" ? script : {}; const steps = Array.isArray(value.steps) ? value.steps : []; return { ok: value.ok === true, result: compactJsonForEvidence(value.result), stepCount: steps.length, }; } function compactStepsForStdout(steps) { if (!Array.isArray(steps)) return []; return steps.slice(-3).map((step) => compactStepForStdout(step)); } function compactStepForStdout(step) { const value = step && typeof step === "object" ? step : {}; return { index: Number.isFinite(value.index) ? value.index : null, name: typeof value.name === "string" ? value.name : null, ok: typeof value.ok === "boolean" ? value.ok : null, atMs: Number.isFinite(value.atMs) ? value.atMs : null, data: compactStepDataForStdout(value.data), }; } function compactStepDataForStdout(data) { if (data && typeof data === "object" && !Array.isArray(data) && Array.isArray(data.items) && typeof data.failedCount === "number") { return compactApiMatrixForStdout(data); } if (data && typeof data === "object" && !Array.isArray(data)) { const out = {}; for (const [key, value] of Object.entries(data).slice(0, 8)) { if (key === "apiMatrix") out[key] = compactApiMatrixForStdout(value); else if (/screenshot/iu.test(key)) out[key] = compactArtifactForStdout(value); else out[key] = compactJsonForStdout(value); } return out; } return compactJsonForStdout(data); } function compactSummaryForStdout(summary) { const value = summary && typeof summary === "object" ? summary : {}; return { ok: value.ok === true, status: value.status ?? null, degradedReason: value.degradedReason ?? null, failureKind: value.failureKind ?? null, failedCondition: typeof value.failedCondition === "string" ? compactText(value.failedCondition, 1200) : value.failedCondition ?? null, nextAction: typeof value.nextAction === "string" ? compactText(value.nextAction, 1200) : value.nextAction ?? null, baseUrl: value.baseUrl ?? null, finalUrl: value.finalUrl ?? null, lastUrl: value.lastUrl ?? null, scriptSha256: value.scriptSha256 ?? null, runDir: value.runDir ?? runDir, reportPath: value.reportPath ?? null, reportSha256: value.reportSha256 ?? null, lastScreenshot: compactArtifactForStdout(value.lastScreenshot), screenshots: Array.isArray(value.screenshots) ? value.screenshots.slice(-5).map((item) => compactArtifactForStdout(item)).filter(Boolean) : [], apiMatrix: compactApiMatrixForStdout(value.apiMatrix), stepCount: Number.isFinite(value.stepCount) ? value.stepCount : null, lastStep: compactStepForStdout(value.lastStep), issueEvidence: compactIssueEvidenceForStdout(value.issueEvidence), valuesRedacted: true, }; } function compactArtifactsForStdout(artifacts) { const items = Array.isArray(artifacts?.items) ? artifacts.items : artifactRecords; return { runDir, count: items.length, screenshots: items.filter((item) => item && typeof item === "object" && item.kind === "screenshot").slice(-5).map((item) => compactArtifactForStdout(item)).filter(Boolean), items: items.slice(-12).map((item) => compactArtifactForStdout(item)).filter(Boolean), }; } function compactArtifactForStdout(item) { if (!item || typeof item !== "object") return null; return { kind: item.kind ?? null, path: item.path ?? null, byteCount: Number.isFinite(item.byteCount) ? item.byteCount : null, sha256: item.sha256 ?? null, error: typeof item.error === "string" ? compactText(item.error, 600) : item.error ?? null, }; } function compactApiMatrixForStdout(matrix) { if (!matrix || typeof matrix !== "object" || Array.isArray(matrix)) return null; return { ok: matrix.ok === true, count: Number.isFinite(matrix.count) ? matrix.count : null, okCount: Number.isFinite(matrix.okCount) ? matrix.okCount : null, failedCount: Number.isFinite(matrix.failedCount) ? matrix.failedCount : null, items: Array.isArray(matrix.items) ? matrix.items.slice(0, 12).map((item) => { const row = item && typeof item === "object" ? item : {}; return { name: typeof row.name === "string" ? row.name : null, path: typeof row.path === "string" ? row.path : null, method: typeof row.method === "string" ? row.method : null, ok: row.ok === true, status: Number.isFinite(row.status) ? row.status : null, error: typeof row.error === "string" ? compactText(row.error, 300) : row.error ?? null, failureKind: typeof row.failureKind === "string" ? row.failureKind : null, bodyKeys: Array.isArray(row.bodyKeys) ? row.bodyKeys.filter((key) => typeof key === "string").slice(0, 12) : [], }; }) : [], }; } function compactJsonForStdout(value, depth = 0) { if (value === null || value === undefined) return value ?? null; if (typeof value === "string") return compactText(value, 180); if (typeof value === "number" || typeof value === "boolean") return value; if (typeof value === "bigint") return String(value); if (typeof value === "function" || typeof value === "symbol") return "[" + typeof value + "]"; if (depth >= 3) return "[max-depth]"; if (Array.isArray(value)) return value.slice(0, 4).map((item) => compactJsonForStdout(item, depth + 1)); if (typeof value === "object") { const out = {}; for (const [key, nested] of Object.entries(value).slice(0, 8)) { out[key] = compactJsonForStdout(nested, depth + 1); } return out; } return compactText(String(value), 180); } function issueEvidenceFromPayload(payload) { const steps = publicSteps(); const artifacts = Array.isArray(payload?.artifacts?.items) ? payload.artifacts.items : artifactRecords; const screenshots = artifacts .filter((item) => item && typeof item === "object" && item.kind === "screenshot") .slice(-5) .map((item) => compactArtifactForStdout(item)) .filter(Boolean); const ok = payload?.ok === true; const degradedReason = ok ? null : payload?.error ?? payload?.failureKind ?? "web-probe-script-failed"; const failureKind = ok ? null : classifyIssueFailureKind(payload?.failureKind ?? payload?.error ?? degradedReason, payload?.errorMessage); const failedCondition = ok ? null : payload?.errorMessage ?? payload?.error ?? payload?.failureKind ?? "script did not pass"; const script = payload?.script && typeof payload.script === "object" ? payload.script : {}; return { ok, status: payload?.status ?? null, degradedReason, failureKind, failedCondition, nextAction: ok ? null : issueNextAction(failureKind, payload), baseUrl: payload?.baseUrl ?? null, finalUrl: payload?.finalUrl ?? payload?.lastUrl ?? null, lastUrl: payload?.lastUrl ?? payload?.finalUrl ?? null, scriptSha256: payload?.scriptSha256 ?? null, runDir, reportPath: payload?.reportPath ?? null, reportSha256: payload?.reportSha256 ?? null, result: compactJsonForEvidence(script.result), apiMatrix: compactApiMatrixForStdout(latestApiMatrixFromSteps(steps)), lastStep: steps.length > 0 ? compactStepForEvidence(steps[steps.length - 1]) : null, steps: steps.slice(-3).map((step) => compactStepForEvidence(step)), lastScreenshot: compactArtifactForStdout(payload?.lastScreenshot), screenshots, valuesRedacted: true, }; } function compactIssueEvidenceForStdout(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; return { ok: value.ok === true, status: value.status ?? null, degradedReason: typeof value.degradedReason === "string" ? compactText(value.degradedReason, 600) : value.degradedReason ?? null, failureKind: typeof value.failureKind === "string" ? compactText(value.failureKind, 300) : value.failureKind ?? null, failedCondition: typeof value.failedCondition === "string" ? compactText(value.failedCondition, 1200) : value.failedCondition ?? null, nextAction: typeof value.nextAction === "string" ? compactText(value.nextAction, 1200) : value.nextAction ?? null, baseUrl: value.baseUrl ?? null, finalUrl: value.finalUrl ?? null, lastUrl: value.lastUrl ?? null, scriptSha256: value.scriptSha256 ?? null, runDir: value.runDir ?? runDir, reportPath: value.reportPath ?? null, reportSha256: value.reportSha256 ?? null, result: compactJsonForEvidence(value.result), apiMatrix: compactApiMatrixForStdout(value.apiMatrix), lastStep: compactStepForEvidence(value.lastStep), steps: Array.isArray(value.steps) ? value.steps.slice(-3).map((step) => compactStepForEvidence(step)) : [], lastScreenshot: compactArtifactForStdout(value.lastScreenshot), screenshots: Array.isArray(value.screenshots) ? value.screenshots.slice(-5).map((item) => compactArtifactForStdout(item)).filter(Boolean) : [], valuesRedacted: true, }; } function compactStepForEvidence(step) { if (!step || typeof step !== "object" || Array.isArray(step)) return null; return { index: Number.isFinite(step.index) ? step.index : null, name: typeof step.name === "string" ? step.name : null, ok: typeof step.ok === "boolean" ? step.ok : null, atMs: Number.isFinite(step.atMs) ? step.atMs : null, data: compactJsonForEvidence(step.data), }; } function compactJsonForEvidence(value, depth = 0) { if (value === null || value === undefined) return value ?? null; if (typeof value === "string") return compactText(value, 600); if (typeof value === "number" || typeof value === "boolean") return value; if (typeof value === "bigint") return String(value); if (typeof value === "function" || typeof value === "symbol") return "[" + typeof value + "]"; if (depth >= 8) return "[max-depth]"; if (Array.isArray(value)) return value.slice(0, 16).map((item) => compactJsonForEvidence(item, depth + 1)); if (typeof value === "object") { const out = {}; for (const [key, nested] of Object.entries(value).slice(0, 32)) { out[key] = compactJsonForEvidence(nested, depth + 1); } return out; } return compactText(String(value), 600); } function compactText(value, maxChars) { return redactString(String(value)).replace(/\s+/gu, " ").trim().slice(0, maxChars); } function scriptIssueSummary(payload) { const artifacts = Array.isArray(payload?.artifacts?.items) ? payload.artifacts.items : artifactRecords; const screenshots = artifacts .filter((item) => item && typeof item === "object" && item.kind === "screenshot") .slice(-5) .map((item) => ({ path: item.path ?? null, sha256: item.sha256 ?? null, byteCount: item.byteCount ?? null, })); const apiMatrix = latestApiMatrixFromSteps(publicSteps()); const ok = payload?.ok === true; const degradedReason = ok ? null : payload?.error ?? payload?.failureKind ?? "web-probe-script-failed"; const failureKind = ok ? null : classifyIssueFailureKind(payload?.failureKind ?? payload?.error ?? degradedReason, payload?.errorMessage); return { ok, status: payload?.status ?? null, degradedReason, failureKind, failedCondition: ok ? null : payload?.errorMessage ?? payload?.error ?? payload?.failureKind ?? "script did not pass", nextAction: ok ? null : issueNextAction(failureKind, payload), baseUrl: payload?.baseUrl ?? null, finalUrl: payload?.finalUrl ?? payload?.lastUrl ?? null, lastUrl: payload?.lastUrl ?? payload?.finalUrl ?? null, scriptSha256: payload?.scriptSha256 ?? null, runDir, reportPath: payload?.reportPath ?? null, reportSha256: payload?.reportSha256 ?? null, lastScreenshot: payload?.lastScreenshot ?? null, screenshots, apiMatrix, stepCount: stepRecords.length, lastStep: stepRecords.length > 0 ? deepClonePlain(stepRecords[stepRecords.length - 1]) : null, issueEvidence: issueEvidenceFromPayload(payload), valuesRedacted: true, }; } function latestApiMatrixFromSteps(steps) { for (let index = steps.length - 1; index >= 0; index -= 1) { const step = steps[index]; const data = step && typeof step === "object" ? step.data : null; if (data && typeof data === "object" && Array.isArray(data.items) && typeof data.failedCount === "number") return data; } return null; } function classifyIssueFailureKind(kind, message = "") { const text = String(kind ?? "") + " " + String(message ?? ""); if (/auth-redirect-login/iu.test(text)) return "auth-redirect-login"; if (/composer-disabled/iu.test(text)) return "composer-disabled"; if (/session-not-selected|session_required/iu.test(text)) return "session-not-selected"; if (/same-origin-api-fetch-failed/iu.test(text)) return "same-origin-api-fetch-failed"; if (/browser-navigation-jitter|browser-load-jitter|page\.(?:reload|goto)|gotoCurrentStable|reloadStable|safeReload|ERR_NETWORK_CHANGED|ERR_ABORTED|net::|navigation failed|domcontentloaded|chrome-error:\/\/chromewebdata/iu.test(text)) return "browser-navigation-jitter"; if (/api|fetch|network|Failed to fetch|HTTP|status/iu.test(text)) return "network-or-api-fetch-bug"; if (/script-api-misuse|page\.evaluate|safeEvaluate|Too many arguments/iu.test(text)) return "script-bug"; if (/auth|login|credential/iu.test(text)) return "target-auth-bug"; if (/browser|chromium|playwright|executable/iu.test(text)) return "browser-environment-bug"; if (/assert|expect|ok:false|validation|final-response|message/iu.test(text)) return "unmet-expectation"; if (/script|ReferenceError|TypeError|SyntaxError/iu.test(text)) return "script-bug"; return "user-facing-web-bug"; } function issueNextAction(failureKind, payload) { if (failureKind === "auth-redirect-login") return "Inspect summary.finalUrl, auth/session bootstrap, and target login redirect handling before rerunning fresh-session."; if (failureKind === "composer-disabled" || failureKind === "session-not-selected") return "Inspect readiness.composer disabledReason and workspace/session summary; create or select a submit-capable session, then rerun the same probe."; if (failureKind === "same-origin-api-fetch-failed") return "Inspect summary.apiMatrix failed rows, current URL, auth state, and browser console/network evidence in reportPath."; if (failureKind === "browser-navigation-jitter") return "Use reloadStable(), gotoCurrentStable(), or safeReload() to retry the same URL/page reload with bounded attempts; inspect summary.finalUrl, summary.lastScreenshot, and readiness attempts instead of the API matrix first."; if (failureKind === "network-or-api-fetch-bug") return "Inspect summary.apiMatrix failed rows and retry the same node/lane after checking API availability."; if (failureKind === "script-bug") return "Fix the probe script or use safeEvaluate/safeFetchJson/fetchApiMatrix helpers, then rerun the same command."; if (failureKind === "target-auth-bug") return "Inspect credential sourceRef/fingerprint and target /auth/login state; do not print secrets."; if (failureKind === "browser-environment-bug") return "Inspect Playwright/browser-launcher availability in the target workspace."; if (failureKind === "unmet-expectation") return "Inspect summary.failedCondition, DOM/API steps, and screenshots to decide whether the Web behavior or the assertion is stale."; return payload?.guidance ?? "Inspect reportPath for full redacted details and rerun the same node/lane entry after the root cause is fixed."; } `; }