14 KiB
Effect TODO
Short roadmap for Effect cleanup in packages/opencode.
Current patterns and examples live in guide.md. Error
boundary migration details live in
error-boundaries-plan.md. Test migration rules live in
test/EFFECT_TEST_MIGRATION.md.
Older deep-dive notes in this directory may still be useful, but treat
this roadmap and the guide as the current entry points.
This is a planning map, not a verified inventory. Before starting a task,
re-run a targeted git grep from current dev and update this file if
the inventory changed.
Priorities
P0 ERR + RENDER + HTTP
Make expected failures typed, render them well, and stop relying on
generic HTTP error guesswork.
P1 TEST
Convert touched tests to the ideal Effect test patterns from the guide.
P2 RF
Move mutable runtime flags into typed runtime/config services.
P3 GLOBAL
Make global paths explicit and remove import-time side effects.
P4 INST + BRIDGE
Remove ambient Instance coupling while keeping Promise/callback interop.
P5 PROC + FS
Replace raw process/filesystem edges with typed Effect services.
P6 OA
Shrink OpenAPI compatibility shims as source schemas improve.
Work Paths
ERRTyped errors — replace legacyNamedError.create(...)andEffect.die(...)for expected service failures withSchema.TaggedErrorClasserrors on the Effect error channel. Shrinks:NamedErrorusage.RENDERUser-visible error rendering — preserve structured typed-error details at CLI, HTTP, and tool boundaries. Shrinks: opaqueError: Namerendering.HTTPHTTP route cleanup — make route errors explicit instead of relying on generic middleware to guess status/body from error names. Shrinks:middleware/error.tsand route-level compatibility shims.TESTEffect test migration — usetestEffect,it.live, andit.instancewith explicit layers. Shrinks: Promise-style tests, sleeps, mutable global test flags.RFRuntimeFlags / Flag deletion — move mutableFlagreads into typed runtime/config services. Shrinks:flag.ts,test/fixture/flag.ts.GLOBALGlobal paths / import side effects — make global path state explicit and testable instead of mutable module state. Shrinks:global.tsimport-time side effects, mutableGlobal.Pathoverrides, and itsFlagdependency.INSTInstance shim — remove ambientInstanceusage and old ALS access patterns. Shrinks:src/project/instance.ts.BRIDGEPromise/callback interop — keep bridge helpers, but reduce legacy ALS coupling. Shrinks:src/effect/bridge.tsdependency onproject/instance.ts.PROCAppProcess migration — preferAppProcess.Serviceover raw process wrappers. Shrinks: direct spawn callsites and legacy process helpers.FSAppFileSystem migration — preferAppFileSystem.Serviceover raw filesystem APIs. Shrinks: directfs/Bun.fileservice callsites where inappropriate.RTRuntime/facade cleanup — remove service-localmakeRuntimefacades when not intentional. Shrinks: async facade exports around services andrun-service.tsusage.OAOpenAPI compatibility — tighten source schemas instead of post-processing generated OpenAPI. Shrinks: schema workaround blocks inpublic.ts.
P0: Errors, Rendering, And HTTP
This should be the next big cleanup theme. The codebase is moving toward typed Effect failures, but the user-facing boundaries still leak old shapes and sometimes collapse rich errors into opaque strings.
Problems
- Some expected service failures still use
NamedError.create(...)or collapse toEffect.die(...). The storage/worktree/provider-auth conversions are done; an inventory sweep is needed for the rest. - HTTP error middleware still guesses status codes from error names —
some entries (e.g. storage
NotFound, provider auth) can now be removed, but the middleware overall has not shrunk. - Route handlers and route groups do not consistently declare the public error body they intend to expose.
- Repeated route error translations do not yet have a clear home: some should stay inline, some deserve tiny shared mapper helpers.
Target Shape
- Services define expected failures with
Schema.TaggedErrorClass. - Services export an
Errorunion and include it in method return types. - Expected failures stay on the Effect error channel.
Effect.die(...)is reserved for defects: bugs, impossible states, violated invariants, or final unknown-boundary fallbacks.- Inside
Effect.gen/Effect.fn, useyield* new MyError(...)for direct expected failures. - Domain services do not import HTTP status codes,
HttpApiError, or route-specific error schemas. - HTTP route groups make their public error contracts obvious.
- Handlers map service errors to declared HTTP errors at the boundary.
- Shared mapper helpers are only for repeated translations, not a giant central registry of every domain error.
- Generic HTTP middleware should shrink; it should not accumulate more name-based domain knowledge.
Recently completed
RENDER-1CLI tagged config error rendering (#27256, tests #27257).ERR-1storage/storage.tstypedNotFoundError(#27265) and removal of the server defect fallback (#27287).ERR-2worktree/index.tstyped errors (#27296).ERR-3provider/auth.tstyped validation/oauth errors (#27301).HTTP-1Unknown-500 details no longer leaked (#27251); follow-up to stop exposing named defects (#27471).- Session message reads typed and made effectful (#27269, #27275, #27280, #27291).
- Session HTTP error contracts tightened (#27308); busy-session mapping centralized (#27375, #27473).
- Provider init (#27484) and LSP init (#27494) errors typed.
First PR Candidates
HTTP-2Audit one route group for explicit error contracts and decide which mappings stay inline vs. shared helper.ERR-4Sweep remainingNamedError.create(...)andEffect.die(...)callsites for expected failures — re-rungit grepto build a current inventory.RENDER-2Audit CLI and TUI surfaces for any remaining opaqueError: Namerendering of typed errors.
P1: Tests
When touching tests, migrate them toward the ideal patterns in
test/EFFECT_TEST_MIGRATION.md:
- Use
testEffect(...)with explicit layers. - Prefer
it.instance(...)for service tests that need an instance. - Prefer
it.live(...)for real timers, filesystem mtimes, child processes, git, locks, or other live integration behavior. - Avoid sleeps; wait on real events or deterministic state transitions.
- Do not mutate
process.envor mutable globals after layers are built. - Use explicit layer variants, such as
RuntimeFlags.layer(...), for behavior changes.
P2: RuntimeFlags / Flag Deletion
Recently completed:
- Plugin/pure-mode flags moved to RuntimeFlags.
- Tool visibility flags moved to RuntimeFlags.
- Built-in websearch provider selection uses the same runtime flags as tool visibility.
- Removed global default-plugin disabling from test preload.
RF-1Scout reads routed through runtime flags (#27318).RF-2Plan-mode prompt read routed through runtime flags (#27320).RF-3Event-system reads routed through runtime flags (#27323).RF-4Workspaces reads routed through runtime flags for session (#27335), sync (#27336), and control-plane (#27337).- LLM client (#27368) and installation client (#27369) routed through runtime flags.
- TUI plugin runtime flags simplified (#27506).
- Background-subagents flag moved to RuntimeFlags, then removed
(
refactor(task): use runtime flag for background subagents,refactor(flags): remove background subagents flag).
Remaining cleanup:
- Sweep lingering
Flag.*reads — many CLI/TUI/config/observability callsites still importflag.ts. Decide per-callsite whether to route through RuntimeFlags, accept as legitimate env/config boundary, or migrate to typedConfig. - Delete
test/fixture/flag.tsonce tests no longer mutateFlag. - Delete
flag.tsonce no packages import it.
P3: Global Paths
global.ts is real connective tissue, not
just cosmetic ugliness. It currently mixes path calculation, import-time
directory creation, Flock setup, mutable exported Path state, and a
Flag dependency.
Problems to reduce:
- Importing the module creates directories.
- Tests override
Global.Pathby mutating exported module state. - Most callers use
Global.Pathdirectly instead of the Effect service. Global.make()still reads mutableFlag.OPENCODE_CONFIG_DIR.
Next PR candidates:
- Replace mutable
Global.Pathtest overrides with explicit test layers or scoped helpers. - Move directory creation and
Flocksetup behind an explicit init boundary where possible. - Remove the
Flagdependency from global path resolution.
P4: Instance And Bridge
project/instance.ts is the deletion
target. effect/bridge.ts is not a near-term
deletion target; Promise/callback interop will continue to exist.
Goal:
- Keep a sanctioned bridge for Promise/callback boundaries.
- Reduce bridge dependence on legacy
Instance.restore/Instance.current. - Move callers toward
InstanceRef,WorkspaceRef,InstanceState, or explicit context where practical. - Delete
project/instance.tsonly after ambient Instance coupling is gone.
Important distinction:
InstanceState.context,InstanceState.directory, andInstanceState.workspaceIDare acceptable inside normal Effect service code whenInstanceRef/WorkspaceRefare provided by the runtime.- The deletion blockers are the fallback and callback paths that rely on
ambient ALS: direct
Instance.*reads,InstanceState.bind(...),AppRuntime.runPromise(...)re-entry from plain JS, and bridge restore code that installs legacy ALS before invoking callbacks.
Current bottom-up inventory from dev:
- Direct
Instance.*value readers:tool/repo_overview.ts,control-plane/adapters/worktree.ts,cli/bootstrap.ts. InstanceState.bind(...)callback boundaries:file/watcher.tsnative watcher callback,storage/db.tstransaction/effect callbacks,session/llm.tsworkflow approval callback.AppRuntime.runPromise(...)/ re-entry from plain JS:project/with-instance.ts,project/instance-runtime.ts,control-plane/adapters/worktree.ts,cli/effect-cmd.ts, plus global/non-instance callsites such as CLI upgrade and ACP agent defaults.- Intentional bridge users to classify, not delete blindly:
workspace adapters in
control-plane/workspace.ts, MCP, command execution, plugins, pty lifecycle, bus scope cleanup, task cancellation, and HTTP lifecycle reload/dispose paths. - Core fallback layer to shrink last:
effect/run-service.ts,effect/bridge.ts, andeffect/instance-state.ts.
Recommended PR order:
INST-1Remove directInstance.*value readers. Start withrepo_overview,worktreeadapter, andcli/bootstrap; pass context explicitly or obtain it from an Effect boundary.INST-2Move type-onlyInstanceContextimports fromproject/instance.tstoproject/instance-context.ts.INST-3Audit eachInstanceState.bind(...)callback from the inside out: list what the callback calls (Bus.publish, database effects, permission/session services), then replace ambient capture with explicitInstanceRef/WorkspaceRefprovision or anEffectBridgecall.INST-4ClassifyAppRuntime.runPromise(...)callsites as global, instance-scoped with explicit refs, or bridge-required. Eliminate the instance-scoped callsites that rely onrun-service.attach()falling back toInstance.current.INST-5After consumers are explicit, removeInstance.currentfallback fromInstanceState.contextandrun-service.attach().INST-6Move any remainingrestore/bindcompatibility helpers to the boundary that still needs them, then deleteproject/instance.ts.
Lower Priority Tracks
PROC/FS— continue AppProcess and AppFileSystem migrations as focused PRs when touching relevant files.RT— remove service-local runtime facades only when they are not an intentional boundary.OA— shrinkpublic.tsby tightening source schemas one workaround at a time.fetch→HttpClient— migrate raw fetch callsites when the caller is already effectful or being effectified.Tools— remaining tool cleanup is narrow:webfetchHTML extraction andshellraw stream/promise edges.