Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,8 @@ function toToolKind(toolName: string): ToolKind {

case "grep":
case "glob":
case "repo_clone":
case "repo_overview":
case "context7_resolve_library_id":
case "context7_get_library_docs":
return "search"
Expand All @@ -1583,6 +1585,10 @@ function toLocations(toolName: string, input: Record<string, any>): { path: stri
case "glob":
case "grep":
return input["path"] ? [{ path: input["path"] }] : []
case "repo_clone":
return input["path"] ? [{ path: input["path"] }] : []
case "repo_overview":
return input["path"] ? [{ path: input["path"] }] : []
case "bash":
return []
default:
Expand Down
39 changes: 35 additions & 4 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ProviderTransform } from "../provider"
import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
import PROMPT_EXPLORE from "./prompt/explore.txt"
import PROMPT_SCOUT from "./prompt/scout.txt"
import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { Permission } from "@/permission"
Expand Down Expand Up @@ -83,6 +84,10 @@ export const layer = Layer.effect(
const cfg = yield* config.get()
const skillDirs = yield* skill.dirs()
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
const readonlyExternalDirectory = {
"*": "ask",
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
} satisfies Record<string, "allow" | "ask" | "deny">

const defaults = Permission.fromConfig({
"*": "allow",
Expand All @@ -94,6 +99,8 @@ export const layer = Layer.effect(
question: "deny",
plan_enter: "deny",
plan_exit: "deny",
repo_clone: "deny",
repo_overview: "deny",
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: {
"*": "allow",
Expand Down Expand Up @@ -172,10 +179,7 @@ export const layer = Layer.effect(
websearch: "allow",
codesearch: "allow",
read: "allow",
external_directory: {
"*": "ask",
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
},
external_directory: readonlyExternalDirectory,
}),
user,
),
Expand All @@ -185,6 +189,33 @@ export const layer = Layer.effect(
mode: "subagent",
native: true,
},
scout: {
name: "scout",
permission: Permission.merge(
defaults,
Permission.fromConfig({
"*": "deny",
grep: "allow",
glob: "allow",
webfetch: "allow",
websearch: "allow",
codesearch: "allow",
read: "allow",
repo_clone: "allow",
repo_overview: "allow",
external_directory: {
...readonlyExternalDirectory,
[path.join(Global.Path.repos, "*")]: "allow",
},
}),
user,
),
description: `Docs and dependency-source specialist. Use this when you need to inspect external documentation, clone dependency repositories into the managed cache, and research library implementation details without modifying the user's workspace.`,
prompt: PROMPT_SCOUT,
options: {},
mode: "subagent",
native: true,
},
compaction: {
name: "compaction",
mode: "primary",
Expand Down
36 changes: 36 additions & 0 deletions packages/opencode/src/agent/prompt/scout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
You are `scout`, a read-only research agent for external libraries, dependency source, and documentation.

Your purpose is to investigate code outside the local workspace and return evidence-backed findings without modifying the user's workspace.

Use this agent when asked to:
- inspect dependency repositories or library source
- compare local code against upstream implementations
- research public GitHub repositories the environment can clone
- explain how a library or framework works by reading its source and docs
- investigate third-party APIs, workflows, or behavior outside the current workspace

Working style:
1. When the task involves a GitHub repository or dependency source, use `repo_clone` first.
2. After cloning, use `Glob`, `Grep`, and `Read` to inspect the cloned repository.
3. Use `WebFetch` for official documentation pages when source alone is not enough.
4. Prefer direct code and documentation evidence over assumptions.
5. If multiple external repositories are relevant, inspect each one before drawing conclusions.

Research standards:
- cite exact absolute file paths and line references whenever possible
- separate what is verified from what is inferred
- if the answer depends on branch state, note that you are reading the repository's current default clone state unless the caller specifies otherwise
- if a repository cannot be cloned or accessed, say so explicitly and continue with whatever evidence is still available
- call out uncertainty clearly instead of smoothing over gaps

Output expectations:
- start with the direct answer
- then explain the evidence repository by repository or source by source
- include file references when relevant
- keep the explanation organized and easy to scan

Constraints:
- do not modify files or run tools that change the user's workspace
- return absolute file paths for cloned-repo findings in your final response

Complete the user's research request efficiently and report your findings clearly.
14 changes: 2 additions & 12 deletions packages/opencode/src/cli/cmd/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git"
import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util"
import { parseGitHubRemote } from "@/util/repository"
import { Effect } from "effect"

type GitHubAuthor = {
Expand Down Expand Up @@ -152,18 +153,7 @@ const SUPPORTED_EVENTS = [...USER_EVENTS, ...REPO_EVENTS] as const
type UserEvent = (typeof USER_EVENTS)[number]
type RepoEvent = (typeof REPO_EVENTS)[number]

// Parses GitHub remote URLs in various formats:
// - https://github.com/owner/repo.git
// - https://github.com/owner/repo
// - git@github.com:owner/repo.git
// - git@github.com:owner/repo
// - ssh://git@github.com/owner/repo.git
// - ssh://git@github.com/owner/repo
export function parseGitHubRemote(url: string): { owner: string; repo: string } | null {
const match = url.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/)
if (!match) return null
return { owner: match[1], repo: match[2] }
}
export { parseGitHubRemote }

/**
* Extracts displayable text from assistant response parts.
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export const Info = Schema.Struct({
// subagent
general: Schema.optional(AgentRef),
explore: Schema.optional(AgentRef),
scout: Schema.optional(AgentRef),
// specialized
title: Schema.optional(AgentRef),
summary: Schema.optional(AgentRef),
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/config/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const InputObject = Schema.StructWithRest(
webfetch: Schema.optional(Action),
websearch: Schema.optional(Action),
codesearch: Schema.optional(Action),
repo_clone: Schema.optional(Rule),
repo_overview: Schema.optional(Rule),
lsp: Schema.optional(Rule),
doom_loop: Schema.optional(Action),
skill: Schema.optional(Rule),
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/global/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const Path = {
data,
bin: path.join(cache, "bin"),
log: path.join(data, "log"),
repos: path.join(data, "repos"),
cache,
config,
state,
Expand All @@ -34,6 +35,7 @@ await Promise.all([
fs.mkdir(Path.state, { recursive: true }),
fs.mkdir(Path.log, { recursive: true }),
fs.mkdir(Path.bin, { recursive: true }),
fs.mkdir(Path.repos, { recursive: true }),
])

const CACHE_VERSION = "21"
Expand Down
11 changes: 11 additions & 0 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { Provider } from "../provider"
import { ProviderID, type ModelID } from "../provider/schema"
import { WebSearchTool } from "./websearch"
import { CodeSearchTool } from "./codesearch"
import { RepoCloneTool } from "./repo_clone"
import { RepoOverviewTool } from "./repo_overview"
import { Flag } from "@/flag/flag"
import { Log } from "@/util"
import { LspTool } from "./lsp"
Expand All @@ -45,6 +47,7 @@ import { Instruction } from "../session/instruction"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Bus } from "../bus"
import { Agent } from "../agent/agent"
import { Git } from "@/git"
import { Skill } from "../skill"
import { Permission } from "@/permission"

Expand Down Expand Up @@ -80,6 +83,7 @@ export const layer: Layer.Layer<
| Skill.Service
| Session.Service
| Provider.Service
| Git.Service
| LSP.Service
| Instruction.Service
| AppFileSystem.Service
Expand Down Expand Up @@ -109,6 +113,8 @@ export const layer: Layer.Layer<
const websearch = yield* WebSearchTool
const bash = yield* BashTool
const codesearch = yield* CodeSearchTool
const repoClone = yield* RepoCloneTool
const repoOverview = yield* RepoOverviewTool
const globtool = yield* GlobTool
const writetool = yield* WriteTool
const edit = yield* EditTool
Expand Down Expand Up @@ -199,6 +205,8 @@ export const layer: Layer.Layer<
todo: Tool.init(todo),
search: Tool.init(websearch),
code: Tool.init(codesearch),
repo_clone: Tool.init(repoClone),
repo_overview: Tool.init(repoOverview),
skill: Tool.init(skilltool),
patch: Tool.init(patchtool),
question: Tool.init(question),
Expand All @@ -222,6 +230,8 @@ export const layer: Layer.Layer<
tool.todo,
tool.search,
tool.code,
tool.repo_clone,
tool.repo_overview,
tool.skill,
tool.patch,
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []),
Expand Down Expand Up @@ -336,6 +346,7 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Agent.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(Instruction.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Expand Down
Loading
Loading