From ec4bb85797fdbd593ab145b5221aa9c72b0f94d6 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Sun, 25 Oct 2026 16:31:38 +0000 Subject: [PATCH 1/2] fix(vscode): create Agent Manager worktrees from a repository subdirectory Opening a subdirectory of a git repository made worktree creation stat a missing .git path. Git now supplies the shared git directory. --- .changeset/agent-manager-subdir-worktree.md | 5 ++++ .../src/agent-manager/WorktreeManager.ts | 2 +- .../src/agent-manager/constants.ts | 26 +++++++++---------- .../tests/unit/worktree-manager.test.ts | 19 ++++++++++++++ 4 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 .changeset/agent-manager-subdir-worktree.md diff --git a/.changeset/agent-manager-subdir-worktree.md b/.changeset/agent-manager-subdir-worktree.md new file mode 100644 index 000000000000..36fbbd0ee547 --- /dev/null +++ b/.changeset/agent-manager-subdir-worktree.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Agent Manager worktree creation when a VS Code window is opened on a subdirectory of a git repository. diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index d81905205c18..833b6a3a35db 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -1349,7 +1349,7 @@ export class WorktreeManager { async repoUsesLfs(): Promise { // Check .git/lfs/ directory - const gitDir = await resolveGitDir(this.root) + const gitDir = await resolveGitDir(this.root, this.binary) if (fs.existsSync(path.join(gitDir, "lfs"))) return true // Check .gitattributes diff --git a/packages/kilo-vscode/src/agent-manager/constants.ts b/packages/kilo-vscode/src/agent-manager/constants.ts index 01979bf7d17b..02d08273e68f 100644 --- a/packages/kilo-vscode/src/agent-manager/constants.ts +++ b/packages/kilo-vscode/src/agent-manager/constants.ts @@ -1,5 +1,6 @@ import * as fs from "node:fs" import * as path from "node:path" +import { exec } from "../util/process" // TODO: Remove the legacy .kilocode -> .kilo migration helpers below after the // GA release cleanup tracked in https://github.com/Kilo-Org/kilocode/issues/6986. @@ -113,21 +114,18 @@ async function isDirectory(filepath: string): Promise { } /** - * Resolve the real git directory for a repository root. - * When root/.git is a directory, returns it directly. - * When root/.git is a file (worktree), follows the gitdir pointer - * up two levels to reach the shared git directory. + * Resolve the shared git directory for `root`. + * + * `git rev-parse --git-common-dir` works for a repository root, a linked + * worktree, and a subdirectory of either. A stat of `root/.git` does not: + * a subdirectory workspace has no `.git` entry. The path may be relative to + * `root`. Unknown flags are rejected because older Git prints them and exits 0. */ -export async function resolveGitDir(root: string): Promise { - const gitPath = path.join(root, ".git") - const stat = await fs.promises.stat(gitPath) - if (stat.isDirectory()) return gitPath - - const content = await fs.promises.readFile(gitPath, "utf-8") - const match = content.match(/^gitdir:\s*(.+)$/m) - if (!match) throw new Error("Invalid .git file format") - // gitdir points to e.g. /repo/.git/worktrees/foo — go up two levels to /repo/.git - return path.resolve(path.dirname(gitPath), match[1].trim(), "..", "..") +export async function resolveGitDir(root: string, binary = "git"): Promise { + const { stdout } = await exec(binary, ["rev-parse", "--git-common-dir"], { cwd: root }) + const dir = stdout.trim() + if (!dir || dir.startsWith("--")) throw new Error("git rev-parse did not return a git directory") + return path.resolve(root, dir) } /** diff --git a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts index c6bc54527b27..0f8a9d49423c 100644 --- a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts @@ -1510,6 +1510,25 @@ describe("WorktreeManager helpers", () => { await fs.mkdir(path.join(root, ".git", "lfs"), { recursive: true }) expect(await mgr.repoUsesLfs()).toBe(true) }) + + it("creates a worktree from a subdirectory of the repository", async () => { + const root = await createTempRepo() + const sub = path.join(root, "web") + await fs.mkdir(sub, { recursive: true }) + const lfs = path.join(root, ".git", "lfs") + await fs.mkdir(lfs, { recursive: true }) + const mgr = createManager(sub) + + expect(await mgr.repoUsesLfs()).toBe(true) + await fs.rm(lfs, { recursive: true, force: true }) + expect(await mgr.repoUsesLfs()).toBe(false) + + const result = await mgr.createWorktree({ prompt: "subdir" }) + expect((await fs.stat(path.join(result.path, ".git"))).isFile()).toBe(true) + expect(result.path.startsWith(sub + path.sep)).toBe(true) + const status = await simpleGit(root).raw(["status", "--porcelain", "--untracked-files=all"]) + expect(status.trim()).toBe("") + }) }) describe("WorktreeManager.resolveStartPoint", () => { From cd0efaafc0592ee9e7027dcc0f2baacc2e3c2399 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Mon, 26 Oct 2026 08:18:59 +0000 Subject: [PATCH 2/2] fix(vscode): resolve the git dir through the shell environment --- packages/kilo-vscode/src/agent-manager/constants.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/constants.ts b/packages/kilo-vscode/src/agent-manager/constants.ts index 02d08273e68f..d66b590a7def 100644 --- a/packages/kilo-vscode/src/agent-manager/constants.ts +++ b/packages/kilo-vscode/src/agent-manager/constants.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs" import * as path from "node:path" -import { exec } from "../util/process" +import { BUDGET } from "./command-budget" +import { execWithShellEnv } from "./shell-env" // TODO: Remove the legacy .kilocode -> .kilo migration helpers below after the // GA release cleanup tracked in https://github.com/Kilo-Org/kilocode/issues/6986. @@ -122,7 +123,10 @@ async function isDirectory(filepath: string): Promise { * `root`. Unknown flags are rejected because older Git prints them and exits 0. */ export async function resolveGitDir(root: string, binary = "git"): Promise { - const { stdout } = await exec(binary, ["rev-parse", "--git-common-dir"], { cwd: root }) + const { stdout } = await execWithShellEnv(binary, ["rev-parse", "--git-common-dir"], { + cwd: root, + timeout: BUDGET.probe, + }) const dir = stdout.trim() if (!dir || dir.startsWith("--")) throw new Error("git rev-parse did not return a git directory") return path.resolve(root, dir)