فهرست منبع

Convert LoadInput.init to Effect + extract InstanceBootstrap as a Service (#25376)

Kit Langton 4 ماه پیش
والد
کامیت
f33aec1139

+ 0 - 2
packages/opencode/src/cli/bootstrap.ts

@@ -1,11 +1,9 @@
 import { AppRuntime } from "@/effect/app-runtime"
 import { AppRuntime } from "@/effect/app-runtime"
-import { InstanceBootstrap } from "../project/bootstrap"
 import { Instance } from "../project/instance"
 import { Instance } from "../project/instance"
 
 
 export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
 export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
   return Instance.provide({
   return Instance.provide({
     directory,
     directory,
-    init: () => AppRuntime.runPromise(InstanceBootstrap),
     fn: async () => {
     fn: async () => {
       try {
       try {
         const result = await cb()
         const result = await cb()

+ 0 - 2
packages/opencode/src/cli/cmd/tui/worker.ts

@@ -2,7 +2,6 @@ import { Installation } from "@/installation"
 import { Server } from "@/server/server"
 import { Server } from "@/server/server"
 import * as Log from "@opencode-ai/core/util/log"
 import * as Log from "@opencode-ai/core/util/log"
 import { Instance } from "@/project/instance"
 import { Instance } from "@/project/instance"
-import { InstanceBootstrap } from "@/project/bootstrap"
 import { Rpc } from "@/util/rpc"
 import { Rpc } from "@/util/rpc"
 import { upgrade } from "@/cli/upgrade"
 import { upgrade } from "@/cli/upgrade"
 import { Config } from "@/config/config"
 import { Config } from "@/config/config"
@@ -77,7 +76,6 @@ export const rpc = {
   async checkUpgrade(input: { directory: string }) {
   async checkUpgrade(input: { directory: string }) {
     await Instance.provide({
     await Instance.provide({
       directory: input.directory,
       directory: input.directory,
-      init: () => AppRuntime.runPromise(InstanceBootstrap),
       fn: async () => {
       fn: async () => {
         await upgrade().catch(() => {})
         await upgrade().catch(() => {})
       },
       },

+ 2 - 1
packages/opencode/src/config/config.ts

@@ -23,6 +23,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { InstanceState } from "@/effect/instance-state"
 import { InstanceState } from "@/effect/instance-state"
 import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
 import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
 import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
 import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
+import { containsPath } from "../project/instance-context"
 import { zod } from "@/util/effect-zod"
 import { zod } from "@/util/effect-zod"
 import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
 import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
 import { ConfigAgent } from "./agent"
 import { ConfigAgent } from "./agent"
@@ -458,7 +459,7 @@ export const layer = Layer.effect(
         const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
         const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
           if (source.startsWith("http://") || source.startsWith("https://")) return "global"
           if (source.startsWith("http://") || source.startsWith("https://")) return "global"
           if (source === "OPENCODE_CONFIG_CONTENT") return "local"
           if (source === "OPENCODE_CONFIG_CONTENT") return "local"
-          if (Instance.containsPath(source, ctx)) return "local"
+          if (containsPath(source, ctx)) return "local"
           return "global"
           return "global"
         })
         })
 
 

+ 3 - 3
packages/opencode/src/file/index.ts

@@ -10,7 +10,7 @@ import fuzzysort from "fuzzysort"
 import ignore from "ignore"
 import ignore from "ignore"
 import path from "path"
 import path from "path"
 import { Global } from "@opencode-ai/core/global"
 import { Global } from "@opencode-ai/core/global"
-import { Instance } from "../project/instance"
+import { containsPath } from "../project/instance-context"
 import * as Log from "@opencode-ai/core/util/log"
 import * as Log from "@opencode-ai/core/util/log"
 import { Protected } from "./protected"
 import { Protected } from "./protected"
 import { Ripgrep } from "./ripgrep"
 import { Ripgrep } from "./ripgrep"
@@ -507,7 +507,7 @@ export const layer = Layer.effect(
       const ctx = yield* InstanceState.context
       const ctx = yield* InstanceState.context
       const full = path.join(ctx.directory, file)
       const full = path.join(ctx.directory, file)
 
 
-      if (!Instance.containsPath(full, ctx)) {
+      if (!containsPath(full, ctx)) {
         throw new Error("Access denied: path escapes project directory")
         throw new Error("Access denied: path escapes project directory")
       }
       }
 
 
@@ -587,7 +587,7 @@ export const layer = Layer.effect(
       }
       }
 
 
       const resolved = dir ? path.join(ctx.directory, dir) : ctx.directory
       const resolved = dir ? path.join(ctx.directory, dir) : ctx.directory
-      if (!Instance.containsPath(resolved, ctx)) {
+      if (!containsPath(resolved, ctx)) {
         throw new Error("Access denied: path escapes project directory")
         throw new Error("Access denied: path escapes project directory")
       }
       }
 
 

+ 2 - 7
packages/opencode/src/lsp/lsp.ts

@@ -12,7 +12,7 @@ import { Process } from "@/util/process"
 import { spawn as lspspawn } from "./launch"
 import { spawn as lspspawn } from "./launch"
 import { Effect, Layer, Context, Schema } from "effect"
 import { Effect, Layer, Context, Schema } from "effect"
 import { InstanceState } from "@/effect/instance-state"
 import { InstanceState } from "@/effect/instance-state"
-import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { containsPath } from "@/project/instance-context"
 import { NonNegativeInt, withStatics } from "@/util/schema"
 import { NonNegativeInt, withStatics } from "@/util/schema"
 import { zod, ZodOverride } from "@/util/effect-zod"
 import { zod, ZodOverride } from "@/util/effect-zod"
 
 
@@ -221,12 +221,7 @@ export const layer = Layer.effect(
 
 
     const getClients = Effect.fnUntraced(function* (file: string) {
     const getClients = Effect.fnUntraced(function* (file: string) {
       const ctx = yield* InstanceState.context
       const ctx = yield* InstanceState.context
-      if (
-        !AppFileSystem.contains(ctx.directory, file) &&
-        (ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, file))
-      ) {
-        return [] as LSPClient.Info[]
-      }
+      if (!containsPath(file, ctx)) return [] as LSPClient.Info[]
       const s = yield* InstanceState.get(state)
       const s = yield* InstanceState.get(state)
       return yield* Effect.promise(async () => {
       return yield* Effect.promise(async () => {
         const extension = path.parse(file).ext || file
         const extension = path.parse(file).ext || file

+ 64 - 30
packages/opencode/src/project/bootstrap.ts

@@ -8,37 +8,71 @@ import * as Vcs from "./vcs"
 import { Bus } from "../bus"
 import { Bus } from "../bus"
 import { Command } from "../command"
 import { Command } from "../command"
 import { InstanceState } from "@/effect/instance-state"
 import { InstanceState } from "@/effect/instance-state"
-import * as Log from "@opencode-ai/core/util/log"
 import { FileWatcher } from "@/file/watcher"
 import { FileWatcher } from "@/file/watcher"
 import { ShareNext } from "@/share/share-next"
 import { ShareNext } from "@/share/share-next"
-import * as Effect from "effect/Effect"
+import { Context, Effect, Layer } from "effect"
 import { Config } from "@/config/config"
 import { Config } from "@/config/config"
 
 
-export const InstanceBootstrap = Effect.gen(function* () {
-  const ctx = yield* InstanceState.context
-  Log.Default.info("bootstrapping", { directory: ctx.directory })
-  // everything depends on config so eager load it for nice traces
-  yield* Config.Service.use((svc) => svc.get())
-  // Plugin can mutate config so it has to be initialized before anything else.
-  yield* Plugin.Service.use((svc) => svc.init())
-  yield* Effect.all(
-    [
-      LSP.Service,
-      ShareNext.Service,
-      Format.Service,
-      File.Service,
-      FileWatcher.Service,
-      Vcs.Service,
-      Snapshot.Service,
-    ].map((s) => Effect.forkDetach(s.use((i) => i.init()))),
-  ).pipe(Effect.withSpan("InstanceBootstrap.init"))
-
-  const projectID = ctx.project.id
-  yield* Bus.Service.use((svc) =>
-    svc.subscribeCallback(Command.Event.Executed, async (payload) => {
-      if (payload.properties.name === Command.Default.INIT) {
-        Project.setInitialized(projectID)
-      }
-    }),
-  )
-}).pipe(Effect.withSpan("InstanceBootstrap"))
+export interface Interface {
+  readonly run: Effect.Effect<void>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceBootstrap") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    // Yield each bootstrap dep at layer init so `run` itself has R = never.
+    // This breaks the circular declaration loop through Config → Instance → InstanceStore
+    // (instance-store.ts only yields this Service tag, never the impl-side services).
+    const bus = yield* Bus.Service
+    const config = yield* Config.Service
+    const file = yield* File.Service
+    const fileWatcher = yield* FileWatcher.Service
+    const format = yield* Format.Service
+    const lsp = yield* LSP.Service
+    const plugin = yield* Plugin.Service
+    const shareNext = yield* ShareNext.Service
+    const snapshot = yield* Snapshot.Service
+    const vcs = yield* Vcs.Service
+
+    const run = Effect.gen(function* () {
+      const ctx = yield* InstanceState.context
+      yield* Effect.logInfo("bootstrapping", { directory: ctx.directory })
+      // everything depends on config so eager load it for nice traces
+      yield* config.get()
+      // Plugin can mutate config so it has to be initialized before anything else.
+      yield* plugin.init()
+      yield* Effect.all(
+        [lsp, shareNext, format, file, fileWatcher, vcs, snapshot].map((s) => Effect.forkDetach(s.init())),
+      ).pipe(Effect.withSpan("InstanceBootstrap.init"))
+
+      const projectID = ctx.project.id
+      yield* bus.subscribeCallback(Command.Event.Executed, async (payload) => {
+        if (payload.properties.name === Command.Default.INIT) {
+          Project.setInitialized(projectID)
+        }
+      })
+    }).pipe(Effect.withSpan("InstanceBootstrap"))
+
+    return Service.of({ run })
+  }),
+)
+
+export const defaultLayer: Layer.Layer<Service> = layer.pipe(
+  Layer.provide([
+    Bus.layer,
+    Config.defaultLayer,
+    File.defaultLayer,
+    FileWatcher.defaultLayer,
+    Format.defaultLayer,
+    LSP.defaultLayer,
+    Plugin.defaultLayer,
+    Project.defaultLayer,
+    ShareNext.defaultLayer,
+    Snapshot.defaultLayer,
+    Vcs.defaultLayer,
+  ]),
+)
+
+export * as InstanceBootstrap from "./bootstrap"

+ 14 - 0
packages/opencode/src/project/instance-context.ts

@@ -1,4 +1,5 @@
 import { LocalContext } from "@/util/local-context"
 import { LocalContext } from "@/util/local-context"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import type * as Project from "./project"
 import type * as Project from "./project"
 
 
 export interface InstanceContext {
 export interface InstanceContext {
@@ -8,3 +9,16 @@ export interface InstanceContext {
 }
 }
 
 
 export const context = LocalContext.create<InstanceContext>("instance")
 export const context = LocalContext.create<InstanceContext>("instance")
+
+/**
+ * Check if a path is within the project boundary.
+ * Returns true if path is inside ctx.directory OR ctx.worktree.
+ * Paths within the worktree but outside the working directory should not trigger external_directory permission.
+ */
+export function containsPath(filepath: string, ctx: InstanceContext): boolean {
+  if (AppFileSystem.contains(ctx.directory, filepath)) return true
+  // Non-git projects set worktree to "/" which would match ANY absolute path.
+  // Skip worktree check in this case to preserve external_directory permissions.
+  if (ctx.worktree === "/") return false
+  return AppFileSystem.contains(ctx.worktree, filepath)
+}

+ 49 - 38
packages/opencode/src/project/instance-store.ts

@@ -5,22 +5,29 @@ import { disposeInstance } from "@/effect/instance-registry"
 import { makeRuntime } from "@/effect/run-service"
 import { makeRuntime } from "@/effect/run-service"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
 import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
-import { context, type InstanceContext } from "./instance-context"
+import { type InstanceContext } from "./instance-context"
 import * as Project from "./project"
 import * as Project from "./project"
 
 
-export interface LoadInput {
+export interface LoadInput<R = never> {
   directory: string
   directory: string
-  init?: () => Promise<unknown>
+  /**
+   * Additional setup to run after the default InstanceBootstrap.
+   * Mainly used by tests for env-var setup or file writes that need the instance ALS context.
+   */
+  init?: Effect.Effect<void, never, R>
   worktree?: string
   worktree?: string
   project?: Project.Info
   project?: Project.Info
 }
 }
 
 
 export interface Interface {
 export interface Interface {
-  readonly load: (input: LoadInput) => Effect.Effect<InstanceContext>
-  readonly reload: (input: LoadInput) => Effect.Effect<InstanceContext>
+  readonly load: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
+  readonly reload: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
   readonly dispose: (ctx: InstanceContext) => Effect.Effect<void>
   readonly dispose: (ctx: InstanceContext) => Effect.Effect<void>
   readonly disposeAll: () => Effect.Effect<void>
   readonly disposeAll: () => Effect.Effect<void>
-  readonly provide: <A, E, R>(input: LoadInput, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
+  readonly provide: <A, E, R, R2 = never>(
+    input: LoadInput<R2>,
+    effect: Effect.Effect<A, E, R>,
+  ) => Effect.Effect<A, E, R | R2>
 }
 }
 
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceStore") {}
 export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceStore") {}
@@ -36,25 +43,25 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
     const scope = yield* Scope.Scope
     const scope = yield* Scope.Scope
     const cache = new Map<string, Entry>()
     const cache = new Map<string, Entry>()
 
 
-    const boot = Effect.fn("InstanceStore.boot")(function* (input: LoadInput & { directory: string }) {
-      const ctx =
-        input.project && input.worktree
-          ? {
-              directory: input.directory,
-              worktree: input.worktree,
-              project: input.project,
-            }
-          : yield* project.fromDirectory(input.directory).pipe(
-              Effect.map((result) => ({
+    const boot = <R>(input: LoadInput<R> & { directory: string }) =>
+      Effect.gen(function* () {
+        const ctx: InstanceContext =
+          input.project && input.worktree
+            ? {
                 directory: input.directory,
                 directory: input.directory,
-                worktree: result.sandbox,
-                project: result.project,
-              })),
-            )
-      const init = input.init
-      if (init) yield* Effect.promise(() => context.provide(ctx, init))
-      return ctx
-    })
+                worktree: input.worktree,
+                project: input.project,
+              }
+            : yield* project.fromDirectory(input.directory).pipe(
+                Effect.map((result) => ({
+                  directory: input.directory,
+                  worktree: result.sandbox,
+                  project: result.project,
+                })),
+              )
+        if (input.init) yield* input.init.pipe(Effect.provideService(InstanceRef, ctx))
+        return ctx
+      }).pipe(Effect.withSpan("InstanceStore.boot"))
 
 
     const removeEntry = (directory: string, entry: Entry) =>
     const removeEntry = (directory: string, entry: Entry) =>
       Effect.sync(() => {
       Effect.sync(() => {
@@ -63,11 +70,12 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
         return true
         return true
       })
       })
 
 
-    const completeLoad = Effect.fnUntraced(function* (directory: string, input: LoadInput, entry: Entry) {
-      const exit = yield* Effect.exit(boot({ ...input, directory }))
-      if (Exit.isFailure(exit)) yield* removeEntry(directory, entry)
-      yield* Deferred.done(entry.deferred, exit).pipe(Effect.asVoid)
-    })
+    const completeLoad = <R>(directory: string, input: LoadInput<R>, entry: Entry) =>
+      Effect.gen(function* () {
+        const exit = yield* Effect.exit(boot({ ...input, directory }))
+        if (Exit.isFailure(exit)) yield* removeEntry(directory, entry)
+        yield* Deferred.done(entry.deferred, exit).pipe(Effect.asVoid)
+      })
 
 
     const emitDisposed = (input: { directory: string; project?: string }) =>
     const emitDisposed = (input: { directory: string; project?: string }) =>
       Effect.sync(() =>
       Effect.sync(() =>
@@ -98,9 +106,9 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
       return true
       return true
     })
     })
 
 
-    const load = Effect.fn("InstanceStore.load")(function* (input: LoadInput) {
+    const load = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
       const directory = AppFileSystem.resolve(input.directory)
       const directory = AppFileSystem.resolve(input.directory)
-      return yield* Effect.uninterruptibleMask((restore) =>
+      return Effect.uninterruptibleMask((restore) =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           const existing = cache.get(directory)
           const existing = cache.get(directory)
           if (existing) return yield* restore(Deferred.await(existing.deferred))
           if (existing) return yield* restore(Deferred.await(existing.deferred))
@@ -113,12 +121,12 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
           }).pipe(Effect.forkIn(scope, { startImmediately: true }))
           }).pipe(Effect.forkIn(scope, { startImmediately: true }))
           return yield* restore(Deferred.await(entry.deferred))
           return yield* restore(Deferred.await(entry.deferred))
         }),
         }),
-      )
-    })
+      ).pipe(Effect.withSpan("InstanceStore.load"))
+    }
 
 
-    const reload = Effect.fn("InstanceStore.reload")(function* (input: LoadInput) {
+    const reload = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
       const directory = AppFileSystem.resolve(input.directory)
       const directory = AppFileSystem.resolve(input.directory)
-      return yield* Effect.uninterruptibleMask((restore) =>
+      return Effect.uninterruptibleMask((restore) =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           const previous = cache.get(directory)
           const previous = cache.get(directory)
           const entry: Entry = { deferred: Deferred.makeUnsafe<InstanceContext>() }
           const entry: Entry = { deferred: Deferred.makeUnsafe<InstanceContext>() }
@@ -134,8 +142,8 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
           }).pipe(Effect.forkIn(scope, { startImmediately: true }))
           }).pipe(Effect.forkIn(scope, { startImmediately: true }))
           return yield* restore(Deferred.await(entry.deferred))
           return yield* restore(Deferred.await(entry.deferred))
         }),
         }),
-      )
-    })
+      ).pipe(Effect.withSpan("InstanceStore.reload"))
+    }
 
 
     const dispose = Effect.fn("InstanceStore.dispose")(function* (ctx: InstanceContext) {
     const dispose = Effect.fn("InstanceStore.dispose")(function* (ctx: InstanceContext) {
       const entry = cache.get(ctx.directory)
       const entry = cache.get(ctx.directory)
@@ -170,7 +178,10 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
       return yield* cachedDisposeAll
       return yield* cachedDisposeAll
     })
     })
 
 
-    const provide = <A, E, R>(input: LoadInput, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
+    const provide = <A, E, R, R2>(
+      input: LoadInput<R2>,
+      effect: Effect.Effect<A, E, R>,
+    ): Effect.Effect<A, E, R | R2> =>
       load(input).pipe(Effect.flatMap((ctx) => effect.pipe(Effect.provideService(InstanceRef, ctx))))
       load(input).pipe(Effect.flatMap((ctx) => effect.pipe(Effect.provideService(InstanceRef, ctx))))
 
 
     yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore))
     yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore))

+ 33 - 20
packages/opencode/src/project/instance.ts

@@ -1,4 +1,5 @@
-import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { Effect } from "effect"
+import { InstanceRef } from "@/effect/instance-ref"
 import * as Project from "./project"
 import * as Project from "./project"
 import { context, type InstanceContext } from "./instance-context"
 import { context, type InstanceContext } from "./instance-context"
 import { InstanceStore } from "./instance-store"
 import { InstanceStore } from "./instance-store"
@@ -6,13 +7,37 @@ import { InstanceStore } from "./instance-store"
 export type { InstanceContext } from "./instance-context"
 export type { InstanceContext } from "./instance-context"
 export type { LoadInput } from "./instance-store"
 export type { LoadInput } from "./instance-store"
 
 
+type LegacyLoadInput = {
+  directory: string
+  init?: () => Promise<unknown>
+  project?: Project.Info
+  worktree?: string
+}
+
+// Promise-style legacy inits often read Instance.directory etc. from the ALS context.
+// The new Effect-typed init path doesn't bind ALS — it provides InstanceRef. To keep
+// legacy inits working without forcing every test to convert, bind ALS around the
+// Promise call here using the instance ctx that the store provides via InstanceRef.
+const liftLegacyInput = (input: LegacyLoadInput): InstanceStore.LoadInput => {
+  const { init, ...rest } = input
+  if (!init) return rest
+  return {
+    ...rest,
+    init: Effect.gen(function* () {
+      const ctx = yield* InstanceRef
+      yield* Effect.promise(() => (ctx ? context.provide(ctx, init) : init()))
+    }),
+  }
+}
+
 export const Instance = {
 export const Instance = {
-  load(input: InstanceStore.LoadInput): Promise<InstanceContext> {
-    return InstanceStore.runtime.runPromise((store) => store.load(input))
+  load(input: LegacyLoadInput): Promise<InstanceContext> {
+    return InstanceStore.runtime.runPromise((store) => store.load(liftLegacyInput(input)))
   },
   },
-  async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
-    return context.provide(await Instance.load({ directory: input.directory, init: input.init }), async () =>
-      input.fn(),
+  async provide<R>(input: { directory: string; init?: () => Promise<unknown>; fn: () => R }): Promise<R> {
+    return context.provide(
+      await Instance.load({ directory: input.directory, init: input.init }),
+      async () => input.fn(),
     )
     )
   },
   },
   get current() {
   get current() {
@@ -28,18 +53,6 @@ export const Instance = {
     return context.use().project
     return context.use().project
   },
   },
 
 
-  /**
-   * Check if a path is within the project boundary.
-   * Returns true if path is inside ctx.directory OR ctx.worktree.
-   * Paths within the worktree but outside the working directory should not trigger external_directory permission.
-   */
-  containsPath(filepath: string, ctx: InstanceContext) {
-    if (AppFileSystem.contains(ctx.directory, filepath)) return true
-    // Non-git projects set worktree to "/" which would match ANY absolute path.
-    // Skip worktree check in this case to preserve external_directory permissions.
-    if (ctx.worktree === "/") return false
-    return AppFileSystem.contains(ctx.worktree, filepath)
-  },
   /**
   /**
    * Captures the current instance ALS context and returns a wrapper that
    * Captures the current instance ALS context and returns a wrapper that
    * restores it when called. Use this for callbacks that fire outside the
    * restores it when called. Use this for callbacks that fire outside the
@@ -57,8 +70,8 @@ export const Instance = {
   restore<R>(ctx: InstanceContext, fn: () => R): R {
   restore<R>(ctx: InstanceContext, fn: () => R): R {
     return context.provide(ctx, fn)
     return context.provide(ctx, fn)
   },
   },
-  async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
-    return InstanceStore.runtime.runPromise((store) => store.reload(input))
+  async reload(input: LegacyLoadInput) {
+    return InstanceStore.runtime.runPromise((store) => store.reload(liftLegacyInput(input)))
   },
   },
   async dispose() {
   async dispose() {
     return InstanceStore.runtime.runPromise((store) => store.dispose(Instance.current))
     return InstanceStore.runtime.runPromise((store) => store.dispose(Instance.current))

+ 0 - 2
packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts

@@ -1,6 +1,5 @@
 import { AppRuntime } from "@/effect/app-runtime"
 import { AppRuntime } from "@/effect/app-runtime"
 import * as InstanceState from "@/effect/instance-state"
 import * as InstanceState from "@/effect/instance-state"
-import { InstanceBootstrap } from "@/project/bootstrap"
 import { Project } from "@/project/project"
 import { Project } from "@/project/project"
 import { ProjectID } from "@/project/schema"
 import { ProjectID } from "@/project/schema"
 import { Effect } from "effect"
 import { Effect } from "effect"
@@ -29,7 +28,6 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
         directory: ctx.directory,
         directory: ctx.directory,
         worktree: ctx.directory,
         worktree: ctx.directory,
         project: next,
         project: next,
-        init: () => AppRuntime.runPromise(InstanceBootstrap),
       })
       })
       return next
       return next
     })
     })

+ 6 - 4
packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts

@@ -1,5 +1,4 @@
 import { WorkspaceRef } from "@/effect/instance-ref"
 import { WorkspaceRef } from "@/effect/instance-ref"
-import { AppRuntime } from "@/effect/app-runtime"
 import { InstanceBootstrap } from "@/project/bootstrap"
 import { InstanceBootstrap } from "@/project/bootstrap"
 import { InstanceStore } from "@/project/instance-store"
 import { InstanceStore } from "@/project/instance-store"
 import { Effect, Layer } from "effect"
 import { Effect, Layer } from "effect"
@@ -25,11 +24,12 @@ function decode(input: string): string {
 function provideInstanceContext<E>(
 function provideInstanceContext<E>(
   effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
   effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
   store: InstanceStore.Interface,
   store: InstanceStore.Interface,
+  bootstrap: InstanceBootstrap.Interface,
 ): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
 ): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
   return Effect.gen(function* () {
   return Effect.gen(function* () {
     const route = yield* WorkspaceRouteContext
     const route = yield* WorkspaceRouteContext
     return yield* store.provide(
     return yield* store.provide(
-      { directory: decode(route.directory), init: () => AppRuntime.runPromise(InstanceBootstrap) },
+      { directory: decode(route.directory), init: bootstrap.run },
       effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
       effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
     )
     )
   })
   })
@@ -39,13 +39,15 @@ export const instanceContextLayer = Layer.effect(
   InstanceContextMiddleware,
   InstanceContextMiddleware,
   Effect.gen(function* () {
   Effect.gen(function* () {
     const store = yield* InstanceStore.Service
     const store = yield* InstanceStore.Service
-    return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store))
+    const bootstrap = yield* InstanceBootstrap.Service
+    return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store, bootstrap))
   }),
   }),
 )
 )
 
 
 export const instanceRouterMiddleware = HttpRouter.middleware()(
 export const instanceRouterMiddleware = HttpRouter.middleware()(
   Effect.gen(function* () {
   Effect.gen(function* () {
     const store = yield* InstanceStore.Service
     const store = yield* InstanceStore.Service
-    return (effect) => provideInstanceContext(effect, store)
+    const bootstrap = yield* InstanceBootstrap.Service
+    return (effect) => provideInstanceContext(effect, store, bootstrap)
   }),
   }),
 )
 )

+ 10 - 0
packages/opencode/src/server/routes/instance/httpapi/server.ts

@@ -11,13 +11,16 @@ import { Config } from "@/config/config"
 import { Command } from "@/command"
 import { Command } from "@/command"
 import * as Observability from "@opencode-ai/core/effect/observability"
 import * as Observability from "@opencode-ai/core/effect/observability"
 import { File } from "@/file"
 import { File } from "@/file"
+import { FileWatcher } from "@/file/watcher"
 import { Ripgrep } from "@/file/ripgrep"
 import { Ripgrep } from "@/file/ripgrep"
 import { Format } from "@/format"
 import { Format } from "@/format"
 import { LSP } from "@/lsp/lsp"
 import { LSP } from "@/lsp/lsp"
 import { MCP } from "@/mcp"
 import { MCP } from "@/mcp"
 import { Permission } from "@/permission"
 import { Permission } from "@/permission"
 import { Installation } from "@/installation"
 import { Installation } from "@/installation"
+import { InstanceBootstrap } from "@/project/bootstrap"
 import { InstanceStore } from "@/project/instance-store"
 import { InstanceStore } from "@/project/instance-store"
+import { Plugin } from "@/plugin"
 import { Project } from "@/project/project"
 import { Project } from "@/project/project"
 import { ProviderAuth } from "@/provider/auth"
 import { ProviderAuth } from "@/provider/auth"
 import { Provider } from "@/provider/provider"
 import { Provider } from "@/provider/provider"
@@ -32,7 +35,9 @@ import { SessionStatus } from "@/session/status"
 import { SessionSummary } from "@/session/summary"
 import { SessionSummary } from "@/session/summary"
 import { Todo } from "@/session/todo"
 import { Todo } from "@/session/todo"
 import { SessionShare } from "@/share/session"
 import { SessionShare } from "@/share/session"
+import { ShareNext } from "@/share/share-next"
 import { Skill } from "@/skill"
 import { Skill } from "@/skill"
+import { Snapshot } from "@/snapshot"
 import { SyncEvent } from "@/sync"
 import { SyncEvent } from "@/sync"
 import { ToolRegistry } from "@/tool/registry"
 import { ToolRegistry } from "@/tool/registry"
 import { lazy } from "@/util/lazy"
 import { lazy } from "@/util/lazy"
@@ -143,12 +148,15 @@ export function createRoutes(corsOptions?: CorsOptions) {
       Command.defaultLayer,
       Command.defaultLayer,
       Config.defaultLayer,
       Config.defaultLayer,
       File.defaultLayer,
       File.defaultLayer,
+      FileWatcher.defaultLayer,
       Format.defaultLayer,
       Format.defaultLayer,
       LSP.defaultLayer,
       LSP.defaultLayer,
       Installation.defaultLayer,
       Installation.defaultLayer,
+      InstanceBootstrap.defaultLayer,
       InstanceStore.defaultLayer,
       InstanceStore.defaultLayer,
       MCP.defaultLayer,
       MCP.defaultLayer,
       Permission.defaultLayer,
       Permission.defaultLayer,
+      Plugin.defaultLayer,
       Project.defaultLayer,
       Project.defaultLayer,
       ProviderAuth.defaultLayer,
       ProviderAuth.defaultLayer,
       Provider.defaultLayer,
       Provider.defaultLayer,
@@ -163,6 +171,8 @@ export function createRoutes(corsOptions?: CorsOptions) {
       SessionRunState.defaultLayer,
       SessionRunState.defaultLayer,
       SessionStatus.defaultLayer,
       SessionStatus.defaultLayer,
       SessionSummary.defaultLayer,
       SessionSummary.defaultLayer,
+      ShareNext.defaultLayer,
+      Snapshot.defaultLayer,
       SyncEvent.defaultLayer,
       SyncEvent.defaultLayer,
       Skill.defaultLayer,
       Skill.defaultLayer,
       Todo.defaultLayer,
       Todo.defaultLayer,

+ 0 - 2
packages/opencode/src/server/routes/instance/middleware.ts

@@ -1,6 +1,5 @@
 import type { MiddlewareHandler } from "hono"
 import type { MiddlewareHandler } from "hono"
 import { Instance } from "@/project/instance"
 import { Instance } from "@/project/instance"
-import { InstanceBootstrap } from "@/project/bootstrap"
 import { AppRuntime } from "@/effect/app-runtime"
 import { AppRuntime } from "@/effect/app-runtime"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -24,7 +23,6 @@ export function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler
       async fn() {
       async fn() {
         return Instance.provide({
         return Instance.provide({
           directory,
           directory,
-          init: () => AppRuntime.runPromise(InstanceBootstrap),
           async fn() {
           async fn() {
             return next()
             return next()
           },
           },

+ 0 - 2
packages/opencode/src/server/routes/instance/project.ts

@@ -7,7 +7,6 @@ import z from "zod"
 import { ProjectID } from "@/project/schema"
 import { ProjectID } from "@/project/schema"
 import { errors } from "../../error"
 import { errors } from "../../error"
 import { lazy } from "@/util/lazy"
 import { lazy } from "@/util/lazy"
-import { InstanceBootstrap } from "@/project/bootstrap"
 import { AppRuntime } from "@/effect/app-runtime"
 import { AppRuntime } from "@/effect/app-runtime"
 import { jsonRequest, runRequest } from "./trace"
 import { jsonRequest, runRequest } from "./trace"
 
 
@@ -86,7 +85,6 @@ export const ProjectRoutes = lazy(() =>
           directory: dir,
           directory: dir,
           worktree: dir,
           worktree: dir,
           project: next,
           project: next,
-          init: () => AppRuntime.runPromise(InstanceBootstrap),
         })
         })
         return c.json(next)
         return c.json(next)
       },
       },

+ 0 - 2
packages/opencode/src/server/workspace.ts

@@ -5,7 +5,6 @@ import { WorkspaceID } from "@/control-plane/schema"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
 import { Workspace } from "@/control-plane/workspace"
 import { Workspace } from "@/control-plane/workspace"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { Flag } from "@opencode-ai/core/flag/flag"
-import { InstanceBootstrap } from "@/project/bootstrap"
 import { Instance } from "@/project/instance"
 import { Instance } from "@/project/instance"
 import { Session } from "@/session/session"
 import { Session } from "@/session/session"
 import { SessionID } from "@/session/schema"
 import { SessionID } from "@/session/schema"
@@ -100,7 +99,6 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
         fn: () =>
         fn: () =>
           Instance.provide({
           Instance.provide({
             directory: target.directory,
             directory: target.directory,
-            init: () => AppRuntime.runPromise(InstanceBootstrap),
             async fn() {
             async fn() {
               return next()
               return next()
             },
             },

+ 3 - 3
packages/opencode/src/tool/bash.ts

@@ -6,7 +6,7 @@ import * as Tool from "./tool"
 import path from "path"
 import path from "path"
 import DESCRIPTION from "./bash.txt"
 import DESCRIPTION from "./bash.txt"
 import * as Log from "@opencode-ai/core/util/log"
 import * as Log from "@opencode-ai/core/util/log"
-import { Instance, type InstanceContext } from "../project/instance"
+import { containsPath, type InstanceContext } from "../project/instance-context"
 import { lazy } from "@/util/lazy"
 import { lazy } from "@/util/lazy"
 import { Language, type Node } from "web-tree-sitter"
 import { Language, type Node } from "web-tree-sitter"
 
 
@@ -386,7 +386,7 @@ export const BashTool = Tool.define(
           for (const arg of pathArgs(command, ps)) {
           for (const arg of pathArgs(command, ps)) {
             const resolved = yield* argPath(arg, cwd, ps, shell)
             const resolved = yield* argPath(arg, cwd, ps, shell)
             log.info("resolved path", { arg, resolved })
             log.info("resolved path", { arg, resolved })
-            if (!resolved || Instance.containsPath(resolved, instance)) continue
+            if (!resolved || containsPath(resolved, instance)) continue
             const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved)
             const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved)
             scan.dirs.add(dir)
             scan.dirs.add(dir)
           }
           }
@@ -612,7 +612,7 @@ export const BashTool = Tool.define(
                     Effect.sync(() => tree.delete()),
                     Effect.sync(() => tree.delete()),
                   )
                   )
                   const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
                   const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
-                  if (!Instance.containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
+                  if (!containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
                   yield* ask(ctx, scan)
                   yield* ask(ctx, scan)
                 }),
                 }),
               )
               )

+ 2 - 2
packages/opencode/src/tool/external-directory.ts

@@ -3,7 +3,7 @@ import { Effect } from "effect"
 import * as EffectLogger from "@opencode-ai/core/effect/logger"
 import * as EffectLogger from "@opencode-ai/core/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
 import { InstanceState } from "@/effect/instance-state"
 import type * as Tool from "./tool"
 import type * as Tool from "./tool"
-import { Instance } from "../project/instance"
+import { containsPath } from "../project/instance-context"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 
 
 type Kind = "file" | "directory"
 type Kind = "file" | "directory"
@@ -24,7 +24,7 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
 
 
   const ins = yield* InstanceState.context
   const ins = yield* InstanceState.context
   const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
   const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
-  if (Instance.containsPath(full, ins)) return
+  if (containsPath(full, ins)) return
 
 
   const kind = options?.kind ?? "file"
   const kind = options?.kind ?? "file"
   const dir = kind === "directory" ? full : path.dirname(full)
   const dir = kind === "directory" ? full : path.dirname(full)

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

@@ -2,7 +2,6 @@ import z from "zod"
 import { NamedError } from "@opencode-ai/core/util/error"
 import { NamedError } from "@opencode-ai/core/util/error"
 import { Global } from "@opencode-ai/core/global"
 import { Global } from "@opencode-ai/core/global"
 import { Instance } from "../project/instance"
 import { Instance } from "../project/instance"
-import { InstanceBootstrap } from "../project/bootstrap"
 import { Project } from "@/project/project"
 import { Project } from "@/project/project"
 import { Database } from "@/storage/db"
 import { Database } from "@/storage/db"
 import { eq } from "drizzle-orm"
 import { eq } from "drizzle-orm"
@@ -255,7 +254,6 @@ export const layer: Layer.Layer<
       const booted = yield* Effect.promise(() =>
       const booted = yield* Effect.promise(() =>
         Instance.provide({
         Instance.provide({
           directory: info.directory,
           directory: info.directory,
-          init: () => BootstrapRuntime.runPromise(InstanceBootstrap),
           fn: () => undefined,
           fn: () => undefined,
         })
         })
           .then(() => true)
           .then(() => true)

+ 15 - 14
packages/opencode/test/file/path-traversal.test.ts

@@ -5,6 +5,7 @@ import fs from "fs/promises"
 import { Filesystem } from "@/util/filesystem"
 import { Filesystem } from "@/util/filesystem"
 import { File } from "../../src/file"
 import { File } from "../../src/file"
 import { Instance } from "../../src/project/instance"
 import { Instance } from "../../src/project/instance"
+import { containsPath } from "../../src/project/instance-context"
 import { provideInstance, tmpdir } from "../fixture/fixture"
 import { provideInstance, tmpdir } from "../fixture/fixture"
 
 
 const run = <A, E>(eff: Effect.Effect<A, E, File.Service>) =>
 const run = <A, E>(eff: Effect.Effect<A, E, File.Service>) =>
@@ -121,15 +122,15 @@ describe("File.list path traversal protection", () => {
   })
   })
 })
 })
 
 
-describe("Instance.containsPath", () => {
+describe("containsPath", () => {
   test("returns true for path inside directory", async () => {
   test("returns true for path inside directory", async () => {
     await using tmp = await tmpdir({ git: true })
     await using tmp = await tmpdir({ git: true })
 
 
     await Instance.provide({
     await Instance.provide({
       directory: tmp.path,
       directory: tmp.path,
       fn: () => {
       fn: () => {
-        expect(Instance.containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
-        expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"), Instance.current)).toBe(true)
+        expect(containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
+        expect(containsPath(path.join(tmp.path, "src", "file.ts"), Instance.current)).toBe(true)
       },
       },
     })
     })
   })
   })
@@ -143,11 +144,11 @@ describe("Instance.containsPath", () => {
       directory: subdir,
       directory: subdir,
       fn: () => {
       fn: () => {
         // .opencode at worktree root, but we're running from packages/lib
         // .opencode at worktree root, but we're running from packages/lib
-        expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"), Instance.current)).toBe(true)
+        expect(containsPath(path.join(tmp.path, ".opencode", "state"), Instance.current)).toBe(true)
         // sibling package should also be accessible
         // sibling package should also be accessible
-        expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"), Instance.current)).toBe(true)
+        expect(containsPath(path.join(tmp.path, "packages", "other", "file.ts"), Instance.current)).toBe(true)
         // worktree root itself
         // worktree root itself
-        expect(Instance.containsPath(tmp.path, Instance.current)).toBe(true)
+        expect(containsPath(tmp.path, Instance.current)).toBe(true)
       },
       },
     })
     })
   })
   })
@@ -158,8 +159,8 @@ describe("Instance.containsPath", () => {
     await Instance.provide({
     await Instance.provide({
       directory: tmp.path,
       directory: tmp.path,
       fn: () => {
       fn: () => {
-        expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
-        expect(Instance.containsPath("/tmp/other-project", Instance.current)).toBe(false)
+        expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
+        expect(containsPath("/tmp/other-project", Instance.current)).toBe(false)
       },
       },
     })
     })
   })
   })
@@ -170,7 +171,7 @@ describe("Instance.containsPath", () => {
     await Instance.provide({
     await Instance.provide({
       directory: tmp.path,
       directory: tmp.path,
       fn: () => {
       fn: () => {
-        expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
+        expect(containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
       },
       },
     })
     })
   })
   })
@@ -182,8 +183,8 @@ describe("Instance.containsPath", () => {
       directory: tmp.path,
       directory: tmp.path,
       fn: () => {
       fn: () => {
         expect(Instance.directory).toBe(Instance.worktree)
         expect(Instance.directory).toBe(Instance.worktree)
-        expect(Instance.containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
-        expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
+        expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
+        expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
       },
       },
     })
     })
   })
   })
@@ -195,9 +196,9 @@ describe("Instance.containsPath", () => {
       directory: tmp.path,
       directory: tmp.path,
       fn: () => {
       fn: () => {
         // worktree is "/" for non-git projects, but containsPath should NOT allow all paths
         // worktree is "/" for non-git projects, but containsPath should NOT allow all paths
-        expect(Instance.containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
-        expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
-        expect(Instance.containsPath("/tmp/other", Instance.current)).toBe(false)
+        expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
+        expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
+        expect(containsPath("/tmp/other", Instance.current)).toBe(false)
       },
       },
     })
     })
   })
   })

+ 19 - 18
packages/opencode/test/project/instance.test.ts

@@ -1,6 +1,7 @@
 import { afterEach, describe, expect } from "bun:test"
 import { afterEach, describe, expect } from "bun:test"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Effect, Fiber, Layer } from "effect"
 import { Effect, Fiber, Layer } from "effect"
+import { InstanceRef } from "../../src/effect/instance-ref"
 import { registerDisposer } from "../../src/effect/instance-registry"
 import { registerDisposer } from "../../src/effect/instance-registry"
 import { Instance } from "../../src/project/instance"
 import { Instance } from "../../src/project/instance"
 import { InstanceStore } from "../../src/project/instance-store"
 import { InstanceStore } from "../../src/project/instance-store"
@@ -26,7 +27,7 @@ describe("InstanceStore", () => {
     }),
     }),
   )
   )
 
 
-  it.live("runs load init inside the loaded legacy instance context", () =>
+  it.live("runs load init with InstanceRef provided", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const dir = yield* tmpdirScoped({ git: true })
       const dir = yield* tmpdirScoped({ git: true })
       const store = yield* InstanceStore.Service
       const store = yield* InstanceStore.Service
@@ -34,9 +35,9 @@ describe("InstanceStore", () => {
 
 
       yield* store.load({
       yield* store.load({
         directory: dir,
         directory: dir,
-        init: async () => {
-          initializedDirectory = Instance.directory
-        },
+        init: Effect.gen(function* () {
+          initializedDirectory = (yield* InstanceRef)?.directory
+        }),
       })
       })
 
 
       expect(initializedDirectory).toBe(dir)
       expect(initializedDirectory).toBe(dir)
@@ -52,15 +53,15 @@ describe("InstanceStore", () => {
 
 
       const first = yield* store.load({
       const first = yield* store.load({
         directory: dir,
         directory: dir,
-        init: async () => {
+        init: Effect.sync(() => {
           initialized++
           initialized++
-        },
+        }),
       })
       })
       const second = yield* store.load({
       const second = yield* store.load({
         directory: dir,
         directory: dir,
-        init: async () => {
+        init: Effect.sync(() => {
           initialized++
           initialized++
-        },
+        }),
       })
       })
 
 
       expect(second).toBe(first)
       expect(second).toBe(first)
@@ -79,11 +80,11 @@ describe("InstanceStore", () => {
       const first = yield* store
       const first = yield* store
         .load({
         .load({
           directory: dir,
           directory: dir,
-          init: async () => {
+          init: Effect.promise(async () => {
             initialized++
             initialized++
             started.resolve()
             started.resolve()
             await release.promise
             await release.promise
-          },
+          }),
         })
         })
         .pipe(Effect.forkScoped)
         .pipe(Effect.forkScoped)
 
 
@@ -92,9 +93,9 @@ describe("InstanceStore", () => {
       const second = yield* store
       const second = yield* store
         .load({
         .load({
           directory: dir,
           directory: dir,
-          init: async () => {
+          init: Effect.sync(() => {
             initialized++
             initialized++
-          },
+          }),
         })
         })
         .pipe(Effect.forkScoped)
         .pipe(Effect.forkScoped)
 
 
@@ -116,10 +117,10 @@ describe("InstanceStore", () => {
       const failed = yield* store
       const failed = yield* store
         .load({
         .load({
           directory: dir,
           directory: dir,
-          init: async () => {
+          init: Effect.sync(() => {
             attempts++
             attempts++
             throw new Error("init failed")
             throw new Error("init failed")
-          },
+          }),
         })
         })
         .pipe(
         .pipe(
           Effect.as(false),
           Effect.as(false),
@@ -130,9 +131,9 @@ describe("InstanceStore", () => {
 
 
       const ctx = yield* store.load({
       const ctx = yield* store.load({
         directory: dir,
         directory: dir,
-        init: async () => {
+        init: Effect.sync(() => {
           attempts++
           attempts++
-        },
+        }),
       })
       })
 
 
       expect(ctx.directory).toBe(dir)
       expect(ctx.directory).toBe(dir)
@@ -170,10 +171,10 @@ describe("InstanceStore", () => {
       const reload = yield* store
       const reload = yield* store
         .reload({
         .reload({
           directory: dir,
           directory: dir,
-          init: async () => {
+          init: Effect.promise(async () => {
             reloading.resolve()
             reloading.resolve()
             await releaseReload.promise
             await releaseReload.promise
-          },
+          }),
         })
         })
         .pipe(Effect.forkScoped)
         .pipe(Effect.forkScoped)
 
 

+ 2 - 0
packages/opencode/test/server/httpapi-instance-context.test.ts

@@ -11,6 +11,7 @@ import { registerAdapter } from "../../src/control-plane/adapters"
 import type { WorkspaceAdapter } from "../../src/control-plane/types"
 import type { WorkspaceAdapter } from "../../src/control-plane/types"
 import { Workspace } from "../../src/control-plane/workspace"
 import { Workspace } from "../../src/control-plane/workspace"
 import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
 import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
+import { InstanceBootstrap } from "../../src/project/bootstrap"
 import { Instance } from "../../src/project/instance"
 import { Instance } from "../../src/project/instance"
 import { InstanceStore } from "../../src/project/instance-store"
 import { InstanceStore } from "../../src/project/instance-store"
 import { Project } from "../../src/project/project"
 import { Project } from "../../src/project/project"
@@ -41,6 +42,7 @@ const it = testEffect(
     testStateLayer,
     testStateLayer,
     NodeHttpServer.layerTest,
     NodeHttpServer.layerTest,
     NodeServices.layer,
     NodeServices.layer,
+    InstanceBootstrap.defaultLayer,
     InstanceStore.defaultLayer,
     InstanceStore.defaultLayer,
     Project.defaultLayer,
     Project.defaultLayer,
     Workspace.defaultLayer,
     Workspace.defaultLayer,