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

This commit is contained in:
Shoubhit Dash
2026-05-10 17:57:11 +05:30
committed by GitHub
parent 903d81819d
commit 5cf9abe743
19 changed files with 848 additions and 200 deletions

View File

@@ -7,6 +7,7 @@ import { Ripgrep } from "../file/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt"
import * as Tool from "./tool"
import { Reference } from "@/reference/reference"
export const Parameters = Schema.Struct({
pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
@@ -20,6 +21,7 @@ export const GlobTool = Tool.define(
Effect.gen(function* () {
const rg = yield* Ripgrep.Service
const fs = yield* AppFileSystem.Service
const reference = yield* Reference.Service
return {
description: DESCRIPTION,
@@ -39,11 +41,15 @@ export const GlobTool = Tool.define(
let search = params.path ?? ins.directory
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)))
if (info?.type === "File") {
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
let truncated = false

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
import path from "path"
import { Effect, Schema } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Flock } from "@opencode-ai/core/util/flock"
import { Git } from "@/git"
import DESCRIPTION from "./repo_clone.txt"
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({
repository: Schema.String.annotate({
@@ -29,36 +28,6 @@ type Metadata = {
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>(
"repo_clone",
Effect.gen(function* () {
@@ -70,16 +39,12 @@ export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSys
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
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 remote = reference.remote
const localPath = repositoryCachePath(reference)
const cloneTarget = parseRepositoryReference(remote) ?? reference
yield* ctx.ask({
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),
} satisfies Tool.DefWithoutID<typeof Parameters, Metadata>
}),