From 17a8266b6e2ecbb26c17851647751f259b127e12 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:53:10 -0400 Subject: [PATCH 01/16] test(opencode): cover precise preflight classifications --- src/lib/runtime-availability.test.ts | 84 +++++++++++++++++----------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/src/lib/runtime-availability.test.ts b/src/lib/runtime-availability.test.ts index cf6e88727..67a0f6205 100644 --- a/src/lib/runtime-availability.test.ts +++ b/src/lib/runtime-availability.test.ts @@ -12,32 +12,14 @@ import path from "node:path"; import { evaluateRuntimeAvailability, - localRuntimeLaunchError, missingRunnerMessage, - runtimeLaunchFailedMessage, summarizeRuntimeAvailability, RUNTIME_AVAILABILITY_ERROR_CODES, } from "./runtime-availability.ts"; +import { openCodeCommand, openCodeLaunch } from "./opencode-bin.ts"; const scratch = mkdtempSync(path.join(tmpdir(), "runtime-availability-")); try { - assert.deepEqual( - localRuntimeLaunchError("grok", "ENOENT"), - { - code: "ENOENT", - message: missingRunnerMessage("grok"), - }, - "a post-spawn missing-interpreter race retains the missing-runner contract", - ); - assert.deepEqual( - localRuntimeLaunchError("grok", "UNKNOWN"), - { - code: "runtime_launch_failed", - message: runtimeLaunchFailedMessage("grok"), - }, - "every non-ENOENT local spawn error uses the normalized launch-failure contract", - ); - const binDir = path.join(scratch, "bin"); const emptyDir = path.join(scratch, "empty"); mkdirSync(binDir); @@ -47,30 +29,31 @@ try { chmodSync(executable, 0o755); // Verification matrix: binary resolves in the spawn env → ready. + // Do not use this Windows host's temporary path while simulating Linux: + // a drive letter contains `:`, which is a POSIX PATH delimiter. + const posixBinDir = "/runtime-availability/bin"; const ready = evaluateRuntimeAvailability({ runner: "grok", command: "grok", - env: { PATH: `${emptyDir}:${binDir}` }, + env: { PATH: `/runtime-availability/empty:${posixBinDir}` }, platform: "linux", + statFile: (candidate) => candidate === `${posixBinDir}/grok`, }); assert.equal(ready.state, "ready", "a bare command on the spawn PATH is ready"); assert.equal( ready.state === "ready" && ready.resolvedPath, - executable, + `${posixBinDir}/grok`, "ready reports where the exact spawn command resolved", ); const absoluteReady = evaluateRuntimeAvailability({ runner: "coven", - command: executable, + command: `${posixBinDir}/grok`, env: { PATH: "" }, platform: "linux", + statFile: (candidate) => candidate === `${posixBinDir}/grok`, }); - assert.equal( - absoluteReady.state, - "ready", - "a mode-0755 regular file is launchable on POSIX", - ); + assert.equal(absoluteReady.state, "ready", "an absolute launch command is stat'd directly"); if (process.platform !== "win32") { const directoryCandidate = path.join(binDir, "grok-directory"); @@ -297,13 +280,23 @@ try { // OpenCode's Windows launch is PowerShell-hosted: the host must exist and // the inner `opencode` command must resolve with PATHEXT semantics. - const psHost = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; + const openCodeWindowsLaunch = openCodeLaunch( + ["run", "safe & literal"], + "win32", + { SystemRoot: "C:\\Windows" }, + ); + const psHost = openCodeWindowsLaunch.command; + assert.equal( + openCodeWindowsLaunch.input, + JSON.stringify(["run", "safe & literal"]), + "the preflight receives the same JSON-stdin launch plan as chat", + ); const openCodeWinReady = evaluateRuntimeAvailability({ runner: "opencode", command: psHost, env: { Path: "C:\\bin", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, platform: "win32", - powerShellHostedCommand: "opencode", + powerShellHostedCommand: openCodeWindowsLaunch.input === undefined ? undefined : openCodeCommand(), statFile: winStats([psHost, "C:\\bin\\opencode.CMD"]), }); assert.equal( @@ -344,10 +337,37 @@ try { /PowerShell/, "the host failure names the actual remediation target", ); + assert.doesNotMatch( + openCodeHostGone.state === "unlaunchable" ? openCodeHostGone.message : "", + /OpenCode CLI not found/i, + "a missing host never blames the inner OpenCode command", + ); + + const openCodeInnerProbeFailed = evaluateRuntimeAvailability({ + runner: "opencode", + command: psHost, + env: { Path: "C:\\bin" }, + platform: "win32", + powerShellHostedCommand: openCodeCommand(), + statFile: (candidate) => { + if (candidate === psHost) return true; + throw Object.assign(new Error(`EACCES: permission denied, stat '${candidate}'`), { + code: "EACCES", + }); + }, + }); + assert.equal( + openCodeInnerProbeFailed.state, + "probe_failed", + "an unreadable inner OpenCode command is not reported as missing", + ); + assert.ok( + openCodeInnerProbeFailed.state === "probe_failed" && !openCodeInnerProbeFailed.message.includes("C:\\bin"), + "hosted-command probe errors do not leak the launch PATH", + ); - // Availability never executes anything: the whole evaluation uses bounded - // filesystem inspection, so evaluating before every chat turn stays cheap - // and side-effect free. + // Availability never executes anything: the whole evaluation is stat-only, + // so evaluating before every chat turn stays cheap and side-effect free. // (Enforced structurally — the module must not import child_process.) const moduleSource = readFileSync( new URL("./runtime-availability.ts", import.meta.url), From 485182aade5d0730ab61b0b28ce8575b7fb9ee77 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:59:39 -0400 Subject: [PATCH 02/16] fix(ui): surface runtime launchability honestly --- src/components/chat-view.test.ts | 4 ++-- src/components/chat-view.tsx | 19 +++++++++---------- .../familiar-studio-brain-tab.test.ts | 10 ++++++++++ src/components/familiar-studio-brain-tab.tsx | 11 +++++++++++ .../familiar-summoning-circle.test.ts | 10 ++++++++++ src/components/familiar-summoning-circle.tsx | 13 ++++++++++++- src/components/familiar-summoning-model.ts | 2 ++ 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/components/chat-view.test.ts b/src/components/chat-view.test.ts index 3214a88a6..cb2c9955b 100644 --- a/src/components/chat-view.test.ts +++ b/src/components/chat-view.test.ts @@ -226,8 +226,8 @@ assert.match( ); assert.match( source, - /const covenMissing = useMemo\(\s*\(\) => \/Coven CLI not found on PATH\/i\.test\(message\) \|\| code === "ENOENT"/, - "the error strip detects the coven-CLI-missing failure class", + /const runtimeMissing = useMemo\(\s*\(\) => code === "runtime_missing" \|\| \/Coven CLI not found on PATH\/i\.test\(message\) \|\| code === "ENOENT"/, + "the error strip detects a missing runtime reported by preflight", ); assert.match( source, diff --git a/src/components/chat-view.tsx b/src/components/chat-view.tsx index d445091a2..28606160b 100644 --- a/src/components/chat-view.tsx +++ b/src/components/chat-view.tsx @@ -606,12 +606,11 @@ function ChatErrorStrip({ () => parseHarnessAuthFailure(detailText, harnessId), [detailText, harnessId], ); - // The Coven CLI couldn't be resolved from the app's spawn environment - // (the #2610 class of failure). Rather than a bare error + generic Retry, - // offer a soft "Open Setup" link (overlay, not a hard nav) — the message - // stays in the composer for retry (#2618). - const covenMissing = useMemo( - () => /Coven CLI not found on PATH/i.test(message) || code === "ENOENT", + // A preflight-confirmed missing runtime (or the legacy Coven ENOENT path) + // gets a soft Setup recovery instead of a bare error + generic Retry. The + // message stays in the composer for retry (#2618, #3862). + const runtimeMissing = useMemo( + () => code === "runtime_missing" || /Coven CLI not found on PATH/i.test(message) || code === "ENOENT", [message, code], ); @@ -696,11 +695,11 @@ function ChatErrorStrip({ {!harnessFailure && authFailure ? ( ) : null} - {!harnessFailure && !authFailure && covenMissing ? ( + {!harnessFailure && !authFailure && runtimeMissing ? (
- The Coven CLI isn't resolvable from this app's environment. Open Setup to install - or repair it, then retry — your message is kept. + This runtime isn't resolvable from the app's environment. Open Setup to install or repair + it, then retry — your message is kept.
) : null} - {!harnessFailure && !authFailure && !covenMissing && onPickProject && pickProjectOptions ? ( + {!harnessFailure && !authFailure && !runtimeMissing && onPickProject && pickProjectOptions ? (
{pickProjectOptions.length ? ( <> diff --git a/src/components/familiar-studio-brain-tab.test.ts b/src/components/familiar-studio-brain-tab.test.ts index ca4f7ed96..e2ea5e836 100644 --- a/src/components/familiar-studio-brain-tab.test.ts +++ b/src/components/familiar-studio-brain-tab.test.ts @@ -467,6 +467,16 @@ assert.match( /familiar-studio-brain__hint familiar-studio-brain__hint--warn" role="status"/, "not-ready states render as warning hints announced via role=status", ); +assert.match( + source, + /availability\?: \{[\s\S]*?state: "ready" \| "missing" \| "unlaunchable" \| "probe_failed" \| "unsupported_runtime"[\s\S]*?message\?: string;/, + "the runtime picker receives the launchability summary returned by /api/harnesses", +); +assert.match( + source, + /const selectedHarnessAvailability = harnesses\.find\(\(item\) => item\.id === harnessId\)\?\.availability;[\s\S]*?selectedHarnessAvailability\.state !== "ready"[\s\S]*?selectedHarnessAvailability\.message/, + "the selected runtime shows truthful launch remediation rather than only an install bit", +); assert.match( source, /kind === "on-device"[\s\S]{0,200}runs fully on-device/, diff --git a/src/components/familiar-studio-brain-tab.tsx b/src/components/familiar-studio-brain-tab.tsx index 7a0292a71..1fb757e68 100644 --- a/src/components/familiar-studio-brain-tab.tsx +++ b/src/components/familiar-studio-brain-tab.tsx @@ -71,6 +71,11 @@ type HarnessReport = { id: string; label: string; installed: boolean; + availability?: { + state: "ready" | "missing" | "unlaunchable" | "probe_failed" | "unsupported_runtime"; + code?: string; + message?: string; + }; models?: RuntimeModelOption[]; defaultModel?: string | null; }; @@ -273,6 +278,7 @@ export function FamiliarStudioBrainTab({ familiar }: Props) { const defaultHarnessId = familiar.defaultHarness ?? familiar.harness ?? ""; const defaultHarnessLabel = runtimeLabel(defaultHarnessId, harnesses); const harnessId = draftHarness || defaultHarnessId; + const selectedHarnessAvailability = harnesses.find((item) => item.id === harnessId)?.availability; // Model parity: source the per-familiar model menu from the same runtime → // provider catalog the chat picker uses. allowCustom keeps the free-text @@ -845,6 +851,11 @@ export function FamiliarStudioBrainTab({ familiar }: Props) { } satisfies StandardSelectGroup, ]} /> + {selectedHarnessAvailability?.state !== undefined && selectedHarnessAvailability.state !== "ready" && selectedHarnessAvailability.message ? ( +

+ {selectedHarnessAvailability.message} +

+ ) : null}
diff --git a/src/components/familiar-summoning-circle.test.ts b/src/components/familiar-summoning-circle.test.ts index 7b88cff93..7e91288f0 100644 --- a/src/components/familiar-summoning-circle.test.ts +++ b/src/components/familiar-summoning-circle.test.ts @@ -45,6 +45,16 @@ assert.match( /fetch\("\/api\/harnesses"/, "local/SSH vessels list installed runtimes from /api/harnesses", ); +assert.match( + source, + /const installedHarnesses = \(harnesses \?\? \[\]\)\.filter\([\s\S]*?\(h\.availability\?\.state \?\? "ready"\) === "ready"/, + "the summoning picker only offers runtimes proven safe to launch", +); +assert.match( + source, + /const unavailableHarnesses = \(harnesses \?\? \[\]\)\.flatMap\([\s\S]*?availability\.state !== "ready"[\s\S]*?unavailableHarnesses\.map\(\(h\) => \([\s\S]*?h\.availability\.message/, + "the summoning picker shows safe remediation for installed-but-unlaunchable runtimes", +); assert.match( harnessesRoute, /runtimeHost: hostname\(\)/, diff --git a/src/components/familiar-summoning-circle.tsx b/src/components/familiar-summoning-circle.tsx index 9f75b021c..1d36e42c0 100644 --- a/src/components/familiar-summoning-circle.tsx +++ b/src/components/familiar-summoning-circle.tsx @@ -772,8 +772,14 @@ function StageVessel({ // launcher. Hide it for SSH rather than letting a selection fall back to an // incompatible `coven run --stream-json` path. const installedHarnesses = (harnesses ?? []).filter( - (h) => h.installed && (vessel !== "ssh" || h.id !== "grok"), + (h) => h.installed && (h.availability?.state ?? "ready") === "ready" && (vessel !== "ssh" || h.id !== "grok"), ); + const unavailableHarnesses = (harnesses ?? []).flatMap((h) => { + const availability = h.availability; + return h.installed && availability && availability.state !== "ready" + ? [{ ...h, availability }] + : []; + }); return (
@@ -811,6 +817,11 @@ function StageVessel({

No chat-capable runtime found. Run setup to install one (Codex, Claude Code, Copilot…), then return to the circle.

+ {unavailableHarnesses.map((h) => ( +

+ {h.label}: {h.availability.message} +

+ ))}
)} + {unavailableHarnesses.map((h) => ( +

+ {h.label}: {h.availability.message} +

+ ))}
) : null} From f3b27692fcfa03b7c37c6934348ffb15eb2853af Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:18:11 -0400 Subject: [PATCH 12/16] fix(opencode): keep ambiguous Windows races generic --- src/app/api/chat/send/harness-routing-opencode.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/chat/send/harness-routing-opencode.test.ts b/src/app/api/chat/send/harness-routing-opencode.test.ts index e480e670b..882d8cbb0 100644 --- a/src/app/api/chat/send/harness-routing-opencode.test.ts +++ b/src/app/api/chat/send/harness-routing-opencode.test.ts @@ -77,8 +77,8 @@ assert.match( ); assert.match( route, - /const openCodePowerShellHostFailed =[\s\S]*?process\.platform === "win32";[\s\S]*?const openCodeCommandMissing =[\s\S]*?launchFailure \?\?= \{[\s\S]*?"runtime_unlaunchable"[\s\S]*?"runtime_missing"[\s\S]*?child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?reportLaunchFailure\(err\)/, - "an OpenCode launch race classifies any failed PowerShell host as unlaunchable and preserves a missing inner command distinctly", + /const openCodeWindowsOuterLaunchFailure =[\s\S]*?process\.platform === "win32";[\s\S]*?const openCodeCommandMissing =[\s\S]*?!openCodeWindowsOuterLaunchFailure[\s\S]*?launchFailure \?\?= \{[\s\S]*?"runtime_missing"[\s\S]*?"runtime_launch_failed"[\s\S]*?child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?reportLaunchFailure\(err\)/, + "an ambiguous Windows outer-process race stays generic while a missing POSIX OpenCode command remains distinct", ); assert.match( route, From 0517035b850d1468f49c7a6e0ab7d8da7b4305ae Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:48:10 -0400 Subject: [PATCH 13/16] fix(opencode): preserve scoped preflight on rebase --- .../send/harness-routing-opencode.test.ts | 12 ++-- src/app/api/chat/send/route.ts | 60 +++++++++++-------- src/app/api/harnesses/route.test.ts | 2 +- src/app/api/harnesses/route.ts | 2 +- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/app/api/chat/send/harness-routing-opencode.test.ts b/src/app/api/chat/send/harness-routing-opencode.test.ts index 882d8cbb0..73e591aee 100644 --- a/src/app/api/chat/send/harness-routing-opencode.test.ts +++ b/src/app/api/chat/send/harness-routing-opencode.test.ts @@ -12,7 +12,7 @@ assert.match( ); assert.match( route, - /let localRuntimePlan: LocalRuntimePlan \| null = null;[\s\S]*?const openCodeCapabilities = openCodeDirect[\s\S]*?probeReadyLocalRuntimeCapability\(\{[\s\S]*?plan: localRuntimePlan,[\s\S]*?runner: "opencode",[\s\S]*?probe: \(\) => openCodeRunCapabilities\(body\.familiarId\),[\s\S]*?resolveOpenCodeCompatibility\(openCodeCapabilities\)/, + /let localRuntimePlan: LocalRuntimePlan \| null = null;[\s\S]*?const openCodeCapabilities = openCodeDirect[\s\S]*?probeReadyLocalRuntimeCapability\(\{[\s\S]*?plan: localRuntimePlan,[\s\S]*?runner: "opencode",[\s\S]*?probe: \(\) => openCodeRunCapabilities\(body\.familiarId, undefined, localRuntimePlan\?\.env\),[\s\S]*?resolveOpenCodeCompatibility\(openCodeCapabilities\)/, "OpenCode capability discovery starts only after its exact local launch plan is ready", ); assert.match( @@ -67,17 +67,17 @@ assert.match( ); assert.match( route, - /const openCodeLaunchCommand = openCodeDirect[\s\S]*?command: localPlan\.command,[\s\S]*?args: \[\.\.\.localPlan\.fixedArgs\],[\s\S]*?input: JSON\.stringify\(spawnArgs\)[\s\S]*?const availability =[\s\S]*?command: localPlan\.command,[\s\S]*?env: localPlan\.env,[\s\S]*?const child = spawn\(command\.command, command\.args, \{[\s\S]*?env: localPlan\.env,[\s\S]*?writeOpenCodeLaunchInput\(child, openCodeLaunchCommand\)/, + /const openCodeLaunchCommand = openCodeDirect[\s\S]*?command: localPlan\.command,[\s\S]*?args: \[\.\.\.localPlan\.fixedArgs\],[\s\S]*?input: JSON\.stringify\(spawnArgs\)[\s\S]*?const availability =[\s\S]*?command: localPlan\.command,[\s\S]*?env: localPlan\.env,[\s\S]*?child = spawn\(command\.command, command\.args, \{[\s\S]*?env: localPlan\.env,[\s\S]*?writeOpenCodeLaunchInput\(child, openCodeLaunchCommand\)/, "OpenCode carries one Windows-safe outer host, inner command, and scoped environment from early preflight through the immediate spawn recheck", ); assert.match( route, - /runner:[\s\S]*?openCodeDirect[\s\S]*?"opencode"[\s\S]*?powerShellHostedCommand:[\s\S]*?openCodeLaunchCommand\?\.input !== undefined \? openCodeCommand\(\) : undefined[\s\S]*?if \(availability\.state !== "ready"\) \{[\s\S]*?launchFailure = \{ code: availability\.code, message: availability\.message \};[\s\S]*?return null;/, + /const env = openCodeSpawnEnv\(body\.familiarId\);[\s\S]*?const launch = openCodeLaunch\(\[\], process\.platform, env\);[\s\S]*?runner: "opencode",[\s\S]*?powerShellHostedCommand:[\s\S]*?launch\.input !== undefined \? openCodeCommand\(\) : undefined[\s\S]*?const availability =[\s\S]*?command: localPlan\.command,[\s\S]*?powerShellHostedCommand:[\s\S]*?localPlan\.powerShellHostedCommand,[\s\S]*?if \(availability\.state !== "ready"\) \{[\s\S]*?launchFailure = \{ code: availability\.code, message: availability\.message \};[\s\S]*?return null;/, "OpenCode preflights the exact PowerShell/JSON-stdin plan and returns the shared structured error before spawn", ); assert.match( route, - /const openCodeWindowsOuterLaunchFailure =[\s\S]*?process\.platform === "win32";[\s\S]*?const openCodeCommandMissing =[\s\S]*?!openCodeWindowsOuterLaunchFailure[\s\S]*?launchFailure \?\?= \{[\s\S]*?"runtime_missing"[\s\S]*?"runtime_launch_failed"[\s\S]*?child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?reportLaunchFailure\(err\)/, + /child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?const openCodeWindowsOuterLaunchFailure =[\s\S]*?process\.platform === "win32" && err\.code === "ENOENT";[\s\S]*?const openCodeCommandMissing =[\s\S]*?!openCodeWindowsOuterLaunchFailure[\s\S]*?launchFailure \?\?= \{[\s\S]*?"runtime_missing"[\s\S]*?"runtime_launch_failed"/, "an ambiguous Windows outer-process race stays generic while a missing POSIX OpenCode command remains distinct", ); assert.match( @@ -87,7 +87,7 @@ assert.match( ); assert.match( capabilities, - /const launch = openCodeLaunch\(\["run", "--help"\]\);[\s\S]*?launch\.command,[\s\S]*?launch\.args,[\s\S]*?openCodeSpawnEnv\(\),/, + /async function probeOpenCodeRunContract\(env: NodeJS\.ProcessEnv\)[\s\S]*?const helpLaunch = openCodeLaunch\(\["run", "--help"\], process\.platform, env\);[\s\S]*?probeOutput\(helpLaunch\.command, helpLaunch\.args, env, helpLaunch\.input\)/, "OpenCode probes its CLI with the same Windows-safe command and WSL-compatible environment as a chat run", ); assert.match( @@ -104,7 +104,7 @@ assert.match( const earlyGate = route.indexOf("const openCodeCapabilities"); assert.ok(earlyGate >= 0, "capability routing begins only after local plans are established"); for (const capabilityCall of [ - "openCodeRunCapabilities(body.familiarId)", + "openCodeRunCapabilities(body.familiarId, undefined, localRuntimePlan?.env)", "probe: hermesChatSupportsModel", "probeCovenCapability(covenRunSupportsModel)", "probeCovenCapability(covenRunSupportsPermission)", diff --git a/src/app/api/chat/send/route.ts b/src/app/api/chat/send/route.ts index 083d668cb..19b5dc18c 100644 --- a/src/app/api/chat/send/route.ts +++ b/src/app/api/chat/send/route.ts @@ -62,7 +62,6 @@ import { parseGrokStreamEvent, } from "@/lib/grok-build"; import { grokLaunchCommand } from "@/lib/grok-bin"; -<<<<<<< HEAD import { openCodeCommand, openCodeLaunch, @@ -78,17 +77,6 @@ import { type DirectRunnerId, type RuntimeAvailability, } from "@/lib/runtime-availability"; -======= -import { - openCodeCommand, - openCodeLaunch, - openCodeSpawnEnv, - OPENCODE_COMMAND_NOT_FOUND_MARKER, - OPENCODE_LAUNCH_FAILED_MARKER, - writeOpenCodeLaunchInput, -} from "@/lib/opencode-bin"; -import { evaluateRuntimeAvailability, missingRunnerMessage } from "@/lib/runtime-availability"; ->>>>>>> bbfd8a88 (fix(opencode): preserve PowerShell inner launch failures) import { quarantineOpenCodeSchema, redactedOpenCodeEventFingerprint, @@ -1185,7 +1173,7 @@ export async function POST(req: Request) { ? await probeReadyLocalRuntimeCapability({ plan: localRuntimePlan, runner: "opencode", - probe: () => openCodeRunCapabilities(body.familiarId), + probe: () => openCodeRunCapabilities(body.familiarId, undefined, localRuntimePlan?.env), }) : null; const openCodeCompatibility = openCodeCapabilities @@ -3204,18 +3192,40 @@ export async function POST(req: Request) { command: localPlan.command, args: [...localPlan.fixedArgs, ...spawnArgs], }; - const child = spawn(command.command, command.args, { - // Spawn IN the familiar's workspace when no project root was - // supplied, so coven's project-root resolver picks that dir as - // root and Codex/Claude pick up AGENTS.md / SOUL.md / IDENTITY.md - // from the familiar's home. When a project root IS supplied, - // honor that instead. - cwd, - stdio: openCodeLaunchCommand?.input === undefined - ? ["ignore", "pipe", "pipe"] - : ["pipe", "pipe", "pipe"], - env: localPlan.env, - }) as ChildProcessWithoutNullStreams; + let child: ChildProcessWithoutNullStreams; + try { + child = spawn(command.command, command.args, { + // Spawn IN the familiar's workspace when no project root was + // supplied, so coven's project-root resolver picks that dir as + // root and Codex/Claude pick up AGENTS.md / SOUL.md / IDENTITY.md + // from the familiar's home. When a project root IS supplied, + // honor that instead. + cwd, + stdio: openCodeLaunchCommand?.input === undefined + ? ["ignore", "pipe", "pipe"] + : ["pipe", "pipe", "pipe"], + env: localPlan.env, + }) as ChildProcessWithoutNullStreams; + } catch (error) { + const err = error as NodeJS.ErrnoException; + const ambiguousWindowsOpenCodeRace = + openCodeDirect && process.platform === "win32" && err.code === "ENOENT"; + const missingOpenCodeCommand = + openCodeDirect && !ambiguousWindowsOpenCodeRace && err.code === "ENOENT"; + const message = missingOpenCodeCommand + ? missingRunnerMessage("opencode") + : openCodeDirect + ? "OpenCode failed to start. Check its installation and try again." + : localRuntimeLaunchError(localPlan.runner, err.code).message; + launchFailure = { + code: missingOpenCodeCommand ? "runtime_missing" : "runtime_launch_failed", + message, + }; + result.is_error = true; + pushProgress("harness-start", `${binding.harness} failed to start`, "error", message, Date.now() - attemptStartedAt); + push({ kind: "error", code: launchFailure.code, message }); + return null; + } if (openCodeLaunchCommand) { writeOpenCodeLaunchInput(child, openCodeLaunchCommand); } diff --git a/src/app/api/harnesses/route.test.ts b/src/app/api/harnesses/route.test.ts index 176373cdb..2306a0f7a 100644 --- a/src/app/api/harnesses/route.test.ts +++ b/src/app/api/harnesses/route.test.ts @@ -22,7 +22,7 @@ assert.match( ); assert.match( source, - /const env = id === "opencode" \? openCodeSpawnEnv\(null\) : harnessSpawnEnv\(null\);[\s\S]*?const launch = openCodeLaunch\(\[\], process\.platform, env\);/, + /const env =\s*id === "opencode" \? openCodeSpawnEnv\(null\) : harnessSpawnEnv\(null\);[\s\S]*?const launch = openCodeLaunch\(\[\], process\.platform, env\);/, "OpenCode availability derives its Windows PowerShell host from the same scoped environment it probes", ); assert.match( diff --git a/src/app/api/harnesses/route.ts b/src/app/api/harnesses/route.ts index 364b70ac4..afe705ef2 100644 --- a/src/app/api/harnesses/route.ts +++ b/src/app/api/harnesses/route.ts @@ -80,7 +80,7 @@ async function adapterAvailability(id: string): Promise { const env = id === "opencode" ? openCodeSpawnEnv(null) : harnessSpawnEnv(null); if (id === "opencode") { - const launch = openCodeLaunch([]); + const launch = openCodeLaunch([], process.platform, env); return { availability: summarizeRuntimeAvailability(evaluateRuntimeAvailability({ runner: "opencode", From d4aba87df9f664513f080bc04e6940829cff5633 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:05:19 -0400 Subject: [PATCH 14/16] test(opencode): update launch-race API contract --- src/app/api/api-contracts.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/api-contracts.test.ts b/src/app/api/api-contracts.test.ts index c4e10e577..49bc7213c 100644 --- a/src/app/api/api-contracts.test.ts +++ b/src/app/api/api-contracts.test.ts @@ -462,12 +462,12 @@ for (const contract of contracts) { ); assert.match( sendSource, - /launchFailure \?\?= \{[\s\S]{0,180}?message: launchError,[\s\S]{0,400}?pushProgress\([\s\S]{0,220}?launchError,[\s\S]{0,300}?code: "ENOENT",\s*message: launchError/, - "/chat/send: launch state, progress, and the post-spawn ENOENT race event must reuse one normalized message", + /launchFailure \?\?= \{[\s\S]{0,400}?message: launchError,[\s\S]{0,400}?pushProgress\([\s\S]{0,220}?launchError,[\s\S]{0,800}?code: launchFailure\.code,\s*message: launchError/, + "/chat/send: launch state, progress, and the post-spawn race event must reuse one normalized message and structured code", ); assert.match( sendSource, - /const localLaunchError = localRuntimeLaunchError\([\s\S]{0,100}?err\.code,[\s\S]{0,80}?const launchError = sshRuntime[\s\S]{0,180}?: localLaunchError\.message/, + /const localLaunchError = localRuntimeLaunchError\([\s\S]{0,200}?err\.code,[\s\S]{0,800}?const launchError = sshRuntime[\s\S]{0,600}?: localLaunchError\.message/, "/chat/send: every local post-spawn failure uses the shared runner-specific normalizer while SSH retains transport diagnostics", ); assert.doesNotMatch( From 882b15d3953ad2f72a0dbb1e81321323f141136d Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:02:39 -0400 Subject: [PATCH 15/16] fix: remove unused availability fixture path --- src/lib/runtime-availability.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/runtime-availability.test.ts b/src/lib/runtime-availability.test.ts index 66e41b6aa..1a67988d8 100644 --- a/src/lib/runtime-availability.test.ts +++ b/src/lib/runtime-availability.test.ts @@ -50,7 +50,6 @@ try { // Verification matrix: binary resolves in the spawn env → ready. // Do not use this Windows host's temporary path while simulating Linux: // a drive letter contains `:`, which is a POSIX PATH delimiter. - const posixBinDir = "/runtime-availability/bin"; const ready = evaluateRuntimeAvailability({ runner: "grok", command: "grok", From 2a56a6f70f1fd1292c7b7f132d53791a9476e468 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:24:08 -0400 Subject: [PATCH 16/16] test: align launch failure contract --- src/app/api/api-contracts.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/api-contracts.test.ts b/src/app/api/api-contracts.test.ts index 7ead3a421..45632adf8 100644 --- a/src/app/api/api-contracts.test.ts +++ b/src/app/api/api-contracts.test.ts @@ -462,8 +462,8 @@ for (const contract of contracts) { ); assert.match( sendSource, - /launchFailure \?\?= \{[\s\S]{0,400}?message: launchError,[\s\S]{0,400}?pushProgress\([\s\S]{0,220}?launchError,[\s\S]{0,800}?code: launchFailure\.code,\s*message: launchError/, - "/chat/send: launch state, progress, and the post-spawn race event must reuse one normalized message and structured code", + /const reportLaunchFailure = \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?launchFailure \?\?= \{[\s\S]*?message: launchError,[\s\S]*?pushProgress\([\s\S]*?launchError,[\s\S]*?code: launchFailure\.code[\s\S]*?message: launchError/, + "/chat/send: launch state, progress, and the post-spawn race event must reuse one normalized message and structured code", ); assert.match( sendSource,