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.
@@ -708,7 +707,7 @@ function ChatErrorStrip({
) : 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}
+
+ ))}
window.dispatchEvent(new CustomEvent("cave:onboarding-open"))}
diff --git a/src/components/familiar-summoning-model.ts b/src/components/familiar-summoning-model.ts
index 2bdd456c3..7ff315727 100644
--- a/src/components/familiar-summoning-model.ts
+++ b/src/components/familiar-summoning-model.ts
@@ -1,4 +1,5 @@
import type { IconName } from "@/lib/icon";
+import type { RuntimeAvailabilitySummary } from "@/lib/runtime-availability";
export type VesselKind = "local" | "ssh" | "openclaw";
@@ -7,6 +8,7 @@ export type HarnessReport = {
label: string;
chatSupported: boolean;
installed: boolean;
+ availability?: RuntimeAvailabilitySummary;
};
export type OpenClawAgent = {
From 10710018efc8042deecc9a4cad1aab7fcaea1635 Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 12:01:05 -0400
Subject: [PATCH 03/16] test(chat): cover unavailable OpenCode launches
---
.../send/harness-routing-opencode.test.ts | 10 ++++++
.../send/route-opencode.integration.test.ts | 32 +++++++++++++++++++
2 files changed, 42 insertions(+)
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 ca56e3e54..e024351ee 100644
--- a/src/app/api/chat/send/harness-routing-opencode.test.ts
+++ b/src/app/api/chat/send/harness-routing-opencode.test.ts
@@ -70,6 +70,16 @@ assert.match(
/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\)/,
"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;/,
+ "OpenCode preflights the exact PowerShell/JSON-stdin plan and returns the shared structured error before spawn",
+);
+assert.match(
+ route,
+ /child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?const launchError = openCodeDirect[\s\S]*?result\.is_error = true;[\s\S]*?launchFailure \?\?= \{[\s\S]*?err\.code === "ENOENT" \? "ENOENT" : "runtime_launch_failed"/,
+ "an OpenCode launch race marks the turn failed before empty-output/auth diagnostics can run",
+);
assert.match(
capabilities,
/const launch = openCodeLaunch\(\["run", "--help"\]\);[\s\S]*?launch\.command,[\s\S]*?launch\.args,[\s\S]*?openCodeSpawnEnv\(\),/,
diff --git a/src/app/api/chat/send/route-opencode.integration.test.ts b/src/app/api/chat/send/route-opencode.integration.test.ts
index 13451c402..25976cd71 100644
--- a/src/app/api/chat/send/route-opencode.integration.test.ts
+++ b/src/app/api/chat/send/route-opencode.integration.test.ts
@@ -141,6 +141,38 @@ try {
const quotedResumeBody = await quotedResumeResponse.text();
assert.match(quotedResumeBody, /Session not found in the documentation\./, "plain fallback preserves assistant text that resembles a resume failure");
assert.doesNotMatch(quotedResumeBody, /No assistant text returned/, "quoted resume-failure text does not become a synthetic empty-response error");
+
+ // The OpenCode-specific preflight runs after capability discovery but before
+ // a prompt/model command is created. Removing the same shim used by the
+ // successful turn proves the route returns the shared structured remediation
+ // and never manufactures an auth/no-output assistant response.
+ await rm(path.join(bin, executable), { force: true });
+ const missingResponse = await POST(new Request("http://localhost/api/chat/send", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ familiarId: "opal", prompt: "missing OpenCode must not run", projectRoot: familiarWorkspace }),
+ }));
+ assert.equal(missingResponse.status, 200, await missingResponse.clone().text());
+ const missingBody = await missingResponse.text();
+ const missingEvents = missingBody
+ .split("\n")
+ .filter((line) => line.startsWith("data: "))
+ .map((line) => JSON.parse(line.slice("data: ".length)));
+ const missingError = missingEvents.find((event) => event.kind === "error");
+ assert.equal(missingError?.code, "runtime_missing", "missing OpenCode is classified before the chat launch");
+ assert.match(missingError?.message ?? "", /OpenCode CLI not found on PATH/, "missing OpenCode uses its install/PATH remediation");
+ assert.doesNotMatch(missingBody, /installed but not authenticated|produced no output/i, "a missing OpenCode binary never becomes an auth or empty-output diagnosis");
+ assert.ok(!missingEvents.some((event) => event.kind === "assistant_chunk"), "a missing OpenCode binary streams no fabricated assistant response");
+ const missingDone = missingEvents.findLast((event) => event.kind === "done");
+ assert.equal(missingDone?.isError, true, "the no-spawn OpenCode result is terminally errored");
+ if (missingDone?.sessionId) {
+ const missingConversation = await loadConversation(missingDone.sessionId);
+ assert.equal(
+ (missingConversation?.turns ?? []).filter((turn) => turn.role === "assistant").length,
+ 0,
+ "the unavailable OpenCode turn never persists a fabricated assistant message",
+ );
+ }
} finally {
if (previousHome === undefined) delete process.env.COVEN_HOME;
else process.env.COVEN_HOME = previousHome;
From f07476bf2eadf9a42ab8a7fb508c8c762d8539db Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 12:02:48 -0400
Subject: [PATCH 04/16] fix(test): type OpenCode launcher fixture
---
src/lib/runtime-availability.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib/runtime-availability.test.ts b/src/lib/runtime-availability.test.ts
index 67a0f6205..9f1678bf8 100644
--- a/src/lib/runtime-availability.test.ts
+++ b/src/lib/runtime-availability.test.ts
@@ -283,7 +283,7 @@ try {
const openCodeWindowsLaunch = openCodeLaunch(
["run", "safe & literal"],
"win32",
- { SystemRoot: "C:\\Windows" },
+ { SystemRoot: "C:\\Windows", NODE_ENV: "test" },
);
const psHost = openCodeWindowsLaunch.command;
assert.equal(
From 5de26e1febd336794c2e8637659d1293800728e1 Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 12:44:07 -0400
Subject: [PATCH 05/16] fix(opencode): preserve PowerShell inner launch
failures
---
.../send/harness-routing-opencode.test.ts | 5 ++
src/app/api/chat/send/route.ts | 51 ++++++++++++++++++-
src/lib/opencode-bin.test.ts | 12 ++++-
src/lib/opencode-bin.ts | 10 +++-
4 files changed, 74 insertions(+), 4 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 e024351ee..7f6ba3248 100644
--- a/src/app/api/chat/send/harness-routing-opencode.test.ts
+++ b/src/app/api/chat/send/harness-routing-opencode.test.ts
@@ -80,6 +80,11 @@ assert.match(
/child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?const launchError = openCodeDirect[\s\S]*?result\.is_error = true;[\s\S]*?launchFailure \?\?= \{[\s\S]*?err\.code === "ENOENT" \? "ENOENT" : "runtime_launch_failed"/,
"an OpenCode launch race marks the turn failed before empty-output/auth diagnostics can run",
);
+assert.match(
+ route,
+ /OPENCODE_COMMAND_NOT_FOUND_MARKER[\s\S]*?OPENCODE_LAUNCH_FAILED_MARKER[\s\S]*?launchFailure = \{[\s\S]*?code: commandMissing \? "runtime_missing" : "runtime_launch_failed"[\s\S]*?push\(\{ kind: "error", code: launchFailure\.code, message: launchError \}\)/,
+ "a PowerShell-hosted inner OpenCode race is streamed as a launch failure instead of becoming synthetic no-output text",
+);
assert.match(
capabilities,
/const launch = openCodeLaunch\(\["run", "--help"\]\);[\s\S]*?launch\.command,[\s\S]*?launch\.args,[\s\S]*?openCodeSpawnEnv\(\),/,
diff --git a/src/app/api/chat/send/route.ts b/src/app/api/chat/send/route.ts
index 2836f27af..5aa75205f 100644
--- a/src/app/api/chat/send/route.ts
+++ b/src/app/api/chat/send/route.ts
@@ -62,13 +62,33 @@ import {
parseGrokStreamEvent,
} from "@/lib/grok-build";
import { grokLaunchCommand } from "@/lib/grok-bin";
-import { openCodeCommand, openCodeLaunch, openCodeSpawnEnv, writeOpenCodeLaunchInput } from "@/lib/opencode-bin";
+<<<<<<< HEAD
+import {
+ openCodeCommand,
+ openCodeLaunch,
+ openCodeSpawnEnv,
+ OPENCODE_COMMAND_NOT_FOUND_MARKER,
+ OPENCODE_LAUNCH_FAILED_MARKER,
+ writeOpenCodeLaunchInput,
+} from "@/lib/opencode-bin";
import {
evaluateRuntimeAvailability,
localRuntimeLaunchError,
+ missingRunnerMessage,
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,
@@ -3279,8 +3299,33 @@ export async function POST(req: Request) {
if (chunk) handleStdoutChunk(chunk);
});
+ let openCodeLaunchMarkerTail = "";
child.stderr.on("data", (data: Buffer) => {
const text = stripAnsi(data.toString("utf8"));
+ if (openCodeDirect) {
+ const markerText = `${openCodeLaunchMarkerTail}${text}`;
+ openCodeLaunchMarkerTail = markerText.slice(-OPENCODE_COMMAND_NOT_FOUND_MARKER.length);
+ const commandMissing = markerText.includes(OPENCODE_COMMAND_NOT_FOUND_MARKER);
+ const launchFailed = markerText.includes(OPENCODE_LAUNCH_FAILED_MARKER);
+ if (!launchFailure && (commandMissing || launchFailed)) {
+ const launchError = commandMissing
+ ? missingRunnerMessage("opencode")
+ : "OpenCode failed to start. Check its installation and try again.";
+ launchFailure = {
+ code: commandMissing ? "runtime_missing" : "runtime_launch_failed",
+ message: launchError,
+ };
+ result.is_error = true;
+ pushProgress(
+ "harness-start",
+ "opencode failed to start",
+ "error",
+ launchError,
+ Date.now() - attemptStartedAt,
+ );
+ push({ kind: "error", code: launchFailure.code, message: launchError });
+ }
+ }
captureHermesSessionFromStderr(text);
if (RESUME_ERR_RE.test(text)) resumeFailed = true;
if (!adapterConflict) {
@@ -3288,6 +3333,10 @@ export async function POST(req: Request) {
}
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
+ if (
+ trimmed === OPENCODE_COMMAND_NOT_FOUND_MARKER
+ || trimmed === OPENCODE_LAUNCH_FAILED_MARKER
+ ) continue;
if (!trimmed) continue;
// Claude stderr can include tool payloads. It must not be copied
// into the generic empty-response diagnostic, which is rendered
diff --git a/src/lib/opencode-bin.test.ts b/src/lib/opencode-bin.test.ts
index e4df8aff7..f566b6772 100644
--- a/src/lib/opencode-bin.test.ts
+++ b/src/lib/opencode-bin.test.ts
@@ -1,6 +1,13 @@
// @ts-nocheck
import assert from "node:assert/strict";
-import { openCodeCommand, openCodeLaunch, openCodeNeedsTmpRuntimeDir, preferOpenCodeLaunchPath } from "./opencode-bin.ts";
+import {
+ openCodeCommand,
+ openCodeLaunch,
+ openCodeNeedsTmpRuntimeDir,
+ OPENCODE_COMMAND_NOT_FOUND_MARKER,
+ OPENCODE_LAUNCH_FAILED_MARKER,
+ preferOpenCodeLaunchPath,
+} from "./opencode-bin.ts";
assert.equal(openCodeCommand(), "opencode", "OpenCode uses the same executable name on all desktop platforms");
@@ -14,6 +21,9 @@ assert.equal(windowsLaunch.command, "C:\\Windows\\System32\\WindowsPowerShell\\v
assert.ok(windowsLaunch.args.includes("-NoProfile"), "Windows launch does not load profile aliases");
assert.equal(windowsLaunch.input, JSON.stringify(["run", "safe & literal", "percent%PATH%"]), "Windows launch preserves untrusted argv as JSON data");
assert.match(windowsLaunch.args.at(-1) ?? "", /\[Console\]::In\.ReadToEnd\(\)/, "Windows launch reads argv from stdin instead of a shell command");
+assert.match(windowsLaunch.args.at(-1) ?? "", new RegExp(OPENCODE_COMMAND_NOT_FOUND_MARKER), "Windows reports an inner OpenCode command race without exposing shell details");
+assert.match(windowsLaunch.args.at(-1) ?? "", new RegExp(OPENCODE_LAUNCH_FAILED_MARKER), "Windows reports other inner launch failures without exposing shell details");
+assert.ok(!windowsLaunch.args.join(" ").includes("safe & literal"), "Windows never interpolates untrusted argv into PowerShell code");
const longPrompt = "x".repeat(40_000);
const longWindowsLaunch = openCodeLaunch(["run", longPrompt], "win32", { SystemRoot: "C:\\Windows" });
assert.equal(longWindowsLaunch.input, JSON.stringify(["run", longPrompt]), "Windows preserves a long prompt outside the command line");
diff --git a/src/lib/opencode-bin.ts b/src/lib/opencode-bin.ts
index 8a4dd2c31..6063645ce 100644
--- a/src/lib/opencode-bin.ts
+++ b/src/lib/opencode-bin.ts
@@ -6,6 +6,13 @@ export function openCodeCommand(): string {
return "opencode";
}
+// Fixed, value-free sentinels emitted only when the Windows PowerShell host
+// could not invoke the inner CLI after the route's preflight passed. They let
+// the route preserve the no-synthetic-turn launch-failure contract for the
+// small race between the stat check and PowerShell resolving `opencode`.
+export const OPENCODE_COMMAND_NOT_FOUND_MARKER = "COVEN_OPENCODE_COMMAND_NOT_FOUND";
+export const OPENCODE_LAUNCH_FAILED_MARKER = "COVEN_OPENCODE_LAUNCH_FAILED";
+
export type OpenCodeLaunch = { command: string; args: string[]; input?: string };
function windowsPowerShell(env: NodeJS.ProcessEnv): string {
@@ -34,8 +41,7 @@ export function openCodeLaunch(
"[Console]::OutputEncoding = $utf8",
"$OutputEncoding = $utf8",
"$openCodeArgs = [Console]::In.ReadToEnd() | ConvertFrom-Json",
- "& opencode @openCodeArgs",
- "exit $LASTEXITCODE",
+ "try { & opencode @openCodeArgs; exit $LASTEXITCODE } catch { if ($_.Exception -is [System.Management.Automation.CommandNotFoundException]) { [Console]::Error.WriteLine('COVEN_OPENCODE_COMMAND_NOT_FOUND'); exit 127 }; [Console]::Error.WriteLine('COVEN_OPENCODE_LAUNCH_FAILED'); exit 126 }",
].join("; ");
return {
command: windowsPowerShell(env),
From 2301a97e1bdb4809de3ebd09d9e36612dac450eb Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 12:56:37 -0400
Subject: [PATCH 06/16] fix(opencode): classify launch races by runner
---
.../send/harness-routing-opencode.test.ts | 4 +--
src/app/api/chat/send/route.ts | 28 +++++++++++++++++--
2 files changed, 27 insertions(+), 5 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 7f6ba3248..20966d37e 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,
- /child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?const launchError = openCodeDirect[\s\S]*?result\.is_error = true;[\s\S]*?launchFailure \?\?= \{[\s\S]*?err\.code === "ENOENT" \? "ENOENT" : "runtime_launch_failed"/,
- "an OpenCode launch race marks the turn failed before empty-output/auth diagnostics can run",
+ /child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?const openCodePowerShellHostMissing =[\s\S]*?process\.platform === "win32"[\s\S]*?const openCodeCommandMissing =[\s\S]*?launchFailure \?\?= \{[\s\S]*?"runtime_unlaunchable"[\s\S]*?"runtime_missing"/,
+ "an OpenCode launch race preserves distinct PowerShell-host and OpenCode-missing classifications before empty-output/auth diagnostics can run",
);
assert.match(
route,
diff --git a/src/app/api/chat/send/route.ts b/src/app/api/chat/send/route.ts
index 5aa75205f..083d668cb 100644
--- a/src/app/api/chat/send/route.ts
+++ b/src/app/api/chat/send/route.ts
@@ -3356,18 +3356,34 @@ export async function POST(req: Request) {
localRuntimePlan?.runner ?? "coven",
err.code,
);
+ // Node uses the same ENOENT shape when either the Windows
+ // PowerShell host or the selected cwd vanishes after preflight.
+ // The preflight owns the precise host diagnosis; this ambiguous
+ // post-preflight race must stay generic rather than blaming either.
+ const openCodeWindowsOuterLaunchFailure =
+ openCodeDirect && process.platform === "win32" && err.code === "ENOENT";
+ const openCodeCommandMissing =
+ openCodeDirect && !openCodeWindowsOuterLaunchFailure && err.code === "ENOENT";
const launchError = sshRuntime
? err.code === "ENOENT"
? "ssh CLI not found on PATH. Install OpenSSH or run this familiar locally."
: err.message
- : localLaunchError.message;
+ : openCodeWindowsOuterLaunchFailure
+ ? "OpenCode failed to start. Check its installation and try again."
+ : openCodeCommandMissing
+ ? missingRunnerMessage("opencode")
+ : localLaunchError.message;
// Race-safe fallback (#3856): the pre-spawn gate can pass and the
// binary still vanish before spawn. Mark the run errored BEFORE
// the empty-output diagnostic can run, so a launch failure is
// never misreported as "installed but not authenticated".
result.is_error = true;
launchFailure ??= {
- code: localLaunchError.code,
+ code: openCodeCommandMissing
+ ? "runtime_missing"
+ : openCodeWindowsOuterLaunchFailure
+ ? "runtime_launch_failed"
+ : localLaunchError.code,
message: launchError,
};
pushProgress(
@@ -3377,7 +3393,13 @@ export async function POST(req: Request) {
launchError,
Date.now() - attemptStartedAt,
);
- if (err.code === "ENOENT") {
+ if (openCodeWindowsOuterLaunchFailure || openCodeCommandMissing) {
+ push({
+ kind: "error",
+ code: launchFailure.code,
+ message: launchError,
+ });
+ } else if (err.code === "ENOENT") {
push({
kind: "error",
code: "ENOENT",
From 4467eac496c86ab02b317c4acfb4bf114fba3806 Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 13:06:59 -0400
Subject: [PATCH 07/16] fix(opencode): gate capability probes on preflight
---
src/app/api/chat/send/route-opencode.integration.test.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/app/api/chat/send/route-opencode.integration.test.ts b/src/app/api/chat/send/route-opencode.integration.test.ts
index 25976cd71..7faa127fe 100644
--- a/src/app/api/chat/send/route-opencode.integration.test.ts
+++ b/src/app/api/chat/send/route-opencode.integration.test.ts
@@ -142,10 +142,10 @@ try {
assert.match(quotedResumeBody, /Session not found in the documentation\./, "plain fallback preserves assistant text that resembles a resume failure");
assert.doesNotMatch(quotedResumeBody, /No assistant text returned/, "quoted resume-failure text does not become a synthetic empty-response error");
- // The OpenCode-specific preflight runs after capability discovery but before
- // a prompt/model command is created. Removing the same shim used by the
- // successful turn proves the route returns the shared structured remediation
- // and never manufactures an auth/no-output assistant response.
+ // The OpenCode-specific preflight runs before capability discovery or a
+ // prompt/model command. Removing the same shim used by the successful turn
+ // proves the route returns the shared structured remediation and never
+ // manufactures an auth/no-output assistant response.
await rm(path.join(bin, executable), { force: true });
const missingResponse = await POST(new Request("http://localhost/api/chat/send", {
method: "POST",
From 1e2b9bd6f72769078ca6e0801e19cc4bca730f9e Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 13:16:17 -0400
Subject: [PATCH 08/16] fix(opencode): bind launcher plan to spawn env
---
src/app/api/chat/send/chat-send-capabilities.ts | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/app/api/chat/send/chat-send-capabilities.ts b/src/app/api/chat/send/chat-send-capabilities.ts
index e5d75dda9..1f6ec044b 100644
--- a/src/app/api/chat/send/chat-send-capabilities.ts
+++ b/src/app/api/chat/send/chat-send-capabilities.ts
@@ -353,8 +353,8 @@ function advertisedStructuredSwitches(options: string[], noValueOptions: string[
type OpenCodeRunContractProbe = { helpProbe: ProbeOutput; versionProbe: ProbeOutput };
async function probeOpenCodeRunContract(env: NodeJS.ProcessEnv): Promise {
- const helpLaunch = openCodeLaunch(["run", "--help"]);
- const versionLaunch = openCodeLaunch(["--version"]);
+ const helpLaunch = openCodeLaunch(["run", "--help"], process.platform, env);
+ const versionLaunch = openCodeLaunch(["--version"], process.platform, env);
const [helpProbe, versionProbe] = await Promise.all([
probeOutput(helpLaunch.command, helpLaunch.args, env, helpLaunch.input),
probeOutput(versionLaunch.command, versionLaunch.args, env, versionLaunch.input),
@@ -452,12 +452,13 @@ export function hermesChatSupportsModel(): Promise {
/** OpenCode is direct-spawned so its own documented capability is authoritative. */
export function openCodeRunSupportsModel(): Promise {
- const launch = openCodeLaunch(["run", "--help"]);
+ const env = openCodeSpawnEnv();
+ const launch = openCodeLaunch(["run", "--help"], process.platform, env);
return (openCodeModelFlagProbe ??= probeHelp(
launch.command,
launch.args,
(help) => /(^|\s)--model(?![\w-])/m.test(help),
- openCodeSpawnEnv(),
+ env,
launch.input,
));
}
From d77bc4960a480aa480fef14ce2ffbe448d72982f Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 13:29:06 -0400
Subject: [PATCH 09/16] fix(opencode): share scoped launch environment
---
.../api/chat/send/chat-send-capabilities.test.ts | 14 ++++++++++----
src/app/api/chat/send/chat-send-capabilities.ts | 3 ++-
src/app/api/harnesses/route.test.ts | 5 +++++
3 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/src/app/api/chat/send/chat-send-capabilities.test.ts b/src/app/api/chat/send/chat-send-capabilities.test.ts
index f8f94f378..1226eb7cb 100644
--- a/src/app/api/chat/send/chat-send-capabilities.test.ts
+++ b/src/app/api/chat/send/chat-send-capabilities.test.ts
@@ -24,10 +24,16 @@ assert.notEqual(
assert.equal(openCodeCapabilityProbeScope(), "default", "the unscoped probe cache has a stable explicit scope");
assert.deepEqual(openCodeProbeSpawnOptions("linux"), { detached: true }, "POSIX OpenCode probes create an isolated process group that timeout cleanup can terminate");
assert.deepEqual(openCodeProbeSpawnOptions("win32"), { detached: false }, "Windows probes rely on taskkill's explicit process-tree cleanup rather than a detached process group");
-const firstCapabilities = await openCodeRunCapabilities("probe-fixture", async () => ({
- helpProbe: { complete: true, output: " --format Output format: text, json\n" },
- versionProbe: { complete: true, output: "opencode 1.0.0" },
-}));
+const probeEnv = { PATH: "/scoped/opencode", XDG_RUNTIME_DIR: "/tmp" };
+let observedProbeEnv: NodeJS.ProcessEnv | undefined;
+const firstCapabilities = await openCodeRunCapabilities("probe-fixture", async (env) => {
+ observedProbeEnv = env;
+ return {
+ helpProbe: { complete: true, output: " --format Output format: text, json\n" },
+ versionProbe: { complete: true, output: "opencode 1.0.0" },
+ };
+}, probeEnv);
+assert.equal(observedProbeEnv, probeEnv, "callers can retain one scoped environment for preflight, probes, and launch");
const replacementCapabilities = await openCodeRunCapabilities("probe-fixture", async () => ({
helpProbe: { complete: true, output: " --format Output format: text\n" },
versionProbe: { complete: true, output: "opencode 1.0.0" },
diff --git a/src/app/api/chat/send/chat-send-capabilities.ts b/src/app/api/chat/send/chat-send-capabilities.ts
index 1f6ec044b..32fae40ae 100644
--- a/src/app/api/chat/send/chat-send-capabilities.ts
+++ b/src/app/api/chat/send/chat-send-capabilities.ts
@@ -471,11 +471,12 @@ export function openCodeRunSupportsModel(): Promise {
export async function openCodeRunCapabilities(
familiarId?: string,
probeRunContract: (env: NodeJS.ProcessEnv) => Promise = probeOpenCodeRunContract,
+ spawnEnv?: NodeJS.ProcessEnv,
): Promise {
// Probe the exact scoped environment used for this chat turn. A version
// string alone is not a safe cache key: package managers and shims can
// replace a CLI in place while preserving both PATH and `--version`.
- const env = openCodeSpawnEnv(familiarId);
+ const env = spawnEnv ?? openCodeSpawnEnv(familiarId);
const { helpProbe, versionProbe } = await probeRunContract(env);
const version = versionProbe.complete
? versionProbe.output.match(/\b\d+(?:\.\d+){1,3}(?:[-+][\w.-]+)?\b/)?.[0] ?? null
diff --git a/src/app/api/harnesses/route.test.ts b/src/app/api/harnesses/route.test.ts
index 5ae99552e..176373cdb 100644
--- a/src/app/api/harnesses/route.test.ts
+++ b/src/app/api/harnesses/route.test.ts
@@ -20,6 +20,11 @@ assert.match(
/grokLaunchCommandForBinary\(path\)/,
"Grok probes must run npm .cmd shims through their spawn-safe launch command",
);
+assert.match(
+ source,
+ /const env = 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(
source,
/const resolvedBinary = h\.id === "grok" \? grokBin\(\) : h\.binary;[\s\S]*?h\.id === "grok" && resolvedBinary !== h\.binary[\s\S]*?: await which\(h\.binary\)/,
From 18a108a60fae29713c130f37177e7647273c6f89 Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 13:42:06 -0400
Subject: [PATCH 10/16] fix(opencode): preserve synchronous launch failures
---
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 20966d37e..e480e670b 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,
- /child\.on\("error", \(err: NodeJS\.ErrnoException\) => \{[\s\S]*?const openCodePowerShellHostMissing =[\s\S]*?process\.platform === "win32"[\s\S]*?const openCodeCommandMissing =[\s\S]*?launchFailure \?\?= \{[\s\S]*?"runtime_unlaunchable"[\s\S]*?"runtime_missing"/,
- "an OpenCode launch race preserves distinct PowerShell-host and OpenCode-missing classifications before empty-output/auth diagnostics can run",
+ /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",
);
assert.match(
route,
From 8b30a1181d0a209035dbc18cd289b2c64904e0a3 Mon Sep 17 00:00:00 2001
From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com>
Date: Sun, 26 Jul 2026 13:42:12 -0400
Subject: [PATCH 11/16] fix(ui): explain unavailable runtimes in summoning
---
src/components/familiar-summoning-circle.test.ts | 5 +++++
src/components/familiar-summoning-circle.tsx | 10 +++++-----
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/src/components/familiar-summoning-circle.test.ts b/src/components/familiar-summoning-circle.test.ts
index 7e91288f0..507f8efce 100644
--- a/src/components/familiar-summoning-circle.test.ts
+++ b/src/components/familiar-summoning-circle.test.ts
@@ -55,6 +55,11 @@ assert.match(
/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(
+ source,
+ /className="summoning-chiprow"[\s\S]*?<\/div>\s*\)}\s*\{unavailableHarnesses\.map/,
+ "an unavailable runtime remains explained even when another runtime is ready to select",
+);
assert.match(
harnessesRoute,
/runtimeHost: hostname\(\)/,
diff --git a/src/components/familiar-summoning-circle.tsx b/src/components/familiar-summoning-circle.tsx
index 1d36e42c0..ac776cd4b 100644
--- a/src/components/familiar-summoning-circle.tsx
+++ b/src/components/familiar-summoning-circle.tsx
@@ -817,11 +817,6 @@ 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}
-
- ))}
window.dispatchEvent(new CustomEvent("cave:onboarding-open"))}
@@ -847,6 +842,11 @@ function StageVessel({
))}
)}
+ {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,