From 26e6950689125536f47bbc4a89dc8e515b3d1dcd Mon Sep 17 00:00:00 2001 From: gmackie Date: Fri, 17 Apr 2026 10:50:14 -0700 Subject: [PATCH 1/5] fix(server): validate workspace roots and back off failed status refreshes --- apps/server/src/codexAppServerManager.test.ts | 33 ++++++++++++++++ apps/server/src/codexAppServerManager.ts | 16 ++++++++ apps/server/src/git/Layers/GitCore.test.ts | 39 +++++++++++++++++++ apps/server/src/git/Layers/GitCore.ts | 6 +-- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index 84225ef1ea6b..847b25bfa718 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -417,6 +417,39 @@ describe("startSession", () => { } }); + it("reports a missing workspace root instead of blaming the codex binary", async () => { + const manager = new CodexAppServerManager(); + const events: Array<{ method: string; kind: string; message?: string }> = []; + manager.on("event", (event) => { + events.push({ + method: event.method, + kind: event.kind, + ...(event.message ? { message: event.message } : {}), + }); + }); + + const missingWorkspaceRoot = path.join(os.tmpdir(), `codex-missing-workspace-${randomUUID()}`); + + await expect( + manager.startSession({ + threadId: asThreadId("thread-1"), + provider: "codex", + binaryPath: process.execPath, + cwd: missingWorkspaceRoot, + runtimeMode: "full-access", + }), + ).rejects.toThrow(`Workspace root does not exist: ${missingWorkspaceRoot}`); + expect(events).toEqual([ + { + method: "session/startFailed", + kind: "error", + message: `Workspace root does not exist: ${missingWorkspaceRoot}`, + }, + ]); + + manager.stopAll(); + }); + it("fails fast with an upgrade message when codex is below the minimum supported version", async () => { const manager = new CodexAppServerManager(); const events: Array<{ method: string; kind: string; message?: string }> = []; diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index 6d98264c9108..f2d1d95fe11c 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -1,6 +1,7 @@ import { type ChildProcessWithoutNullStreams, spawn, spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { EventEmitter } from "node:events"; +import { statSync } from "node:fs"; import readline from "node:readline"; import { @@ -470,6 +471,7 @@ export class CodexAppServerManager extends EventEmitter( return normalized?.length ? maker(normalized) : undefined; } +function assertWorkspaceRootExists(workspaceRoot: string): void { + try { + const stat = statSync(workspaceRoot); + if (!stat.isDirectory()) { + throw new Error(`Workspace root is not a directory: ${workspaceRoot}`); + } + } catch (error) { + if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") { + throw new Error(`Workspace root does not exist: ${workspaceRoot}`, { cause: error }); + } + throw error; + } +} + function normalizeProviderThreadId(value: string | undefined): string | undefined { return brandIfNonEmpty(value, (normalized) => normalized); } diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 665c4b138f98..a9043c991558 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -1713,6 +1713,45 @@ it.layer(TestLayer)("git integration", (it) => { }), ); + it.effect("backs off upstream auto-refresh after a failed status fetch", () => + Effect.gen(function* () { + const remote = yield* makeTmpDir(); + const source = yield* makeTmpDir(); + yield* git(remote, ["init", "--bare"]); + + yield* initRepoWithCommit(source); + const initialBranch = (yield* (yield* GitCore).listBranches({ + cwd: source, + })).branches.find((branch) => branch.current)!.name; + yield* git(source, ["remote", "add", "origin", remote]); + yield* git(source, ["push", "-u", "origin", initialBranch]); + + const realGitCore = yield* GitCore; + let refreshFetchAttempts = 0; + const core = yield* makeIsolatedGitCore((input) => { + if (input.args[0] === "--git-dir" && input.args[2] === "fetch") { + refreshFetchAttempts += 1; + return Effect.fail( + new GitCommandError({ + operation: "git.test.statusRefreshFailure", + command: `git ${input.args.join(" ")}`, + cwd: input.cwd, + detail: "simulated fetch timeout", + }), + ); + } + return realGitCore.execute(input); + }); + + const firstStatus = yield* core.statusDetails(source); + const secondStatus = yield* core.statusDetails(source); + + expect(firstStatus.branch).toBe(initialBranch); + expect(secondStatus.branch).toBe(initialBranch); + expect(refreshFetchAttempts).toBe(1); + }), + ); + it.effect("computes ahead count against base branch when no upstream is configured", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 3e9df316f1e9..a6eb62321601 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -55,8 +55,8 @@ const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [ "core.untrackedCache=false", ] as const; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); +const STATUS_UPSTREAM_REFRESH_FAILURE_RETRY_INTERVAL = Duration.minutes(10); const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5); -const STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN = Duration.seconds(5); const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048; const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const; const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100; @@ -951,11 +951,11 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { const statusRemoteRefreshCache = yield* Cache.makeWith(refreshStatusRemoteCacheEntry, { capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY, - // Keep successful refreshes warm and briefly back off failed refreshes to avoid retry storms. + // Keep successful refreshes warm and back off failed auto-refreshes to avoid retry storms. timeToLive: (exit) => Exit.isSuccess(exit) ? STATUS_UPSTREAM_REFRESH_INTERVAL - : STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN, + : STATUS_UPSTREAM_REFRESH_FAILURE_RETRY_INTERVAL, }); const refreshStatusUpstreamIfStale = Effect.fn("refreshStatusUpstreamIfStale")(function* ( From 6ad618fd8102d28adda73de039bd4bbd7b8edfce Mon Sep 17 00:00:00 2001 From: gmackie Date: Fri, 17 Apr 2026 14:34:44 -0700 Subject: [PATCH 2/5] fix(server): report invalid codex cwd before version checks --- apps/server/src/codexAppServerManager.test.ts | 45 ++++++++++--------- apps/server/src/git/Layers/GitCore.test.ts | 39 ---------------- apps/server/src/git/Layers/GitCore.ts | 6 +-- 3 files changed, 28 insertions(+), 62 deletions(-) diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index 847b25bfa718..b9cec920f02f 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -417,7 +417,7 @@ describe("startSession", () => { } }); - it("reports a missing workspace root instead of blaming the codex binary", async () => { + it("reports a file cwd instead of blaming the codex binary", async () => { const manager = new CodexAppServerManager(); const events: Array<{ method: string; kind: string; message?: string }> = []; manager.on("event", (event) => { @@ -428,26 +428,31 @@ describe("startSession", () => { }); }); - const missingWorkspaceRoot = path.join(os.tmpdir(), `codex-missing-workspace-${randomUUID()}`); + const tempDir = mkdtempSync(path.join(os.tmpdir(), "codex-cwd-file-")); + const tempFilePath = path.join(tempDir, "cwd.txt"); + writeFileSync(tempFilePath, "not-a-directory\n"); - await expect( - manager.startSession({ - threadId: asThreadId("thread-1"), - provider: "codex", - binaryPath: process.execPath, - cwd: missingWorkspaceRoot, - runtimeMode: "full-access", - }), - ).rejects.toThrow(`Workspace root does not exist: ${missingWorkspaceRoot}`); - expect(events).toEqual([ - { - method: "session/startFailed", - kind: "error", - message: `Workspace root does not exist: ${missingWorkspaceRoot}`, - }, - ]); - - manager.stopAll(); + try { + await expect( + manager.startSession({ + threadId: asThreadId("thread-1"), + provider: "codex", + binaryPath: process.execPath, + cwd: tempFilePath, + runtimeMode: "full-access", + }), + ).rejects.toThrow(`Workspace root is not a directory: ${tempFilePath}`); + expect(events).toEqual([ + { + method: "session/startFailed", + kind: "error", + message: `Workspace root is not a directory: ${tempFilePath}`, + }, + ]); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + manager.stopAll(); + } }); it("fails fast with an upgrade message when codex is below the minimum supported version", async () => { diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index a9043c991558..665c4b138f98 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -1713,45 +1713,6 @@ it.layer(TestLayer)("git integration", (it) => { }), ); - it.effect("backs off upstream auto-refresh after a failed status fetch", () => - Effect.gen(function* () { - const remote = yield* makeTmpDir(); - const source = yield* makeTmpDir(); - yield* git(remote, ["init", "--bare"]); - - yield* initRepoWithCommit(source); - const initialBranch = (yield* (yield* GitCore).listBranches({ - cwd: source, - })).branches.find((branch) => branch.current)!.name; - yield* git(source, ["remote", "add", "origin", remote]); - yield* git(source, ["push", "-u", "origin", initialBranch]); - - const realGitCore = yield* GitCore; - let refreshFetchAttempts = 0; - const core = yield* makeIsolatedGitCore((input) => { - if (input.args[0] === "--git-dir" && input.args[2] === "fetch") { - refreshFetchAttempts += 1; - return Effect.fail( - new GitCommandError({ - operation: "git.test.statusRefreshFailure", - command: `git ${input.args.join(" ")}`, - cwd: input.cwd, - detail: "simulated fetch timeout", - }), - ); - } - return realGitCore.execute(input); - }); - - const firstStatus = yield* core.statusDetails(source); - const secondStatus = yield* core.statusDetails(source); - - expect(firstStatus.branch).toBe(initialBranch); - expect(secondStatus.branch).toBe(initialBranch); - expect(refreshFetchAttempts).toBe(1); - }), - ); - it.effect("computes ahead count against base branch when no upstream is configured", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index a6eb62321601..3e9df316f1e9 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -55,8 +55,8 @@ const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [ "core.untrackedCache=false", ] as const; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); -const STATUS_UPSTREAM_REFRESH_FAILURE_RETRY_INTERVAL = Duration.minutes(10); const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5); +const STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN = Duration.seconds(5); const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048; const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const; const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100; @@ -951,11 +951,11 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { const statusRemoteRefreshCache = yield* Cache.makeWith(refreshStatusRemoteCacheEntry, { capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY, - // Keep successful refreshes warm and back off failed auto-refreshes to avoid retry storms. + // Keep successful refreshes warm and briefly back off failed refreshes to avoid retry storms. timeToLive: (exit) => Exit.isSuccess(exit) ? STATUS_UPSTREAM_REFRESH_INTERVAL - : STATUS_UPSTREAM_REFRESH_FAILURE_RETRY_INTERVAL, + : STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN, }); const refreshStatusUpstreamIfStale = Effect.fn("refreshStatusUpstreamIfStale")(function* ( From 7ea6e101422e759216d7798dc852ba66c07564db Mon Sep 17 00:00:00 2001 From: gmackie Date: Fri, 17 Apr 2026 14:44:56 -0700 Subject: [PATCH 3/5] fix(server): back off failed git status refreshes --- apps/server/src/git/Layers/GitCore.test.ts | 49 +++++++++++++++++++++- apps/server/src/git/Layers/GitCore.ts | 2 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 665c4b138f98..954d26efc4e5 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -3,7 +3,8 @@ import path from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; -import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect"; +import { Duration, Effect, FileSystem, Layer, PlatformError, Scope } from "effect"; +import { TestClock } from "effect/testing"; import { describe, expect, vi } from "vitest"; import { GitCoreLive, makeGitCore } from "./GitCore.ts"; @@ -1713,6 +1714,52 @@ it.layer(TestLayer)("git integration", (it) => { }), ); + it.effect("backs off upstream auto-refresh after a failed status fetch", () => + Effect.gen(function* () { + const remote = yield* makeTmpDir(); + const source = yield* makeTmpDir(); + yield* git(remote, ["init", "--bare"]); + + yield* initRepoWithCommit(source); + const initialBranch = (yield* (yield* GitCore).listBranches({ + cwd: source, + })).branches.find((branch) => branch.current)!.name; + yield* git(source, ["remote", "add", "origin", remote]); + yield* git(source, ["push", "-u", "origin", initialBranch]); + + const realGitCore = yield* GitCore; + let refreshFetchAttempts = 0; + const core = yield* makeIsolatedGitCore((input) => { + if (input.args[0] === "--git-dir" && input.args[2] === "fetch") { + refreshFetchAttempts += 1; + return Effect.fail( + new GitCommandError({ + operation: "git.test.statusRefreshFailure", + command: `git ${input.args.join(" ")}`, + cwd: input.cwd, + detail: "simulated fetch timeout", + }), + ); + } + return realGitCore.execute(input); + }); + + const firstStatus = yield* core.statusDetails(source); + expect(firstStatus.branch).toBe(initialBranch); + expect(refreshFetchAttempts).toBe(1); + + yield* TestClock.adjust(Duration.seconds(15)); + const secondStatus = yield* core.statusDetails(source); + expect(secondStatus.branch).toBe(initialBranch); + expect(refreshFetchAttempts).toBe(1); + + yield* TestClock.adjust(Duration.minutes(10)); + const thirdStatus = yield* core.statusDetails(source); + expect(thirdStatus.branch).toBe(initialBranch); + expect(refreshFetchAttempts).toBe(2); + }), + ); + it.effect("computes ahead count against base branch when no upstream is configured", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 3e9df316f1e9..ab0f606e477c 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -56,7 +56,7 @@ const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [ ] as const; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5); -const STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN = Duration.seconds(5); +const STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN = Duration.minutes(10); const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048; const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const; const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100; From d4b95a9aa9fab817a72e2ce339741ed3a8f5c1dc Mon Sep 17 00:00:00 2001 From: gmackie Date: Fri, 17 Apr 2026 14:46:45 -0700 Subject: [PATCH 4/5] Revert "fix(server): report invalid codex cwd before version checks" This reverts commit 6ad618fd8102d28adda73de039bd4bbd7b8edfce. --- apps/server/src/codexAppServerManager.test.ts | 38 ------------------- apps/server/src/codexAppServerManager.ts | 17 --------- apps/server/src/git/Layers/GitCore.ts | 2 +- 3 files changed, 1 insertion(+), 56 deletions(-) diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index b9cec920f02f..84225ef1ea6b 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -417,44 +417,6 @@ describe("startSession", () => { } }); - it("reports a file cwd instead of blaming the codex binary", async () => { - const manager = new CodexAppServerManager(); - const events: Array<{ method: string; kind: string; message?: string }> = []; - manager.on("event", (event) => { - events.push({ - method: event.method, - kind: event.kind, - ...(event.message ? { message: event.message } : {}), - }); - }); - - const tempDir = mkdtempSync(path.join(os.tmpdir(), "codex-cwd-file-")); - const tempFilePath = path.join(tempDir, "cwd.txt"); - writeFileSync(tempFilePath, "not-a-directory\n"); - - try { - await expect( - manager.startSession({ - threadId: asThreadId("thread-1"), - provider: "codex", - binaryPath: process.execPath, - cwd: tempFilePath, - runtimeMode: "full-access", - }), - ).rejects.toThrow(`Workspace root is not a directory: ${tempFilePath}`); - expect(events).toEqual([ - { - method: "session/startFailed", - kind: "error", - message: `Workspace root is not a directory: ${tempFilePath}`, - }, - ]); - } finally { - rmSync(tempDir, { recursive: true, force: true }); - manager.stopAll(); - } - }); - it("fails fast with an upgrade message when codex is below the minimum supported version", async () => { const manager = new CodexAppServerManager(); const events: Array<{ method: string; kind: string; message?: string }> = []; diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index f2d1d95fe11c..5dc1bcdc5d2c 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -1,7 +1,6 @@ import { type ChildProcessWithoutNullStreams, spawn, spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { EventEmitter } from "node:events"; -import { statSync } from "node:fs"; import readline from "node:readline"; import { @@ -471,8 +470,6 @@ export class CodexAppServerManager extends EventEmitter( return normalized?.length ? maker(normalized) : undefined; } -function assertWorkspaceRootExists(workspaceRoot: string): void { - try { - const stat = statSync(workspaceRoot); - if (!stat.isDirectory()) { - throw new Error(`Workspace root is not a directory: ${workspaceRoot}`); - } - } catch (error) { - if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") { - throw new Error(`Workspace root does not exist: ${workspaceRoot}`, { cause: error }); - } - throw error; - } -} - function normalizeProviderThreadId(value: string | undefined): string | undefined { return brandIfNonEmpty(value, (normalized) => normalized); } diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index ab0f606e477c..308c67e42e1c 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -951,7 +951,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { const statusRemoteRefreshCache = yield* Cache.makeWith(refreshStatusRemoteCacheEntry, { capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY, - // Keep successful refreshes warm and briefly back off failed refreshes to avoid retry storms. + // Keep successful refreshes warm and back off failed auto-refreshes to avoid retry storms. timeToLive: (exit) => Exit.isSuccess(exit) ? STATUS_UPSTREAM_REFRESH_INTERVAL From 6861654962fd05086f0d72a4bef2ceb14eedfce7 Mon Sep 17 00:00:00 2001 From: gmackie Date: Fri, 17 Apr 2026 14:48:32 -0700 Subject: [PATCH 5/5] fix(server): treat failed git status refreshes as failures --- apps/server/src/codexAppServerManager.ts | 1 + apps/server/src/git/Layers/GitCore.test.ts | 15 +++++++-------- apps/server/src/git/Layers/GitCore.ts | 18 +++++++++++++++++- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index 5dc1bcdc5d2c..6d98264c9108 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -470,6 +470,7 @@ export class CodexAppServerManager extends EventEmitter { const core = yield* makeIsolatedGitCore((input) => { if (input.args[0] === "--git-dir" && input.args[2] === "fetch") { refreshFetchAttempts += 1; - return Effect.fail( - new GitCommandError({ - operation: "git.test.statusRefreshFailure", - command: `git ${input.args.join(" ")}`, - cwd: input.cwd, - detail: "simulated fetch timeout", - }), - ); + return Effect.succeed({ + code: 128, + stdout: "", + stderr: "simulated fetch timeout", + stdoutTruncated: false, + stderrTruncated: false, + }); } return realGitCore.execute(input); }); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 308c67e42e1c..b655806ff444 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -931,7 +931,23 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { allowNonZeroExit: true, timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT), }, - ).pipe(Effect.asVoid); + ).pipe( + Effect.flatMap((result) => { + if (result.code === 0) { + return Effect.void; + } + + const stderr = result.stderr.trim(); + return Effect.fail( + createGitCommandError( + "GitCore.fetchRemoteForStatus", + fetchCwd, + ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", remoteName], + stderr.length > 0 ? stderr : "git fetch failed", + ), + ); + }), + ); }; const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) {