12 KiB
Facade removal checklist
Concrete inventory of the remaining makeRuntime(...)-backed service facades in packages/opencode.
As of 2026-04-13, latest origin/dev:
src/still has 15makeRuntime(...)call sites.- 13 of those are still in scope for facade removal.
- 2 are excluded from this checklist:
bus/index.tsandeffect/cross-spawn-spawner.ts.
Recent progress:
- Wave 1 is merged:
Pty,Skill,Vcs,ToolRegistry,Auth. - Wave 2 is merged:
Config,Provider,File,LSP,MCP.
Priority hotspots
server/instance/session.tsstill depends onSession,SessionPrompt,SessionRevert,SessionCompaction,SessionSummary,ShareSession,Agent, andPermissionfacades.src/effect/app-runtime.tsstill references many facade namespaces directly, so it should stay in view during each deletion.
Completed Batches
Low-risk batch, all merged:
src/pty/index.tssrc/skill/index.tssrc/project/vcs.tssrc/tool/registry.tssrc/auth/index.ts
Caller-heavy batch, all merged:
src/config/config.tssrc/provider/provider.tssrc/file/index.tssrc/lsp/index.tssrc/mcp/index.ts
Shared pattern:
- one service file still exports
makeRuntime(...)+ async facades - one or two route or CLI entrypoints call those facades directly
- tests call the facade directly and need to switch to
yield* svc.method(...) - once callers are gone, delete
makeRuntime(...), remove async facade exports, and drop themakeRuntimeimport
Done means
For each service in the low-risk batch, the work is complete only when all of these are true:
- all production callers stop using
Namespace.method(...)facade calls - all direct test callers stop using the facade and instead yield the service from context
- the service file no longer has
makeRuntime(...) - the service file no longer exports runtime-backed facade helpers
grepfor the migrated facade methods only finds the service implementation itself or unrelated names
Caller templates
Route handlers
Use one AppRuntime.runPromise(Effect.gen(...)) body and yield the service inside it.
const value = await AppRuntime.runPromise(
Effect.gen(function* () {
const pty = yield* Pty.Service
return yield* pty.list()
}),
)
If two service calls are independent, keep them in the same effect body and use Effect.all(...).
Plain async CLI or script entrypoints
If the caller is not itself an Effect service yet, still prefer one contiguous AppRuntime.runPromise(Effect.gen(...)) block for the whole unit of work.
const skills = await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
const skill = yield* Skill.Service
yield* auth.set(key, info)
return yield* skill.all()
}),
)
Only fall back to AppRuntime.runPromise(Service.use(...)) for truly isolated one-off calls or awkward callback boundaries. Do not stack multiple tiny runPromise(...) calls in the same contiguous workflow.
This is the right intermediate state. Do not block facade removal on effectifying the whole CLI file.
Bootstrap or fire-and-forget startup code
If the old facade call existed only to kick off initialization, call the service through the existing runtime for that file.
void BootstrapRuntime.runPromise(Vcs.Service.use((svc) => svc.init()))
Do not reintroduce a dedicated runtime in the service just for bootstrap.
Tests
Convert facade tests to full effect style.
it.effect("does the thing", () =>
Effect.gen(function* () {
const svc = yield* Pty.Service
const info = yield* svc.create({ command: "cat", title: "a" })
yield* svc.remove(info.id)
}).pipe(Effect.provide(Pty.defaultLayer)),
)
If the repo test already uses testEffect(...), prefer testEffect(Service.defaultLayer) and yield* Service.Service inside the test body.
Do not route tests through AppRuntime unless the test is explicitly exercising the app runtime. For facade removal, tests should usually provide the specific service layer they need.
If the test uses provideTmpdirInstance(...), remember that fixture needs a live ChildProcessSpawner layer. For services whose defaultLayer does not already provide that infra, prefer the repo-standard cross-spawn layer:
const infra = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(MyService.defaultLayer, infra))
Without that extra layer, tests fail at runtime with Service not found: effect/process/ChildProcessSpawner.
Questions already answered
Do we need to effectify the whole caller first?
No.
- route files: compose the handler with
AppRuntime.runPromise(Effect.gen(...)) - CLI and scripts: use
AppRuntime.runPromise(Service.use(...)) - bootstrap: use the existing bootstrap runtime
Facade removal does not require a bigger refactor than that.
Should tests keep calling the namespace from async test bodies?
No. Convert them now.
The end state is yield* svc.method(...), not await Namespace.method(...) inside async tests.
Should we keep runPromise exported for convenience?
No. For this batch the goal is to delete the service-local runtime entirely.
What if a route has websocket callbacks or nested async handlers?
Keep the route shape, but replace each facade call with AppRuntime.runPromise(Service.use(...)) or wrap the surrounding async section in one Effect.gen(...) when practical. Do not keep the service facade just because the route has callback-shaped code.
Should we use one runPromise per service call?
No.
Default to one contiguous AppRuntime.runPromise(Effect.gen(...)) block per handler, command, or workflow. Yield every service you need inside that block.
Multiple tiny runPromise(...) calls are only acceptable when the caller structure forces it, such as websocket lifecycle callbacks, external callback APIs, or genuinely unrelated one-off operations.
Should we wrap a single service expression in Effect.gen(...)?
Usually no.
Prefer the direct form when there is only one expression:
await AppRuntime.runPromise(File.Service.use((svc) => svc.read(path)))
Use Effect.gen(...) when the workflow actually needs multiple yielded values or branching.
Learnings
These were the recurring mistakes and useful corrections from the first two batches:
- Tests should usually provide the specific service layer, not
AppRuntime. - If a test uses
provideTmpdirInstance(...)and needs child processes, preferCrossSpawnSpawner.defaultLayer. - Instance-scoped services may need both the service layer and the right instance fixture.
Filetests, for example, neededprovideInstance(...)plusFile.defaultLayer. - Do not wrap a single
Service.use(...)call inEffect.gen(...)just to return it. Use the direct form. - For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
- When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
Next batch
Recommended next five, in order:
src/permission/index.tssrc/agent/agent.tssrc/session/summary.tssrc/session/revert.tssrc/mcp/auth.ts
Why this batch:
- It keeps pushing the session-adjacent cleanup without jumping straight into
session/index.tsorsession/prompt.ts. Permission,Agent,SessionSummary, andSessionRevertall reduce fanout inserver/instance/session.ts.McpAuthis small and closely related to the just-landedMCPcleanup.
After that batch, the expected follow-up is the main session cluster:
src/session/index.tssrc/session/prompt.tssrc/session/compaction.ts
Checklist
src/session/index.ts(Session) - facades:create,fork,get,setTitle,setArchived,setPermission,setRevert,messages,children,remove,updateMessage,removeMessage,removePart,updatePart; main callers:server/instance/session.ts,cli/cmd/session.ts,cli/cmd/export.ts,cli/cmd/github.ts; tests:test/server/session-actions.test.ts,test/server/session-list.test.ts,test/server/global-session-list.test.tssrc/session/prompt.ts(SessionPrompt) - facades:prompt,resolvePromptParts,cancel,loop,shell,command; main callers:server/instance/session.ts,cli/cmd/github.ts; tests:test/session/prompt.test.ts,test/session/prompt-effect.test.ts,test/session/structured-output-integration.test.tssrc/session/revert.ts(SessionRevert) - facades:revert,unrevert,cleanup; main callers:server/instance/session.ts; tests:test/session/revert-compact.test.tssrc/session/compaction.ts(SessionCompaction) - facades:isOverflow,prune,create; main callers:server/instance/session.ts; tests:test/session/compaction.test.tssrc/session/summary.ts(SessionSummary) - facades:summarize,diff; main callers:session/prompt.ts,session/processor.ts,server/instance/session.ts; tests:test/session/snapshot-tool-race.test.tssrc/share/session.ts(ShareSession) - facades:create,share,unshare; main callers:server/instance/session.ts,cli/cmd/github.tssrc/agent/agent.ts(Agent) - facades:get,list,defaultAgent,generate; main callers:cli/cmd/agent.ts,server/instance/session.ts,server/instance/experimental.ts; tests:test/agent/agent.test.tssrc/permission/index.ts(Permission) - facades:ask,reply,list; main callers:server/instance/permission.ts,server/instance/session.ts,session/llm.ts; tests:test/permission/next.test.tssrc/file/index.ts(File) - facades removed and merged.src/lsp/index.ts(LSP) - facades removed and merged.src/mcp/index.ts(MCP) - facades removed and merged.src/config/config.ts(Config) - facades removed and merged.src/provider/provider.ts(Provider) - facades removed and merged.src/pty/index.ts(Pty) - facades removed and merged.src/skill/index.ts(Skill) - facades removed and merged.src/project/vcs.ts(Vcs) - facades removed and merged.src/tool/registry.ts(ToolRegistry) - facades removed and merged.src/worktree/index.ts(Worktree) - facades:makeWorktreeInfo,createFromInfo,create,remove,reset; main callers:control-plane/adaptors/worktree.ts,server/instance/experimental.ts; tests:test/project/worktree.test.ts,test/project/worktree-remove.test.tssrc/auth/index.ts(Auth) - facades removed and merged.src/mcp/auth.ts(McpAuth) - facades:get,getForUrl,all,set,remove,updateTokens,updateClientInfo,updateCodeVerifier,updateOAuthState; main callers:mcp/oauth-provider.ts,cli/cmd/mcp.ts; tests:test/mcp/oauth-auto-connect.test.tssrc/plugin/index.ts(Plugin) - facades:trigger,list,init; main callers:agent/agent.ts,session/llm.ts,project/bootstrap.ts; tests:test/plugin/trigger.test.ts,test/provider/provider.test.tssrc/project/project.ts(Project) - facades:fromDirectory,discover,initGit,update,sandboxes,addSandbox,removeSandbox; main callers:project/instance.ts,server/instance/project.ts,server/instance/experimental.ts; tests:test/project/project.test.ts,test/project/migrate-global.test.tssrc/snapshot/index.ts(Snapshot) - facades:init,track,patch,restore,revert,diff,diffFull; main callers:project/bootstrap.ts,cli/cmd/debug/snapshot.ts; tests:test/snapshot/snapshot.test.ts,test/session/revert-compact.test.ts
Excluded makeRuntime(...) sites
src/bus/index.ts- core bus plumbing, not a normal facade-removal target.src/effect/cross-spawn-spawner.ts- runtime helper forChildProcessSpawner, not a service namespace facade.