浏览代码

Merge origin/dev into fix/lsp-dead-root-prune

Kit Langton 4 天之前
父节点
当前提交
8de3010c90
共有 42 个文件被更改,包括 1956 次插入1674 次删除
  1. 8 1
      packages/opencode/script/seed-e2e.ts
  2. 0 19
      packages/opencode/src/auth/index.ts
  3. 20 6
      packages/opencode/src/cli/cmd/debug/agent.ts
  4. 14 5
      packages/opencode/src/cli/cmd/debug/lsp.ts
  5. 8 1
      packages/opencode/src/cli/cmd/debug/skill.ts
  6. 41 31
      packages/opencode/src/cli/cmd/models.ts
  7. 38 12
      packages/opencode/src/cli/cmd/providers.ts
  8. 47 12
      packages/opencode/src/file/ripgrep.ts
  9. 0 32
      packages/opencode/src/lsp/index.ts
  10. 0 19
      packages/opencode/src/project/vcs.ts
  11. 0 31
      packages/opencode/src/provider/provider.ts
  12. 0 31
      packages/opencode/src/pty/index.ts
  13. 14 2
      packages/opencode/src/server/control/index.ts
  14. 8 1
      packages/opencode/src/server/instance/config.ts
  15. 18 6
      packages/opencode/src/server/instance/experimental.ts
  16. 0 5
      packages/opencode/src/server/instance/file.ts
  17. 28 8
      packages/opencode/src/server/instance/index.ts
  18. 28 19
      packages/opencode/src/server/instance/provider.ts
  19. 56 8
      packages/opencode/src/server/instance/pty.ts
  20. 18 8
      packages/opencode/src/session/llm.ts
  21. 0 19
      packages/opencode/src/skill/index.ts
  22. 0 15
      packages/opencode/src/tool/registry.ts
  23. 2 2
      packages/opencode/src/worktree/index.ts
  24. 80 52
      packages/opencode/test/auth/auth.test.ts
  25. 2 4
      packages/opencode/test/bus/bus-effect.test.ts
  26. 47 47
      packages/opencode/test/lsp/index.test.ts
  27. 97 92
      packages/opencode/test/lsp/lifecycle.test.ts
  28. 69 12
      packages/opencode/test/project/vcs.test.ts
  29. 21 10
      packages/opencode/test/provider/amazon-bedrock.test.ts
  30. 18 18
      packages/opencode/test/provider/gitlab-duo.test.ts
  31. 117 78
      packages/opencode/test/provider/provider.test.ts
  32. 115 110
      packages/opencode/test/pty/pty-output-isolation.test.ts
  33. 54 44
      packages/opencode/test/pty/pty-session.test.ts
  34. 26 16
      packages/opencode/test/pty/pty-shell.test.ts
  35. 53 0
      packages/opencode/test/session/instruction.test.ts
  36. 19 9
      packages/opencode/test/session/llm.test.ts
  37. 1 1
      packages/opencode/test/session/prompt-effect.test.ts
  38. 271 272
      packages/opencode/test/skill/skill.test.ts
  39. 128 133
      packages/opencode/test/tool/registry.test.ts
  40. 77 70
      packages/opencode/test/tool/skill.test.ts
  41. 119 119
      packages/sdk/js/src/v2/gen/types.gen.ts
  42. 294 294
      packages/sdk/openapi.json

+ 8 - 1
packages/opencode/script/seed-e2e.ts

@@ -18,6 +18,7 @@ const seed = async () => {
   const { Project } = await import("../src/project/project")
   const { ModelID, ProviderID } = await import("../src/provider/schema")
   const { ToolRegistry } = await import("../src/tool/registry")
+  const { Effect } = await import("effect")
 
   try {
     await Instance.provide({
@@ -25,7 +26,12 @@ const seed = async () => {
       init: () => AppRuntime.runPromise(InstanceBootstrap),
       fn: async () => {
         await Config.waitForDependencies()
-        await ToolRegistry.ids()
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const registry = yield* ToolRegistry.Service
+            yield* registry.ids()
+          }),
+        )
 
         const session = await Session.create({ title })
         const messageID = MessageID.ascending()
@@ -56,6 +62,7 @@ const seed = async () => {
     })
   } finally {
     await Instance.disposeAll().catch(() => {})
+    await AppRuntime.dispose().catch(() => {})
   }
 }
 

+ 0 - 19
packages/opencode/src/auth/index.ts

@@ -1,6 +1,5 @@
 import path from "path"
 import { Effect, Layer, Record, Result, Schema, Context } from "effect"
-import { makeRuntime } from "@/effect/run-service"
 import { zod } from "@/util/effect-zod"
 import { Global } from "../global"
 import { AppFileSystem } from "../filesystem"
@@ -89,22 +88,4 @@ export namespace Auth {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function get(providerID: string) {
-    return runPromise((service) => service.get(providerID))
-  }
-
-  export async function all(): Promise<Record<string, Info>> {
-    return runPromise((service) => service.all())
-  }
-
-  export async function set(key: string, info: Info) {
-    return runPromise((service) => service.set(key, info))
-  }
-
-  export async function remove(key: string) {
-    return runPromise((service) => service.remove(key))
-  }
 }

+ 20 - 6
packages/opencode/src/cli/cmd/debug/agent.ts

@@ -12,6 +12,7 @@ import { Permission } from "../../../permission"
 import { iife } from "../../../util/iife"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"
+import { AppRuntime } from "@/effect/app-runtime"
 
 export const AgentCommand = cmd({
   command: "agent <name>",
@@ -71,11 +72,17 @@ export const AgentCommand = cmd({
 })
 
 async function getAvailableTools(agent: Agent.Info) {
-  const model = agent.model ?? (await Provider.defaultModel())
-  return ToolRegistry.tools({
-    ...model,
-    agent,
-  })
+  return AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const provider = yield* Provider.Service
+      const registry = yield* ToolRegistry.Service
+      const model = agent.model ?? (yield* provider.defaultModel())
+      return yield* registry.tools({
+        ...model,
+        agent,
+      })
+    }),
+  )
 }
 
 async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
@@ -118,7 +125,14 @@ function parseToolParams(input?: string) {
 async function createToolContext(agent: Agent.Info) {
   const session = await Session.create({ title: `Debug tool run (${agent.name})` })
   const messageID = MessageID.ascending()
-  const model = agent.model ?? (await Provider.defaultModel())
+  const model =
+    agent.model ??
+    (await AppRuntime.runPromise(
+      Effect.gen(function* () {
+        const provider = yield* Provider.Service
+        return yield* provider.defaultModel()
+      }),
+    ))
   const now = Date.now()
   const message: MessageV2.Assistant = {
     id: messageID,

+ 14 - 5
packages/opencode/src/cli/cmd/debug/lsp.ts

@@ -1,4 +1,6 @@
 import { LSP } from "../../../lsp"
+import { AppRuntime } from "../../../effect/app-runtime"
+import { Effect } from "effect"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"
 import { Log } from "../../../util/log"
@@ -19,9 +21,16 @@ const DiagnosticsCommand = cmd({
   builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
-      await LSP.touchFile(args.file, true)
-      await sleep(1000)
-      process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
+      const out = await AppRuntime.runPromise(
+        LSP.Service.use((lsp) =>
+          Effect.gen(function* () {
+            yield* lsp.touchFile(args.file, true)
+            yield* Effect.sleep(1000)
+            return yield* lsp.diagnostics()
+          }),
+        ),
+      )
+      process.stdout.write(JSON.stringify(out, null, 2) + EOL)
     })
   },
 })
@@ -33,7 +42,7 @@ export const SymbolsCommand = cmd({
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
       using _ = Log.Default.time("symbols")
-      const results = await LSP.workspaceSymbol(args.query)
+      const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
       process.stdout.write(JSON.stringify(results, null, 2) + EOL)
     })
   },
@@ -46,7 +55,7 @@ export const DocumentSymbolsCommand = cmd({
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
       using _ = Log.Default.time("document-symbols")
-      const results = await LSP.documentSymbol(args.uri)
+      const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
       process.stdout.write(JSON.stringify(results, null, 2) + EOL)
     })
   },

+ 8 - 1
packages/opencode/src/cli/cmd/debug/skill.ts

@@ -1,4 +1,6 @@
 import { EOL } from "os"
+import { Effect } from "effect"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Skill } from "../../../skill"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"
@@ -9,7 +11,12 @@ export const SkillCommand = cmd({
   builder: (yargs) => yargs,
   async handler() {
     await bootstrap(process.cwd(), async () => {
-      const skills = await Skill.all()
+      const skills = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const skill = yield* Skill.Service
+          return yield* skill.all()
+        }),
+      )
       process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
     })
   },

+ 41 - 31
packages/opencode/src/cli/cmd/models.ts

@@ -6,6 +6,8 @@ import { ModelsDev } from "../../provider/models"
 import { cmd } from "./cmd"
 import { UI } from "../ui"
 import { EOL } from "os"
+import { AppRuntime } from "@/effect/app-runtime"
+import { Effect } from "effect"
 
 export const ModelsCommand = cmd({
   command: "models [provider]",
@@ -35,43 +37,51 @@ export const ModelsCommand = cmd({
     await Instance.provide({
       directory: process.cwd(),
       async fn() {
-        const providers = await Provider.list()
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const svc = yield* Provider.Service
+            const providers = yield* svc.list()
 
-        function printModels(providerID: ProviderID, verbose?: boolean) {
-          const provider = providers[providerID]
-          const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
-          for (const [modelID, model] of sortedModels) {
-            process.stdout.write(`${providerID}/${modelID}`)
-            process.stdout.write(EOL)
-            if (verbose) {
-              process.stdout.write(JSON.stringify(model, null, 2))
-              process.stdout.write(EOL)
+            const print = (providerID: ProviderID, verbose?: boolean) => {
+              const provider = providers[providerID]
+              const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
+              for (const [modelID, model] of sorted) {
+                process.stdout.write(`${providerID}/${modelID}`)
+                process.stdout.write(EOL)
+                if (verbose) {
+                  process.stdout.write(JSON.stringify(model, null, 2))
+                  process.stdout.write(EOL)
+                }
+              }
             }
-          }
-        }
 
-        if (args.provider) {
-          const provider = providers[ProviderID.make(args.provider)]
-          if (!provider) {
-            UI.error(`Provider not found: ${args.provider}`)
-            return
-          }
+            if (args.provider) {
+              const providerID = ProviderID.make(args.provider)
+              const provider = providers[providerID]
+              if (!provider) {
+                yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
+                return
+              }
 
-          printModels(ProviderID.make(args.provider), args.verbose)
-          return
-        }
+              yield* Effect.sync(() => print(providerID, args.verbose))
+              return
+            }
 
-        const providerIDs = Object.keys(providers).sort((a, b) => {
-          const aIsOpencode = a.startsWith("opencode")
-          const bIsOpencode = b.startsWith("opencode")
-          if (aIsOpencode && !bIsOpencode) return -1
-          if (!aIsOpencode && bIsOpencode) return 1
-          return a.localeCompare(b)
-        })
+            const ids = Object.keys(providers).sort((a, b) => {
+              const aIsOpencode = a.startsWith("opencode")
+              const bIsOpencode = b.startsWith("opencode")
+              if (aIsOpencode && !bIsOpencode) return -1
+              if (!aIsOpencode && bIsOpencode) return 1
+              return a.localeCompare(b)
+            })
 
-        for (const providerID of providerIDs) {
-          printModels(ProviderID.make(providerID), args.verbose)
-        }
+            yield* Effect.sync(() => {
+              for (const providerID of ids) {
+                print(ProviderID.make(providerID), args.verbose)
+              }
+            })
+          }),
+        )
       },
     })
   },

+ 38 - 12
packages/opencode/src/cli/cmd/providers.ts

@@ -1,4 +1,5 @@
 import { Auth } from "../../auth"
+import { AppRuntime } from "../../effect/app-runtime"
 import { cmd } from "./cmd"
 import * as prompts from "@clack/prompts"
 import { UI } from "../ui"
@@ -13,9 +14,18 @@ import { Instance } from "../../project/instance"
 import type { Hooks } from "@opencode-ai/plugin"
 import { Process } from "../../util/process"
 import { text } from "node:stream/consumers"
+import { Effect } from "effect"
 
 type PluginAuth = NonNullable<Hooks["auth"]>
 
+const put = (key: string, info: Auth.Info) =>
+  AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const auth = yield* Auth.Service
+      yield* auth.set(key, info)
+    }),
+  )
+
 async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
   let index = 0
   if (methodName) {
@@ -93,7 +103,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
         const saveProvider = result.provider ?? provider
         if ("refresh" in result) {
           const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
-          await Auth.set(saveProvider, {
+          await put(saveProvider, {
             type: "oauth",
             refresh,
             access,
@@ -102,7 +112,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
           })
         }
         if ("key" in result) {
-          await Auth.set(saveProvider, {
+          await put(saveProvider, {
             type: "api",
             key: result.key,
           })
@@ -125,7 +135,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
         const saveProvider = result.provider ?? provider
         if ("refresh" in result) {
           const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
-          await Auth.set(saveProvider, {
+          await put(saveProvider, {
             type: "oauth",
             refresh,
             access,
@@ -134,7 +144,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
           })
         }
         if ("key" in result) {
-          await Auth.set(saveProvider, {
+          await put(saveProvider, {
             type: "api",
             key: result.key,
           })
@@ -161,7 +171,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
       }
       if (result.type === "success") {
         const saveProvider = result.provider ?? provider
-        await Auth.set(saveProvider, {
+        await put(saveProvider, {
           type: "api",
           key: result.key ?? key,
         })
@@ -221,7 +231,12 @@ export const ProvidersListCommand = cmd({
     const homedir = os.homedir()
     const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
     prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
-    const results = Object.entries(await Auth.all())
+    const results = await AppRuntime.runPromise(
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        return Object.entries(yield* auth.all())
+      }),
+    )
     const database = await ModelsDev.get()
 
     for (const [providerID, result] of results) {
@@ -300,7 +315,7 @@ export const ProvidersLoginCommand = cmd({
             prompts.outro("Done")
             return
           }
-          await Auth.set(url, {
+          await put(url, {
             type: "wellknown",
             key: wellknown.auth.env,
             token: token.trim(),
@@ -447,7 +462,7 @@ export const ProvidersLoginCommand = cmd({
           validate: (x) => (x && x.length > 0 ? undefined : "Required"),
         })
         if (prompts.isCancel(key)) throw new UI.CancelledError()
-        await Auth.set(provider, {
+        await put(provider, {
           type: "api",
           key,
         })
@@ -463,22 +478,33 @@ export const ProvidersLogoutCommand = cmd({
   describe: "log out from a configured provider",
   async handler(_args) {
     UI.empty()
-    const credentials = await Auth.all().then((x) => Object.entries(x))
+    const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        return Object.entries(yield* auth.all())
+      }),
+    )
     prompts.intro("Remove credential")
     if (credentials.length === 0) {
       prompts.log.error("No credentials found")
       return
     }
     const database = await ModelsDev.get()
-    const providerID = await prompts.select({
+    const selected = await prompts.select({
       message: "Select provider",
       options: credentials.map(([key, value]) => ({
         label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
         value: key,
       })),
     })
-    if (prompts.isCancel(providerID)) throw new UI.CancelledError()
-    await Auth.remove(providerID)
+    if (prompts.isCancel(selected)) throw new UI.CancelledError()
+    const providerID = selected as string
+    await AppRuntime.runPromise(
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        yield* auth.remove(providerID)
+      }),
+    )
     prompts.outro("Logout successful")
   },
 })

+ 47 - 12
packages/opencode/src/file/ripgrep.ts

@@ -3,7 +3,7 @@ import path from "path"
 import { Global } from "../global"
 import fs from "fs/promises"
 import z from "zod"
-import { Effect, Layer, Context } from "effect"
+import { Effect, Layer, Context, Schema } from "effect"
 import * as Stream from "effect/Stream"
 import { ChildProcess } from "effect/unstable/process"
 import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
@@ -94,6 +94,40 @@ export namespace Ripgrep {
 
   const Result = z.union([Begin, Match, End, Summary])
 
+  const Hit = Schema.Struct({
+    type: Schema.Literal("match"),
+    data: Schema.Struct({
+      path: Schema.Struct({
+        text: Schema.String,
+      }),
+      lines: Schema.Struct({
+        text: Schema.String,
+      }),
+      line_number: Schema.Number,
+      absolute_offset: Schema.Number,
+      submatches: Schema.mutable(
+        Schema.Array(
+          Schema.Struct({
+            match: Schema.Struct({
+              text: Schema.String,
+            }),
+            start: Schema.Number,
+            end: Schema.Number,
+          }),
+        ),
+      ),
+    }),
+  })
+
+  const Row = Schema.Union([
+    Schema.Struct({ type: Schema.Literal("begin"), data: Schema.Unknown }),
+    Hit,
+    Schema.Struct({ type: Schema.Literal("end"), data: Schema.Unknown }),
+    Schema.Struct({ type: Schema.Literal("summary"), data: Schema.Unknown }),
+  ])
+
+  const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Row))
+
   export type Result = z.infer<typeof Result>
   export type Match = z.infer<typeof Match>
   export type Item = Match["data"]
@@ -389,9 +423,19 @@ export namespace Ripgrep {
               }),
             )
 
-            const [stdout, stderr, code] = yield* Effect.all(
+            const [items, stderr, code] = yield* Effect.all(
               [
-                Stream.mkString(Stream.decodeText(handle.stdout)),
+                Stream.decodeText(handle.stdout).pipe(
+                  Stream.splitLines,
+                  Stream.filter((line) => line.length > 0),
+                  Stream.mapEffect((line) =>
+                    decode(line).pipe(Effect.mapError((cause) => new Error("invalid ripgrep output", { cause }))),
+                  ),
+                  Stream.filter((row): row is Schema.Schema.Type<typeof Hit> => row.type === "match"),
+                  Stream.map((row): Item => row.data),
+                  Stream.runCollect,
+                  Effect.map((chunk) => [...chunk]),
+                ),
                 Stream.mkString(Stream.decodeText(handle.stderr)),
                 handle.exitCode,
               ],
@@ -402,15 +446,6 @@ export namespace Ripgrep {
               return yield* Effect.fail(new Error(`ripgrep failed: ${stderr}`))
             }
 
-            const items = stdout
-              .trim()
-              .split(/\r?\n/)
-              .filter(Boolean)
-              .map((line) => JSON.parse(line))
-              .map((parsed) => Result.parse(parsed))
-              .filter((row): row is Match => row.type === "match")
-              .map((row) => row.data)
-
             return {
               items,
               partial: code === 2,

+ 0 - 32
packages/opencode/src/lsp/index.ts

@@ -14,7 +14,6 @@ import { Process } from "../util/process"
 import { spawn as lspspawn } from "./launch"
 import { Effect, Layer, Context } from "effect"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Filesystem } from "@/util/filesystem"
 
 export namespace LSP {
@@ -594,37 +593,6 @@ export namespace LSP {
 
   export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
 
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export const init = async () => runPromise((svc) => svc.init())
-
-  export const status = async () => runPromise((svc) => svc.status())
-
-  export const hasClients = async (file: string) => runPromise((svc) => svc.hasClients(file))
-
-  export const touchFile = async (input: string, waitForDiagnostics?: boolean) =>
-    runPromise((svc) => svc.touchFile(input, waitForDiagnostics))
-
-  export const diagnostics = async () => runPromise((svc) => svc.diagnostics())
-
-  export const hover = async (input: LocInput) => runPromise((svc) => svc.hover(input))
-
-  export const definition = async (input: LocInput) => runPromise((svc) => svc.definition(input))
-
-  export const references = async (input: LocInput) => runPromise((svc) => svc.references(input))
-
-  export const implementation = async (input: LocInput) => runPromise((svc) => svc.implementation(input))
-
-  export const documentSymbol = async (uri: string) => runPromise((svc) => svc.documentSymbol(uri))
-
-  export const workspaceSymbol = async (query: string) => runPromise((svc) => svc.workspaceSymbol(query))
-
-  export const prepareCallHierarchy = async (input: LocInput) => runPromise((svc) => svc.prepareCallHierarchy(input))
-
-  export const incomingCalls = async (input: LocInput) => runPromise((svc) => svc.incomingCalls(input))
-
-  export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
-
   export namespace Diagnostic {
     const MAX_PER_FILE = 20
 

+ 0 - 19
packages/opencode/src/project/vcs.ts

@@ -4,7 +4,6 @@ import path from "path"
 import { Bus } from "@/bus"
 import { BusEvent } from "@/bus/bus-event"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { AppFileSystem } from "@/filesystem"
 import { FileWatcher } from "@/file/watcher"
 import { Git } from "@/git"
@@ -231,22 +230,4 @@ export namespace Vcs {
     Layer.provide(AppFileSystem.defaultLayer),
     Layer.provide(Bus.layer),
   )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function init() {
-    return runPromise((svc) => svc.init())
-  }
-
-  export async function branch() {
-    return runPromise((svc) => svc.branch())
-  }
-
-  export async function defaultBranch() {
-    return runPromise((svc) => svc.defaultBranch())
-  }
-
-  export async function diff(mode: Mode) {
-    return runPromise((svc) => svc.diff(mode))
-  }
 }

+ 0 - 31
packages/opencode/src/provider/provider.ts

@@ -21,7 +21,6 @@ import path from "path"
 import { Effect, Layer, Context } from "effect"
 import { EffectLogger } from "@/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { AppFileSystem } from "@/filesystem"
 import { isRecord } from "@/util/record"
 
@@ -1693,36 +1692,6 @@ export namespace Provider {
     ),
   )
 
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function list() {
-    return runPromise((svc) => svc.list())
-  }
-
-  export async function getProvider(providerID: ProviderID) {
-    return runPromise((svc) => svc.getProvider(providerID))
-  }
-
-  export async function getModel(providerID: ProviderID, modelID: ModelID) {
-    return runPromise((svc) => svc.getModel(providerID, modelID))
-  }
-
-  export async function getLanguage(model: Model) {
-    return runPromise((svc) => svc.getLanguage(model))
-  }
-
-  export async function closest(providerID: ProviderID, query: string[]) {
-    return runPromise((svc) => svc.closest(providerID, query))
-  }
-
-  export async function getSmallModel(providerID: ProviderID) {
-    return runPromise((svc) => svc.getSmallModel(providerID))
-  }
-
-  export async function defaultModel() {
-    return runPromise((svc) => svc.defaultModel())
-  }
-
   const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
   export function sort<T extends { id: string }>(models: T[]) {
     return sortBy(

+ 0 - 31
packages/opencode/src/pty/index.ts

@@ -1,7 +1,6 @@
 import { BusEvent } from "@/bus/bus-event"
 import { Bus } from "@/bus"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Instance } from "@/project/instance"
 import type { Proc } from "#pty"
 import z from "zod"
@@ -361,34 +360,4 @@ export namespace Pty {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer))
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function list() {
-    return runPromise((svc) => svc.list())
-  }
-
-  export async function get(id: PtyID) {
-    return runPromise((svc) => svc.get(id))
-  }
-
-  export async function write(id: PtyID, data: string) {
-    return runPromise((svc) => svc.write(id, data))
-  }
-
-  export async function connect(id: PtyID, ws: Socket, cursor?: number) {
-    return runPromise((svc) => svc.connect(id, ws, cursor))
-  }
-
-  export async function create(input: CreateInput) {
-    return runPromise((svc) => svc.create(input))
-  }
-
-  export async function update(id: PtyID, input: UpdateInput) {
-    return runPromise((svc) => svc.update(id, input))
-  }
-
-  export async function remove(id: PtyID) {
-    return runPromise((svc) => svc.remove(id))
-  }
 }

+ 14 - 2
packages/opencode/src/server/control/index.ts

@@ -1,5 +1,7 @@
 import { Auth } from "@/auth"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Log } from "@/util/log"
+import { Effect } from "effect"
 import { ProviderID } from "@/provider/schema"
 import { Hono } from "hono"
 import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi"
@@ -39,7 +41,12 @@ export function ControlPlaneRoutes(): Hono {
       async (c) => {
         const providerID = c.req.valid("param").providerID
         const info = c.req.valid("json")
-        await Auth.set(providerID, info)
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const auth = yield* Auth.Service
+            yield* auth.set(providerID, info)
+          }),
+        )
         return c.json(true)
       },
     )
@@ -69,7 +76,12 @@ export function ControlPlaneRoutes(): Hono {
       ),
       async (c) => {
         const providerID = c.req.valid("param").providerID
-        await Auth.remove(providerID)
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const auth = yield* Auth.Service
+            yield* auth.remove(providerID)
+          }),
+        )
         return c.json(true)
       },
     )

+ 8 - 1
packages/opencode/src/server/instance/config.ts

@@ -7,6 +7,8 @@ import { mapValues } from "remeda"
 import { errors } from "../error"
 import { Log } from "../../util/log"
 import { lazy } from "../../util/lazy"
+import { AppRuntime } from "../../effect/app-runtime"
+import { Effect } from "effect"
 
 const log = Log.create({ service: "server" })
 
@@ -82,7 +84,12 @@ export const ConfigRoutes = lazy(() =>
       }),
       async (c) => {
         using _ = log.time("providers")
-        const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
+        const providers = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const svc = yield* Provider.Service
+            return mapValues(yield* svc.list(), (item) => item)
+          }),
+        )
         return c.json({
           providers: Object.values(providers),
           default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),

+ 18 - 6
packages/opencode/src/server/instance/experimental.ts

@@ -162,7 +162,13 @@ export const ExperimentalRoutes = lazy(() =>
         },
       }),
       async (c) => {
-        return c.json(await ToolRegistry.ids())
+        const ids = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const registry = yield* ToolRegistry.Service
+            return yield* registry.ids()
+          }),
+        )
+        return c.json(ids)
       },
     )
     .get(
@@ -205,11 +211,17 @@ export const ExperimentalRoutes = lazy(() =>
       ),
       async (c) => {
         const { provider, model } = c.req.valid("query")
-        const tools = await ToolRegistry.tools({
-          providerID: ProviderID.make(provider),
-          modelID: ModelID.make(model),
-          agent: await Agent.get(await Agent.defaultAgent()),
-        })
+        const tools = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const agents = yield* Agent.Service
+            const registry = yield* ToolRegistry.Service
+            return yield* registry.tools({
+              providerID: ProviderID.make(provider),
+              modelID: ModelID.make(model),
+              agent: yield* agents.get(yield* agents.defaultAgent()),
+            })
+          }),
+        )
         return c.json(
           tools.map((t) => ({
             id: t.id,

+ 0 - 5
packages/opencode/src/server/instance/file.ts

@@ -105,11 +105,6 @@ export const FileRoutes = lazy(() =>
         }),
       ),
       async (c) => {
-        /*
-      const query = c.req.valid("query").query
-      const result = await LSP.workspaceSymbol(query)
-      return c.json(result)
-      */
         return c.json([])
       },
     )

+ 28 - 8
packages/opencode/src/server/instance/index.ts

@@ -1,6 +1,7 @@
 import { describeRoute, resolver, validator } from "hono-openapi"
 import { Hono } from "hono"
 import type { UpgradeWebSocket } from "hono/ws"
+import { Effect } from "effect"
 import z from "zod"
 import { Format } from "../../format"
 import { TuiRoutes } from "./tui"
@@ -119,11 +120,17 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
         },
       }),
       async (c) => {
-        const [branch, default_branch] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()])
-        return c.json({
-          branch,
-          default_branch,
-        })
+        return c.json(
+          await AppRuntime.runPromise(
+            Effect.gen(function* () {
+              const vcs = yield* Vcs.Service
+              const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], {
+                concurrency: 2,
+              })
+              return { branch, default_branch }
+            }),
+          ),
+        )
       },
     )
     .get(
@@ -150,7 +157,14 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
         }),
       ),
       async (c) => {
-        return c.json(await Vcs.diff(c.req.valid("query").mode))
+        return c.json(
+          await AppRuntime.runPromise(
+            Effect.gen(function* () {
+              const vcs = yield* Vcs.Service
+              return yield* vcs.diff(c.req.valid("query").mode)
+            }),
+          ),
+        )
       },
     )
     .get(
@@ -215,7 +229,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
         },
       }),
       async (c) => {
-        const skills = await Skill.all()
+        const skills = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const skill = yield* Skill.Service
+            return yield* skill.all()
+          }),
+        )
         return c.json(skills)
       },
     )
@@ -237,7 +256,8 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
         },
       }),
       async (c) => {
-        return c.json(await LSP.status())
+        const items = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.status()))
+        return c.json(items)
       },
     )
     .get(

+ 28 - 19
packages/opencode/src/server/instance/provider.ts

@@ -11,6 +11,7 @@ import { mapValues } from "remeda"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
 import { Log } from "../../util/log"
+import { Effect } from "effect"
 
 const log = Log.create({ service: "server" })
 
@@ -40,27 +41,35 @@ export const ProviderRoutes = lazy(() =>
         },
       }),
       async (c) => {
-        const config = await Config.get()
-        const disabled = new Set(config.disabled_providers ?? [])
-        const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
-
-        const allProviders = await ModelsDev.get()
-        const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
-        for (const [key, value] of Object.entries(allProviders)) {
-          if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
-            filteredProviders[key] = value
-          }
-        }
-
-        const connected = await Provider.list()
-        const providers = Object.assign(
-          mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
-          connected,
+        const result = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const svc = yield* Provider.Service
+            const config = yield* Effect.promise(() => Config.get())
+            const all = yield* Effect.promise(() => ModelsDev.get())
+            const disabled = new Set(config.disabled_providers ?? [])
+            const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
+            const filtered: Record<string, (typeof all)[string]> = {}
+            for (const [key, value] of Object.entries(all)) {
+              if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
+                filtered[key] = value
+              }
+            }
+            const connected = yield* svc.list()
+            const providers = Object.assign(
+              mapValues(filtered, (x) => Provider.fromModelsDevProvider(x)),
+              connected,
+            )
+            return {
+              all: Object.values(providers),
+              default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
+              connected: Object.keys(connected),
+            }
+          }),
         )
         return c.json({
-          all: Object.values(providers),
-          default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
-          connected: Object.keys(connected),
+          all: result.all,
+          default: result.default,
+          connected: result.connected,
         })
       },
     )

+ 56 - 8
packages/opencode/src/server/instance/pty.ts

@@ -1,7 +1,9 @@
 import { Hono, type MiddlewareHandler } from "hono"
 import { describeRoute, validator, resolver } from "hono-openapi"
 import type { UpgradeWebSocket } from "hono/ws"
+import { Effect } from "effect"
 import z from "zod"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Pty } from "@/pty"
 import { PtyID } from "@/pty/schema"
 import { NotFoundError } from "../../storage/db"
@@ -27,7 +29,14 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
         },
       }),
       async (c) => {
-        return c.json(await Pty.list())
+        return c.json(
+          await AppRuntime.runPromise(
+            Effect.gen(function* () {
+              const pty = yield* Pty.Service
+              return yield* pty.list()
+            }),
+          ),
+        )
       },
     )
     .post(
@@ -50,7 +59,12 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
       }),
       validator("json", Pty.CreateInput),
       async (c) => {
-        const info = await Pty.create(c.req.valid("json"))
+        const info = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            return yield* pty.create(c.req.valid("json"))
+          }),
+        )
         return c.json(info)
       },
     )
@@ -74,7 +88,12 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
       }),
       validator("param", z.object({ ptyID: PtyID.zod })),
       async (c) => {
-        const info = await Pty.get(c.req.valid("param").ptyID)
+        const info = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            return yield* pty.get(c.req.valid("param").ptyID)
+          }),
+        )
         if (!info) {
           throw new NotFoundError({ message: "Session not found" })
         }
@@ -102,7 +121,12 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
       validator("param", z.object({ ptyID: PtyID.zod })),
       validator("json", Pty.UpdateInput),
       async (c) => {
-        const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
+        const info = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
+          }),
+        )
         return c.json(info)
       },
     )
@@ -126,7 +150,12 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
       }),
       validator("param", z.object({ ptyID: PtyID.zod })),
       async (c) => {
-        await Pty.remove(c.req.valid("param").ptyID)
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            yield* pty.remove(c.req.valid("param").ptyID)
+          }),
+        )
         return c.json(true)
       },
     )
@@ -150,6 +179,11 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
       }),
       validator("param", z.object({ ptyID: PtyID.zod })),
       upgradeWebSocket(async (c) => {
+        type Handler = {
+          onMessage: (message: string | ArrayBuffer) => void
+          onClose: () => void
+        }
+
         const id = PtyID.zod.parse(c.req.param("ptyID"))
         const cursor = (() => {
           const value = c.req.query("cursor")
@@ -158,8 +192,17 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
           if (!Number.isSafeInteger(parsed) || parsed < -1) return
           return parsed
         })()
-        let handler: Awaited<ReturnType<typeof Pty.connect>>
-        if (!(await Pty.get(id))) throw new Error("Session not found")
+        let handler: Handler | undefined
+        if (
+          !(await AppRuntime.runPromise(
+            Effect.gen(function* () {
+              const pty = yield* Pty.Service
+              return yield* pty.get(id)
+            }),
+          ))
+        ) {
+          throw new Error("Session not found")
+        }
 
         type Socket = {
           readyState: number
@@ -185,7 +228,12 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
               ws.close()
               return
             }
-            handler = await Pty.connect(id, socket, cursor)
+            handler = await AppRuntime.runPromise(
+              Effect.gen(function* () {
+                const pty = yield* Pty.Service
+                return yield* pty.connect(id, socket, cursor)
+              }),
+            )
             ready = true
             for (const msg of pending) handler?.onMessage(msg)
             pending.length = 0

+ 18 - 8
packages/opencode/src/session/llm.ts

@@ -94,14 +94,24 @@ export namespace LLM {
       modelID: input.model.id,
       providerID: input.model.providerID,
     })
-    const [language, cfg, provider, auth] = await Promise.all([
-      Provider.getLanguage(input.model),
-      Config.get(),
-      Provider.getProvider(input.model.providerID),
-      Auth.get(input.model.providerID),
-    ])
+    const [language, cfg, provider, info] = await Effect.runPromise(
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        const cfg = yield* Config.Service
+        const provider = yield* Provider.Service
+        return yield* Effect.all(
+          [
+            provider.getLanguage(input.model),
+            cfg.get(),
+            provider.getProvider(input.model.providerID),
+            auth.get(input.model.providerID),
+          ],
+          { concurrency: "unbounded" },
+        )
+      }).pipe(Effect.provide(Layer.mergeAll(Auth.defaultLayer, Config.defaultLayer, Provider.defaultLayer))),
+    )
     // TODO: move this to a proper hook
-    const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth"
+    const isOpenaiOauth = provider.id === "openai" && info?.type === "oauth"
 
     const system: string[] = []
     system.push(
@@ -200,7 +210,7 @@ export namespace LLM {
       },
     )
 
-    const tools = await resolveTools(input)
+    const tools = resolveTools(input)
 
     // LiteLLM and some Anthropic proxies require the tools parameter to be present
     // when message history contains tool calls, even if no tools are being used.

+ 0 - 19
packages/opencode/src/skill/index.ts

@@ -7,7 +7,6 @@ import { NamedError } from "@opencode-ai/util/error"
 import type { Agent } from "@/agent/agent"
 import { Bus } from "@/bus"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Flag } from "@/flag/flag"
 import { Global } from "@/global"
 import { Permission } from "@/permission"
@@ -262,22 +261,4 @@ export namespace Skill {
         .map((skill) => `- **${skill.name}**: ${skill.description}`),
     ].join("\n")
   }
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function get(name: string) {
-    return runPromise((skill) => skill.get(name))
-  }
-
-  export async function all() {
-    return runPromise((skill) => skill.all())
-  }
-
-  export async function dirs() {
-    return runPromise((skill) => skill.dirs())
-  }
-
-  export async function available(agent?: Agent.Info) {
-    return runPromise((skill) => skill.available(agent))
-  }
 }

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

@@ -36,7 +36,6 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { Ripgrep } from "../file/ripgrep"
 import { Format } from "../format"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Env } from "../env"
 import { Question } from "../question"
 import { Todo } from "../session/todo"
@@ -344,18 +343,4 @@ export namespace ToolRegistry {
       Layer.provide(Truncate.defaultLayer),
     ),
   )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function ids() {
-    return runPromise((svc) => svc.ids())
-  }
-
-  export async function tools(input: {
-    providerID: ProviderID
-    modelID: ModelID
-    agent: Agent.Info
-  }): Promise<(Tool.Def & { id: string })[]> {
-    return runPromise((svc) => svc.tools(input))
-  }
 }

+ 2 - 2
packages/opencode/src/worktree/index.ts

@@ -17,10 +17,10 @@ import { Effect, Layer, Path, Scope, Context, Stream } from "effect"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import { NodePath } from "@effect/platform-node"
 import { AppFileSystem } from "@/filesystem"
+import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
 import { makeRuntime } from "@/effect/run-service"
 import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { InstanceState } from "@/effect/instance-state"
-import { AppRuntime } from "@/effect/app-runtime"
 
 export namespace Worktree {
   const log = Log.create({ service: "worktree" })
@@ -267,7 +267,7 @@ export namespace Worktree {
         const booted = yield* Effect.promise(() =>
           Instance.provide({
             directory: info.directory,
-            init: () => AppRuntime.runPromise(InstanceBootstrap),
+            init: () => BootstrapRuntime.runPromise(InstanceBootstrap),
             fn: () => undefined,
           })
             .then(() => true)

+ 80 - 52
packages/opencode/test/auth/auth.test.ts

@@ -1,58 +1,86 @@
-import { test, expect } from "bun:test"
+import { describe, expect } from "bun:test"
+import { Effect, Layer } from "effect"
 import { Auth } from "../../src/auth"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 
-test("set normalizes trailing slashes in keys", async () => {
-  await Auth.set("https://example.com/", {
-    type: "wellknown",
-    key: "TOKEN",
-    token: "abc",
-  })
-  const data = await Auth.all()
-  expect(data["https://example.com"]).toBeDefined()
-  expect(data["https://example.com/"]).toBeUndefined()
-})
+const node = CrossSpawnSpawner.defaultLayer
 
-test("set cleans up pre-existing trailing-slash entry", async () => {
-  // Simulate a pre-fix entry with trailing slash
-  await Auth.set("https://example.com/", {
-    type: "wellknown",
-    key: "TOKEN",
-    token: "old",
-  })
-  // Re-login with normalized key (as the CLI does post-fix)
-  await Auth.set("https://example.com", {
-    type: "wellknown",
-    key: "TOKEN",
-    token: "new",
-  })
-  const data = await Auth.all()
-  const keys = Object.keys(data).filter((k) => k.includes("example.com"))
-  expect(keys).toEqual(["https://example.com"])
-  const entry = data["https://example.com"]!
-  expect(entry.type).toBe("wellknown")
-  if (entry.type === "wellknown") expect(entry.token).toBe("new")
-})
+const it = testEffect(Layer.mergeAll(Auth.defaultLayer, node))
 
-test("remove deletes both trailing-slash and normalized keys", async () => {
-  await Auth.set("https://example.com", {
-    type: "wellknown",
-    key: "TOKEN",
-    token: "abc",
-  })
-  await Auth.remove("https://example.com/")
-  const data = await Auth.all()
-  expect(data["https://example.com"]).toBeUndefined()
-  expect(data["https://example.com/"]).toBeUndefined()
-})
+describe("Auth", () => {
+  it.live("set normalizes trailing slashes in keys", () =>
+    provideTmpdirInstance(() =>
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        yield* auth.set("https://example.com/", {
+          type: "wellknown",
+          key: "TOKEN",
+          token: "abc",
+        })
+        const data = yield* auth.all()
+        expect(data["https://example.com"]).toBeDefined()
+        expect(data["https://example.com/"]).toBeUndefined()
+      }),
+    ),
+  )
+
+  it.live("set cleans up pre-existing trailing-slash entry", () =>
+    provideTmpdirInstance(() =>
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        yield* auth.set("https://example.com/", {
+          type: "wellknown",
+          key: "TOKEN",
+          token: "old",
+        })
+        yield* auth.set("https://example.com", {
+          type: "wellknown",
+          key: "TOKEN",
+          token: "new",
+        })
+        const data = yield* auth.all()
+        const keys = Object.keys(data).filter((key) => key.includes("example.com"))
+        expect(keys).toEqual(["https://example.com"])
+        const entry = data["https://example.com"]!
+        expect(entry.type).toBe("wellknown")
+        if (entry.type === "wellknown") expect(entry.token).toBe("new")
+      }),
+    ),
+  )
+
+  it.live("remove deletes both trailing-slash and normalized keys", () =>
+    provideTmpdirInstance(() =>
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        yield* auth.set("https://example.com", {
+          type: "wellknown",
+          key: "TOKEN",
+          token: "abc",
+        })
+        yield* auth.remove("https://example.com/")
+        const data = yield* auth.all()
+        expect(data["https://example.com"]).toBeUndefined()
+        expect(data["https://example.com/"]).toBeUndefined()
+      }),
+    ),
+  )
 
-test("set and remove are no-ops on keys without trailing slashes", async () => {
-  await Auth.set("anthropic", {
-    type: "api",
-    key: "sk-test",
-  })
-  const data = await Auth.all()
-  expect(data["anthropic"]).toBeDefined()
-  await Auth.remove("anthropic")
-  const after = await Auth.all()
-  expect(after["anthropic"]).toBeUndefined()
+  it.live("set and remove are no-ops on keys without trailing slashes", () =>
+    provideTmpdirInstance(() =>
+      Effect.gen(function* () {
+        const auth = yield* Auth.Service
+        yield* auth.set("anthropic", {
+          type: "api",
+          key: "sk-test",
+        })
+        const data = yield* auth.all()
+        expect(data["anthropic"]).toBeDefined()
+        yield* auth.remove("anthropic")
+        const after = yield* auth.all()
+        expect(after["anthropic"]).toBeUndefined()
+      }),
+    ),
+  )
 })

+ 2 - 4
packages/opencode/test/bus/bus-effect.test.ts

@@ -1,10 +1,10 @@
-import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
 import { describe, expect } from "bun:test"
 import { Deferred, Effect, Layer, Stream } from "effect"
 import z from "zod"
 import { Bus } from "../../src/bus"
 import { BusEvent } from "../../src/bus/bus-event"
 import { Instance } from "../../src/project/instance"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
@@ -13,9 +13,7 @@ const TestEvent = {
   Pong: BusEvent.define("test.effect.pong", z.object({ message: z.string() })),
 }
 
-const node = NodeChildProcessSpawner.layer.pipe(
-  Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
-)
+const node = CrossSpawnSpawner.defaultLayer
 
 const live = Layer.mergeAll(Bus.layer, node)
 

+ 47 - 47
packages/opencode/test/lsp/index.test.ts

@@ -1,55 +1,55 @@
-import { describe, expect, spyOn, test } from "bun:test"
+import { describe, expect, spyOn } from "bun:test"
 import path from "path"
-import * as Lsp from "../../src/lsp/index"
+import { Effect, Layer } from "effect"
+import { LSP } from "../../src/lsp"
 import { LSPServer } from "../../src/lsp/server"
-import { Instance } from "../../src/project/instance"
-import { tmpdir } from "../fixture/fixture"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 
-describe("lsp.spawn", () => {
-  test("does not spawn builtin LSP for files outside instance", async () => {
-    await using tmp = await tmpdir()
-    const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
-
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await Lsp.LSP.touchFile(path.join(tmp.path, "..", "outside.ts"))
-          await Lsp.LSP.hover({
-            file: path.join(tmp.path, "..", "hover.ts"),
-            line: 0,
-            character: 0,
-          })
-        },
-      })
+const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
 
-      expect(spy).toHaveBeenCalledTimes(0)
-    } finally {
-      spy.mockRestore()
-      await Instance.disposeAll()
-    }
-  })
+describe("lsp.spawn", () => {
+  it.live("does not spawn builtin LSP for files outside instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
 
-  test("would spawn builtin LSP for files inside instance", async () => {
-    await using tmp = await tmpdir()
-    const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
+          try {
+            yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
+            yield* lsp.hover({
+              file: path.join(dir, "..", "hover.ts"),
+              line: 0,
+              character: 0,
+            })
+            expect(spy).toHaveBeenCalledTimes(0)
+          } finally {
+            spy.mockRestore()
+          }
+        }),
+      ),
+    ),
+  )
 
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await Lsp.LSP.hover({
-            file: path.join(tmp.path, "src", "inside.ts"),
-            line: 0,
-            character: 0,
-          })
-        },
-      })
+  it.live("would spawn builtin LSP for files inside instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
 
-      expect(spy).toHaveBeenCalledTimes(1)
-    } finally {
-      spy.mockRestore()
-      await Instance.disposeAll()
-    }
-  })
+          try {
+            yield* lsp.hover({
+              file: path.join(dir, "src", "inside.ts"),
+              line: 0,
+              character: 0,
+            })
+            expect(spy).toHaveBeenCalledTimes(1)
+          } finally {
+            spy.mockRestore()
+          }
+        }),
+      ),
+    ),
+  )
 })

+ 97 - 92
packages/opencode/test/lsp/lifecycle.test.ts

@@ -1,23 +1,13 @@
-import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
+import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
 import path from "path"
-import * as Lsp from "../../src/lsp/index"
+import { Effect, Layer } from "effect"
+import { LSP } from "../../src/lsp"
 import { LSPServer } from "../../src/lsp/server"
-import { Instance } from "../../src/project/instance"
-import { tmpdir } from "../fixture/fixture"
-
-function withInstance(fn: (dir: string) => Promise<void>) {
-  return async () => {
-    await using tmp = await tmpdir()
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: () => fn(tmp.path),
-      })
-    } finally {
-      await Instance.disposeAll()
-    }
-  }
-}
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
 
 describe("LSP service lifecycle", () => {
   let spawnSpy: ReturnType<typeof spyOn>
@@ -30,97 +20,112 @@ describe("LSP service lifecycle", () => {
     spawnSpy.mockRestore()
   })
 
-  test(
-    "init() completes without error",
-    withInstance(async () => {
-      await Lsp.LSP.init()
-    }),
-  )
-
-  test(
-    "status() returns empty array initially",
-    withInstance(async () => {
-      const result = await Lsp.LSP.status()
-      expect(Array.isArray(result)).toBe(true)
-      expect(result.length).toBe(0)
-    }),
+  it.live("init() completes without error", () => provideTmpdirInstance(() => LSP.Service.use((lsp) => lsp.init())))
+
+  it.live("status() returns empty array initially", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.status()
+          expect(Array.isArray(result)).toBe(true)
+          expect(result.length).toBe(0)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "diagnostics() returns empty object initially",
-    withInstance(async () => {
-      const result = await Lsp.LSP.diagnostics()
-      expect(typeof result).toBe("object")
-      expect(Object.keys(result).length).toBe(0)
-    }),
+  it.live("diagnostics() returns empty object initially", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.diagnostics()
+          expect(typeof result).toBe("object")
+          expect(Object.keys(result).length).toBe(0)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "hasClients() returns true for .ts files in instance",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.hasClients(path.join(dir, "test.ts"))
-      expect(result).toBe(true)
-    }),
+  it.live("hasClients() returns true for .ts files in instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
+          expect(result).toBe(true)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "hasClients() returns false for files outside instance",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.hasClients(path.join(dir, "..", "outside.ts"))
-      // hasClients checks servers but doesn't check containsPath — getClients does
-      // So hasClients may return true even for outside files (it checks extension + root)
-      // The guard is in getClients, not hasClients
-      expect(typeof result).toBe("boolean")
-    }),
+  it.live("hasClients() returns false for files outside instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.hasClients(path.join(dir, "..", "outside.ts"))
+          expect(typeof result).toBe("boolean")
+        }),
+      ),
+    ),
   )
 
-  test(
-    "workspaceSymbol() returns empty array with no clients",
-    withInstance(async () => {
-      const result = await Lsp.LSP.workspaceSymbol("test")
-      expect(Array.isArray(result)).toBe(true)
-      expect(result.length).toBe(0)
-    }),
+  it.live("workspaceSymbol() returns empty array with no clients", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.workspaceSymbol("test")
+          expect(Array.isArray(result)).toBe(true)
+          expect(result.length).toBe(0)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "definition() returns empty array for unknown file",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.definition({
-        file: path.join(dir, "nonexistent.ts"),
-        line: 0,
-        character: 0,
-      })
-      expect(Array.isArray(result)).toBe(true)
-    }),
+  it.live("definition() returns empty array for unknown file", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.definition({
+            file: path.join(dir, "nonexistent.ts"),
+            line: 0,
+            character: 0,
+          })
+          expect(Array.isArray(result)).toBe(true)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "references() returns empty array for unknown file",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.references({
-        file: path.join(dir, "nonexistent.ts"),
-        line: 0,
-        character: 0,
-      })
-      expect(Array.isArray(result)).toBe(true)
-    }),
+  it.live("references() returns empty array for unknown file", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.references({
+            file: path.join(dir, "nonexistent.ts"),
+            line: 0,
+            character: 0,
+          })
+          expect(Array.isArray(result)).toBe(true)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "multiple init() calls are idempotent",
-    withInstance(async () => {
-      await Lsp.LSP.init()
-      await Lsp.LSP.init()
-      await Lsp.LSP.init()
-      // Should not throw or create duplicate state
-    }),
+  it.live("multiple init() calls are idempotent", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          yield* lsp.init()
+          yield* lsp.init()
+          yield* lsp.init()
+        }),
+      ),
+    ),
   )
 })
 
 describe("LSP.Diagnostic", () => {
   test("pretty() formats error diagnostic", () => {
-    const result = Lsp.LSP.Diagnostic.pretty({
+    const result = LSP.Diagnostic.pretty({
       range: { start: { line: 9, character: 4 }, end: { line: 9, character: 10 } },
       message: "Type 'string' is not assignable to type 'number'",
       severity: 1,
@@ -129,7 +134,7 @@ describe("LSP.Diagnostic", () => {
   })
 
   test("pretty() formats warning diagnostic", () => {
-    const result = Lsp.LSP.Diagnostic.pretty({
+    const result = LSP.Diagnostic.pretty({
       range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
       message: "Unused variable",
       severity: 2,
@@ -138,7 +143,7 @@ describe("LSP.Diagnostic", () => {
   })
 
   test("pretty() defaults to ERROR when no severity", () => {
-    const result = Lsp.LSP.Diagnostic.pretty({
+    const result = LSP.Diagnostic.pretty({
       range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
       message: "Something wrong",
     } as any)

+ 69 - 12
packages/opencode/test/project/vcs.test.ts

@@ -1,5 +1,6 @@
 import { $ } from "bun"
 import { afterEach, describe, expect, test } from "bun:test"
+import { Effect } from "effect"
 import fs from "fs/promises"
 import path from "path"
 import { tmpdir } from "../fixture/fixture"
@@ -20,8 +21,14 @@ async function withVcs(directory: string, body: () => Promise<void>) {
   return Instance.provide({
     directory,
     fn: async () => {
-      void AppRuntime.runPromise(FileWatcher.Service.use((svc) => svc.init()))
-      Vcs.init()
+      await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const watcher = yield* FileWatcher.Service
+          const vcs = yield* Vcs.Service
+          yield* watcher.init()
+          yield* vcs.init()
+        }),
+      )
       await Bun.sleep(500)
       await body()
     },
@@ -32,7 +39,12 @@ function withVcsOnly(directory: string, body: () => Promise<void>) {
   return Instance.provide({
     directory,
     fn: async () => {
-      Vcs.init()
+      await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          yield* vcs.init()
+        }),
+      )
       await body()
     },
   })
@@ -80,7 +92,12 @@ describeVcs("Vcs", () => {
     await using tmp = await tmpdir({ git: true })
 
     await withVcs(tmp.path, async () => {
-      const branch = await Vcs.branch()
+      const branch = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.branch()
+        }),
+      )
       expect(branch).toBeDefined()
       expect(typeof branch).toBe("string")
     })
@@ -90,7 +107,12 @@ describeVcs("Vcs", () => {
     await using tmp = await tmpdir()
 
     await withVcs(tmp.path, async () => {
-      const branch = await Vcs.branch()
+      const branch = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.branch()
+        }),
+      )
       expect(branch).toBeUndefined()
     })
   })
@@ -123,7 +145,12 @@ describeVcs("Vcs", () => {
       await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
 
       await pending
-      const current = await Vcs.branch()
+      const current = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.branch()
+        }),
+      )
       expect(current).toBe(branch)
     })
   })
@@ -139,7 +166,12 @@ describe("Vcs diff", () => {
     await $`git branch -M main`.cwd(tmp.path).quiet()
 
     await withVcsOnly(tmp.path, async () => {
-      const branch = await Vcs.defaultBranch()
+      const branch = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.defaultBranch()
+        }),
+      )
       expect(branch).toBe("main")
     })
   })
@@ -150,7 +182,12 @@ describe("Vcs diff", () => {
     await $`git config init.defaultBranch trunk`.cwd(tmp.path).quiet()
 
     await withVcsOnly(tmp.path, async () => {
-      const branch = await Vcs.defaultBranch()
+      const branch = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.defaultBranch()
+        }),
+      )
       expect(branch).toBe("trunk")
     })
   })
@@ -163,7 +200,12 @@ describe("Vcs diff", () => {
     await $`git worktree add -b feature/test ${dir} HEAD`.cwd(tmp.path).quiet()
 
     await withVcsOnly(dir, async () => {
-      const [branch, base] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()])
+      const [branch, base] = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 })
+        }),
+      )
       expect(branch).toBe("feature/test")
       expect(base).toBe("main")
     })
@@ -177,7 +219,12 @@ describe("Vcs diff", () => {
     await fs.writeFile(path.join(tmp.path, "file.txt"), "changed\n", "utf-8")
 
     await withVcsOnly(tmp.path, async () => {
-      const diff = await Vcs.diff("git")
+      const diff = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.diff("git")
+        }),
+      )
       expect(diff).toEqual(
         expect.arrayContaining([
           expect.objectContaining({
@@ -194,7 +241,12 @@ describe("Vcs diff", () => {
     await fs.writeFile(path.join(tmp.path, weird), "hello\n", "utf-8")
 
     await withVcsOnly(tmp.path, async () => {
-      const diff = await Vcs.diff("git")
+      const diff = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.diff("git")
+        }),
+      )
       expect(diff).toEqual(
         expect.arrayContaining([
           expect.objectContaining({
@@ -215,7 +267,12 @@ describe("Vcs diff", () => {
     await $`git commit --no-gpg-sign -m "branch file"`.cwd(tmp.path).quiet()
 
     await withVcsOnly(tmp.path, async () => {
-      const diff = await Vcs.diff("branch")
+      const diff = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const vcs = yield* Vcs.Service
+          return yield* vcs.diff("branch")
+        }),
+      )
       expect(diff).toEqual(
         expect.arrayContaining([
           expect.objectContaining({

+ 21 - 10
packages/opencode/test/provider/amazon-bedrock.test.ts

@@ -9,6 +9,17 @@ import { Provider } from "../../src/provider/provider"
 import { Env } from "../../src/env"
 import { Global } from "../../src/global"
 import { Filesystem } from "../../src/util/filesystem"
+import { Effect } from "effect"
+import { AppRuntime } from "../../src/effect/app-runtime"
+
+async function list() {
+  return AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const provider = yield* Provider.Service
+      return yield* provider.list()
+    }),
+  )
+}
 
 test("Bedrock: config region takes precedence over AWS_REGION env var", async () => {
   await using tmp = await tmpdir({
@@ -35,7 +46,7 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async ()
       Env.set("AWS_PROFILE", "default")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
     },
@@ -60,7 +71,7 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async ()
       Env.set("AWS_PROFILE", "default")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
     },
@@ -116,7 +127,7 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
         Env.set("AWS_BEARER_TOKEN_BEDROCK", "")
       },
       fn: async () => {
-        const providers = await Provider.list()
+        const providers = await list()
         expect(providers[ProviderID.amazonBedrock]).toBeDefined()
         expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
       },
@@ -161,7 +172,7 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async
       Env.set("AWS_ACCESS_KEY_ID", "test-key-id")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
     },
@@ -192,7 +203,7 @@ test("Bedrock: includes custom endpoint in options when specified", async () =>
       Env.set("AWS_PROFILE", "default")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe(
         "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
@@ -228,7 +239,7 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async ()
       Env.set("AWS_ACCESS_KEY_ID", "")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
     },
@@ -268,7 +279,7 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () =>
       Env.set("AWS_PROFILE", "default")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       // The model should exist with the us. prefix
       expect(providers[ProviderID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
@@ -305,7 +316,7 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => {
       Env.set("AWS_PROFILE", "default")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
     },
@@ -341,7 +352,7 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () =>
       Env.set("AWS_PROFILE", "default")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
     },
@@ -377,7 +388,7 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a
       Env.set("AWS_PROFILE", "default")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       // Non-prefixed model should still be registered
       expect(providers[ProviderID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()

+ 18 - 18
packages/opencode/test/provider/gitlab-duo.test.ts

@@ -30,7 +30,7 @@
 //       Env.set("GITLAB_TOKEN", "test-gitlab-token")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //       expect(providers[ProviderID.gitlab].key).toBe("test-gitlab-token")
 //     },
@@ -62,7 +62,7 @@
 //       Env.set("GITLAB_INSTANCE_URL", "https://gitlab.example.com")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //       expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.example.com")
 //     },
@@ -100,7 +100,7 @@
 //       Env.set("GITLAB_TOKEN", "")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //     },
 //   })
@@ -135,7 +135,7 @@
 //       Env.set("GITLAB_TOKEN", "")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //       expect(providers[ProviderID.gitlab].key).toBe("glpat-test-pat-token")
 //     },
@@ -167,7 +167,7 @@
 //       Env.set("GITLAB_INSTANCE_URL", "https://gitlab.company.internal")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //       expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.company.internal")
 //     },
@@ -198,7 +198,7 @@
 //       Env.set("GITLAB_TOKEN", "env-token")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //     },
 //   })
@@ -221,7 +221,7 @@
 //       Env.set("GITLAB_TOKEN", "test-token")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //       expect(providers[ProviderID.gitlab].options?.aiGatewayHeaders?.["anthropic-beta"]).toContain(
 //         "context-1m-2025-08-07",
@@ -257,7 +257,7 @@
 //       Env.set("GITLAB_TOKEN", "test-token")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //       expect(providers[ProviderID.gitlab].options?.featureFlags).toBeDefined()
 //       expect(providers[ProviderID.gitlab].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true)
@@ -282,7 +282,7 @@
 //       Env.set("GITLAB_TOKEN", "test-token")
 //     },
 //     fn: async () => {
-//       const providers = await Provider.list()
+//       const providers = await list()
 //       expect(providers[ProviderID.gitlab]).toBeDefined()
 //       const models = Object.keys(providers[ProviderID.gitlab].models)
 //       expect(models.length).toBeGreaterThan(0)
@@ -306,7 +306,7 @@
 //         Env.set("GITLAB_TOKEN", "test-token")
 //       },
 //       fn: async () => {
-//         const providers = await Provider.list()
+//         const providers = await list()
 //         const gitlab = providers[ProviderID.gitlab]
 //         expect(gitlab).toBeDefined()
 //         gitlab.models["duo-workflow-sonnet-4-6"] = {
@@ -332,10 +332,10 @@
 //           release_date: "",
 //           variants: {},
 //         }
-//         const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-workflow-sonnet-4-6"))
+//         const model = await getModel(ProviderID.gitlab, ModelID.make("duo-workflow-sonnet-4-6"))
 //         expect(model).toBeDefined()
 //         expect(model.options?.workflowRef).toBe("claude_sonnet_4_6")
-//         const language = await Provider.getLanguage(model)
+//         const language = await getLanguage(model)
 //         expect(language).toBeDefined()
 //         expect(language).toBeInstanceOf(GitLabWorkflowLanguageModel)
 //       },
@@ -354,11 +354,11 @@
 //         Env.set("GITLAB_TOKEN", "test-token")
 //       },
 //       fn: async () => {
-//         const providers = await Provider.list()
+//         const providers = await list()
 //         expect(providers[ProviderID.gitlab]).toBeDefined()
-//         const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
+//         const model = await getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
 //         expect(model).toBeDefined()
-//         const language = await Provider.getLanguage(model)
+//         const language = await getLanguage(model)
 //         expect(language).toBeDefined()
 //         expect(language).not.toBeInstanceOf(GitLabWorkflowLanguageModel)
 //       },
@@ -377,10 +377,10 @@
 //         Env.set("GITLAB_TOKEN", "test-token")
 //       },
 //       fn: async () => {
-//         const providers = await Provider.list()
+//         const providers = await list()
 //         const gitlab = providers[ProviderID.gitlab]
 //         expect(gitlab.options?.featureFlags).toBeDefined()
-//         const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
+//         const model = await getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5"))
 //         expect(model).toBeDefined()
 //         expect(model.options).toBeDefined()
 //       },
@@ -401,7 +401,7 @@
 //         Env.set("GITLAB_TOKEN", "test-token")
 //       },
 //       fn: async () => {
-//         const providers = await Provider.list()
+//         const providers = await list()
 //         const models = Object.keys(providers[ProviderID.gitlab].models)
 //         expect(models).toContain("duo-chat-haiku-4-5")
 //         expect(models).toContain("duo-chat-sonnet-4-5")

+ 117 - 78
packages/opencode/test/provider/provider.test.ts

@@ -11,8 +11,47 @@ import { Provider } from "../../src/provider/provider"
 import { ProviderID, ModelID } from "../../src/provider/schema"
 import { Filesystem } from "../../src/util/filesystem"
 import { Env } from "../../src/env"
+import { Effect } from "effect"
+import { AppRuntime } from "../../src/effect/app-runtime"
+
+async function run<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
+  return AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const provider = yield* Provider.Service
+      return yield* fn(provider)
+    }),
+  )
+}
+
+async function list() {
+  return run((provider) => provider.list())
+}
+
+async function getProvider(providerID: ProviderID) {
+  return run((provider) => provider.getProvider(providerID))
+}
+
+async function getModel(providerID: ProviderID, modelID: ModelID) {
+  return run((provider) => provider.getModel(providerID, modelID))
+}
+
+async function getLanguage(model: Provider.Model) {
+  return run((provider) => provider.getLanguage(model))
+}
+
+async function closest(providerID: ProviderID, query: string[]) {
+  return run((provider) => provider.closest(providerID, query))
+}
+
+async function getSmallModel(providerID: ProviderID) {
+  return run((provider) => provider.getSmallModel(providerID))
+}
+
+async function defaultModel() {
+  return run((provider) => provider.defaultModel())
+}
 
-function paid(providers: Awaited<ReturnType<typeof Provider.list>>) {
+function paid(providers: Awaited<ReturnType<typeof list>>) {
   const item = providers[ProviderID.make("opencode")]
   expect(item).toBeDefined()
   return Object.values(item.models).filter((model) => model.cost.input > 0).length
@@ -35,7 +74,7 @@ test("provider loaded from env variable", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       // Provider should retain its connection source even if custom loaders
       // merge additional options.
@@ -66,7 +105,7 @@ test("provider loaded from config with apiKey option", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
     },
   })
@@ -90,7 +129,7 @@ test("disabled_providers excludes provider", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeUndefined()
     },
   })
@@ -115,7 +154,7 @@ test("enabled_providers restricts to only listed providers", async () => {
       Env.set("OPENAI_API_KEY", "test-openai-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.openai]).toBeUndefined()
     },
@@ -144,7 +183,7 @@ test("model whitelist filters models for provider", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       const models = Object.keys(providers[ProviderID.anthropic].models)
       expect(models).toContain("claude-sonnet-4-20250514")
@@ -175,7 +214,7 @@ test("model blacklist excludes specific models", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       const models = Object.keys(providers[ProviderID.anthropic].models)
       expect(models).not.toContain("claude-sonnet-4-20250514")
@@ -210,7 +249,7 @@ test("custom model alias via config", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
       expect(providers[ProviderID.anthropic].models["my-alias"].name).toBe("My Custom Alias")
@@ -253,7 +292,7 @@ test("custom provider with npm package", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("custom-provider")]).toBeDefined()
       expect(providers[ProviderID.make("custom-provider")].name).toBe("Custom Provider")
       expect(providers[ProviderID.make("custom-provider")].models["custom-model"]).toBeDefined()
@@ -286,7 +325,7 @@ test("env variable takes precedence, config merges options", async () => {
       Env.set("ANTHROPIC_API_KEY", "env-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       // Config options should be merged
       expect(providers[ProviderID.anthropic].options.timeout).toBe(60000)
@@ -312,11 +351,11 @@ test("getModel returns model for valid provider/model", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const model = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
+      const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
       expect(model).toBeDefined()
       expect(String(model.providerID)).toBe("anthropic")
       expect(String(model.id)).toBe("claude-sonnet-4-20250514")
-      const language = await Provider.getLanguage(model)
+      const language = await getLanguage(model)
       expect(language).toBeDefined()
     },
   })
@@ -339,7 +378,7 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      expect(Provider.getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
+      expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
     },
   })
 })
@@ -358,7 +397,7 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      expect(Provider.getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
+      expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
     },
   })
 })
@@ -392,7 +431,7 @@ test("defaultModel returns first available model when no config set", async () =
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const model = await Provider.defaultModel()
+      const model = await defaultModel()
       expect(model.providerID).toBeDefined()
       expect(model.modelID).toBeDefined()
     },
@@ -417,7 +456,7 @@ test("defaultModel respects config model setting", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const model = await Provider.defaultModel()
+      const model = await defaultModel()
       expect(String(model.providerID)).toBe("anthropic")
       expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
     },
@@ -456,7 +495,7 @@ test("provider with baseURL from config", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("custom-openai")]).toBeDefined()
       expect(providers[ProviderID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1")
     },
@@ -494,7 +533,7 @@ test("model cost defaults to zero when not specified", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.make("test-provider")].models["test-model"]
       expect(model.cost.input).toBe(0)
       expect(model.cost.output).toBe(0)
@@ -532,7 +571,7 @@ test("model options are merged from existing model", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.options.customOption).toBe("custom-value")
     },
@@ -561,7 +600,7 @@ test("provider removed when all models filtered out", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeUndefined()
     },
   })
@@ -584,7 +623,7 @@ test("closest finds model by partial match", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const result = await Provider.closest(ProviderID.anthropic, ["sonnet-4"])
+      const result = await closest(ProviderID.anthropic, ["sonnet-4"])
       expect(result).toBeDefined()
       expect(String(result?.providerID)).toBe("anthropic")
       expect(String(result?.modelID)).toContain("sonnet-4")
@@ -606,7 +645,7 @@ test("closest returns undefined for nonexistent provider", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const result = await Provider.closest(ProviderID.make("nonexistent"), ["model"])
+      const result = await closest(ProviderID.make("nonexistent"), ["model"])
       expect(result).toBeUndefined()
     },
   })
@@ -639,10 +678,10 @@ test("getModel uses realIdByKey for aliased models", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
 
-      const model = await Provider.getModel(ProviderID.anthropic, ModelID.make("my-sonnet"))
+      const model = await getModel(ProviderID.anthropic, ModelID.make("my-sonnet"))
       expect(model).toBeDefined()
       expect(String(model.id)).toBe("my-sonnet")
       expect(model.name).toBe("My Sonnet Alias")
@@ -682,7 +721,7 @@ test("provider api field sets model api.url", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       // api field is stored on model.api.url, used by getSDK to set baseURL
       expect(providers[ProviderID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1")
     },
@@ -722,7 +761,7 @@ test("explicit baseURL overrides api field", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
     },
   })
@@ -754,7 +793,7 @@ test("model inherits properties from existing database model", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.name).toBe("Custom Name for Sonnet")
       expect(model.capabilities.toolcall).toBe(true)
@@ -782,7 +821,7 @@ test("disabled_providers prevents loading even with env var", async () => {
       Env.set("OPENAI_API_KEY", "test-openai-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.openai]).toBeUndefined()
     },
   })
@@ -807,7 +846,7 @@ test("enabled_providers with empty array allows no providers", async () => {
       Env.set("OPENAI_API_KEY", "test-openai-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(Object.keys(providers).length).toBe(0)
     },
   })
@@ -836,7 +875,7 @@ test("whitelist and blacklist can be combined", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       const models = Object.keys(providers[ProviderID.anthropic].models)
       expect(models).toContain("claude-sonnet-4-20250514")
@@ -875,7 +914,7 @@ test("model modalities default correctly", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.make("test-provider")].models["test-model"]
       expect(model.capabilities.input.text).toBe(true)
       expect(model.capabilities.output.text).toBe(true)
@@ -918,7 +957,7 @@ test("model with custom cost values", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.make("test-provider")].models["test-model"]
       expect(model.cost.input).toBe(5)
       expect(model.cost.output).toBe(15)
@@ -945,7 +984,7 @@ test("getSmallModel returns appropriate small model", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const model = await Provider.getSmallModel(ProviderID.anthropic)
+      const model = await getSmallModel(ProviderID.anthropic)
       expect(model).toBeDefined()
       expect(model?.id).toContain("haiku")
     },
@@ -970,7 +1009,7 @@ test("getSmallModel respects config small_model override", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const model = await Provider.getSmallModel(ProviderID.anthropic)
+      const model = await getSmallModel(ProviderID.anthropic)
       expect(model).toBeDefined()
       expect(String(model?.providerID)).toBe("anthropic")
       expect(String(model?.id)).toBe("claude-sonnet-4-20250514")
@@ -1019,7 +1058,7 @@ test("multiple providers can be configured simultaneously", async () => {
       Env.set("OPENAI_API_KEY", "test-openai-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.openai]).toBeDefined()
       expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
@@ -1060,7 +1099,7 @@ test("provider with custom npm package", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("local-llm")]).toBeDefined()
       expect(providers[ProviderID.make("local-llm")].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
       expect(providers[ProviderID.make("local-llm")].options.baseURL).toBe("http://localhost:11434/v1")
@@ -1097,7 +1136,7 @@ test("model alias name defaults to alias key when id differs", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
     },
   })
@@ -1137,7 +1176,7 @@ test("provider with multiple env var options only includes apiKey when single en
       Env.set("MULTI_ENV_KEY_1", "test-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("multi-env")]).toBeDefined()
       // When multiple env options exist, key should NOT be auto-set
       expect(providers[ProviderID.make("multi-env")].key).toBeUndefined()
@@ -1179,7 +1218,7 @@ test("provider with single env var includes apiKey automatically", async () => {
       Env.set("SINGLE_ENV_KEY", "my-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("single-env")]).toBeDefined()
       // Single env option should auto-set key
       expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key")
@@ -1216,7 +1255,7 @@ test("model cost overrides existing cost values", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.cost.input).toBe(999)
       expect(model.cost.output).toBe(888)
@@ -1263,7 +1302,7 @@ test("completely new provider not in database can be configured", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("brand-new-provider")]).toBeDefined()
       expect(providers[ProviderID.make("brand-new-provider")].name).toBe("Brand New")
       const model = providers[ProviderID.make("brand-new-provider")].models["new-model"]
@@ -1297,7 +1336,7 @@ test("disabled_providers and enabled_providers interaction", async () => {
       Env.set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       // anthropic: in enabled, not in disabled = allowed
       expect(providers[ProviderID.anthropic]).toBeDefined()
       // openai: in enabled, but also in disabled = NOT allowed
@@ -1337,7 +1376,7 @@ test("model with tool_call false", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
     },
   })
@@ -1372,7 +1411,7 @@ test("model defaults tool_call to true when not specified", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
     },
   })
@@ -1411,7 +1450,7 @@ test("model headers are preserved", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.make("headers-provider")].models["model"]
       expect(model.headers).toEqual({
         "X-Custom-Header": "custom-value",
@@ -1454,7 +1493,7 @@ test("provider env fallback - second env var used if first missing", async () =>
       Env.set("FALLBACK_KEY", "fallback-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       // Provider should load because fallback env var is set
       expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
     },
@@ -1478,8 +1517,8 @@ test("getModel returns consistent results", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const model1 = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
-      const model2 = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
+      const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
+      const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
       expect(model1.providerID).toEqual(model2.providerID)
       expect(model1.id).toEqual(model2.id)
       expect(model1).toEqual(model2)
@@ -1516,7 +1555,7 @@ test("provider name defaults to id when not in database", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id")
     },
   })
@@ -1540,7 +1579,7 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
     },
     fn: async () => {
       try {
-        await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
+        await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
         expect(true).toBe(false) // Should not reach here
       } catch (e: any) {
         expect(e.data.suggestions).toBeDefined()
@@ -1568,7 +1607,7 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
     },
     fn: async () => {
       try {
-        await Provider.getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
+        await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
         expect(true).toBe(false) // Should not reach here
       } catch (e: any) {
         expect(e.data.suggestions).toBeDefined()
@@ -1592,7 +1631,7 @@ test("getProvider returns undefined for nonexistent provider", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const provider = await Provider.getProvider(ProviderID.make("nonexistent"))
+      const provider = await getProvider(ProviderID.make("nonexistent"))
       expect(provider).toBeUndefined()
     },
   })
@@ -1615,7 +1654,7 @@ test("getProvider returns provider info", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const provider = await Provider.getProvider(ProviderID.anthropic)
+      const provider = await getProvider(ProviderID.anthropic)
       expect(provider).toBeDefined()
       expect(String(provider?.id)).toBe("anthropic")
     },
@@ -1639,7 +1678,7 @@ test("closest returns undefined when no partial match found", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const result = await Provider.closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
+      const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
       expect(result).toBeUndefined()
     },
   })
@@ -1663,7 +1702,7 @@ test("closest checks multiple query terms in order", async () => {
     },
     fn: async () => {
       // First term won't match, second will
-      const result = await Provider.closest(ProviderID.anthropic, ["nonexistent", "haiku"])
+      const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"])
       expect(result).toBeDefined()
       expect(result?.modelID).toContain("haiku")
     },
@@ -1699,7 +1738,7 @@ test("model limit defaults to zero when not specified", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.make("no-limit")].models["model"]
       expect(model.limit.context).toBe(0)
       expect(model.limit.output).toBe(0)
@@ -1734,7 +1773,7 @@ test("provider options are deeply merged", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       // Custom options should be merged
       expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
       expect(providers[ProviderID.anthropic].options.headers["X-Custom"]).toBe("custom-value")
@@ -1772,7 +1811,7 @@ test("custom model inherits npm package from models.dev provider config", async
       Env.set("OPENAI_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.openai].models["my-custom-model"]
       expect(model).toBeDefined()
       expect(model.api.npm).toBe("@ai-sdk/openai")
@@ -1807,7 +1846,7 @@ test("custom model inherits api.url from models.dev provider", async () => {
       Env.set("OPENROUTER_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.openrouter]).toBeDefined()
 
       // New model not in database should inherit api.url from provider
@@ -1908,7 +1947,7 @@ test("model variants are generated for reasoning models", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       // Claude sonnet 4 has reasoning capability
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.capabilities.reasoning).toBe(true)
@@ -1946,7 +1985,7 @@ test("model variants can be disabled via config", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants).toBeDefined()
       expect(model.variants!["high"]).toBeUndefined()
@@ -1989,7 +2028,7 @@ test("model variants can be customized via config", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants!["high"]).toBeDefined()
       expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
@@ -2028,7 +2067,7 @@ test("disabled key is stripped from variant config", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants!["max"]).toBeDefined()
       expect(model.variants!["max"].disabled).toBeUndefined()
@@ -2066,7 +2105,7 @@ test("all variants can be disabled via config", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants).toBeDefined()
       expect(Object.keys(model.variants!).length).toBe(0)
@@ -2104,7 +2143,7 @@ test("variant config merges with generated variants", async () => {
       Env.set("ANTHROPIC_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants!["high"]).toBeDefined()
       // Should have both the generated thinking config and the custom option
@@ -2142,7 +2181,7 @@ test("variants filtered in second pass for database models", async () => {
       Env.set("OPENAI_API_KEY", "test-api-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.openai].models["gpt-5"]
       expect(model.variants).toBeDefined()
       expect(model.variants!["high"]).toBeUndefined()
@@ -2188,7 +2227,7 @@ test("custom model with variants enabled and disabled", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"]
       expect(model.variants).toBeDefined()
       // Enabled variants should exist
@@ -2246,7 +2285,7 @@ test("Google Vertex: retains baseURL for custom proxy", async () => {
       Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
       expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
     },
@@ -2291,7 +2330,7 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
       Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
 
       expect(model).toBeDefined()
@@ -2319,7 +2358,7 @@ test("cloudflare-ai-gateway loads with env variables", async () => {
       Env.set("CLOUDFLARE_API_TOKEN", "test-token")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
     },
   })
@@ -2351,7 +2390,7 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
       Env.set("CLOUDFLARE_API_TOKEN", "test-token")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
       expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
         invoked_by: "test",
@@ -2399,7 +2438,7 @@ test("plugin config providers persist after instance dispose", async () => {
     directory: tmp.path,
     fn: async () => {
       await Plugin.init()
-      return Provider.list()
+      return list()
     },
   })
   expect(first[ProviderID.make("demo")]).toBeDefined()
@@ -2409,7 +2448,7 @@ test("plugin config providers persist after instance dispose", async () => {
 
   const second = await Instance.provide({
     directory: tmp.path,
-    fn: async () => Provider.list(),
+    fn: async () => list(),
   })
   expect(second[ProviderID.make("demo")]).toBeDefined()
   expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
@@ -2445,7 +2484,7 @@ test("plugin config enabled and disabled providers are honored", async () => {
       Env.set("OPENAI_API_KEY", "test-openai-key")
     },
     fn: async () => {
-      const providers = await Provider.list()
+      const providers = await list()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.openai]).toBeUndefined()
     },
@@ -2466,7 +2505,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
 
   const none = await Instance.provide({
     directory: base.path,
-    fn: async () => paid(await Provider.list()),
+    fn: async () => paid(await list()),
   })
 
   await using keyed = await tmpdir({
@@ -2489,7 +2528,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
 
   const keyedCount = await Instance.provide({
     directory: keyed.path,
-    fn: async () => paid(await Provider.list()),
+    fn: async () => paid(await list()),
   })
 
   expect(none).toBe(0)
@@ -2510,7 +2549,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
 
   const none = await Instance.provide({
     directory: base.path,
-    fn: async () => paid(await Provider.list()),
+    fn: async () => paid(await list()),
   })
 
   await using keyed = await tmpdir({
@@ -2544,7 +2583,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
 
     const keyedCount = await Instance.provide({
       directory: keyed.path,
-      fn: async () => paid(await Provider.list()),
+      fn: async () => paid(await list()),
     })
 
     expect(none).toBe(0)

+ 115 - 110
packages/opencode/test/pty/pty-output-isolation.test.ts

@@ -1,4 +1,6 @@
 import { describe, expect, test } from "bun:test"
+import { AppRuntime } from "../../src/effect/app-runtime"
+import { Effect } from "effect"
 import { Instance } from "../../src/project/instance"
 import { Pty } from "../../src/pty"
 import { tmpdir } from "../fixture/fixture"
@@ -10,48 +12,48 @@ describe("pty", () => {
 
     await Instance.provide({
       directory: dir.path,
-      fn: async () => {
-        const a = await Pty.create({ command: "cat", title: "a" })
-        const b = await Pty.create({ command: "cat", title: "b" })
-        try {
-          const outA: string[] = []
-          const outB: string[] = []
-
-          const ws = {
-            readyState: 1,
-            data: { events: { connection: "a" } },
-            send: (data: unknown) => {
-              outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
-            },
-            close: () => {
-              // no-op (simulate abrupt drop)
-            },
-          }
-
-          // Connect "a" first with ws.
-          Pty.connect(a.id, ws as any)
-
-          // Now "reuse" the same ws object for another connection.
-          ws.data = { events: { connection: "b" } }
-          ws.send = (data: unknown) => {
-            outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
-          }
-          Pty.connect(b.id, ws as any)
-
-          // Clear connect metadata writes.
-          outA.length = 0
-          outB.length = 0
-
-          // Output from a must never show up in b.
-          Pty.write(a.id, "AAA\n")
-          await sleep(100)
-
-          expect(outB.join("")).not.toContain("AAA")
-        } finally {
-          await Pty.remove(a.id)
-          await Pty.remove(b.id)
-        }
-      },
+      fn: () =>
+        AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            const a = yield* pty.create({ command: "cat", title: "a" })
+            const b = yield* pty.create({ command: "cat", title: "b" })
+            try {
+              const outA: string[] = []
+              const outB: string[] = []
+
+              const ws = {
+                readyState: 1,
+                data: { events: { connection: "a" } },
+                send: (data: unknown) => {
+                  outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
+                },
+                close: () => {
+                  // no-op (simulate abrupt drop)
+                },
+              }
+
+              yield* pty.connect(a.id, ws as any)
+
+              ws.data = { events: { connection: "b" } }
+              ws.send = (data: unknown) => {
+                outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
+              }
+              yield* pty.connect(b.id, ws as any)
+
+              outA.length = 0
+              outB.length = 0
+
+              yield* pty.write(a.id, "AAA\n")
+              yield* Effect.promise(() => sleep(100))
+
+              expect(outB.join("")).not.toContain("AAA")
+            } finally {
+              yield* pty.remove(a.id)
+              yield* pty.remove(b.id)
+            }
+          }),
+        ),
     })
   })
 
@@ -60,42 +62,43 @@ describe("pty", () => {
 
     await Instance.provide({
       directory: dir.path,
-      fn: async () => {
-        const a = await Pty.create({ command: "cat", title: "a" })
-        try {
-          const outA: string[] = []
-          const outB: string[] = []
-
-          const ws = {
-            readyState: 1,
-            data: { events: { connection: "a" } },
-            send: (data: unknown) => {
-              outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
-            },
-            close: () => {
-              // no-op (simulate abrupt drop)
-            },
-          }
-
-          // Connect "a" first.
-          Pty.connect(a.id, ws as any)
-          outA.length = 0
-
-          // Simulate Bun reusing the same websocket object for another
-          // connection before the next onOpen calls Pty.connect.
-          ws.data = { events: { connection: "b" } }
-          ws.send = (data: unknown) => {
-            outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
-          }
-
-          Pty.write(a.id, "AAA\n")
-          await sleep(100)
-
-          expect(outB.join("")).not.toContain("AAA")
-        } finally {
-          await Pty.remove(a.id)
-        }
-      },
+      fn: () =>
+        AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            const a = yield* pty.create({ command: "cat", title: "a" })
+            try {
+              const outA: string[] = []
+              const outB: string[] = []
+
+              const ws = {
+                readyState: 1,
+                data: { events: { connection: "a" } },
+                send: (data: unknown) => {
+                  outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
+                },
+                close: () => {
+                  // no-op (simulate abrupt drop)
+                },
+              }
+
+              yield* pty.connect(a.id, ws as any)
+              outA.length = 0
+
+              ws.data = { events: { connection: "b" } }
+              ws.send = (data: unknown) => {
+                outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
+              }
+
+              yield* pty.write(a.id, "AAA\n")
+              yield* Effect.promise(() => sleep(100))
+
+              expect(outB.join("")).not.toContain("AAA")
+            } finally {
+              yield* pty.remove(a.id)
+            }
+          }),
+        ),
     })
   })
 
@@ -104,38 +107,40 @@ describe("pty", () => {
 
     await Instance.provide({
       directory: dir.path,
-      fn: async () => {
-        const a = await Pty.create({ command: "cat", title: "a" })
-        try {
-          const out: string[] = []
-
-          const ctx = { connId: 1 }
-          const ws = {
-            readyState: 1,
-            data: ctx,
-            send: (data: unknown) => {
-              out.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
-            },
-            close: () => {
-              // no-op
-            },
-          }
-
-          Pty.connect(a.id, ws as any)
-          out.length = 0
-
-          // Mutating fields on ws.data should not look like a new
-          // connection lifecycle when the object identity stays stable.
-          ctx.connId = 2
-
-          Pty.write(a.id, "AAA\n")
-          await sleep(100)
-
-          expect(out.join("")).toContain("AAA")
-        } finally {
-          await Pty.remove(a.id)
-        }
-      },
+      fn: () =>
+        AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            const a = yield* pty.create({ command: "cat", title: "a" })
+            try {
+              const out: string[] = []
+
+              const ctx = { connId: 1 }
+              const ws = {
+                readyState: 1,
+                data: ctx,
+                send: (data: unknown) => {
+                  out.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
+                },
+                close: () => {
+                  // no-op
+                },
+              }
+
+              yield* pty.connect(a.id, ws as any)
+              out.length = 0
+
+              ctx.connId = 2
+
+              yield* pty.write(a.id, "AAA\n")
+              yield* Effect.promise(() => sleep(100))
+
+              expect(out.join("")).toContain("AAA")
+            } finally {
+              yield* pty.remove(a.id)
+            }
+          }),
+        ),
     })
   })
 })

+ 54 - 44
packages/opencode/test/pty/pty-session.test.ts

@@ -1,5 +1,7 @@
 import { describe, expect, test } from "bun:test"
+import { AppRuntime } from "../../src/effect/app-runtime"
 import { Bus } from "../../src/bus"
+import { Effect } from "effect"
 import { Instance } from "../../src/project/instance"
 import { Pty } from "../../src/pty"
 import type { PtyID } from "../../src/pty/schema"
@@ -27,33 +29,37 @@ describe("pty", () => {
 
     await Instance.provide({
       directory: dir.path,
-      fn: async () => {
-        const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
-        const off = [
-          Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
-          Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
-          Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
-        ]
+      fn: () =>
+        AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
+            const off = [
+              Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
+              Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
+              Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
+            ]
 
-        let id: PtyID | undefined
-        try {
-          const info = await Pty.create({
-            command: "/usr/bin/env",
-            args: ["sh", "-c", "sleep 0.1"],
-            title: "sleep",
-          })
-          id = info.id
+            let id: PtyID | undefined
+            try {
+              const info = yield* pty.create({
+                command: "/usr/bin/env",
+                args: ["sh", "-c", "sleep 0.1"],
+                title: "sleep",
+              })
+              id = info.id
 
-          await wait(() => pick(log, id!).includes("exited"))
+              yield* Effect.promise(() => wait(() => pick(log, id!).includes("exited")))
 
-          await Pty.remove(id)
-          await wait(() => pick(log, id!).length >= 3)
-          expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
-        } finally {
-          off.forEach((x) => x())
-          if (id) await Pty.remove(id)
-        }
-      },
+              yield* pty.remove(id)
+              yield* Effect.promise(() => wait(() => pick(log, id!).length >= 3))
+              expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
+            } finally {
+              off.forEach((x) => x())
+              if (id) yield* pty.remove(id)
+            }
+          }),
+        ),
     })
   })
 
@@ -64,29 +70,33 @@ describe("pty", () => {
 
     await Instance.provide({
       directory: dir.path,
-      fn: async () => {
-        const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
-        const off = [
-          Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
-          Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
-          Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
-        ]
+      fn: () =>
+        AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const pty = yield* Pty.Service
+            const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
+            const off = [
+              Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
+              Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
+              Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
+            ]
 
-        let id: PtyID | undefined
-        try {
-          const info = await Pty.create({ command: "/bin/sh", title: "sh" })
-          id = info.id
+            let id: PtyID | undefined
+            try {
+              const info = yield* pty.create({ command: "/bin/sh", title: "sh" })
+              id = info.id
 
-          await sleep(100)
+              yield* Effect.promise(() => sleep(100))
 
-          await Pty.remove(id)
-          await wait(() => pick(log, id!).length >= 3)
-          expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
-        } finally {
-          off.forEach((x) => x())
-          if (id) await Pty.remove(id)
-        }
-      },
+              yield* pty.remove(id)
+              yield* Effect.promise(() => wait(() => pick(log, id!).length >= 3))
+              expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
+            } finally {
+              off.forEach((x) => x())
+              if (id) yield* pty.remove(id)
+            }
+          }),
+        ),
     })
   })
 })

+ 26 - 16
packages/opencode/test/pty/pty-shell.test.ts

@@ -1,4 +1,6 @@
 import { describe, expect, test } from "bun:test"
+import { AppRuntime } from "../../src/effect/app-runtime"
+import { Effect } from "effect"
 import { Instance } from "../../src/project/instance"
 import { Pty } from "../../src/pty"
 import { Shell } from "../../src/shell/shell"
@@ -17,14 +19,18 @@ describe("pty shell args", () => {
         await using dir = await tmpdir()
         await Instance.provide({
           directory: dir.path,
-          fn: async () => {
-            const info = await Pty.create({ command: ps, title: "pwsh" })
-            try {
-              expect(info.args).toEqual([])
-            } finally {
-              await Pty.remove(info.id)
-            }
-          },
+          fn: () =>
+            AppRuntime.runPromise(
+              Effect.gen(function* () {
+                const pty = yield* Pty.Service
+                const info = yield* pty.create({ command: ps, title: "pwsh" })
+                try {
+                  expect(info.args).toEqual([])
+                } finally {
+                  yield* pty.remove(info.id)
+                }
+              }),
+            ),
         })
       },
       { timeout: 30000 },
@@ -43,14 +49,18 @@ describe("pty shell args", () => {
         await using dir = await tmpdir()
         await Instance.provide({
           directory: dir.path,
-          fn: async () => {
-            const info = await Pty.create({ command: bash, title: "bash" })
-            try {
-              expect(info.args).toEqual(["-l"])
-            } finally {
-              await Pty.remove(info.id)
-            }
-          },
+          fn: () =>
+            AppRuntime.runPromise(
+              Effect.gen(function* () {
+                const pty = yield* Pty.Service
+                const info = yield* pty.create({ command: bash, title: "bash" })
+                try {
+                  expect(info.args).toEqual(["-l"])
+                } finally {
+                  yield* pty.remove(info.id)
+                }
+              }),
+            ),
         })
       },
       { timeout: 30000 },

+ 53 - 0
packages/opencode/test/session/instruction.test.ts

@@ -219,6 +219,59 @@ describe("Instruction.resolve", () => {
   test.todo("fetches remote instructions from config URLs via HttpClient", () => {})
 })
 
+describe("Instruction.system", () => {
+  test("loads both project and global AGENTS.md when both exist", async () => {
+    const originalConfigDir = process.env["OPENCODE_CONFIG_DIR"]
+    delete process.env["OPENCODE_CONFIG_DIR"]
+
+    await using globalTmp = await tmpdir({
+      init: async (dir) => {
+        await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
+      },
+    })
+    await using projectTmp = await tmpdir({
+      init: async (dir) => {
+        await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions")
+      },
+    })
+
+    const originalGlobalConfig = Global.Path.config
+    ;(Global.Path as { config: string }).config = globalTmp.path
+
+    try {
+      await Instance.provide({
+        directory: projectTmp.path,
+        fn: () =>
+          run(
+            Instruction.Service.use((svc) =>
+              Effect.gen(function* () {
+                const paths = yield* svc.systemPaths()
+                expect(paths.has(path.join(projectTmp.path, "AGENTS.md"))).toBe(true)
+                expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
+
+                const rules = yield* svc.system()
+                expect(rules).toHaveLength(2)
+                expect(rules).toContain(
+                  `Instructions from: ${path.join(projectTmp.path, "AGENTS.md")}\n# Project Instructions`,
+                )
+                expect(rules).toContain(
+                  `Instructions from: ${path.join(globalTmp.path, "AGENTS.md")}\n# Global Instructions`,
+                )
+              }),
+            ),
+          ),
+      })
+    } finally {
+      ;(Global.Path as { config: string }).config = originalGlobalConfig
+      if (originalConfigDir === undefined) {
+        delete process.env["OPENCODE_CONFIG_DIR"]
+      } else {
+        process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir
+      }
+    }
+  })
+})
+
 describe("Instruction.systemPaths OPENCODE_CONFIG_DIR", () => {
   let originalConfigDir: string | undefined
 

+ 19 - 9
packages/opencode/test/session/llm.test.ts

@@ -1,7 +1,7 @@
 import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
 import path from "path"
 import { tool, type ModelMessage } from "ai"
-import { Cause, Exit, Stream } from "effect"
+import { Cause, Effect, Exit, Stream } from "effect"
 import z from "zod"
 import { makeRuntime } from "../../src/effect/run-service"
 import { LLM } from "../../src/session/llm"
@@ -15,6 +15,16 @@ import { tmpdir } from "../fixture/fixture"
 import type { Agent } from "../../src/agent/agent"
 import type { MessageV2 } from "../../src/session/message-v2"
 import { SessionID, MessageID } from "../../src/session/schema"
+import { AppRuntime } from "../../src/effect/app-runtime"
+
+async function getModel(providerID: ProviderID, modelID: ModelID) {
+  return AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const provider = yield* Provider.Service
+      return yield* provider.getModel(providerID, modelID)
+    }),
+  )
+}
 
 describe("session.llm.hasToolCalls", () => {
   test("returns false for empty messages array", () => {
@@ -325,7 +335,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-1")
         const agent = {
           name: "test",
@@ -416,7 +426,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-raw-abort")
         const agent = {
           name: "test",
@@ -490,7 +500,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-service-abort")
         const agent = {
           name: "test",
@@ -581,7 +591,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-tools")
         const agent = {
           name: "test",
@@ -699,7 +709,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.openai, ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-2")
         const agent = {
           name: "test",
@@ -819,7 +829,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.openai, ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-data-url")
         const agent = {
           name: "test",
@@ -942,7 +952,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-3")
         const agent = {
           name: "test",
@@ -1043,7 +1053,7 @@ describe("session.llm.stream", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
         const sessionID = SessionID.make("session-test-4")
         const agent = {
           name: "test",

+ 1 - 1
packages/opencode/test/session/prompt-effect.test.ts

@@ -204,7 +204,7 @@ const it = testEffect(makeHttp())
 const unix = process.platform !== "win32" ? it.live : it.live.skip
 
 // Config that registers a custom "test" provider with a "test-model" model
-// so Provider.getModel("test", "test-model") succeeds inside the loop.
+// so provider model lookup succeeds inside the loop.
 const cfg = {
   provider: {
     test: {

+ 271 - 272
packages/opencode/test/skill/skill.test.ts

@@ -1,13 +1,15 @@
-import { afterEach, test, expect } from "bun:test"
+import { describe, expect } from "bun:test"
+import { Effect, Layer } from "effect"
 import { Skill } from "../../src/skill"
-import { Instance } from "../../src/project/instance"
-import { tmpdir } from "../fixture/fixture"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 import path from "path"
 import fs from "fs/promises"
 
-afterEach(async () => {
-  await Instance.disposeAll()
-})
+const node = CrossSpawnSpawner.defaultLayer
+
+const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node))
 
 async function createGlobalSkill(homeDir: string) {
   const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill")
@@ -26,14 +28,29 @@ This skill is loaded from the global home directory.
   )
 }
 
-test("discovers skills from .opencode/skill/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".opencode", "skill", "test-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+const withHome = <A, E, R>(home: string, self: Effect.Effect<A, E, R>) =>
+  Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const prev = process.env.OPENCODE_TEST_HOME
+      process.env.OPENCODE_TEST_HOME = home
+      return prev
+    }),
+    () => self,
+    (prev) =>
+      Effect.sync(() => {
+        process.env.OPENCODE_TEST_HOME = prev
+      }),
+  )
+
+describe("skill", () => {
+  it.live("discovers skills from .opencode/skill/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".opencode", "skill", "test-skill", "SKILL.md"),
+              `---
 name: test-skill
 description: A test skill for verification.
 ---
@@ -42,230 +59,217 @@ description: A test skill for verification.
 
 Instructions here.
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(1)
-      const testSkill = skills.find((s) => s.name === "test-skill")
-      expect(testSkill).toBeDefined()
-      expect(testSkill!.description).toBe("A test skill for verification.")
-      expect(testSkill!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
-    },
-  })
-})
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(1)
+          const item = list.find((x) => x.name === "test-skill")
+          expect(item).toBeDefined()
+          expect(item!.description).toBe("A test skill for verification.")
+          expect(item!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
+        }),
+      { git: true },
+    ),
+  )
 
-test("returns skill directories from Skill.dirs", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".opencode", "skill", "dir-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+  it.live("returns skill directories from Skill.dirs", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        withHome(
+          dir,
+          Effect.gen(function* () {
+            yield* Effect.promise(() =>
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "dir-skill", "SKILL.md"),
+                `---
 name: dir-skill
 description: Skill for dirs test.
 ---
 
 # Dir Skill
 `,
-      )
-    },
-  })
-
-  const home = process.env.OPENCODE_TEST_HOME
-  process.env.OPENCODE_TEST_HOME = tmp.path
-
-  try {
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const dirs = await Skill.dirs()
-        const skillDir = path.join(tmp.path, ".opencode", "skill", "dir-skill")
-        expect(dirs).toContain(skillDir)
-        expect(dirs.length).toBe(1)
-      },
-    })
-  } finally {
-    process.env.OPENCODE_TEST_HOME = home
-  }
-})
+              ),
+            )
+
+            const skill = yield* Skill.Service
+            const dirs = yield* skill.dirs()
+            expect(dirs).toContain(path.join(dir, ".opencode", "skill", "dir-skill"))
+            expect(dirs.length).toBe(1)
+          }),
+        ),
+      { git: true },
+    ),
+  )
 
-test("discovers multiple skills from .opencode/skill/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir1 = path.join(dir, ".opencode", "skill", "skill-one")
-      const skillDir2 = path.join(dir, ".opencode", "skill", "skill-two")
-      await Bun.write(
-        path.join(skillDir1, "SKILL.md"),
-        `---
+  it.live("discovers multiple skills from .opencode/skill/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Promise.all([
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "skill-one", "SKILL.md"),
+                `---
 name: skill-one
 description: First test skill.
 ---
 
 # Skill One
 `,
-      )
-      await Bun.write(
-        path.join(skillDir2, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "skill-two", "SKILL.md"),
+                `---
 name: skill-two
 description: Second test skill.
 ---
 
 # Skill Two
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(2)
-      expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
-      expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
-    },
-  })
-})
+              ),
+            ]),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(2)
+          expect(list.find((x) => x.name === "skill-one")).toBeDefined()
+          expect(list.find((x) => x.name === "skill-two")).toBeDefined()
+        }),
+      { git: true },
+    ),
+  )
 
-test("skips skills with missing frontmatter", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `# No Frontmatter
+  it.live("skips skills with missing frontmatter", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".opencode", "skill", "no-frontmatter", "SKILL.md"),
+              `# No Frontmatter
 
 Just some content without YAML frontmatter.
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills).toEqual([])
-    },
-  })
-})
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          expect(yield* skill.all()).toEqual([])
+        }),
+      { git: true },
+    ),
+  )
 
-test("discovers skills from .claude/skills/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+  it.live("discovers skills from .claude/skills/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
+              `---
 name: claude-skill
 description: A skill in the .claude/skills directory.
 ---
 
 # Claude Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(1)
-      const claudeSkill = skills.find((s) => s.name === "claude-skill")
-      expect(claudeSkill).toBeDefined()
-      expect(claudeSkill!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md"))
-    },
-  })
-})
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(1)
+          const item = list.find((x) => x.name === "claude-skill")
+          expect(item).toBeDefined()
+          expect(item!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md"))
+        }),
+      { git: true },
+    ),
+  )
 
-test("discovers global skills from ~/.claude/skills/ directory", async () => {
-  await using tmp = await tmpdir({ git: true })
-
-  const originalHome = process.env.OPENCODE_TEST_HOME
-  process.env.OPENCODE_TEST_HOME = tmp.path
-
-  try {
-    await createGlobalSkill(tmp.path)
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const skills = await Skill.all()
-        expect(skills.length).toBe(1)
-        expect(skills[0].name).toBe("global-test-skill")
-        expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
-        expect(skills[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md"))
-      },
-    })
-  } finally {
-    process.env.OPENCODE_TEST_HOME = originalHome
-  }
-})
+  it.live("discovers global skills from ~/.claude/skills/ directory", () =>
+    Effect.gen(function* () {
+      const tmp = yield* Effect.acquireRelease(
+        Effect.promise(() => tmpdir({ git: true })),
+        (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+      )
 
-test("returns empty array when no skills exist", async () => {
-  await using tmp = await tmpdir({ git: true })
+      yield* withHome(
+        tmp.path,
+        Effect.gen(function* () {
+          yield* Effect.promise(() => createGlobalSkill(tmp.path))
+          yield* Effect.gen(function* () {
+            const skill = yield* Skill.Service
+            const list = yield* skill.all()
+            expect(list.length).toBe(1)
+            expect(list[0].name).toBe("global-test-skill")
+            expect(list[0].description).toBe("A global skill from ~/.claude/skills for testing.")
+            expect(list[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md"))
+          }).pipe(provideInstance(tmp.path))
+        }),
+      )
+    }),
+  )
 
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills).toEqual([])
-    },
-  })
-})
+  it.live("returns empty array when no skills exist", () =>
+    provideTmpdirInstance(
+      () =>
+        Effect.gen(function* () {
+          const skill = yield* Skill.Service
+          expect(yield* skill.all()).toEqual([])
+        }),
+      { git: true },
+    ),
+  )
 
-test("discovers skills from .agents/skills/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".agents", "skills", "agent-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+  it.live("discovers skills from .agents/skills/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
+              `---
 name: agent-skill
 description: A skill in the .agents/skills directory.
 ---
 
 # Agent Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(1)
-      const agentSkill = skills.find((s) => s.name === "agent-skill")
-      expect(agentSkill).toBeDefined()
-      expect(agentSkill!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md"))
-    },
-  })
-})
-
-test("discovers global skills from ~/.agents/skills/ directory", async () => {
-  await using tmp = await tmpdir({ git: true })
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(1)
+          const item = list.find((x) => x.name === "agent-skill")
+          expect(item).toBeDefined()
+          expect(item!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md"))
+        }),
+      { git: true },
+    ),
+  )
 
-  const originalHome = process.env.OPENCODE_TEST_HOME
-  process.env.OPENCODE_TEST_HOME = tmp.path
+  it.live("discovers global skills from ~/.agents/skills/ directory", () =>
+    Effect.gen(function* () {
+      const tmp = yield* Effect.acquireRelease(
+        Effect.promise(() => tmpdir({ git: true })),
+        (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+      )
 
-  try {
-    const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
-    await fs.mkdir(skillDir, { recursive: true })
-    await Bun.write(
-      path.join(skillDir, "SKILL.md"),
-      `---
+      yield* withHome(
+        tmp.path,
+        Effect.gen(function* () {
+          const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
+          yield* Effect.promise(() => fs.mkdir(skillDir, { recursive: true }))
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(skillDir, "SKILL.md"),
+              `---
 name: global-agent-skill
 description: A global skill from ~/.agents/skills for testing.
 ---
@@ -274,119 +278,114 @@ description: A global skill from ~/.agents/skills for testing.
 
 This skill is loaded from the global home directory.
 `,
-    )
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const skills = await Skill.all()
-        expect(skills.length).toBe(1)
-        expect(skills[0].name).toBe("global-agent-skill")
-        expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.")
-        expect(skills[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md"))
-      },
-    })
-  } finally {
-    process.env.OPENCODE_TEST_HOME = originalHome
-  }
-})
+            ),
+          )
+
+          yield* Effect.gen(function* () {
+            const skill = yield* Skill.Service
+            const list = yield* skill.all()
+            expect(list.length).toBe(1)
+            expect(list[0].name).toBe("global-agent-skill")
+            expect(list[0].description).toBe("A global skill from ~/.agents/skills for testing.")
+            expect(list[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md"))
+          }).pipe(provideInstance(tmp.path))
+        }),
+      )
+    }),
+  )
 
-test("discovers skills from both .claude/skills/ and .agents/skills/", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const claudeDir = path.join(dir, ".claude", "skills", "claude-skill")
-      const agentDir = path.join(dir, ".agents", "skills", "agent-skill")
-      await Bun.write(
-        path.join(claudeDir, "SKILL.md"),
-        `---
+  it.live("discovers skills from both .claude/skills/ and .agents/skills/", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Promise.all([
+              Bun.write(
+                path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
+                `---
 name: claude-skill
 description: A skill in the .claude/skills directory.
 ---
 
 # Claude Skill
 `,
-      )
-      await Bun.write(
-        path.join(agentDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
+                `---
 name: agent-skill
 description: A skill in the .agents/skills directory.
 ---
 
 # Agent Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(2)
-      expect(skills.find((s) => s.name === "claude-skill")).toBeDefined()
-      expect(skills.find((s) => s.name === "agent-skill")).toBeDefined()
-    },
-  })
-})
+              ),
+            ]),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(2)
+          expect(list.find((x) => x.name === "claude-skill")).toBeDefined()
+          expect(list.find((x) => x.name === "agent-skill")).toBeDefined()
+        }),
+      { git: true },
+    ),
+  )
 
-test("properly resolves directories that skills live in", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const opencodeSkillDir = path.join(dir, ".opencode", "skill", "agent-skill")
-      const opencodeSkillsDir = path.join(dir, ".opencode", "skills", "agent-skill")
-      const claudeDir = path.join(dir, ".claude", "skills", "claude-skill")
-      const agentDir = path.join(dir, ".agents", "skills", "agent-skill")
-      await Bun.write(
-        path.join(claudeDir, "SKILL.md"),
-        `---
+  it.live("properly resolves directories that skills live in", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Promise.all([
+              Bun.write(
+                path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
+                `---
 name: claude-skill
 description: A skill in the .claude/skills directory.
 ---
 
 # Claude Skill
 `,
-      )
-      await Bun.write(
-        path.join(agentDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
+                `---
 name: agent-skill
 description: A skill in the .agents/skills directory.
 ---
 
 # Agent Skill
 `,
-      )
-      await Bun.write(
-        path.join(opencodeSkillDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "agent-skill", "SKILL.md"),
+                `---
 name: opencode-skill
 description: A skill in the .opencode/skill directory.
 ---
 
 # OpenCode Skill
 `,
-      )
-      await Bun.write(
-        path.join(opencodeSkillsDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".opencode", "skills", "agent-skill", "SKILL.md"),
+                `---
 name: opencode-skill
 description: A skill in the .opencode/skills directory.
 ---
 
 # OpenCode Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const dirs = await Skill.dirs()
-      expect(dirs.length).toBe(4)
-    },
-  })
+              ),
+            ]),
+          )
+
+          const skill = yield* Skill.Service
+          expect((yield* skill.dirs()).length).toBe(4)
+        }),
+      { git: true },
+    ),
+  )
 })

+ 128 - 133
packages/opencode/test/tool/registry.test.ts

@@ -1,157 +1,152 @@
-import { afterEach, describe, expect, test } from "bun:test"
+import { afterEach, describe, expect } from "bun:test"
 import path from "path"
 import fs from "fs/promises"
-import { tmpdir } from "../fixture/fixture"
+import { Effect, Layer } from "effect"
 import { Instance } from "../../src/project/instance"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { ToolRegistry } from "../../src/tool/registry"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+const node = CrossSpawnSpawner.defaultLayer
+
+const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
 
 afterEach(async () => {
   await Instance.disposeAll()
 })
 
 describe("tool.registry", () => {
-  test("loads tools from .opencode/tool (singular)", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        const opencodeDir = path.join(dir, ".opencode")
-        await fs.mkdir(opencodeDir, { recursive: true })
-
-        const toolDir = path.join(opencodeDir, "tool")
-        await fs.mkdir(toolDir, { recursive: true })
-
-        await Bun.write(
-          path.join(toolDir, "hello.ts"),
-          [
-            "export default {",
-            "  description: 'hello tool',",
-            "  args: {},",
-            "  execute: async () => {",
-            "    return 'hello world'",
-            "  },",
-            "}",
-            "",
-          ].join("\n"),
+  it.live("loads tools from .opencode/tool (singular)", () =>
+    provideTmpdirInstance((dir) =>
+      Effect.gen(function* () {
+        const opencode = path.join(dir, ".opencode")
+        const tool = path.join(opencode, "tool")
+        yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(tool, "hello.ts"),
+            [
+              "export default {",
+              "  description: 'hello tool',",
+              "  args: {},",
+              "  execute: async () => {",
+              "    return 'hello world'",
+              "  },",
+              "}",
+              "",
+            ].join("\n"),
+          ),
         )
-      },
-    })
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const ids = await ToolRegistry.ids()
+        const registry = yield* ToolRegistry.Service
+        const ids = yield* registry.ids()
         expect(ids).toContain("hello")
-      },
-    })
-  })
-
-  test("loads tools from .opencode/tools (plural)", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        const opencodeDir = path.join(dir, ".opencode")
-        await fs.mkdir(opencodeDir, { recursive: true })
-
-        const toolsDir = path.join(opencodeDir, "tools")
-        await fs.mkdir(toolsDir, { recursive: true })
-
-        await Bun.write(
-          path.join(toolsDir, "hello.ts"),
-          [
-            "export default {",
-            "  description: 'hello tool',",
-            "  args: {},",
-            "  execute: async () => {",
-            "    return 'hello world'",
-            "  },",
-            "}",
-            "",
-          ].join("\n"),
+      }),
+    ),
+  )
+
+  it.live("loads tools from .opencode/tools (plural)", () =>
+    provideTmpdirInstance((dir) =>
+      Effect.gen(function* () {
+        const opencode = path.join(dir, ".opencode")
+        const tools = path.join(opencode, "tools")
+        yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(tools, "hello.ts"),
+            [
+              "export default {",
+              "  description: 'hello tool',",
+              "  args: {},",
+              "  execute: async () => {",
+              "    return 'hello world'",
+              "  },",
+              "}",
+              "",
+            ].join("\n"),
+          ),
         )
-      },
-    })
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const ids = await ToolRegistry.ids()
+        const registry = yield* ToolRegistry.Service
+        const ids = yield* registry.ids()
         expect(ids).toContain("hello")
-      },
-    })
-  })
-
-  test("loads tools with external dependencies without crashing", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        const opencodeDir = path.join(dir, ".opencode")
-        await fs.mkdir(opencodeDir, { recursive: true })
-
-        const toolsDir = path.join(opencodeDir, "tools")
-        await fs.mkdir(toolsDir, { recursive: true })
-
-        await Bun.write(
-          path.join(opencodeDir, "package.json"),
-          JSON.stringify({
-            name: "custom-tools",
-            dependencies: {
-              "@opencode-ai/plugin": "^0.0.0",
-              cowsay: "^1.6.0",
-            },
-          }),
+      }),
+    ),
+  )
+
+  it.live("loads tools with external dependencies without crashing", () =>
+    provideTmpdirInstance((dir) =>
+      Effect.gen(function* () {
+        const opencode = path.join(dir, ".opencode")
+        const tools = path.join(opencode, "tools")
+        yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(opencode, "package.json"),
+            JSON.stringify({
+              name: "custom-tools",
+              dependencies: {
+                "@opencode-ai/plugin": "^0.0.0",
+                cowsay: "^1.6.0",
+              },
+            }),
+          ),
         )
-
-        await Bun.write(
-          path.join(opencodeDir, "package-lock.json"),
-          JSON.stringify({
-            name: "custom-tools",
-            lockfileVersion: 3,
-            packages: {
-              "": {
-                dependencies: {
-                  "@opencode-ai/plugin": "^0.0.0",
-                  cowsay: "^1.6.0",
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(opencode, "package-lock.json"),
+            JSON.stringify({
+              name: "custom-tools",
+              lockfileVersion: 3,
+              packages: {
+                "": {
+                  dependencies: {
+                    "@opencode-ai/plugin": "^0.0.0",
+                    cowsay: "^1.6.0",
+                  },
                 },
               },
-            },
-          }),
+            }),
+          ),
         )
 
-        const cowsayDir = path.join(opencodeDir, "node_modules", "cowsay")
-        await fs.mkdir(cowsayDir, { recursive: true })
-        await Bun.write(
-          path.join(cowsayDir, "package.json"),
-          JSON.stringify({
-            name: "cowsay",
-            type: "module",
-            exports: "./index.js",
-          }),
+        const cowsay = path.join(opencode, "node_modules", "cowsay")
+        yield* Effect.promise(() => fs.mkdir(cowsay, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(cowsay, "package.json"),
+            JSON.stringify({
+              name: "cowsay",
+              type: "module",
+              exports: "./index.js",
+            }),
+          ),
         )
-        await Bun.write(
-          path.join(cowsayDir, "index.js"),
-          ["export function say({ text }) {", "  return `moo ${text}`", "}", ""].join("\n"),
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(cowsay, "index.js"),
+            ["export function say({ text }) {", "  return `moo ${text}`", "}", ""].join("\n"),
+          ),
         )
-
-        await Bun.write(
-          path.join(toolsDir, "cowsay.ts"),
-          [
-            "import { say } from 'cowsay'",
-            "export default {",
-            "  description: 'tool that imports cowsay at top level',",
-            "  args: { text: { type: 'string' } },",
-            "  execute: async ({ text }: { text: string }) => {",
-            "    return say({ text })",
-            "  },",
-            "}",
-            "",
-          ].join("\n"),
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(tools, "cowsay.ts"),
+            [
+              "import { say } from 'cowsay'",
+              "export default {",
+              "  description: 'tool that imports cowsay at top level',",
+              "  args: { text: { type: 'string' } },",
+              "  execute: async ({ text }: { text: string }) => {",
+              "    return say({ text })",
+              "  },",
+              "}",
+              "",
+            ].join("\n"),
+          ),
         )
-      },
-    })
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const ids = await ToolRegistry.ids()
+        const registry = yield* ToolRegistry.Service
+        const ids = yield* registry.ids()
         expect(ids).toContain("cowsay")
-      },
-    })
-  })
+      }),
+    ),
+  )
 })

+ 77 - 70
packages/opencode/test/tool/skill.test.ts

@@ -1,6 +1,7 @@
 import { Effect, Layer, ManagedRuntime } from "effect"
 import { Agent } from "../../src/agent/agent"
 import { Skill } from "../../src/skill"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { Truncate } from "../../src/tool/truncate"
 import { afterEach, describe, expect, test } from "bun:test"
@@ -11,8 +12,9 @@ import type { Tool } from "../../src/tool/tool"
 import { Instance } from "../../src/project/instance"
 import { SkillTool } from "../../src/tool/skill"
 import { ToolRegistry } from "../../src/tool/registry"
-import { tmpdir } from "../fixture/fixture"
+import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
 import { SessionID, MessageID } from "../../src/session/schema"
+import { testEffect } from "../lib/effect"
 
 const baseCtx: Omit<Tool.Context, "ask"> = {
   sessionID: SessionID.make("ses_test"),
@@ -28,85 +30,92 @@ afterEach(async () => {
   await Instance.disposeAll()
 })
 
+const node = CrossSpawnSpawner.defaultLayer
+
+const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
+
 describe("tool.skill", () => {
-  test("description lists skill location URL", async () => {
-    await using tmp = await tmpdir({
-      git: true,
-      init: async (dir) => {
-        const skillDir = path.join(dir, ".opencode", "skill", "tool-skill")
-        await Bun.write(
-          path.join(skillDir, "SKILL.md"),
-          `---
+  it.live("description lists skill location URL", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          const skill = path.join(dir, ".opencode", "skill", "tool-skill")
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(skill, "SKILL.md"),
+              `---
 name: tool-skill
 description: Skill for tool tests.
 ---
 
 # Tool Skill
 `,
-        )
-      },
-    })
-
-    const home = process.env.OPENCODE_TEST_HOME
-    process.env.OPENCODE_TEST_HOME = tmp.path
-
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          const desc = await ToolRegistry.tools({
-            providerID: "opencode" as any,
-            modelID: "gpt-5" as any,
-            agent: { name: "build", mode: "primary" as const, permission: [], options: {} },
-          }).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
-          expect(desc).toContain(`**tool-skill**: Skill for tool tests.`)
-        },
-      })
-    } finally {
-      process.env.OPENCODE_TEST_HOME = home
-    }
-  })
-
-  test("description sorts skills by name and is stable across calls", async () => {
-    await using tmp = await tmpdir({
-      git: true,
-      init: async (dir) => {
-        for (const [name, description] of [
-          ["zeta-skill", "Zeta skill."],
-          ["alpha-skill", "Alpha skill."],
-          ["middle-skill", "Middle skill."],
-        ]) {
-          const skillDir = path.join(dir, ".opencode", "skill", name)
-          await Bun.write(
-            path.join(skillDir, "SKILL.md"),
-            `---
+            ),
+          )
+          const home = process.env.OPENCODE_TEST_HOME
+          process.env.OPENCODE_TEST_HOME = dir
+          yield* Effect.addFinalizer(() =>
+            Effect.sync(() => {
+              process.env.OPENCODE_TEST_HOME = home
+            }),
+          )
+          const registry = yield* ToolRegistry.Service
+          const desc =
+            (yield* registry.tools({
+              providerID: "opencode" as any,
+              modelID: "gpt-5" as any,
+              agent: { name: "build", mode: "primary", permission: [], options: {} },
+            })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
+          expect(desc).toContain("**tool-skill**: Skill for tool tests.")
+        }),
+      { git: true },
+    ),
+  )
+
+  it.live("description sorts skills by name and is stable across calls", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          for (const [name, description] of [
+            ["zeta-skill", "Zeta skill."],
+            ["alpha-skill", "Alpha skill."],
+            ["middle-skill", "Middle skill."],
+          ]) {
+            const skill = path.join(dir, ".opencode", "skill", name)
+            yield* Effect.promise(() =>
+              Bun.write(
+                path.join(skill, "SKILL.md"),
+                `---
 name: ${name}
 description: ${description}
 ---
 
 # ${name}
 `,
+              ),
+            )
+          }
+          const home = process.env.OPENCODE_TEST_HOME
+          process.env.OPENCODE_TEST_HOME = dir
+          yield* Effect.addFinalizer(() =>
+            Effect.sync(() => {
+              process.env.OPENCODE_TEST_HOME = home
+            }),
           )
-        }
-      },
-    })
-
-    const home = process.env.OPENCODE_TEST_HOME
-    process.env.OPENCODE_TEST_HOME = tmp.path
 
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
           const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
-          const load = () =>
-            ToolRegistry.tools({
-              providerID: "opencode" as any,
-              modelID: "gpt-5" as any,
-              agent,
-            }).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
-          const first = await load()
-          const second = await load()
+          const registry = yield* ToolRegistry.Service
+          const load = Effect.fnUntraced(function* () {
+            return (
+              (yield* registry.tools({
+                providerID: "opencode" as any,
+                modelID: "gpt-5" as any,
+                agent,
+              })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
+            )
+          })
+          const first = yield* load()
+          const second = yield* load()
 
           expect(first).toBe(second)
 
@@ -117,12 +126,10 @@ description: ${description}
           expect(alpha).toBeGreaterThan(-1)
           expect(middle).toBeGreaterThan(alpha)
           expect(zeta).toBeGreaterThan(middle)
-        },
-      })
-    } finally {
-      process.env.OPENCODE_TEST_HOME = home
-    }
-  })
+        }),
+      { git: true },
+    ),
+  )
 
   test("execute returns skill content block with files", async () => {
     await using tmp = await tmpdir({

+ 119 - 119
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -33,6 +33,13 @@ export type EventProjectUpdated = {
   properties: Project
 }
 
+export type EventServerInstanceDisposed = {
+  type: "server.instance.disposed"
+  properties: {
+    directory: string
+  }
+}
+
 export type EventInstallationUpdated = {
   type: "installation.updated"
   properties: {
@@ -47,13 +54,6 @@ export type EventInstallationUpdateAvailable = {
   }
 }
 
-export type EventServerInstanceDisposed = {
-  type: "server.instance.disposed"
-  properties: {
-    directory: string
-  }
-}
-
 export type EventServerConnected = {
   type: "server.connected"
   properties: {
@@ -68,6 +68,21 @@ export type EventGlobalDisposed = {
   }
 }
 
+export type EventFileEdited = {
+  type: "file.edited"
+  properties: {
+    file: string
+  }
+}
+
+export type EventFileWatcherUpdated = {
+  type: "file.watcher.updated"
+  properties: {
+    file: string
+    event: "add" | "change" | "unlink"
+  }
+}
+
 export type EventLspClientDiagnostics = {
   type: "lsp.client.diagnostics"
   properties: {
@@ -215,107 +230,6 @@ export type EventSessionError = {
   }
 }
 
-export type EventFileEdited = {
-  type: "file.edited"
-  properties: {
-    file: string
-  }
-}
-
-export type EventFileWatcherUpdated = {
-  type: "file.watcher.updated"
-  properties: {
-    file: string
-    event: "add" | "change" | "unlink"
-  }
-}
-
-export type EventVcsBranchUpdated = {
-  type: "vcs.branch.updated"
-  properties: {
-    branch?: string
-  }
-}
-
-export type EventTuiPromptAppend = {
-  type: "tui.prompt.append"
-  properties: {
-    text: string
-  }
-}
-
-export type EventTuiCommandExecute = {
-  type: "tui.command.execute"
-  properties: {
-    command:
-      | "session.list"
-      | "session.new"
-      | "session.share"
-      | "session.interrupt"
-      | "session.compact"
-      | "session.page.up"
-      | "session.page.down"
-      | "session.line.up"
-      | "session.line.down"
-      | "session.half.page.up"
-      | "session.half.page.down"
-      | "session.first"
-      | "session.last"
-      | "prompt.clear"
-      | "prompt.submit"
-      | "agent.cycle"
-      | string
-  }
-}
-
-export type EventTuiToastShow = {
-  type: "tui.toast.show"
-  properties: {
-    title?: string
-    message: string
-    variant: "info" | "success" | "warning" | "error"
-    /**
-     * Duration in milliseconds
-     */
-    duration?: number
-  }
-}
-
-export type EventTuiSessionSelect = {
-  type: "tui.session.select"
-  properties: {
-    /**
-     * Session ID to navigate to
-     */
-    sessionID: string
-  }
-}
-
-export type EventMcpToolsChanged = {
-  type: "mcp.tools.changed"
-  properties: {
-    server: string
-  }
-}
-
-export type EventMcpBrowserOpenFailed = {
-  type: "mcp.browser.open.failed"
-  properties: {
-    mcpName: string
-    url: string
-  }
-}
-
-export type EventCommandExecuted = {
-  type: "command.executed"
-  properties: {
-    name: string
-    sessionID: string
-    arguments: string
-    messageID: string
-  }
-}
-
 export type QuestionOption = {
   /**
    * Display text (1-5 words, concise)
@@ -446,6 +360,92 @@ export type EventSessionCompacted = {
   }
 }
 
+export type EventTuiPromptAppend = {
+  type: "tui.prompt.append"
+  properties: {
+    text: string
+  }
+}
+
+export type EventTuiCommandExecute = {
+  type: "tui.command.execute"
+  properties: {
+    command:
+      | "session.list"
+      | "session.new"
+      | "session.share"
+      | "session.interrupt"
+      | "session.compact"
+      | "session.page.up"
+      | "session.page.down"
+      | "session.line.up"
+      | "session.line.down"
+      | "session.half.page.up"
+      | "session.half.page.down"
+      | "session.first"
+      | "session.last"
+      | "prompt.clear"
+      | "prompt.submit"
+      | "agent.cycle"
+      | string
+  }
+}
+
+export type EventTuiToastShow = {
+  type: "tui.toast.show"
+  properties: {
+    title?: string
+    message: string
+    variant: "info" | "success" | "warning" | "error"
+    /**
+     * Duration in milliseconds
+     */
+    duration?: number
+  }
+}
+
+export type EventTuiSessionSelect = {
+  type: "tui.session.select"
+  properties: {
+    /**
+     * Session ID to navigate to
+     */
+    sessionID: string
+  }
+}
+
+export type EventMcpToolsChanged = {
+  type: "mcp.tools.changed"
+  properties: {
+    server: string
+  }
+}
+
+export type EventMcpBrowserOpenFailed = {
+  type: "mcp.browser.open.failed"
+  properties: {
+    mcpName: string
+    url: string
+  }
+}
+
+export type EventCommandExecuted = {
+  type: "command.executed"
+  properties: {
+    name: string
+    sessionID: string
+    arguments: string
+    messageID: string
+  }
+}
+
+export type EventVcsBranchUpdated = {
+  type: "vcs.branch.updated"
+  properties: {
+    branch?: string
+  }
+}
+
 export type EventWorktreeReady = {
   type: "worktree.ready"
   properties: {
@@ -973,11 +973,13 @@ export type EventSessionDeleted = {
 
 export type Event =
   | EventProjectUpdated
+  | EventServerInstanceDisposed
   | EventInstallationUpdated
   | EventInstallationUpdateAvailable
-  | EventServerInstanceDisposed
   | EventServerConnected
   | EventGlobalDisposed
+  | EventFileEdited
+  | EventFileWatcherUpdated
   | EventLspClientDiagnostics
   | EventLspUpdated
   | EventMessagePartDelta
@@ -985,16 +987,6 @@ export type Event =
   | EventPermissionReplied
   | EventSessionDiff
   | EventSessionError
-  | EventFileEdited
-  | EventFileWatcherUpdated
-  | EventVcsBranchUpdated
-  | EventTuiPromptAppend
-  | EventTuiCommandExecute
-  | EventTuiToastShow
-  | EventTuiSessionSelect
-  | EventMcpToolsChanged
-  | EventMcpBrowserOpenFailed
-  | EventCommandExecuted
   | EventQuestionAsked
   | EventQuestionReplied
   | EventQuestionRejected
@@ -1002,6 +994,14 @@ export type Event =
   | EventSessionStatus
   | EventSessionIdle
   | EventSessionCompacted
+  | EventTuiPromptAppend
+  | EventTuiCommandExecute
+  | EventTuiToastShow
+  | EventTuiSessionSelect
+  | EventMcpToolsChanged
+  | EventMcpBrowserOpenFailed
+  | EventCommandExecuted
+  | EventVcsBranchUpdated
   | EventWorktreeReady
   | EventWorktreeFailed
   | EventPtyCreated

+ 294 - 294
packages/sdk/openapi.json

@@ -7230,31 +7230,31 @@
         },
         "required": ["type", "properties"]
       },
-      "Event.installation.updated": {
+      "Event.server.instance.disposed": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "installation.updated"
+            "const": "server.instance.disposed"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "version": {
+              "directory": {
                 "type": "string"
               }
             },
-            "required": ["version"]
+            "required": ["directory"]
           }
         },
         "required": ["type", "properties"]
       },
-      "Event.installation.update-available": {
+      "Event.installation.updated": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "installation.update-available"
+            "const": "installation.updated"
           },
           "properties": {
             "type": "object",
@@ -7268,21 +7268,21 @@
         },
         "required": ["type", "properties"]
       },
-      "Event.server.instance.disposed": {
+      "Event.installation.update-available": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "server.instance.disposed"
+            "const": "installation.update-available"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "directory": {
+              "version": {
                 "type": "string"
               }
             },
-            "required": ["directory"]
+            "required": ["version"]
           }
         },
         "required": ["type", "properties"]
@@ -7315,6 +7315,60 @@
         },
         "required": ["type", "properties"]
       },
+      "Event.file.edited": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "file.edited"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "file": {
+                "type": "string"
+              }
+            },
+            "required": ["file"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.file.watcher.updated": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "file.watcher.updated"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "file": {
+                "type": "string"
+              },
+              "event": {
+                "anyOf": [
+                  {
+                    "type": "string",
+                    "const": "add"
+                  },
+                  {
+                    "type": "string",
+                    "const": "change"
+                  },
+                  {
+                    "type": "string",
+                    "const": "unlink"
+                  }
+                ]
+              }
+            },
+            "required": ["file", "event"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
       "Event.lsp.client.diagnostics": {
         "type": "object",
         "properties": {
@@ -7724,267 +7778,9 @@
                   {
                     "$ref": "#/components/schemas/APIError"
                   }
-                ]
-              }
-            }
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.file.edited": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "file.edited"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "file": {
-                "type": "string"
-              }
-            },
-            "required": ["file"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.file.watcher.updated": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "file.watcher.updated"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "file": {
-                "type": "string"
-              },
-              "event": {
-                "anyOf": [
-                  {
-                    "type": "string",
-                    "const": "add"
-                  },
-                  {
-                    "type": "string",
-                    "const": "change"
-                  },
-                  {
-                    "type": "string",
-                    "const": "unlink"
-                  }
-                ]
-              }
-            },
-            "required": ["file", "event"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.vcs.branch.updated": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "vcs.branch.updated"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "branch": {
-                "type": "string"
-              }
-            }
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.tui.prompt.append": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "tui.prompt.append"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "text": {
-                "type": "string"
-              }
-            },
-            "required": ["text"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.tui.command.execute": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "tui.command.execute"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "anyOf": [
-                  {
-                    "type": "string",
-                    "enum": [
-                      "session.list",
-                      "session.new",
-                      "session.share",
-                      "session.interrupt",
-                      "session.compact",
-                      "session.page.up",
-                      "session.page.down",
-                      "session.line.up",
-                      "session.line.down",
-                      "session.half.page.up",
-                      "session.half.page.down",
-                      "session.first",
-                      "session.last",
-                      "prompt.clear",
-                      "prompt.submit",
-                      "agent.cycle"
-                    ]
-                  },
-                  {
-                    "type": "string"
-                  }
-                ]
-              }
-            },
-            "required": ["command"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.tui.toast.show": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "tui.toast.show"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "title": {
-                "type": "string"
-              },
-              "message": {
-                "type": "string"
-              },
-              "variant": {
-                "type": "string",
-                "enum": ["info", "success", "warning", "error"]
-              },
-              "duration": {
-                "description": "Duration in milliseconds",
-                "default": 5000,
-                "type": "number"
-              }
-            },
-            "required": ["message", "variant"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.tui.session.select": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "tui.session.select"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "sessionID": {
-                "description": "Session ID to navigate to",
-                "type": "string",
-                "pattern": "^ses.*"
-              }
-            },
-            "required": ["sessionID"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.mcp.tools.changed": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "mcp.tools.changed"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "server": {
-                "type": "string"
-              }
-            },
-            "required": ["server"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.mcp.browser.open.failed": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "mcp.browser.open.failed"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "mcpName": {
-                "type": "string"
-              },
-              "url": {
-                "type": "string"
-              }
-            },
-            "required": ["mcpName", "url"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "Event.command.executed": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "command.executed"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "name": {
-                "type": "string"
-              },
-              "sessionID": {
-                "type": "string",
-                "pattern": "^ses.*"
-              },
-              "arguments": {
-                "type": "string"
-              },
-              "messageID": {
-                "type": "string",
-                "pattern": "^msg.*"
+                ]
               }
-            },
-            "required": ["name", "sessionID", "arguments", "messageID"]
+            }
           }
         },
         "required": ["type", "properties"]
@@ -8289,6 +8085,210 @@
         },
         "required": ["type", "properties"]
       },
+      "Event.tui.prompt.append": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "tui.prompt.append"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "text": {
+                "type": "string"
+              }
+            },
+            "required": ["text"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.tui.command.execute": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "tui.command.execute"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "command": {
+                "anyOf": [
+                  {
+                    "type": "string",
+                    "enum": [
+                      "session.list",
+                      "session.new",
+                      "session.share",
+                      "session.interrupt",
+                      "session.compact",
+                      "session.page.up",
+                      "session.page.down",
+                      "session.line.up",
+                      "session.line.down",
+                      "session.half.page.up",
+                      "session.half.page.down",
+                      "session.first",
+                      "session.last",
+                      "prompt.clear",
+                      "prompt.submit",
+                      "agent.cycle"
+                    ]
+                  },
+                  {
+                    "type": "string"
+                  }
+                ]
+              }
+            },
+            "required": ["command"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.tui.toast.show": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "tui.toast.show"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "title": {
+                "type": "string"
+              },
+              "message": {
+                "type": "string"
+              },
+              "variant": {
+                "type": "string",
+                "enum": ["info", "success", "warning", "error"]
+              },
+              "duration": {
+                "description": "Duration in milliseconds",
+                "default": 5000,
+                "type": "number"
+              }
+            },
+            "required": ["message", "variant"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.tui.session.select": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "tui.session.select"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "sessionID": {
+                "description": "Session ID to navigate to",
+                "type": "string",
+                "pattern": "^ses.*"
+              }
+            },
+            "required": ["sessionID"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.mcp.tools.changed": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "mcp.tools.changed"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "server": {
+                "type": "string"
+              }
+            },
+            "required": ["server"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.mcp.browser.open.failed": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "mcp.browser.open.failed"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "mcpName": {
+                "type": "string"
+              },
+              "url": {
+                "type": "string"
+              }
+            },
+            "required": ["mcpName", "url"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.command.executed": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "command.executed"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "name": {
+                "type": "string"
+              },
+              "sessionID": {
+                "type": "string",
+                "pattern": "^ses.*"
+              },
+              "arguments": {
+                "type": "string"
+              },
+              "messageID": {
+                "type": "string",
+                "pattern": "^msg.*"
+              }
+            },
+            "required": ["name", "sessionID", "arguments", "messageID"]
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "Event.vcs.branch.updated": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "vcs.branch.updated"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "branch": {
+                "type": "string"
+              }
+            }
+          }
+        },
+        "required": ["type", "properties"]
+      },
       "Event.worktree.ready": {
         "type": "object",
         "properties": {
@@ -9875,13 +9875,13 @@
             "$ref": "#/components/schemas/Event.project.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.installation.updated"
+            "$ref": "#/components/schemas/Event.server.instance.disposed"
           },
           {
-            "$ref": "#/components/schemas/Event.installation.update-available"
+            "$ref": "#/components/schemas/Event.installation.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.server.instance.disposed"
+            "$ref": "#/components/schemas/Event.installation.update-available"
           },
           {
             "$ref": "#/components/schemas/Event.server.connected"
@@ -9889,6 +9889,12 @@
           {
             "$ref": "#/components/schemas/Event.global.disposed"
           },
+          {
+            "$ref": "#/components/schemas/Event.file.edited"
+          },
+          {
+            "$ref": "#/components/schemas/Event.file.watcher.updated"
+          },
           {
             "$ref": "#/components/schemas/Event.lsp.client.diagnostics"
           },
@@ -9911,55 +9917,49 @@
             "$ref": "#/components/schemas/Event.session.error"
           },
           {
-            "$ref": "#/components/schemas/Event.file.edited"
-          },
-          {
-            "$ref": "#/components/schemas/Event.file.watcher.updated"
-          },
-          {
-            "$ref": "#/components/schemas/Event.vcs.branch.updated"
+            "$ref": "#/components/schemas/Event.question.asked"
           },
           {
-            "$ref": "#/components/schemas/Event.tui.prompt.append"
+            "$ref": "#/components/schemas/Event.question.replied"
           },
           {
-            "$ref": "#/components/schemas/Event.tui.command.execute"
+            "$ref": "#/components/schemas/Event.question.rejected"
           },
           {
-            "$ref": "#/components/schemas/Event.tui.toast.show"
+            "$ref": "#/components/schemas/Event.todo.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.tui.session.select"
+            "$ref": "#/components/schemas/Event.session.status"
           },
           {
-            "$ref": "#/components/schemas/Event.mcp.tools.changed"
+            "$ref": "#/components/schemas/Event.session.idle"
           },
           {
-            "$ref": "#/components/schemas/Event.mcp.browser.open.failed"
+            "$ref": "#/components/schemas/Event.session.compacted"
           },
           {
-            "$ref": "#/components/schemas/Event.command.executed"
+            "$ref": "#/components/schemas/Event.tui.prompt.append"
           },
           {
-            "$ref": "#/components/schemas/Event.question.asked"
+            "$ref": "#/components/schemas/Event.tui.command.execute"
           },
           {
-            "$ref": "#/components/schemas/Event.question.replied"
+            "$ref": "#/components/schemas/Event.tui.toast.show"
           },
           {
-            "$ref": "#/components/schemas/Event.question.rejected"
+            "$ref": "#/components/schemas/Event.tui.session.select"
           },
           {
-            "$ref": "#/components/schemas/Event.todo.updated"
+            "$ref": "#/components/schemas/Event.mcp.tools.changed"
           },
           {
-            "$ref": "#/components/schemas/Event.session.status"
+            "$ref": "#/components/schemas/Event.mcp.browser.open.failed"
           },
           {
-            "$ref": "#/components/schemas/Event.session.idle"
+            "$ref": "#/components/schemas/Event.command.executed"
           },
           {
-            "$ref": "#/components/schemas/Event.session.compacted"
+            "$ref": "#/components/schemas/Event.vcs.branch.updated"
           },
           {
             "$ref": "#/components/schemas/Event.worktree.ready"