Quellcode durchsuchen

feat(scout): materialize configured reference repos (#26692)

Shoubhit Dash vor 4 Monaten
Ursprung
Commit
5cf9abe743

+ 49 - 48
packages/opencode/src/agent/agent.ts

@@ -26,9 +26,7 @@ import * as Option from "effect/Option"
 import * as OtelTracer from "@effect/opentelemetry/Tracer"
 import * as OtelTracer from "@effect/opentelemetry/Tracer"
 import { zod } from "@opencode-ai/core/effect-zod"
 import { zod } from "@opencode-ai/core/effect-zod"
 import { withStatics, type DeepMutable } from "@opencode-ai/core/schema"
 import { withStatics, type DeepMutable } from "@opencode-ai/core/schema"
-
-type ReferenceEntry = NonNullable<Config.Info["reference"]>[string]
-type ResolvedReference = { kind: "git"; repository: string; branch?: string } | { kind: "local"; path: string }
+import { Reference } from "@/reference/reference"
 
 
 export const Info = Schema.Struct({
 export const Info = Schema.Struct({
   name: Schema.String,
   name: Schema.String,
@@ -303,69 +301,72 @@ export const layer = Layer.effect(
           item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
           item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
         }
         }
 
 
-        function referencePath(value: string) {
-          if (value.startsWith("~/")) return path.join(Global.Path.home, value.slice(2))
-          return path.isAbsolute(value)
-            ? value
-            : path.resolve(ctx.worktree === "/" ? ctx.directory : ctx.worktree, value)
-        }
-
-        function resolveReference(reference: ReferenceEntry): ResolvedReference {
-          if (typeof reference === "string") {
-            if (reference.startsWith(".") || reference.startsWith("/") || reference.startsWith("~")) {
-              return { kind: "local", path: referencePath(reference) }
-            }
-            return { kind: "git", repository: reference }
-          }
-          if ("path" in reference) return { kind: "local", path: referencePath(reference.path) }
-          return { kind: "git", repository: reference.repository, branch: reference.branch }
-        }
-
-        function referencePrompt(name: string, reference: ResolvedReference) {
+        function referencePrompt(reference: Reference.Resolved) {
           if (reference.kind === "local") {
           if (reference.kind === "local") {
             return [
             return [
-              PROMPT_SCOUT,
-              `You are Scout reference @${name}. This reference points to a local directory outside or alongside the current workspace.`,
+              `You are configured reference @${reference.name}, a read-only research agent for external reference material.`,
               `Local directory: ${reference.path}`,
               `Local directory: ${reference.path}`,
-              `When invoked, inspect this directory as the primary reference source. Prefer repo_overview with path ${JSON.stringify(reference.path)} before broader searches. Do not edit files.`,
+              `Inspect this directory as the primary reference source. Prefer repo_overview with path ${JSON.stringify(reference.path)} before broader searches. Do not edit files.`,
+              `Return exact absolute file paths for findings whenever possible.`,
+            ].join("\n\n")
+          }
+
+          if (reference.kind === "invalid") {
+            return [
+              `You are configured reference @${reference.name}, but this reference is not usable yet.`,
+              `Configured repository: ${reference.repository}`,
+              `Problem: ${reference.message}`,
+              `Explain this configuration problem if invoked. Do not edit files or attempt fallback clones.`,
             ].join("\n\n")
             ].join("\n\n")
           }
           }
 
 
           return [
           return [
-            PROMPT_SCOUT,
-            `You are Scout reference @${name}. This reference points to a git repository.`,
+            `You are configured reference @${reference.name}, a read-only research agent for external reference material.`,
             `Repository: ${reference.repository}`,
             `Repository: ${reference.repository}`,
             ...(reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
             ...(reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
-            `When invoked, clone or refresh this repository with repo_clone, then inspect the cached repository as the primary reference source. Do not edit files.`,
+            `Cached directory: ${reference.path}`,
+            `OpenCode materializes this configured repository before use. Do not call repo_clone for this reference.`,
+            `Inspect the cached directory as the primary reference source. Prefer repo_overview with path ${JSON.stringify(reference.path)} before broader searches, then use Glob, Grep, and Read inside that directory. Do not edit files.`,
+            `Return exact absolute file paths for findings whenever possible.`,
           ].join("\n\n")
           ].join("\n\n")
         }
         }
 
 
+        function referenceDescription(reference: Reference.Resolved) {
+          if (reference.kind === "local") return `Scout reference for local directory ${reference.path}`
+          if (reference.kind === "git") return `Scout reference for repository ${reference.repository}`
+          return `Invalid Scout reference for repository ${reference.repository}`
+        }
+
         if (Flag.OPENCODE_EXPERIMENTAL_SCOUT) {
         if (Flag.OPENCODE_EXPERIMENTAL_SCOUT) {
-          for (const [name, reference] of Object.entries(cfg.reference ?? {})) {
-            if (agents[name]) continue
-            const resolved = resolveReference(reference)
-            const localPath = resolved.kind === "local" ? resolved.path : undefined
-            agents[name] = {
-              name,
-              description:
-                resolved.kind === "local"
-                  ? `Scout reference for local directory ${resolved.path}`
-                  : `Scout reference for repository ${resolved.repository}`,
+          const resolvedReferences = Reference.resolveAll({
+            references: cfg.reference ?? {},
+            directory: ctx.directory,
+            worktree: ctx.worktree,
+          })
+          for (const resolved of resolvedReferences) {
+            if (agents[resolved.name]) continue
+            const localPath = resolved.kind === "invalid" ? undefined : resolved.path
+            agents[resolved.name] = {
+              name: resolved.name,
+              description: referenceDescription(resolved),
               permission: Permission.merge(
               permission: Permission.merge(
                 agents.scout.permission,
                 agents.scout.permission,
                 Permission.fromConfig(
                 Permission.fromConfig(
-                  localPath
-                    ? {
-                        external_directory: {
-                          [localPath]: "allow",
-                          [path.join(localPath, "*")]: "allow",
-                        },
-                      }
-                    : {},
+                  {
+                    repo_clone: "deny",
+                    ...(localPath
+                      ? {
+                          external_directory: {
+                            [localPath]: "allow",
+                            [path.join(localPath, "*")]: "allow",
+                          },
+                        }
+                      : {}),
+                  },
                 ),
                 ),
               ),
               ),
-              prompt: referencePrompt(name, resolved),
-              options: { reference },
+              prompt: referencePrompt(resolved),
+              options: { reference: cfg.reference?.[resolved.name], resolved },
               mode: "subagent",
               mode: "subagent",
               native: false,
               native: false,
             }
             }

+ 2 - 0
packages/opencode/src/effect/app-runtime.ts

@@ -43,6 +43,7 @@ import { Format } from "@/format"
 import { InstanceLayer } from "@/project/instance-layer"
 import { InstanceLayer } from "@/project/instance-layer"
 import { Project } from "@/project/project"
 import { Project } from "@/project/project"
 import { Vcs } from "@/project/vcs"
 import { Vcs } from "@/project/vcs"
+import { Reference } from "@/reference/reference"
 import { Workspace } from "@/control-plane/workspace"
 import { Workspace } from "@/control-plane/workspace"
 import { Worktree } from "@/worktree"
 import { Worktree } from "@/worktree"
 import { Pty } from "@/pty"
 import { Pty } from "@/pty"
@@ -96,6 +97,7 @@ export const AppLayer = Layer.mergeAll(
   Format.defaultLayer,
   Format.defaultLayer,
   Project.defaultLayer,
   Project.defaultLayer,
   Vcs.defaultLayer,
   Vcs.defaultLayer,
+  Reference.defaultLayer,
   Workspace.defaultLayer,
   Workspace.defaultLayer,
   Worktree.appLayer,
   Worktree.appLayer,
   Pty.defaultLayer,
   Pty.defaultLayer,

+ 4 - 1
packages/opencode/src/project/bootstrap.ts

@@ -12,6 +12,7 @@ import { ShareNext } from "@/share/share-next"
 import { Effect, Layer } from "effect"
 import { Effect, Layer } from "effect"
 import { Config } from "@/config/config"
 import { Config } from "@/config/config"
 import { Service } from "./bootstrap-service"
 import { Service } from "./bootstrap-service"
+import { Reference } from "@/reference/reference"
 
 
 export { Service } from "./bootstrap-service"
 export { Service } from "./bootstrap-service"
 export type { Interface } from "./bootstrap-service"
 export type { Interface } from "./bootstrap-service"
@@ -29,6 +30,7 @@ export const layer = Layer.effect(
     const lsp = yield* LSP.Service
     const lsp = yield* LSP.Service
     const plugin = yield* Plugin.Service
     const plugin = yield* Plugin.Service
     const project = yield* Project.Service
     const project = yield* Project.Service
+    const reference = yield* Reference.Service
     const shareNext = yield* ShareNext.Service
     const shareNext = yield* ShareNext.Service
     const snapshot = yield* Snapshot.Service
     const snapshot = yield* Snapshot.Service
     const vcs = yield* Vcs.Service
     const vcs = yield* Vcs.Service
@@ -43,7 +45,7 @@ export const layer = Layer.effect(
       // Each service self-manages its own slow work via Effect.forkScoped against
       // Each service self-manages its own slow work via Effect.forkScoped against
       // its per-instance state scope. We just await materialization here.
       // its per-instance state scope. We just await materialization here.
       yield* Effect.forEach(
       yield* Effect.forEach(
-        [lsp, shareNext, format, file, fileWatcher, vcs, snapshot, project],
+        [reference, lsp, shareNext, format, file, fileWatcher, vcs, snapshot, project],
         (s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
         (s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
         { concurrency: "unbounded", discard: true },
         { concurrency: "unbounded", discard: true },
       ).pipe(Effect.withSpan("InstanceBootstrap.init"))
       ).pipe(Effect.withSpan("InstanceBootstrap.init"))
@@ -63,6 +65,7 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
     LSP.defaultLayer,
     LSP.defaultLayer,
     Plugin.defaultLayer,
     Plugin.defaultLayer,
     Project.defaultLayer,
     Project.defaultLayer,
+    Reference.defaultLayer,
     ShareNext.defaultLayer,
     ShareNext.defaultLayer,
     Snapshot.defaultLayer,
     Snapshot.defaultLayer,
     Vcs.defaultLayer,
     Vcs.defaultLayer,

+ 228 - 0
packages/opencode/src/reference/reference.ts

@@ -0,0 +1,228 @@
+import path from "path"
+import { Effect, Context, Layer, Scope } from "effect"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { Flag } from "@opencode-ai/core/flag/flag"
+import { Global } from "@opencode-ai/core/global"
+import { Config } from "@/config/config"
+import { InstanceState } from "@/effect/instance-state"
+import { Git } from "@/git"
+import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
+import { RepositoryCache } from "./repository-cache"
+
+type ReferenceEntry = NonNullable<Config.Info["reference"]>[string]
+
+export type Resolved =
+  | {
+      name: string
+      kind: "local"
+      path: string
+    }
+  | {
+      name: string
+      kind: "git"
+      repository: string
+      reference: RepositoryReference
+      path: string
+      branch?: string
+    }
+  | {
+      name: string
+      kind: "invalid"
+      repository: string
+      message: string
+    }
+
+type State = {
+  references: Resolved[]
+  materializeAll: Effect.Effect<void>
+  materializeByPath: { path: string; run: Effect.Effect<void> }[]
+}
+
+export interface Interface {
+  readonly init: () => Effect.Effect<void>
+  readonly list: () => Effect.Effect<Resolved[]>
+  readonly get: (name: string) => Effect.Effect<Resolved | undefined>
+  readonly ensure: (target?: string) => Effect.Effect<void>
+  readonly contains: (target?: string) => Effect.Effect<boolean>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/Reference") {}
+
+export function referencePath(input: { directory: string; worktree: string; value: string }) {
+  if (input.value.startsWith("~/")) return path.join(Global.Path.home, input.value.slice(2))
+  return path.isAbsolute(input.value)
+    ? input.value
+    : path.resolve(input.worktree === "/" ? input.directory : input.worktree, input.value)
+}
+
+function resolveGit(
+  input: { name: string; repository: string } | { name: string; repository: string; branch: string | undefined },
+): Resolved {
+  const parsed = parseRepositoryReference(input.repository)
+  if (!parsed || parsed.protocol === "file:") {
+    return {
+      name: input.name,
+      kind: "invalid",
+      repository: input.repository,
+      message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand",
+    }
+  }
+  return {
+    name: input.name,
+    kind: "git",
+    repository: input.repository,
+    reference: parsed,
+    path: repositoryCachePath(parsed),
+    ...("branch" in input ? { branch: input.branch } : {}),
+  }
+}
+
+function branchLabel(branch: string | undefined) {
+  return branch ?? "default branch"
+}
+
+function normalizedTarget(target?: string) {
+  if (!target) return
+  return process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
+}
+
+function containsReferencePath(referencePath: string, target: string) {
+  return AppFileSystem.contains(normalizedTarget(referencePath) ?? referencePath, target)
+}
+
+export function resolve(input: { name: string; reference: ReferenceEntry; directory: string; worktree: string }): Resolved {
+  if (typeof input.reference === "string") {
+    if (input.reference.startsWith(".") || input.reference.startsWith("/") || input.reference.startsWith("~")) {
+      return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference }) }
+    }
+    return resolveGit({ name: input.name, repository: input.reference })
+  }
+
+  if ("path" in input.reference) {
+    return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference.path }) }
+  }
+
+  return resolveGit({ name: input.name, repository: input.reference.repository, branch: input.reference.branch })
+}
+
+export function resolveAll(input: {
+  references: NonNullable<Config.Info["reference"]>
+  directory: string
+  worktree: string
+}) {
+  const seen = new Map<string, { name: string; branch?: string }>()
+  return Object.entries(input.references).map(([name, reference]) => {
+    const resolved = resolve({ name, reference, directory: input.directory, worktree: input.worktree })
+    if (resolved.kind !== "git") return resolved
+
+    const existing = seen.get(resolved.path)
+    if (!existing) {
+      seen.set(resolved.path, { name, branch: resolved.branch })
+      return resolved
+    }
+    if (existing.branch === resolved.branch) return resolved
+
+    return {
+      name,
+      kind: "invalid" as const,
+      repository: resolved.repository,
+      message: `Reference conflicts with @${existing.name}: both use ${resolved.path}, but @${existing.name} requests ${branchLabel(existing.branch)} and @${name} requests ${branchLabel(resolved.branch)}`,
+    }
+  })
+}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const config = yield* Config.Service
+    const fs = yield* AppFileSystem.Service
+    const git = yield* Git.Service
+    const scope = yield* Scope.Scope
+
+    const state = yield* InstanceState.make<State>(
+      Effect.fn("Reference.state")(function* (ctx) {
+        const cfg = yield* config.get()
+        const references = resolveAll({ references: cfg.reference ?? {}, directory: ctx.directory, worktree: ctx.worktree })
+        const seenPath = new Set<string>()
+        const gitReferences = references.filter((reference): reference is Extract<Resolved, { kind: "git" }> => {
+          if (reference.kind !== "git") return false
+          if (seenPath.has(reference.path)) return false
+          seenPath.add(reference.path)
+          return true
+        })
+        const materializeByPath = yield* Effect.forEach(
+          gitReferences,
+          Effect.fnUntraced(function* (reference) {
+            const run = yield* Effect.cached(
+              RepositoryCache.ensure(
+                { reference: reference.reference, branch: reference.branch, refresh: true },
+                { fs, git },
+              ).pipe(
+                Effect.asVoid,
+                Effect.catchCause((cause) =>
+                  Effect.logWarning("failed to materialize reference repository", { name: reference.name, cause }),
+                ),
+              ),
+            )
+            return { path: reference.path, run }
+          }),
+          { concurrency: "unbounded" },
+        )
+
+        const materializeAll = yield* Effect.cached(
+          Flag.OPENCODE_EXPERIMENTAL_SCOUT
+            ? Effect.gen(function* () {
+                yield* Effect.forEach(
+                  materializeByPath,
+                  Effect.fnUntraced(function* (item) {
+                    yield* item.run
+                  }),
+                  { concurrency: 4, discard: true },
+                )
+              })
+            : Effect.void,
+        )
+
+        return { references, materializeAll, materializeByPath }
+      }),
+    )
+
+    return Service.of({
+      init: Effect.fn("Reference.init")(function* () {
+        if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return
+        yield* InstanceState.useEffect(state, (s) => s.materializeAll).pipe(Effect.forkIn(scope), Effect.asVoid)
+      }),
+      list: Effect.fn("Reference.list")(function* () {
+        return yield* InstanceState.use(state, (s) => s.references)
+      }),
+      get: Effect.fn("Reference.get")(function* (name: string) {
+        return yield* InstanceState.use(state, (s) => s.references.find((reference) => reference.name === name))
+      }),
+      ensure: Effect.fn("Reference.ensure")(function* (target?: string) {
+        if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return
+        const full = normalizedTarget(target)
+        if (!full) return yield* InstanceState.useEffect(state, (s) => s.materializeAll)
+        return yield* InstanceState.useEffect(
+          state,
+          (s) => s.materializeByPath.find((item) => containsReferencePath(item.path, full))?.run ?? Effect.void,
+        )
+      }),
+      contains: Effect.fn("Reference.contains")(function* (target?: string) {
+        if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return false
+        const full = normalizedTarget(target)
+        if (!full) return false
+        return yield* InstanceState.use(state, (s) =>
+          s.references.some((reference) => reference.kind === "git" && containsReferencePath(reference.path, full)),
+        )
+      }),
+    })
+  }),
+)
+
+export const defaultLayer = layer.pipe(
+  Layer.provide(Config.defaultLayer),
+  Layer.provide(AppFileSystem.defaultLayer),
+  Layer.provide(Git.defaultLayer),
+)
+
+export * as Reference from "./reference"

+ 155 - 0
packages/opencode/src/reference/repository-cache.ts

@@ -0,0 +1,155 @@
+import path from "path"
+import { Effect } from "effect"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { Flock } from "@opencode-ai/core/util/flock"
+import { Git } from "@/git"
+import {
+  repositoryCachePath,
+  sameRepositoryReference,
+  parseRepositoryReference,
+  validateRepositoryBranch,
+  type Reference as RepositoryReference,
+} from "@/util/repository"
+
+export type Result = {
+  repository: string
+  host: string
+  remote: string
+  localPath: string
+  status: "cached" | "cloned" | "refreshed"
+  head?: string
+  branch?: string
+}
+
+function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) {
+  if (!input.reuse) return "cloned" as const
+  if (input.branchMatches === false) return "refreshed" as const
+  if (input.refresh) return "refreshed" as const
+  return "cached" as const
+}
+
+function resetTarget(input: {
+  requestedBranch?: string
+  remoteHead: { code: number; stdout: string }
+  branch: { code: number; stdout: string }
+}) {
+  if (input.requestedBranch) return `origin/${input.requestedBranch}`
+  if (input.remoteHead.code === 0 && input.remoteHead.stdout) {
+    return input.remoteHead.stdout.replace(/^refs\/remotes\//, "")
+  }
+  if (input.branch.code === 0 && input.branch.stdout) {
+    return `origin/${input.branch.stdout}`
+  }
+  return "HEAD"
+}
+
+export const ensure = Effect.fn("RepositoryCache.ensure")(function* (
+  input: {
+    reference: RepositoryReference
+    refresh?: boolean
+    branch?: string
+  },
+  services: {
+    fs: AppFileSystem.Interface
+    git: Git.Interface
+  },
+) {
+  if (input.branch) validateRepositoryBranch(input.branch)
+
+  const repository = input.reference.label
+  const remote = input.reference.remote
+  const localPath = repositoryCachePath(input.reference)
+  const cloneTarget = parseRepositoryReference(remote) ?? input.reference
+
+  return yield* Effect.acquireUseRelease(
+    Effect.promise((signal) => Flock.acquire(`repo-clone:${localPath}`, { signal })),
+    () =>
+      Effect.gen(function* () {
+        yield* services.fs.ensureDir(path.dirname(localPath)).pipe(Effect.orDie)
+
+        const exists = yield* services.fs.existsSafe(localPath)
+        const hasGitDir = yield* services.fs.existsSafe(path.join(localPath, ".git"))
+        const origin = hasGitDir
+          ? yield* services.git.run(["config", "--get", "remote.origin.url"], { cwd: localPath })
+          : undefined
+        const originReference = origin?.exitCode === 0 ? parseRepositoryReference(origin.text().trim()) : undefined
+        const reuse = hasGitDir && Boolean(originReference && sameRepositoryReference(originReference, cloneTarget))
+        if (exists && !reuse) {
+          yield* services.fs.remove(localPath, { recursive: true }).pipe(Effect.orDie)
+        }
+
+        const currentBranch = hasGitDir ? yield* services.git.branch(localPath) : undefined
+        const status = statusForRepository({
+          reuse,
+          refresh: input.refresh,
+          branchMatches: input.branch ? currentBranch === input.branch : undefined,
+        })
+
+        if (status === "cloned") {
+          const clone = yield* services.git.run(
+            [
+              "clone",
+              "--depth",
+              "100",
+              ...(input.branch ? ["--branch", input.branch] : []),
+              "--",
+              remote,
+              localPath,
+            ],
+            { cwd: path.dirname(localPath) },
+          )
+          if (clone.exitCode !== 0) {
+            throw new Error(clone.stderr.toString().trim() || clone.text().trim() || `Failed to clone ${repository}`)
+          }
+        }
+
+        if (status === "refreshed") {
+          const fetch = yield* services.git.run(["fetch", "--all", "--prune"], { cwd: localPath })
+          if (fetch.exitCode !== 0) {
+            throw new Error(fetch.stderr.toString().trim() || fetch.text().trim() || `Failed to refresh ${repository}`)
+          }
+
+          if (input.branch) {
+            const checkout = yield* services.git.run(["checkout", "-B", input.branch, `origin/${input.branch}`], {
+              cwd: localPath,
+            })
+            if (checkout.exitCode !== 0) {
+              throw new Error(
+                checkout.stderr.toString().trim() || checkout.text().trim() || `Failed to checkout ${input.branch}`,
+              )
+            }
+          }
+
+          const remoteHead = yield* services.git.run(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: localPath })
+          const branch = yield* services.git.run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: localPath })
+          const target = resetTarget({
+            requestedBranch: input.branch,
+            remoteHead: { code: remoteHead.exitCode, stdout: remoteHead.text().trim() },
+            branch: { code: branch.exitCode, stdout: branch.text().trim() },
+          })
+
+          const reset = yield* services.git.run(["reset", "--hard", target], { cwd: localPath })
+          if (reset.exitCode !== 0) {
+            throw new Error(reset.stderr.toString().trim() || reset.text().trim() || `Failed to reset ${repository}`)
+          }
+        }
+
+        const head = yield* services.git.run(["rev-parse", "HEAD"], { cwd: localPath })
+        const branch = yield* services.git.branch(localPath)
+        const headText = head.exitCode === 0 ? head.text().trim() : undefined
+
+        return {
+          repository,
+          host: input.reference.host,
+          remote,
+          localPath,
+          status,
+          head: headText,
+          branch,
+        } satisfies Result
+      }),
+    (lock) => Effect.promise(() => lock.release()).pipe(Effect.ignore),
+  )
+})
+
+export * as RepositoryCache from "./repository-cache"

+ 7 - 1
packages/opencode/src/tool/glob.ts

@@ -7,6 +7,7 @@ import { Ripgrep } from "../file/ripgrep"
 import { assertExternalDirectoryEffect } from "./external-directory"
 import { assertExternalDirectoryEffect } from "./external-directory"
 import DESCRIPTION from "./glob.txt"
 import DESCRIPTION from "./glob.txt"
 import * as Tool from "./tool"
 import * as Tool from "./tool"
+import { Reference } from "@/reference/reference"
 
 
 export const Parameters = Schema.Struct({
 export const Parameters = Schema.Struct({
   pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
   pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
@@ -20,6 +21,7 @@ export const GlobTool = Tool.define(
   Effect.gen(function* () {
   Effect.gen(function* () {
     const rg = yield* Ripgrep.Service
     const rg = yield* Ripgrep.Service
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
+    const reference = yield* Reference.Service
 
 
     return {
     return {
       description: DESCRIPTION,
       description: DESCRIPTION,
@@ -39,11 +41,15 @@ export const GlobTool = Tool.define(
 
 
           let search = params.path ?? ins.directory
           let search = params.path ?? ins.directory
           search = path.isAbsolute(search) ? search : path.resolve(ins.directory, search)
           search = path.isAbsolute(search) ? search : path.resolve(ins.directory, search)
+          yield* reference.ensure(search)
           const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
           const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
           if (info?.type === "File") {
           if (info?.type === "File") {
             throw new Error(`glob path must be a directory: ${search}`)
             throw new Error(`glob path must be a directory: ${search}`)
           }
           }
-          yield* assertExternalDirectoryEffect(ctx, search, { kind: "directory" })
+          yield* assertExternalDirectoryEffect(ctx, search, {
+            bypass: yield* reference.contains(search),
+            kind: "directory",
+          })
 
 
           const limit = 100
           const limit = 100
           let truncated = false
           let truncated = false

+ 4 - 0
packages/opencode/src/tool/grep.ts

@@ -7,6 +7,7 @@ import { Ripgrep } from "../file/ripgrep"
 import { assertExternalDirectoryEffect } from "./external-directory"
 import { assertExternalDirectoryEffect } from "./external-directory"
 import DESCRIPTION from "./grep.txt"
 import DESCRIPTION from "./grep.txt"
 import * as Tool from "./tool"
 import * as Tool from "./tool"
+import { Reference } from "@/reference/reference"
 
 
 const MAX_LINE_LENGTH = 2000
 const MAX_LINE_LENGTH = 2000
 
 
@@ -25,6 +26,7 @@ export const GrepTool = Tool.define(
   Effect.gen(function* () {
   Effect.gen(function* () {
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
     const rg = yield* Ripgrep.Service
     const rg = yield* Ripgrep.Service
+    const reference = yield* Reference.Service
 
 
     return {
     return {
       description: DESCRIPTION,
       description: DESCRIPTION,
@@ -57,10 +59,12 @@ export const GrepTool = Tool.define(
               ? (params.path ?? ins.directory)
               ? (params.path ?? ins.directory)
               : path.join(ins.directory, params.path ?? "."),
               : path.join(ins.directory, params.path ?? "."),
           )
           )
+          yield* reference.ensure(search)
           const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
           const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
           const cwd = info?.type === "Directory" ? search : path.dirname(search)
           const cwd = info?.type === "Directory" ? search : path.dirname(search)
           const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
           const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
           yield* assertExternalDirectoryEffect(ctx, search, {
           yield* assertExternalDirectoryEffect(ctx, search, {
+            bypass: yield* reference.contains(search),
             kind: info?.type === "Directory" ? "directory" : "file",
             kind: info?.type === "Directory" ? "directory" : "file",
           })
           })
 
 

+ 4 - 1
packages/opencode/src/tool/read.ts

@@ -11,6 +11,7 @@ import { InstanceState } from "@/effect/instance-state"
 import { assertExternalDirectoryEffect } from "./external-directory"
 import { assertExternalDirectoryEffect } from "./external-directory"
 import { Instruction } from "../session/instruction"
 import { Instruction } from "../session/instruction"
 import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
 import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
+import { Reference } from "@/reference/reference"
 
 
 const DEFAULT_READ_LIMIT = 2000
 const DEFAULT_READ_LIMIT = 2000
 const MAX_LINE_LENGTH = 2000
 const MAX_LINE_LENGTH = 2000
@@ -41,6 +42,7 @@ export const ReadTool = Tool.define(
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
     const instruction = yield* Instruction.Service
     const instruction = yield* Instruction.Service
     const lsp = yield* LSP.Service
     const lsp = yield* LSP.Service
+    const reference = yield* Reference.Service
     const scope = yield* Scope.Scope
     const scope = yield* Scope.Scope
 
 
     const miss = Effect.fn("ReadTool.miss")(function* (filepath: string) {
     const miss = Effect.fn("ReadTool.miss")(function* (filepath: string) {
@@ -162,6 +164,7 @@ export const ReadTool = Tool.define(
       if (process.platform === "win32") {
       if (process.platform === "win32") {
         filepath = AppFileSystem.normalizePath(filepath)
         filepath = AppFileSystem.normalizePath(filepath)
       }
       }
+      yield* reference.ensure(filepath)
       const title = path.relative(instance.worktree, filepath)
       const title = path.relative(instance.worktree, filepath)
 
 
       const stat = yield* fs.stat(filepath).pipe(
       const stat = yield* fs.stat(filepath).pipe(
@@ -172,7 +175,7 @@ export const ReadTool = Tool.define(
       )
       )
 
 
       yield* assertExternalDirectoryEffect(ctx, filepath, {
       yield* assertExternalDirectoryEffect(ctx, filepath, {
-        bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
+        bypass: Boolean(ctx.extra?.["bypassCwdCheck"]) || (yield* reference.contains(filepath)),
         kind: stat?.type === "Directory" ? "directory" : "file",
         kind: stat?.type === "Directory" ? "directory" : "file",
       })
       })
 
 

+ 3 - 0
packages/opencode/src/tool/registry.ts

@@ -50,6 +50,7 @@ import { Agent } from "../agent/agent"
 import { Git } from "@/git"
 import { Git } from "@/git"
 import { Skill } from "../skill"
 import { Skill } from "../skill"
 import { Permission } from "@/permission"
 import { Permission } from "@/permission"
+import { Reference } from "@/reference/reference"
 
 
 const log = Log.create({ service: "tool.registry" })
 const log = Log.create({ service: "tool.registry" })
 
 
@@ -91,6 +92,7 @@ export const layer: Layer.Layer<
   | Session.Service
   | Session.Service
   | Provider.Service
   | Provider.Service
   | Git.Service
   | Git.Service
+  | Reference.Service
   | LSP.Service
   | LSP.Service
   | Instruction.Service
   | Instruction.Service
   | AppFileSystem.Service
   | AppFileSystem.Service
@@ -361,6 +363,7 @@ export const defaultLayer = Layer.suspend(() =>
     Layer.provide(Session.defaultLayer),
     Layer.provide(Session.defaultLayer),
     Layer.provide(Provider.defaultLayer),
     Layer.provide(Provider.defaultLayer),
     Layer.provide(Git.defaultLayer),
     Layer.provide(Git.defaultLayer),
+    Layer.provide(Reference.defaultLayer),
     Layer.provide(LSP.defaultLayer),
     Layer.provide(LSP.defaultLayer),
     Layer.provide(Instruction.defaultLayer),
     Layer.provide(Instruction.defaultLayer),
     Layer.provide(AppFileSystem.defaultLayer),
     Layer.provide(AppFileSystem.defaultLayer),

+ 18 - 147
packages/opencode/src/tool/repo_clone.ts

@@ -1,11 +1,10 @@
-import path from "path"
 import { Effect, Schema } from "effect"
 import { Effect, Schema } from "effect"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
-import { Flock } from "@opencode-ai/core/util/flock"
 import { Git } from "@/git"
 import { Git } from "@/git"
 import DESCRIPTION from "./repo_clone.txt"
 import DESCRIPTION from "./repo_clone.txt"
 import * as Tool from "./tool"
 import * as Tool from "./tool"
-import { parseRepositoryReference, repositoryCachePath, sameRepositoryReference } from "@/util/repository"
+import { parseRemoteRepositoryReference, repositoryCachePath, validateRepositoryBranch } from "@/util/repository"
+import { RepositoryCache } from "@/reference/repository-cache"
 
 
 export const Parameters = Schema.Struct({
 export const Parameters = Schema.Struct({
   repository: Schema.String.annotate({
   repository: Schema.String.annotate({
@@ -29,36 +28,6 @@ type Metadata = {
   branch?: string
   branch?: string
 }
 }
 
 
-function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) {
-  if (!input.reuse) return "cloned" as const
-  if (input.branchMatches === false) return "refreshed" as const
-  if (input.refresh) return "refreshed" as const
-  return "cached" as const
-}
-
-function resetTarget(input: {
-  requestedBranch?: string
-  remoteHead: { code: number; stdout: string }
-  branch: { code: number; stdout: string }
-}) {
-  if (input.requestedBranch) return `origin/${input.requestedBranch}`
-  if (input.remoteHead.code === 0 && input.remoteHead.stdout) {
-    return input.remoteHead.stdout.replace(/^refs\/remotes\//, "")
-  }
-  if (input.branch.code === 0 && input.branch.stdout) {
-    return `origin/${input.branch.stdout}`
-  }
-  return "HEAD"
-}
-
-function validateBranch(branch: string) {
-  if (!/^[A-Za-z0-9/_.-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) {
-    throw new Error(
-      "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain ..",
-    )
-  }
-}
-
 export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSystem.Service | Git.Service>(
 export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSystem.Service | Git.Service>(
   "repo_clone",
   "repo_clone",
   Effect.gen(function* () {
   Effect.gen(function* () {
@@ -70,16 +39,12 @@ export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSys
       parameters: Parameters,
       parameters: Parameters,
       execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
       execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
         Effect.gen(function* () {
         Effect.gen(function* () {
-          const reference = parseRepositoryReference(params.repository)
-          if (!reference)
-            throw new Error("Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand")
-          if (reference.protocol === "file:") throw new Error("Local file repositories are not supported")
-          if (params.branch) validateBranch(params.branch)
+          const reference = parseRemoteRepositoryReference(params.repository)
+          if (params.branch) validateRepositoryBranch(params.branch)
 
 
           const repository = reference.label
           const repository = reference.label
           const remote = reference.remote
           const remote = reference.remote
           const localPath = repositoryCachePath(reference)
           const localPath = repositoryCachePath(reference)
-          const cloneTarget = parseRepositoryReference(remote) ?? reference
 
 
           yield* ctx.ask({
           yield* ctx.ask({
             permission: "repo_clone",
             permission: "repo_clone",
@@ -94,115 +59,21 @@ export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSys
             },
             },
           })
           })
 
 
-          return yield* Effect.acquireUseRelease(
-            Effect.promise((signal) => Flock.acquire(`repo-clone:${localPath}`, { signal })),
-            () =>
-              Effect.gen(function* () {
-                yield* fs.ensureDir(path.dirname(localPath)).pipe(Effect.orDie)
-
-                const exists = yield* fs.existsSafe(localPath)
-                const hasGitDir = yield* fs.existsSafe(path.join(localPath, ".git"))
-                const origin = hasGitDir
-                  ? yield* git.run(["config", "--get", "remote.origin.url"], { cwd: localPath })
-                  : undefined
-                const originReference =
-                  origin?.exitCode === 0 ? parseRepositoryReference(origin.text().trim()) : undefined
-                const reuse =
-                  hasGitDir && Boolean(originReference && sameRepositoryReference(originReference, cloneTarget))
-                if (exists && !reuse) {
-                  yield* fs.remove(localPath, { recursive: true }).pipe(Effect.orDie)
-                }
-
-                const currentBranch = hasGitDir ? yield* git.branch(localPath) : undefined
-                const status = statusForRepository({
-                  reuse,
-                  refresh: params.refresh,
-                  branchMatches: params.branch ? currentBranch === params.branch : undefined,
-                })
-
-                if (status === "cloned") {
-                  const clone = yield* git.run(
-                    [
-                      "clone",
-                      "--depth",
-                      "100",
-                      ...(params.branch ? ["--branch", params.branch] : []),
-                      "--",
-                      remote,
-                      localPath,
-                    ],
-                    { cwd: path.dirname(localPath) },
-                  )
-                  if (clone.exitCode !== 0) {
-                    throw new Error(
-                      clone.stderr.toString().trim() || clone.text().trim() || `Failed to clone ${repository}`,
-                    )
-                  }
-                }
-
-                if (status === "refreshed") {
-                  const fetch = yield* git.run(["fetch", "--all", "--prune"], { cwd: localPath })
-                  if (fetch.exitCode !== 0) {
-                    throw new Error(
-                      fetch.stderr.toString().trim() || fetch.text().trim() || `Failed to refresh ${repository}`,
-                    )
-                  }
-
-                  if (params.branch) {
-                    const checkout = yield* git.run(["checkout", "-B", params.branch, `origin/${params.branch}`], {
-                      cwd: localPath,
-                    })
-                    if (checkout.exitCode !== 0) {
-                      throw new Error(
-                        checkout.stderr.toString().trim() ||
-                          checkout.text().trim() ||
-                          `Failed to checkout ${params.branch}`,
-                      )
-                    }
-                  }
-
-                  const remoteHead = yield* git.run(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: localPath })
-                  const branch = yield* git.run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: localPath })
-                  const target = resetTarget({
-                    requestedBranch: params.branch,
-                    remoteHead: { code: remoteHead.exitCode, stdout: remoteHead.text().trim() },
-                    branch: { code: branch.exitCode, stdout: branch.text().trim() },
-                  })
-
-                  const reset = yield* git.run(["reset", "--hard", target], { cwd: localPath })
-                  if (reset.exitCode !== 0) {
-                    throw new Error(
-                      reset.stderr.toString().trim() || reset.text().trim() || `Failed to reset ${repository}`,
-                    )
-                  }
-                }
-
-                const head = yield* git.run(["rev-parse", "HEAD"], { cwd: localPath })
-                const branch = yield* git.branch(localPath)
-                const headText = head.exitCode === 0 ? head.text().trim() : undefined
-
-                return {
-                  title: repository,
-                  metadata: {
-                    repository,
-                    host: reference.host,
-                    remote,
-                    localPath,
-                    status,
-                    head: headText,
-                    branch,
-                  },
-                  output: [
-                    `Repository ready: ${repository}`,
-                    `Status: ${status}`,
-                    `Local path: ${localPath}`,
-                    ...(branch ? [`Branch: ${branch}`] : []),
-                    ...(headText ? [`HEAD: ${headText}`] : []),
-                  ].join("\n"),
-                }
-              }),
-            (lock) => Effect.promise(() => lock.release()).pipe(Effect.ignore),
+          const result = yield* RepositoryCache.ensure(
+            { reference, refresh: params.refresh, branch: params.branch },
+            { fs, git },
           )
           )
+          return {
+            title: repository,
+            metadata: result,
+            output: [
+              `Repository ready: ${repository}`,
+              `Status: ${result.status}`,
+              `Local path: ${localPath}`,
+              ...(result.branch ? [`Branch: ${result.branch}`] : []),
+              ...(result.head ? [`HEAD: ${result.head}`] : []),
+            ].join("\n"),
+          }
         }).pipe(Effect.orDie),
         }).pipe(Effect.orDie),
     } satisfies Tool.DefWithoutID<typeof Parameters, Metadata>
     } satisfies Tool.DefWithoutID<typeof Parameters, Metadata>
   }),
   }),

+ 15 - 0
packages/opencode/src/util/repository.ts

@@ -125,6 +125,21 @@ export function parseRepositoryReference(input: string) {
   }
   }
 }
 }
 
 
+export function parseRemoteRepositoryReference(input: string) {
+  const reference = parseRepositoryReference(input)
+  if (!reference) throw new Error("Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand")
+  if (reference.protocol === "file:") throw new Error("Local file repositories are not supported")
+  return reference
+}
+
+export function validateRepositoryBranch(branch: string) {
+  if (!/^[A-Za-z0-9/_.-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) {
+    throw new Error(
+      "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain ..",
+    )
+  }
+}
+
 export function parseGitHubRemote(input: string) {
 export function parseGitHubRemote(input: string) {
   const cleaned = normalize(input)
   const cleaned = normalize(input)
   if (!cleaned.includes("://") && !cleaned.match(/^(?:[^@/\s]+@)?github\.com:/)) return null
   if (!cleaned.includes("://") && !cleaned.match(/^(?:[^@/\s]+@)?github\.com:/)) return null

+ 13 - 2
packages/opencode/test/agent/agent.test.ts

@@ -147,6 +147,10 @@ test("reference config creates scout-backed subagents", async () => {
       config: {
       config: {
         reference: {
         reference: {
           effect: "github.com/effect/effect-smol",
           effect: "github.com/effect/effect-smol",
+          effectDev: {
+            repository: "https://github.com/effect/effect-smol",
+            branch: "dev",
+          },
           effectFull: {
           effectFull: {
             repository: "Effect-TS/effect",
             repository: "Effect-TS/effect",
             branch: "main",
             branch: "main",
@@ -162,6 +166,7 @@ test("reference config creates scout-backed subagents", async () => {
       directory: tmp.path,
       directory: tmp.path,
       fn: async () => {
       fn: async () => {
         const effect = await load(tmp.path, (svc) => svc.get("effect"))
         const effect = await load(tmp.path, (svc) => svc.get("effect"))
+        const effectDev = await load(tmp.path, (svc) => svc.get("effectDev"))
         const effectFull = await load(tmp.path, (svc) => svc.get("effectFull"))
         const effectFull = await load(tmp.path, (svc) => svc.get("effectFull"))
         const local = await load(tmp.path, (svc) => svc.get("localdocs"))
         const local = await load(tmp.path, (svc) => svc.get("localdocs"))
         const localFull = await load(tmp.path, (svc) => svc.get("localdocsFull"))
         const localFull = await load(tmp.path, (svc) => svc.get("localdocsFull"))
@@ -169,13 +174,19 @@ test("reference config creates scout-backed subagents", async () => {
         expect(effect).toBeDefined()
         expect(effect).toBeDefined()
         expect(effect?.mode).toBe("subagent")
         expect(effect?.mode).toBe("subagent")
         expect(effect?.prompt).toContain("Repository: github.com/effect/effect-smol")
         expect(effect?.prompt).toContain("Repository: github.com/effect/effect-smol")
-        expect(evalPerm(effect, "repo_clone")).toBe("allow")
+        expect(effect?.prompt).toContain(`Cached directory: ${path.join(Global.Path.repos, "github.com", "effect", "effect-smol")}`)
+        expect(effect?.prompt).toContain("Do not call repo_clone")
+        expect(evalPerm(effect, "repo_clone")).toBe("deny")
+
+        expect(effectDev).toBeDefined()
+        expect(effectDev?.prompt).toContain("Problem: Reference conflicts with @effect")
+        expect(effectDev?.prompt).not.toContain("Cached directory:")
 
 
         expect(effectFull).toBeDefined()
         expect(effectFull).toBeDefined()
         expect(effectFull?.mode).toBe("subagent")
         expect(effectFull?.mode).toBe("subagent")
         expect(effectFull?.prompt).toContain("Repository: Effect-TS/effect")
         expect(effectFull?.prompt).toContain("Repository: Effect-TS/effect")
         expect(effectFull?.prompt).toContain("Branch/ref: main")
         expect(effectFull?.prompt).toContain("Branch/ref: main")
-        expect(evalPerm(effectFull, "repo_clone")).toBe("allow")
+        expect(evalPerm(effectFull, "repo_clone")).toBe("deny")
 
 
         expect(local).toBeDefined()
         expect(local).toBeDefined()
         expect(local?.mode).toBe("subagent")
         expect(local?.mode).toBe("subagent")

+ 249 - 0
packages/opencode/test/reference/reference.test.ts

@@ -0,0 +1,249 @@
+import { afterEach, describe, expect } from "bun:test"
+import path from "path"
+import { Effect, Layer } from "effect"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Flag } from "@opencode-ai/core/flag/flag"
+import { Global } from "@opencode-ai/core/global"
+import { Git } from "../../src/git"
+import { Reference } from "../../src/reference/reference"
+import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+afterEach(async () => {
+  await disposeAllInstances()
+})
+
+const it = testEffect(
+  Layer.mergeAll(
+    AppFileSystem.defaultLayer,
+    CrossSpawnSpawner.defaultLayer,
+    Git.defaultLayer,
+    Reference.defaultLayer,
+  ),
+)
+
+const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) =>
+  Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Flag.OPENCODE_EXPERIMENTAL_SCOUT
+      Flag.OPENCODE_EXPERIMENTAL_SCOUT = true
+      return previous
+    }),
+    () => self,
+    (previous) =>
+      Effect.sync(() => {
+        Flag.OPENCODE_EXPERIMENTAL_SCOUT = previous
+      }),
+  )
+
+const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
+  Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
+      process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = url
+      return previous
+    }),
+    () => self,
+    (previous) =>
+      Effect.sync(() => {
+        if (previous) process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = previous
+        else delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
+      }),
+  )
+
+const git = Effect.fn("ReferenceTest.git")(function* (cwd: string, args: string[]) {
+  return yield* Effect.promise(async () => {
+    const proc = Bun.spawn(["git", ...args], {
+      cwd,
+      stdout: "pipe",
+      stderr: "pipe",
+    })
+    const [stdout, stderr, code] = await Promise.all([
+      new Response(proc.stdout).text(),
+      new Response(proc.stderr).text(),
+      proc.exited,
+    ])
+    if (code !== 0) throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`)
+    return stdout.trim()
+  })
+})
+
+const waitForContent = (
+  fs: AppFileSystem.Interface,
+  file: string,
+  content: string,
+  attempts = 50,
+): Effect.Effect<void, AppFileSystem.Error> =>
+  Effect.gen(function* () {
+    if ((yield* fs.readFileStringSafe(file)) === content) return
+    if (attempts <= 0) throw new Error(`timed out waiting for ${file}`)
+    yield* Effect.sleep("100 millis")
+    yield* waitForContent(fs, file, content, attempts - 1)
+  })
+
+describe("reference", () => {
+  it.live("resolves local and git references", () =>
+    Effect.gen(function* () {
+      const root = path.resolve("opencode-reference-root")
+      const local = Reference.resolve({
+        name: "docs",
+        reference: { path: "../docs" },
+        directory: path.join(root, "packages", "app"),
+        worktree: root,
+      })
+      const repo = Reference.resolve({
+        name: "effect",
+        reference: { repository: "Effect-TS/effect", branch: "main" },
+        directory: path.join(root, "packages", "app"),
+        worktree: root,
+      })
+
+      expect(local.kind).toBe("local")
+      if (local.kind === "local") expect(local.path).toBe(path.resolve(root, "../docs"))
+      expect(repo.kind).toBe("git")
+      if (repo.kind === "git") {
+        expect(repo.repository).toBe("Effect-TS/effect")
+        expect(repo.branch).toBe("main")
+        expect(repo.path).toBe(path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"))
+      }
+    }),
+  )
+
+  it.live("marks same-cache references with different branches invalid", () =>
+    Effect.gen(function* () {
+      const root = path.resolve("opencode-reference-root")
+      const references = Reference.resolveAll({
+        directory: root,
+        worktree: root,
+        references: {
+          main: { repository: "owner/repo", branch: "main" },
+          dev: { repository: "github.com/owner/repo", branch: "dev" },
+          alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
+        },
+      })
+
+      expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"])
+      expect(references[1]?.kind).toBe("invalid")
+      if (references[1]?.kind === "invalid") {
+        expect(references[1].message).toContain("conflicts with @main")
+        expect(references[1].message).toContain("@dev requests dev")
+      }
+    }),
+  )
+
+  it.live("materializes configured git references during init", () =>
+    experimentalScout(
+      provideTmpdirInstance(
+        (_dir) =>
+          Effect.gen(function* () {
+            const fs = yield* AppFileSystem.Service
+            const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-test", "repo")
+            yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
+            yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
+
+            const source = yield* tmpdirScoped({ git: true })
+            const remoteRoot = yield* tmpdirScoped()
+            const remoteDir = path.join(remoteRoot, "opencode-reference-test")
+            const remoteRepo = path.join(remoteDir, "repo.git")
+
+            yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "configured\n"))
+            yield* git(source, ["add", "."])
+            yield* git(source, ["commit", "-m", "add readme"])
+            yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
+            yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
+
+            const reference = yield* Reference.Service
+            yield* githubBase(
+              `file://${remoteRoot}/`,
+              Effect.gen(function* () {
+                yield* reference.init()
+                yield* waitForContent(fs, path.join(cache, "README.md"), "configured\n")
+              }),
+            )
+
+            expect(yield* fs.existsSafe(path.join(cache, ".git"))).toBe(true)
+            expect(yield* fs.readFileString(path.join(cache, "README.md"))).toBe("configured\n")
+
+            const resolved = yield* reference.get("docs")
+            expect(resolved?.kind).toBe("git")
+            if (resolved?.kind === "git") expect(resolved.path).toBe(cache)
+          }),
+        {
+          config: {
+            reference: {
+              docs: "opencode-reference-test/repo",
+            },
+          },
+        },
+      ),
+    ),
+  )
+
+  it.live("refreshes configured git references on new instance init", () =>
+    experimentalScout(
+      Effect.gen(function* () {
+        const fs = yield* AppFileSystem.Service
+        const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-refresh", "repo")
+        yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
+        yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
+
+        const source = yield* tmpdirScoped({ git: true })
+        const remoteRoot = yield* tmpdirScoped()
+        const remoteDir = path.join(remoteRoot, "opencode-reference-refresh")
+        const remoteRepo = path.join(remoteDir, "repo.git")
+
+        yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
+        yield* git(source, ["add", "."])
+        yield* git(source, ["commit", "-m", "add readme"])
+        yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
+        yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
+
+        yield* githubBase(
+          `file://${remoteRoot}/`,
+          provideTmpdirInstance(
+            (_dir) =>
+              Effect.gen(function* () {
+                const reference = yield* Reference.Service
+                yield* reference.init()
+                yield* waitForContent(fs, path.join(cache, "README.md"), "v1\n")
+              }),
+            {
+              config: {
+                reference: {
+                  docs: "opencode-reference-refresh/repo",
+                },
+              },
+            },
+          ),
+        )
+
+        const branch = yield* git(source, ["branch", "--show-current"])
+        yield* git(source, ["remote", "add", "origin", remoteRepo])
+        yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
+        yield* git(source, ["add", "."])
+        yield* git(source, ["commit", "-m", "update readme"])
+        yield* git(source, ["push", "origin", `${branch}:${branch}`])
+
+        yield* githubBase(
+          `file://${remoteRoot}/`,
+          provideTmpdirInstance(
+            (_dir) =>
+              Effect.gen(function* () {
+                const reference = yield* Reference.Service
+                yield* reference.init()
+                yield* waitForContent(fs, path.join(cache, "README.md"), "v2\n")
+              }),
+            {
+              config: {
+                reference: {
+                  docs: "opencode-reference-refresh/repo",
+                },
+              },
+            },
+          ),
+        )
+      }),
+    ),
+  )
+})

+ 2 - 0
packages/opencode/test/session/prompt.test.ts

@@ -46,6 +46,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import * as Database from "../../src/storage/db"
 import * as Database from "../../src/storage/db"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { Format } from "../../src/format"
 import { Format } from "../../src/format"
+import { Reference } from "../../src/reference/reference"
 import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
 import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
 import { reply, TestLLMServer } from "../lib/llm-server"
 import { reply, TestLLMServer } from "../lib/llm-server"
@@ -181,6 +182,7 @@ function makeHttp() {
     Layer.provide(FetchHttpClient.layer),
     Layer.provide(FetchHttpClient.layer),
     Layer.provide(CrossSpawnSpawner.defaultLayer),
     Layer.provide(CrossSpawnSpawner.defaultLayer),
     Layer.provide(Git.defaultLayer),
     Layer.provide(Git.defaultLayer),
+    Layer.provide(Reference.defaultLayer),
     Layer.provide(Ripgrep.defaultLayer),
     Layer.provide(Ripgrep.defaultLayer),
     Layer.provide(Format.defaultLayer),
     Layer.provide(Format.defaultLayer),
     Layer.provideMerge(todo),
     Layer.provideMerge(todo),

+ 2 - 0
packages/opencode/test/session/snapshot-tool-race.test.ts

@@ -57,6 +57,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { Format } from "../../src/format"
 import { Format } from "../../src/format"
+import { Reference } from "../../src/reference/reference"
 
 
 void Log.init({ print: false })
 void Log.init({ print: false })
 
 
@@ -131,6 +132,7 @@ function makeHttp() {
     Layer.provide(FetchHttpClient.layer),
     Layer.provide(FetchHttpClient.layer),
     Layer.provide(CrossSpawnSpawner.defaultLayer),
     Layer.provide(CrossSpawnSpawner.defaultLayer),
     Layer.provide(Git.defaultLayer),
     Layer.provide(Git.defaultLayer),
+    Layer.provide(Reference.defaultLayer),
     Layer.provide(Ripgrep.defaultLayer),
     Layer.provide(Ripgrep.defaultLayer),
     Layer.provide(Format.defaultLayer),
     Layer.provide(Format.defaultLayer),
     Layer.provideMerge(todo),
     Layer.provideMerge(todo),

+ 2 - 0
packages/opencode/test/tool/glob.test.ts

@@ -10,6 +10,7 @@ import { Truncate } from "@/tool/truncate"
 import { Agent } from "../../src/agent/agent"
 import { Agent } from "../../src/agent/agent"
 import { TestInstance } from "../fixture/fixture"
 import { TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { Reference } from "@/reference/reference"
 
 
 const it = testEffect(
 const it = testEffect(
   Layer.mergeAll(
   Layer.mergeAll(
@@ -18,6 +19,7 @@ const it = testEffect(
     Ripgrep.defaultLayer,
     Ripgrep.defaultLayer,
     Truncate.defaultLayer,
     Truncate.defaultLayer,
     Agent.defaultLayer,
     Agent.defaultLayer,
+    Reference.defaultLayer,
   ),
   ),
 )
 )
 
 

+ 2 - 0
packages/opencode/test/tool/grep.test.ts

@@ -10,6 +10,7 @@ import { Agent } from "../../src/agent/agent"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { Reference } from "@/reference/reference"
 
 
 const it = testEffect(
 const it = testEffect(
   Layer.mergeAll(
   Layer.mergeAll(
@@ -18,6 +19,7 @@ const it = testEffect(
     Ripgrep.defaultLayer,
     Ripgrep.defaultLayer,
     Truncate.defaultLayer,
     Truncate.defaultLayer,
     Agent.defaultLayer,
     Agent.defaultLayer,
+    Reference.defaultLayer,
   ),
   ),
 )
 )
 
 

+ 87 - 0
packages/opencode/test/tool/read.test.ts

@@ -4,6 +4,8 @@ import path from "path"
 import { Agent } from "../../src/agent/agent"
 import { Agent } from "../../src/agent/agent"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { Flag } from "@opencode-ai/core/flag/flag"
+import { Global } from "@opencode-ai/core/global"
 import { LSP } from "@/lsp/lsp"
 import { LSP } from "@/lsp/lsp"
 import { Permission } from "../../src/permission"
 import { Permission } from "../../src/permission"
 import { Instance } from "../../src/project/instance"
 import { Instance } from "../../src/project/instance"
@@ -15,6 +17,7 @@ import { Tool } from "@/tool/tool"
 import { Filesystem } from "@/util/filesystem"
 import { Filesystem } from "@/util/filesystem"
 import { disposeAllInstances, provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
 import { disposeAllInstances, provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { Reference } from "@/reference/reference"
 
 
 const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
 const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
 
 
@@ -40,6 +43,7 @@ const it = testEffect(
     CrossSpawnSpawner.defaultLayer,
     CrossSpawnSpawner.defaultLayer,
     Instruction.defaultLayer,
     Instruction.defaultLayer,
     LSP.defaultLayer,
     LSP.defaultLayer,
+    Reference.defaultLayer,
     Truncate.defaultLayer,
     Truncate.defaultLayer,
   ),
   ),
 )
 )
@@ -81,6 +85,49 @@ const fail = Effect.fn("ReadToolTest.fail")(function* (
 const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
 const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
 const glob = (p: string) =>
 const glob = (p: string) =>
   process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
   process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
+const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) =>
+  Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Flag.OPENCODE_EXPERIMENTAL_SCOUT
+      Flag.OPENCODE_EXPERIMENTAL_SCOUT = true
+      return previous
+    }),
+    () => self,
+    (previous) =>
+      Effect.sync(() => {
+        Flag.OPENCODE_EXPERIMENTAL_SCOUT = previous
+      }),
+  )
+const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
+  Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
+      process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = url
+      return previous
+    }),
+    () => self,
+    (previous) =>
+      Effect.sync(() => {
+        if (previous) process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = previous
+        else delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
+      }),
+  )
+const git = Effect.fn("ReadToolTest.git")(function* (cwd: string, args: string[]) {
+  return yield* Effect.promise(async () => {
+    const proc = Bun.spawn(["git", ...args], {
+      cwd,
+      stdout: "pipe",
+      stderr: "pipe",
+    })
+    const [stdout, stderr, code] = await Promise.all([
+      new Response(proc.stdout).text(),
+      new Response(proc.stderr).text(),
+      proc.exited,
+    ])
+    if (code !== 0) throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`)
+    return stdout.trim()
+  })
+})
 const put = Effect.fn("ReadToolTest.put")(function* (p: string, content: string | Buffer | Uint8Array) {
 const put = Effect.fn("ReadToolTest.put")(function* (p: string, content: string | Buffer | Uint8Array) {
   const fs = yield* AppFileSystem.Service
   const fs = yield* AppFileSystem.Service
   yield* fs.writeWithDirs(p, content)
   yield* fs.writeWithDirs(p, content)
@@ -212,6 +259,46 @@ describe("tool.read external_directory permission", () => {
       expect(ext).toBeUndefined()
       expect(ext).toBeUndefined()
     }),
     }),
   )
   )
+
+  it.live("does not ask for external_directory permission when reading configured references", () =>
+    experimentalScout(
+      Effect.gen(function* () {
+        const fs = yield* AppFileSystem.Service
+        const cache = path.join(Global.Path.repos, "github.com", "opencode-read-reference", "repo")
+        yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
+        yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
+
+        const source = yield* tmpdirScoped({ git: true })
+        const remoteRoot = yield* tmpdirScoped()
+        const remoteDir = path.join(remoteRoot, "opencode-read-reference")
+        const remoteRepo = path.join(remoteDir, "repo.git")
+        yield* put(path.join(source, "notes.md"), "reference notes")
+        yield* git(source, ["add", "."])
+        yield* git(source, ["commit", "-m", "add notes"])
+        yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
+        yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
+
+        const dir = yield* tmpdirScoped({
+          git: true,
+          config: {
+            reference: {
+              docs: "opencode-read-reference/repo",
+            },
+          },
+        })
+
+        const { items, next } = asks()
+        const result = yield* githubBase(
+          `file://${remoteRoot}/`,
+          exec(dir, { filePath: path.join(cache, "notes.md") }, next),
+        )
+        const ext = items.find((item) => item.permission === "external_directory")
+
+        expect(result.output).toContain("reference notes")
+        expect(ext).toBeUndefined()
+      }),
+    ),
+  )
 })
 })
 
 
 describe("tool.read env file permissions", () => {
 describe("tool.read env file permissions", () => {

+ 2 - 0
packages/opencode/test/tool/registry.test.ts

@@ -25,6 +25,7 @@ import { Format } from "@/format"
 import { Ripgrep } from "@/file/ripgrep"
 import { Ripgrep } from "@/file/ripgrep"
 import * as Truncate from "@/tool/truncate"
 import * as Truncate from "@/tool/truncate"
 import { InstanceState } from "@/effect/instance-state"
 import { InstanceState } from "@/effect/instance-state"
+import { Reference } from "@/reference/reference"
 
 
 const node = CrossSpawnSpawner.defaultLayer
 const node = CrossSpawnSpawner.defaultLayer
 const originalExperimentalScout = Flag.OPENCODE_EXPERIMENTAL_SCOUT
 const originalExperimentalScout = Flag.OPENCODE_EXPERIMENTAL_SCOUT
@@ -42,6 +43,7 @@ const registryLayer = ToolRegistry.layer.pipe(
   Layer.provide(Session.defaultLayer),
   Layer.provide(Session.defaultLayer),
   Layer.provide(Provider.defaultLayer),
   Layer.provide(Provider.defaultLayer),
   Layer.provide(Git.defaultLayer),
   Layer.provide(Git.defaultLayer),
+  Layer.provide(Reference.defaultLayer),
   Layer.provide(LSP.defaultLayer),
   Layer.provide(LSP.defaultLayer),
   Layer.provide(Instruction.defaultLayer),
   Layer.provide(Instruction.defaultLayer),
   Layer.provide(AppFileSystem.defaultLayer),
   Layer.provide(AppFileSystem.defaultLayer),