From d6141a49b1516049f075eafa52a9b1407e7a5548 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 23 Jul 2026 23:37:35 -0500 Subject: [PATCH 1/8] fix(windows): resolve npm-global coven CLI (.cmd shim) find_coven() only probed coven.exe under .volta/.bun/.cargo and then fell back to where.exe. npm global installs land at %APPDATA%\npm\coven.cmd, and a GUI-launched Tauri app does not inherit %APPDATA%\npm on PATH, so where.exe also missed it -> 'Coven CLI not found on PATH' despite a healthy install. Probe the npm global .cmd/.exe shim explicitly, first, with an APPDATA fallback. Reported by a user on Windows who had also historically run an older Cave build; the fresh dev box did not repro because its coven resolves via a different install path. --- src-tauri/src/sidecar_discovery.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src-tauri/src/sidecar_discovery.rs b/src-tauri/src/sidecar_discovery.rs index 6dca01b44..0d9503933 100644 --- a/src-tauri/src/sidecar_discovery.rs +++ b/src-tauri/src/sidecar_discovery.rs @@ -154,7 +154,16 @@ pub(super) fn find_coven() -> Option { #[cfg(target_os = "windows")] { let home = std::env::var("USERPROFILE").unwrap_or_default(); + // npm global installs land in %APPDATA%\\npm as a .cmd shim (not .exe), + // which is the most common way users install the Coven CLI. A GUI-launched + // Tauri app frequently does not inherit that dir on PATH, so we must probe + // it explicitly or `where.exe` below will miss it. Fall back to + // %USERPROFILE%\\AppData\\Roaming\\npm when APPDATA is unset. + let appdata = std::env::var("APPDATA") + .unwrap_or_else(|_| format!("{}\\AppData\\Roaming", home)); let candidates = [ + PathBuf::from(format!("{}\\npm\\coven.cmd", appdata)), + PathBuf::from(format!("{}\\npm\\coven.exe", appdata)), PathBuf::from(format!("{}\\.volta\\bin\\coven.exe", home)), PathBuf::from(format!("{}\\.bun\\bin\\coven.exe", home)), PathBuf::from(format!("{}\\.cargo\\bin\\coven.exe", home)), From e48ca635aba6ffa1f887953b841d76b81f010b07 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 23 Jul 2026 23:41:13 -0500 Subject: [PATCH 2/8] fix(assist): surface auth failure instead of 'produced no output' codex can exit 0 yet write no last-message file when it is not signed in (or refuses workspace trust). The runner already keeps a bounded stderr tail; on the empty-output path we now include that tail and, when it looks like an auth failure, return an explicit 'run `codex login`' message instead of the opaque 'assist produced no output'. Pairs with the Windows npm-CLI resolution fix; both surfaced from the same user report (Windows, historically ran an older Cave build). --- src/lib/server/assist-runner.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/lib/server/assist-runner.ts b/src/lib/server/assist-runner.ts index abda387a6..1890e89fb 100644 --- a/src/lib/server/assist-runner.ts +++ b/src/lib/server/assist-runner.ts @@ -150,7 +150,33 @@ export async function runBoundedAssist(opts: { try { lastMessage = await readFile(/* turbopackIgnore: true */ lastMessagePath, "utf8"); } catch { - return { ok: false, error: "assist produced no output" }; + // codex can exit 0 yet write no last-message file when it is not signed + // in or refuses trust for the workspace. The real reason lands on stderr, + // so surface that (and an explicit sign-in hint for the auth case) + // instead of an opaque "produced no output". + const tail = stderrTail + .trim() + .split(/\r?\n/) + .filter(Boolean) + .slice(-3) + .join(" · ") + .slice(-300); + const looksLikeAuth = + /not logged in|log ?in|sign ?in|unauthorized|authenticate|auth|credential|token|401/i.test( + stderrTail, + ); + if (looksLikeAuth) { + return { + ok: false, + error: `${inv.command} isn't signed in, so this assist produced no output. Run \`${inv.command} login\` in a terminal, then try again${ + tail ? ` (${tail})` : "" + }.`, + }; + } + return { + ok: false, + error: `assist produced no output${tail ? ` — ${tail}` : ""}`, + }; } return { ok: true, lastMessage }; } catch (err) { From 632a792c6396f4e10aa6d0e276a1ee15dc1fec7f Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 24 Jul 2026 00:10:51 -0500 Subject: [PATCH 3/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/lib/server/assist-runner.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/lib/server/assist-runner.ts b/src/lib/server/assist-runner.ts index 1890e89fb..0f8e9497c 100644 --- a/src/lib/server/assist-runner.ts +++ b/src/lib/server/assist-runner.ts @@ -165,13 +165,15 @@ export async function runBoundedAssist(opts: { /not logged in|log ?in|sign ?in|unauthorized|authenticate|auth|credential|token|401/i.test( stderrTail, ); - if (looksLikeAuth) { - return { - ok: false, - error: `${inv.command} isn't signed in, so this assist produced no output. Run \`${inv.command} login\` in a terminal, then try again${ - tail ? ` (${tail})` : "" - }.`, - }; + const loginCommand = /\s/.test(inv.command) + ? `"${inv.command}" login` + : `${inv.command} login`; + return { + ok: false, + error: `${inv.command} isn't signed in, so this assist produced no output. Run \`${loginCommand}\` in a terminal, then try again${ + tail ? ` (${tail})` : "" + }.`, + }; } return { ok: false, From 2444519297c9a1d569aaa0b402f4eaeabd5b93b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 05:13:25 +0000 Subject: [PATCH 4/8] fix: add missing if(looksLikeAuth) guard in runBoundedAssist catch block --- src/lib/server/assist-runner.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/lib/server/assist-runner.ts b/src/lib/server/assist-runner.ts index 0f8e9497c..72685cb58 100644 --- a/src/lib/server/assist-runner.ts +++ b/src/lib/server/assist-runner.ts @@ -165,15 +165,16 @@ export async function runBoundedAssist(opts: { /not logged in|log ?in|sign ?in|unauthorized|authenticate|auth|credential|token|401/i.test( stderrTail, ); - const loginCommand = /\s/.test(inv.command) - ? `"${inv.command}" login` - : `${inv.command} login`; - return { - ok: false, - error: `${inv.command} isn't signed in, so this assist produced no output. Run \`${loginCommand}\` in a terminal, then try again${ - tail ? ` (${tail})` : "" - }.`, - }; + if (looksLikeAuth) { + const loginCommand = /\s/.test(inv.command) + ? `"${inv.command}" login` + : `${inv.command} login`; + return { + ok: false, + error: `${inv.command} isn't signed in, so this assist produced no output. Run \`${loginCommand}\` in a terminal, then try again${ + tail ? ` (${tail})` : "" + }.`, + }; } return { ok: false, From 122f0e760fd6be50bf67168084589f25a3f689a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 05:14:09 +0000 Subject: [PATCH 5/8] fix: escape embedded double-quotes in loginCommand suggestion --- src/lib/server/assist-runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/server/assist-runner.ts b/src/lib/server/assist-runner.ts index 72685cb58..7c7143c8f 100644 --- a/src/lib/server/assist-runner.ts +++ b/src/lib/server/assist-runner.ts @@ -167,7 +167,7 @@ export async function runBoundedAssist(opts: { ); if (looksLikeAuth) { const loginCommand = /\s/.test(inv.command) - ? `"${inv.command}" login` + ? `"${inv.command.replace(/"/g, '\\"')}" login` : `${inv.command} login`; return { ok: false, From 8c15e4e91f6aa777c8577790cd192c48ca4383f5 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 24 Jul 2026 07:24:36 -0500 Subject: [PATCH 6/8] fix(assist): escape backslashes before quotes in the login-command hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL js/incomplete-sanitization (high): the sign-in suggestion escaped double quotes but not backslashes, so a Windows coven path (C:\…\coven.cmd — exactly the case this PR adds discovery for) could break out of the double-quoted command. Escape backslashes first, then quotes. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/server/assist-runner.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/server/assist-runner.ts b/src/lib/server/assist-runner.ts index 7c7143c8f..08e72813d 100644 --- a/src/lib/server/assist-runner.ts +++ b/src/lib/server/assist-runner.ts @@ -167,7 +167,9 @@ export async function runBoundedAssist(opts: { ); if (looksLikeAuth) { const loginCommand = /\s/.test(inv.command) - ? `"${inv.command.replace(/"/g, '\\"')}" login` + ? // Escape backslashes before quotes so a Windows path (C:\…\coven.cmd) + // can't break out of the double-quoted suggestion (js/incomplete-sanitization). + `"${inv.command.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}" login` : `${inv.command} login`; return { ok: false, From babe1a3ae4b7a7ebcb3e2730f5f12a2fc83bc0d5 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 24 Jul 2026 07:26:43 -0500 Subject: [PATCH 7/8] test(assist): extract + unit-test empty-output diagnostics; tighten auth heuristic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review hardening for the empty-output branch added in this PR: - Extract the classification into pure, exported helpers (stderrReason, describeEmptyAssistOutput) per this module's stated stance — pure logic unit-tested, spawn verified manually — and dedupe the tail formatting shared with the non-zero-exit path. - Tighten the auth heuristic: the previous pattern matched bare 'auth', 'token', 'log ?in', and 'sign ?in' substrings, so stderr like 'Unexpected token < in JSON' or 'redesign in progress' would falsely tell users to run 'codex login'. Precision over recall is safe here: a missed auth match still surfaces the stderr tail verbatim. - Cover both branches (auth phrasing, runnable quoted login command for paths with spaces, false-positive regressions, tail carry-through, empty-stderr terseness) in assist-runner.test.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/lib/server/assist-runner.test.ts | 51 +++++++++++++++++- src/lib/server/assist-runner.ts | 77 ++++++++++++++++------------ 2 files changed, 95 insertions(+), 33 deletions(-) diff --git a/src/lib/server/assist-runner.test.ts b/src/lib/server/assist-runner.test.ts index 778128da4..6a6f944e0 100644 --- a/src/lib/server/assist-runner.test.ts +++ b/src/lib/server/assist-runner.test.ts @@ -1,6 +1,11 @@ // @ts-nocheck import assert from "node:assert/strict"; -import { buildAssistInvocation, ASSIST_TIMEOUT_MS } from "./assist-runner.ts"; +import { + buildAssistInvocation, + describeEmptyAssistOutput, + stderrReason, + ASSIST_TIMEOUT_MS, +} from "./assist-runner.ts"; const prevBin = process.env.COVEN_CODEX_BIN; @@ -36,6 +41,50 @@ try { // The default budget matches the sew's historical bound. assert.equal(ASSIST_TIMEOUT_MS, 180_000); + // ── stderr reason collapsing ─────────────────────────────────────────────── + assert.equal(stderrReason(""), ""); + assert.equal(stderrReason(" \n\t\n"), "", "whitespace-only tail collapses to nothing"); + assert.equal( + stderrReason("line1\r\nline2\nline3\n\nline4\n"), + "line2 · line3 · line4", + "keeps the last 3 non-empty lines across CRLF", + ); + assert.equal(stderrReason("x".repeat(400)), "x".repeat(300), "caps at the final 300 chars"); + + // ── empty-output diagnostics ─────────────────────────────────────────────── + // Auth-shaped stderr → explicit sign-in guidance with a runnable command. + let msg = describeEmptyAssistOutput( + "codex", + "ERROR: Not logged in. Run `codex login` to authenticate.", + ); + assert.ok(msg.includes("isn't signed in"), msg); + assert.ok(msg.includes("`codex login`"), msg); + assert.ok(msg.includes("Not logged in"), "carries the stderr tail"); + + msg = describeEmptyAssistOutput("codex", "request failed: 401 Unauthorized"); + assert.ok(msg.includes("`codex login`"), msg); + + // A COVEN_CODEX_BIN path with spaces still yields a runnable login command, + // with backslashes escaped ahead of quotes (js/incomplete-sanitization). + msg = describeEmptyAssistOutput("C:\\Program Files\\codex.exe", "stream error: unauthorized"); + assert.ok(msg.includes('`"C:\\\\Program Files\\\\codex.exe" login`'), msg); + + // Non-auth stderr must NOT claim a sign-in problem — loose `token` / `auth` + // / `sign ?in` substring matching used to misfire on lines like these. + for (const stderr of [ + "SyntaxError: Unexpected token < in JSON at position 0", + "error: not inside a trusted directory; pass --skip-git-repo-check", + "warning: redesign in progress for the author page catalog index", + ]) { + msg = describeEmptyAssistOutput("codex", stderr); + assert.ok(!msg.includes("signed in"), `false auth positive for: ${stderr}`); + assert.ok(msg.startsWith("assist produced no output"), msg); + assert.ok(msg.includes(stderrReason(stderr)), "still surfaces the stderr tail"); + } + + // No stderr at all → the original terse message, no dangling separator. + assert.equal(describeEmptyAssistOutput("codex", ""), "assist produced no output"); + console.log("assist-runner.test.ts OK"); } finally { if (prevBin === undefined) delete process.env.COVEN_CODEX_BIN; diff --git a/src/lib/server/assist-runner.ts b/src/lib/server/assist-runner.ts index 08e72813d..374c7a4ff 100644 --- a/src/lib/server/assist-runner.ts +++ b/src/lib/server/assist-runner.ts @@ -57,6 +57,46 @@ export type AssistRunResult = | { ok: true; lastMessage: string } | { ok: false; error: string }; +/** + * Pure: collapse a bounded stderr capture into a one-line reason suitable + * for appending to an error message — last 3 non-empty lines, joined, capped + * at the final 300 chars. Unit-tested. + */ +export function stderrReason(stderrTail: string): string { + return stderrTail.trim().split(/\r?\n/).filter(Boolean).slice(-3).join(" · ").slice(-300); +} + +// Signals that codex exited without output because it isn't signed in. Kept +// deliberately precise: loose substrings like `auth`, `token`, or `sign ?in` +// would misread "unexpected token" / "author" / "design in…" stderr as a +// sign-in problem and send the user to `codex login` for an unrelated +// failure. A missed match is benign — the generic path below still surfaces +// the stderr tail verbatim. +const AUTH_STDERR_RE = + /not (?:logged|signed) in|codex login|please (?:log|sign) ?in|unauthorized|authentication (?:required|failed|error)|(?:invalid|missing|expired) (?:credentials?|api key)|credentials? (?:invalid|missing|expired)|token (?:expired|revoked)|\b401\b/i; + +/** + * Pure: error message for the "exited 0 but wrote no last message" case. + * codex does this when it is not signed in, leaving the real reason on + * stderr — so surface the stderr tail, plus an explicit sign-in hint (with a + * runnable login command) when that tail looks like an auth failure. + * Unit-tested. + */ +export function describeEmptyAssistOutput(command: string, stderrTail: string): string { + const tail = stderrReason(stderrTail); + if (AUTH_STDERR_RE.test(stderrTail)) { + const loginCommand = /\s/.test(command) + ? // Escape backslashes before quotes so a Windows path (C:\…\coven.cmd) + // can't break out of the double-quoted suggestion (js/incomplete-sanitization). + `"${command.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}" login` + : `${command} login`; + return `${command} isn't signed in, so this assist produced no output. Run \`${loginCommand}\` in a terminal, then try again${ + tail ? ` (${tail})` : "" + }.`; + } + return `assist produced no output${tail ? ` — ${tail}` : ""}`; +} + /** * Run one bounded assist end-to-end: spawn `codex exec`, wait (bounded), and * return the final message for the caller to parse against its own output @@ -140,7 +180,7 @@ export async function runBoundedAssist(opts: { }); if (spawned.error) return { ok: false, error: spawned.error }; if (spawned.code !== 0) { - const reason = stderrTail.trim().split(/\r?\n/).filter(Boolean).slice(-3).join(" · ").slice(-300); + const reason = stderrReason(stderrTail); return { ok: false, error: `codex exec exited with ${spawned.code}${reason ? ` — ${reason}` : ""}`, @@ -151,37 +191,10 @@ export async function runBoundedAssist(opts: { lastMessage = await readFile(/* turbopackIgnore: true */ lastMessagePath, "utf8"); } catch { // codex can exit 0 yet write no last-message file when it is not signed - // in or refuses trust for the workspace. The real reason lands on stderr, - // so surface that (and an explicit sign-in hint for the auth case) - // instead of an opaque "produced no output". - const tail = stderrTail - .trim() - .split(/\r?\n/) - .filter(Boolean) - .slice(-3) - .join(" · ") - .slice(-300); - const looksLikeAuth = - /not logged in|log ?in|sign ?in|unauthorized|authenticate|auth|credential|token|401/i.test( - stderrTail, - ); - if (looksLikeAuth) { - const loginCommand = /\s/.test(inv.command) - ? // Escape backslashes before quotes so a Windows path (C:\…\coven.cmd) - // can't break out of the double-quoted suggestion (js/incomplete-sanitization). - `"${inv.command.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}" login` - : `${inv.command} login`; - return { - ok: false, - error: `${inv.command} isn't signed in, so this assist produced no output. Run \`${loginCommand}\` in a terminal, then try again${ - tail ? ` (${tail})` : "" - }.`, - }; - } - return { - ok: false, - error: `assist produced no output${tail ? ` — ${tail}` : ""}`, - }; + // in. The real reason lands on stderr, so surface that (and an explicit + // sign-in hint for the auth case) instead of an opaque "produced no + // output". + return { ok: false, error: describeEmptyAssistOutput(inv.command, stderrTail) }; } return { ok: true, lastMessage }; } catch (err) { From 8fd019796895755b0ec9d0dce3281251a382cd12 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 24 Jul 2026 07:34:48 -0500 Subject: [PATCH 8/8] =?UTF-8?q?docs(assist):=20docstring=20=E2=80=94=20CI?= =?UTF-8?q?=20now=20covers=20all=20pure=20pieces=20of=20this=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/lib/server/assist-runner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/server/assist-runner.ts b/src/lib/server/assist-runner.ts index 374c7a4ff..96f0b667c 100644 --- a/src/lib/server/assist-runner.ts +++ b/src/lib/server/assist-runner.ts @@ -101,7 +101,8 @@ export function describeEmptyAssistOutput(command: string, stderrTail: string): * Run one bounded assist end-to-end: spawn `codex exec`, wait (bounded), and * return the final message for the caller to parse against its own output * contract. Never throws. Spawn behavior is exercised manually — CI covers - * `buildAssistInvocation` only. + * the pure pieces only (`buildAssistInvocation`, `stderrReason`, + * `describeEmptyAssistOutput`). */ export async function runBoundedAssist(opts: { prompt: string;