refactor(flags): route scout through runtime flags (#27318)

This commit is contained in:
Shoubhit Dash
2026-05-13 17:07:53 +05:30
committed by GitHub
parent 5975547c84
commit 4d205027ca
6 changed files with 227 additions and 234 deletions
+4 -2
View File
@@ -15,12 +15,12 @@ import PROMPT_TITLE from "./prompt/title.txt"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { mergeDeep, pipe, sortBy, values } from "remeda" import { mergeDeep, pipe, sortBy, values } from "remeda"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Flag } from "@opencode-ai/core/flag/flag"
import path from "path" import path from "path"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Effect, Context, Layer, Schema } from "effect" import { Effect, Context, Layer, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import * as Option from "effect/Option" import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer" import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { type DeepMutable } from "@opencode-ai/core/schema" import { type DeepMutable } from "@opencode-ai/core/schema"
@@ -81,6 +81,7 @@ export const layer = Layer.effect(
const plugin = yield* Plugin.Service const plugin = yield* Plugin.Service
const skill = yield* Skill.Service const skill = yield* Skill.Service
const provider = yield* Provider.Service const provider = yield* Provider.Service
const flags = yield* RuntimeFlags.Service
const state = yield* InstanceState.make<State>( const state = yield* InstanceState.make<State>(
Effect.fn("Agent.state")(function* (ctx) { Effect.fn("Agent.state")(function* (ctx) {
@@ -195,7 +196,7 @@ export const layer = Layer.effect(
mode: "subagent", mode: "subagent",
native: true, native: true,
}, },
...(Flag.OPENCODE_EXPERIMENTAL_SCOUT ...(flags.experimentalScout
? { ? {
scout: { scout: {
name: "scout", name: "scout",
@@ -453,6 +454,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(Auth.defaultLayer), Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer), Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer), Layer.provide(Skill.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
) )
export * as Agent from "./agent" export * as Agent from "./agent"
+7 -5
View File
@@ -1,10 +1,10 @@
import path from "path" import path from "path"
import { Effect, Context, Layer, Scope } from "effect" import { Effect, Context, Layer, Scope } from "effect"
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 { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Git } from "@/git" import { Git } from "@/git"
import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository" import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
import { RepositoryCache } from "./repository-cache" import { RepositoryCache } from "./repository-cache"
@@ -143,6 +143,7 @@ export const layer = Layer.effect(
const fs = yield* AppFileSystem.Service const fs = yield* AppFileSystem.Service
const git = yield* Git.Service const git = yield* Git.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const flags = yield* RuntimeFlags.Service
const state = yield* InstanceState.make<State>( const state = yield* InstanceState.make<State>(
Effect.fn("Reference.state")(function* (ctx) { Effect.fn("Reference.state")(function* (ctx) {
@@ -181,7 +182,7 @@ export const layer = Layer.effect(
) )
const materializeAll = yield* Effect.cached( const materializeAll = yield* Effect.cached(
Flag.OPENCODE_EXPERIMENTAL_SCOUT flags.experimentalScout
? Effect.gen(function* () { ? Effect.gen(function* () {
yield* Effect.forEach( yield* Effect.forEach(
materializeByPath, materializeByPath,
@@ -200,7 +201,7 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
init: Effect.fn("Reference.init")(function* () { init: Effect.fn("Reference.init")(function* () {
if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return if (!flags.experimentalScout) return
yield* InstanceState.useEffect(state, (s) => s.materializeAll).pipe(Effect.forkIn(scope), Effect.asVoid) yield* InstanceState.useEffect(state, (s) => s.materializeAll).pipe(Effect.forkIn(scope), Effect.asVoid)
}), }),
list: Effect.fn("Reference.list")(function* () { list: Effect.fn("Reference.list")(function* () {
@@ -210,7 +211,7 @@ export const layer = Layer.effect(
return yield* InstanceState.use(state, (s) => s.references.find((reference) => reference.name === name)) return yield* InstanceState.use(state, (s) => s.references.find((reference) => reference.name === name))
}), }),
ensure: Effect.fn("Reference.ensure")(function* (target?: string) { ensure: Effect.fn("Reference.ensure")(function* (target?: string) {
if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return if (!flags.experimentalScout) return
const full = normalizedTarget(target) const full = normalizedTarget(target)
if (!full) return yield* InstanceState.useEffect(state, (s) => s.materializeAll) if (!full) return yield* InstanceState.useEffect(state, (s) => s.materializeAll)
return yield* InstanceState.useEffect( return yield* InstanceState.useEffect(
@@ -219,7 +220,7 @@ export const layer = Layer.effect(
) )
}), }),
contains: Effect.fn("Reference.contains")(function* (target?: string) { contains: Effect.fn("Reference.contains")(function* (target?: string) {
if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return false if (!flags.experimentalScout) return false
const full = normalizedTarget(target) const full = normalizedTarget(target)
if (!full) return false if (!full) return false
return yield* InstanceState.use(state, (s) => return yield* InstanceState.use(state, (s) =>
@@ -234,6 +235,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer), Layer.provide(Config.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Git.defaultLayer), Layer.provide(Git.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
) )
export * as Reference from "./reference" export * as Reference from "./reference"
+57 -65
View File
@@ -1,15 +1,31 @@
import { afterEach, expect } from "bun:test" import { afterEach, expect } from "bun:test"
import { Cause, Effect, Exit } from "effect" import { Cause, Effect, Exit, Layer } from "effect"
import path from "path" import path from "path"
import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Permission } from "../../src/permission" import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
import { Skill } from "../../src/skill"
import { Truncate } from "../../src/tool/truncate" import { Truncate } from "../../src/tool/truncate"
const it = testEffect(Agent.defaultLayer) const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Agent.layer.pipe(
Layer.provide(Plugin.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
)
const it = testEffect(agentLayer())
const scout = testEffect(agentLayer({ experimentalScout: true }))
// Helper to evaluate permission for a tool with wildcard pattern // Helper to evaluate permission for a tool with wildcard pattern
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined { function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
@@ -21,21 +37,6 @@ function load<A>(fn: (svc: Agent.Interface) => Effect.Effect<A>) {
return Agent.Service.use(fn) return Agent.Service.use(fn)
} }
function withExperimentalScout<A, E, R>(enabled: boolean, self: Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => {
const original = Flag.OPENCODE_EXPERIMENTAL_SCOUT
Flag.OPENCODE_EXPERIMENTAL_SCOUT = enabled
return original
}),
() => self,
(original) =>
Effect.sync(() => {
Flag.OPENCODE_EXPERIMENTAL_SCOUT = original
}),
)
}
const expectDefaultAgentError = Effect.fn("AgentTest.expectDefaultAgentError")(function* (message: string) { const expectDefaultAgentError = Effect.fn("AgentTest.expectDefaultAgentError")(function* (message: string) {
const exit = yield* load((svc) => svc.defaultAgent()).pipe(Effect.exit) const exit = yield* load((svc) => svc.defaultAgent()).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true) expect(Exit.isFailure(exit)).toBe(true)
@@ -47,21 +48,18 @@ afterEach(async () => {
}) })
it.instance("returns default native agents when no config", () => it.instance("returns default native agents when no config", () =>
withExperimentalScout( Effect.gen(function* () {
false, const agents = yield* load((svc) => svc.list())
Effect.gen(function* () { const names = agents.map((a) => a.name)
const agents = yield* load((svc) => svc.list()) expect(names).toContain("build")
const names = agents.map((a) => a.name) expect(names).toContain("plan")
expect(names).toContain("build") expect(names).toContain("general")
expect(names).toContain("plan") expect(names).toContain("explore")
expect(names).toContain("general") expect(names).not.toContain("scout")
expect(names).toContain("explore") expect(names).toContain("compaction")
expect(names).not.toContain("scout") expect(names).toContain("title")
expect(names).toContain("compaction") expect(names).toContain("summary")
expect(names).toContain("title") }),
expect(names).toContain("summary")
}),
),
) )
it.instance("build agent has correct default properties", () => it.instance("build agent has correct default properties", () =>
@@ -111,42 +109,36 @@ it.instance("explore agent asks for external directories and allows whitelisted
}), }),
) )
it.instance("scout agent allows repo cloning and repo cache reads", () => scout.instance("scout agent allows repo cloning and repo cache reads", () =>
withExperimentalScout( Effect.gen(function* () {
true, const scout = yield* load((svc) => svc.get("scout"))
Effect.gen(function* () { expect(scout).toBeDefined()
const scout = yield* load((svc) => svc.get("scout")) expect(scout?.mode).toBe("subagent")
expect(scout).toBeDefined() expect(evalPerm(scout, "repo_clone")).toBe("allow")
expect(scout?.mode).toBe("subagent") expect(evalPerm(scout, "repo_overview")).toBe("allow")
expect(evalPerm(scout, "repo_clone")).toBe("allow") expect(evalPerm(scout, "edit")).toBe("deny")
expect(evalPerm(scout, "repo_overview")).toBe("allow") expect(
expect(evalPerm(scout, "edit")).toBe("deny") Permission.evaluate(
expect( "external_directory",
Permission.evaluate( path.join(Global.Path.repos, "github.com", "owner", "repo", "README.md"),
"external_directory", scout!.permission,
path.join(Global.Path.repos, "github.com", "owner", "repo", "README.md"), ).action,
scout!.permission, ).toBe("allow")
).action, }),
).toBe("allow")
}),
),
) )
it.instance( scout.instance(
"reference config does not create subagents", "reference config does not create subagents",
() => () =>
withExperimentalScout( Effect.gen(function* () {
true, const agents = yield* load((svc) => svc.list())
Effect.gen(function* () { const names = agents.map((agent) => agent.name)
const agents = yield* load((svc) => svc.list()) expect(names).toContain("scout")
const names = agents.map((agent) => agent.name) expect(names).not.toContain("effect")
expect(names).toContain("scout") expect(names).not.toContain("effectFull")
expect(names).not.toContain("effect") expect(names).not.toContain("localdocs")
expect(names).not.toContain("effectFull") expect(names).not.toContain("localdocsFull")
expect(names).not.toContain("localdocs") }),
expect(names).not.toContain("localdocsFull")
}),
),
{ {
config: { config: {
reference: { reference: {
@@ -41,6 +41,7 @@ const agentLayer = Agent.layer.pipe(
Layer.provide(SkillTest.empty), Layer.provide(SkillTest.empty),
Layer.provide(provider.layer), Layer.provide(provider.layer),
Layer.provide(pluginLayer), Layer.provide(pluginLayer),
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
) )
const it = testEffect(Layer.mergeAll(agentLayer, pluginLayer)) const it = testEffect(Layer.mergeAll(agentLayer, pluginLayer))
+110 -111
View File
@@ -3,8 +3,9 @@ import path from "path"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem" 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 { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Git } from "../../src/git" import { Git } from "../../src/git"
import { Reference } from "../../src/reference/reference" import { Reference } from "../../src/reference/reference"
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
@@ -14,24 +15,26 @@ afterEach(async () => {
await disposeAllInstances() await disposeAllInstances()
}) })
const it = testEffect( const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll(AppFileSystem.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer, Reference.defaultLayer), Reference.layer.pipe(
) Layer.provide(Config.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) => Layer.provide(Git.defaultLayer),
Effect.acquireUseRelease( Layer.provide(RuntimeFlags.layer(flags)),
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 it = testEffect(
Layer.mergeAll(AppFileSystem.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer, referenceLayer()),
)
const scout = testEffect(
Layer.mergeAll(
AppFileSystem.defaultLayer,
CrossSpawnSpawner.defaultLayer,
Git.defaultLayer,
referenceLayer({ experimentalScout: true }),
),
)
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) => const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
Effect.acquireUseRelease( Effect.acquireUseRelease(
Effect.sync(() => { Effect.sync(() => {
@@ -127,118 +130,114 @@ describe("reference", () => {
}), }),
) )
it.live("materializes configured git references during init", () => scout.live("materializes configured git references during init", () =>
experimentalScout( provideTmpdirInstance(
provideTmpdirInstance( (_dir) =>
(_dir) => Effect.gen(function* () {
Effect.gen(function* () { const fs = yield* AppFileSystem.Service
const fs = yield* AppFileSystem.Service const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-test", "repo")
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-test", "repo") yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore) yield* Effect.addFinalizer(() => 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 source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped() const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "opencode-reference-test") const remoteDir = path.join(remoteRoot, "opencode-reference-test")
const remoteRepo = path.join(remoteDir, "repo.git") const remoteRepo = path.join(remoteDir, "repo.git")
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "configured\n")) yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "configured\n"))
yield* git(source, ["add", "."]) yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"]) yield* git(source, ["commit", "-m", "add readme"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie) yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo]) yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const reference = yield* Reference.Service const reference = yield* Reference.Service
yield* githubBase( yield* githubBase(
`file://${remoteRoot}/`, `file://${remoteRoot}/`,
Effect.gen(function* () { Effect.gen(function* () {
yield* reference.init() yield* reference.init()
yield* waitForContent(fs, path.join(cache, "README.md"), "configured\n") yield* waitForContent(fs, path.join(cache, "README.md"), "configured\n")
}), }),
) )
expect(yield* fs.existsSafe(path.join(cache, ".git"))).toBe(true) expect(yield* fs.existsSafe(path.join(cache, ".git"))).toBe(true)
expect(yield* fs.readFileString(path.join(cache, "README.md"))).toBe("configured\n") expect(yield* fs.readFileString(path.join(cache, "README.md"))).toBe("configured\n")
const resolved = yield* reference.get("docs") const resolved = yield* reference.get("docs")
expect(resolved?.kind).toBe("git") expect(resolved?.kind).toBe("git")
if (resolved?.kind === "git") expect(resolved.path).toBe(cache) if (resolved?.kind === "git") expect(resolved.path).toBe(cache)
}), }),
{ {
config: { config: {
reference: { reference: {
docs: "opencode-reference-test/repo", docs: "opencode-reference-test/repo",
},
}, },
}, },
), },
), ),
) )
it.live("refreshes configured git references on new instance init", () => scout.live("refreshes configured git references on new instance init", () =>
experimentalScout( Effect.gen(function* () {
Effect.gen(function* () { const fs = yield* AppFileSystem.Service
const fs = yield* AppFileSystem.Service const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-refresh", "repo")
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-refresh", "repo") yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore) yield* Effect.addFinalizer(() => 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 source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped() const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "opencode-reference-refresh") const remoteDir = path.join(remoteRoot, "opencode-reference-refresh")
const remoteRepo = path.join(remoteDir, "repo.git") const remoteRepo = path.join(remoteDir, "repo.git")
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n")) yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
yield* git(source, ["add", "."]) yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"]) yield* git(source, ["commit", "-m", "add readme"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie) yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo]) yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
yield* githubBase( yield* githubBase(
`file://${remoteRoot}/`, `file://${remoteRoot}/`,
provideTmpdirInstance( provideTmpdirInstance(
(_dir) => (_dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const reference = yield* Reference.Service const reference = yield* Reference.Service
yield* reference.init() yield* reference.init()
yield* waitForContent(fs, path.join(cache, "README.md"), "v1\n") yield* waitForContent(fs, path.join(cache, "README.md"), "v1\n")
}), }),
{ {
config: { config: {
reference: { reference: {
docs: "opencode-reference-refresh/repo", docs: "opencode-reference-refresh/repo",
},
}, },
}, },
), },
) ),
)
const branch = yield* git(source, ["branch", "--show-current"]) const branch = yield* git(source, ["branch", "--show-current"])
yield* git(source, ["remote", "add", "origin", remoteRepo]) yield* git(source, ["remote", "add", "origin", remoteRepo])
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n")) yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
yield* git(source, ["add", "."]) yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "update readme"]) yield* git(source, ["commit", "-m", "update readme"])
yield* git(source, ["push", "origin", `${branch}:${branch}`]) yield* git(source, ["push", "origin", `${branch}:${branch}`])
yield* githubBase( yield* githubBase(
`file://${remoteRoot}/`, `file://${remoteRoot}/`,
provideTmpdirInstance( provideTmpdirInstance(
(_dir) => (_dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const reference = yield* Reference.Service const reference = yield* Reference.Service
yield* reference.init() yield* reference.init()
yield* waitForContent(fs, path.join(cache, "README.md"), "v2\n") yield* waitForContent(fs, path.join(cache, "README.md"), "v2\n")
}), }),
{ {
config: { config: {
reference: { reference: {
docs: "opencode-reference-refresh/repo", docs: "opencode-reference-refresh/repo",
},
}, },
}, },
), },
) ),
}), )
), }),
) )
}) })
+48 -51
View File
@@ -4,8 +4,10 @@ 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 { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Git } from "@/git"
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"
@@ -36,17 +38,27 @@ const ctx = {
ask: () => Effect.void, ask: () => Effect.void,
} }
const it = testEffect( const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Reference.layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
)
const readLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll( Layer.mergeAll(
Agent.defaultLayer, Agent.defaultLayer,
AppFileSystem.defaultLayer, AppFileSystem.defaultLayer,
CrossSpawnSpawner.defaultLayer, CrossSpawnSpawner.defaultLayer,
Instruction.defaultLayer, Instruction.defaultLayer,
LSP.defaultLayer, LSP.defaultLayer,
Reference.defaultLayer, referenceLayer(flags),
Truncate.defaultLayer, Truncate.defaultLayer,
), )
)
const it = testEffect(readLayer())
const scout = testEffect(readLayer({ experimentalScout: true }))
const init = Effect.fn("ReadToolTest.init")(function* () { const init = Effect.fn("ReadToolTest.init")(function* () {
const info = yield* ReadTool const info = yield* ReadTool
@@ -85,19 +97,6 @@ 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>) => const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
Effect.acquireUseRelease( Effect.acquireUseRelease(
Effect.sync(() => { Effect.sync(() => {
@@ -260,44 +259,42 @@ describe("tool.read external_directory permission", () => {
}), }),
) )
it.live("does not ask for external_directory permission when reading configured references", () => scout.live("does not ask for external_directory permission when reading configured references", () =>
experimentalScout( Effect.gen(function* () {
Effect.gen(function* () { const fs = yield* AppFileSystem.Service
const fs = yield* AppFileSystem.Service const cache = path.join(Global.Path.repos, "github.com", "opencode-read-reference", "repo")
const cache = path.join(Global.Path.repos, "github.com", "opencode-read-reference", "repo") yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore) yield* Effect.addFinalizer(() => 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 source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped() const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "opencode-read-reference") const remoteDir = path.join(remoteRoot, "opencode-read-reference")
const remoteRepo = path.join(remoteDir, "repo.git") const remoteRepo = path.join(remoteDir, "repo.git")
yield* put(path.join(source, "notes.md"), "reference notes") yield* put(path.join(source, "notes.md"), "reference notes")
yield* git(source, ["add", "."]) yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add notes"]) yield* git(source, ["commit", "-m", "add notes"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie) yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo]) yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const dir = yield* tmpdirScoped({ const dir = yield* tmpdirScoped({
git: true, git: true,
config: { config: {
reference: { reference: {
docs: "opencode-read-reference/repo", docs: "opencode-read-reference/repo",
},
}, },
}) },
})
const { items, next } = asks() const { items, next } = asks()
const result = yield* githubBase( const result = yield* githubBase(
`file://${remoteRoot}/`, `file://${remoteRoot}/`,
exec(dir, { filePath: path.join(cache, "notes.md") }, next), exec(dir, { filePath: path.join(cache, "notes.md") }, next),
) )
const ext = items.find((item) => item.permission === "external_directory") const ext = items.find((item) => item.permission === "external_directory")
expect(result.output).toContain("reference notes") expect(result.output).toContain("reference notes")
expect(ext).toBeUndefined() expect(ext).toBeUndefined()
}), }),
),
) )
}) })