Skip to content

V2: worktree created by a plugin strategy resolves to a new project when the project has no .git/.hg #51672

Open

Description

@mayamika

Description

A plugin can register its own worktree strategy. If the project directory has no .git or .hg, the new worktree does not belong to the source project. opencode resolves its directory to a new project with a new ID.

Possible cause:
Project.resolve finds the project by .git or .hg only. For any other directory it uses Hash("directory:" + path). A new worktree has a new path, so it gets a new project ID.

Worktree.create already saves the new directory in the worktree table with the source project ID and the strategy ID. But Project.resolve does not read this table.

Plugins

No response

OpenCode version

v2.0.18

Steps to reproduce

  1. Open a project whose directory has no .git or .hg.
  2. Register a worktree strategy through ctx.worktree.transform.
  3. Create a worktree.
  4. Open the new worktree directory.
  5. Compare the project ID of the new location with the source project ID.

Screenshot and/or share link

No response

Operating System

macOS 26.6.2

Terminal

Ghostty

Activity

  1. github-actions commented on Sep 27, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    • #48870: Project.resolve does not consult project_directory for non-git directories, so a registered directory may not resolve to its owning project. This closely overlaps with the worktree-table association described here.
    • #47885: project identity handling for non-git directories can assign a separate identity based on the directory path.
  2. changed the title [-]Worktree created by a plugin strategy resolves to a new project when the project has no .git/.hg[/-] [+]V2: worktree created by a plugin strategy resolves to a new project when the project has no .git/.hg[/+] on Sep 28, 2026
  3. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the report. I reproduced this on 2.0.18, and it still happens on the latest dev (86ce19d).

    What I did, using the embedded SDK (the script below):

    1. Created a source project, registered a plugin worktree strategy that copies the source into the new directory without .git, and created a worktree with it.
    2. worktree.list for the source project includes the new directory with strategy: "copy", so the worktree row is stored under the source project ID.
    3. A session opened in the new worktree directory gets a different project ID: the directory:-hash fallback in Project.resolve, which never reads the worktree table. This matches your analysis.

    One difference: I could only get this far when the source directory was a git repo. With a source that has no VCS at all, worktree.create fails earlier with Worktree source not found: <source dir> on both 2.0.18 and dev. For projects without VCS, the source directory's own worktree row is never saved. If your setup does get past that step, a short note on how would help.

    Reproduction script (repro.ts)
    // Repro for anomalyco/opencode#51672: a worktree created by a plugin strategy in a
    // project with no .git/.hg resolves to a brand-new project ID.
    // Run: bun add @opencode/[email protected] @opencode/[email protected] && bun repro.ts   (NO_GIT=1 for a non-VCS source)
    import { mkdtemp, mkdir, rm, cp, writeFile } from "node:fs/promises"
    import { tmpdir } from "node:os"
    import { join } from "node:path"
    import { Plugin } from "@opencode/plugin"
    import { OpenCode } from "@opencode/sdk"
    
    const root = await mkdtemp(join(tmpdir(), "oc-51672-"))
    const source = join(root, "project") // plain directory: no .git, no .hg
    await mkdir(source)
    await writeFile(join(source, "README.md"), "hello\n")
    // Source is a git repo; the plugin strategy produces a copy WITHOUT .git/.hg.
    // (With a plain non-VCS source, worktree.create already fails: "Worktree source not found".)
    if (process.env.NO_GIT !== "1") Bun.spawnSync(["sh", "-c", "git init -q && git add . && git -c user.email=a@b -c user.name=a commit -qm init"], { cwd: source })
    process.env.HOME = join(root, "home")
    process.env.XDG_DATA_HOME = join(root, "data")
    process.env.XDG_CONFIG_HOME = join(root, "config")
    process.env.XDG_STATE_HOME = join(root, "state")
    process.env.XDG_CACHE_HOME = join(root, "cache")
    
    const plugin = Plugin.define({
      id: "copy-worktrees",
      async setup(ctx) {
        await ctx.worktree.transform((editor) =>
          editor.add({
            id: "copy",
            async create(input) {
              await cp(input.sourceDirectory, input.directory, { recursive: true, filter: (src) => !src.includes("/.git") })
              return { directory: input.directory }
            },
            async remove(input) {
              await rm(input.directory, { recursive: true, force: true })
            },
            async list() {
              return []
            },
          }),
        )
      },
    })
    
    await using opencode = await OpenCode.create({
      plugins: [plugin],
      config: { directory: source, project: false },
      models: { fetch: false },
      fs: { filewatcher: false },
    } as any)
    
    const s1 = await opencode.sessions.create({ location: { directory: source } })
    console.log("source project ID:  ", s1.projectID)
    for (let i = 0; i < 50 && (await opencode.worktree.list({ projectID: s1.projectID })).length === 0; i++) await Bun.sleep(100)
    const wt = await opencode.worktree.create({ projectID: s1.projectID, directory: join(root, "copies"), name: "task" })
    console.log("worktree:           ", JSON.stringify(wt))
    console.log("worktree list:      ", JSON.stringify(await opencode.worktree.list({ projectID: s1.projectID })))
    const s2 = await opencode.sessions.create({ location: { directory: wt.directory } })
    console.log("worktree project ID:", s2.projectID)
    console.log(s1.projectID === s2.projectID ? "OK: same project" : "BUG: worktree resolved to a different project")

    Run it: bun repro.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions