fix: 修正 logo 中 N 和 G 字母造型
N 添加对角线笔画(█▄ █),G 添加内横杠(█ ▀█), 避免与 O 字母造型雷同。同步更新 ui.ts 中的硬编码 wordmark。
This commit is contained in:
235
packages/opencode/specs/effect/error-boundaries-plan.md
Normal file
235
packages/opencode/specs/effect/error-boundaries-plan.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Error Boundaries Plan
|
||||
|
||||
Plan for removing `NamedError` as connective tissue while keeping public
|
||||
wire contracts stable.
|
||||
|
||||
## Desired Shape
|
||||
|
||||
```text
|
||||
Domain/service error
|
||||
Schema.TaggedErrorClass
|
||||
- catchable with catchTag / catchTags
|
||||
- appears in service method error type
|
||||
- no HTTP status
|
||||
- no toObject()
|
||||
|
||||
HTTP public error
|
||||
Schema.ErrorClass / TaggedErrorClass with httpApiStatus
|
||||
- endpoint-declared public contract
|
||||
- owns legacy { name, data } only when that is the SDK wire shape
|
||||
|
||||
CLI/user rendering
|
||||
FormatError and small format helpers
|
||||
- converts domain errors to text
|
||||
- preserves useful structured fields
|
||||
|
||||
Session/model-visible error
|
||||
first-class session/message error schema or helper
|
||||
- owns { name, data } event/message shape
|
||||
- not a service error class
|
||||
```
|
||||
|
||||
The important rule: a service error should not also be the HTTP body, CLI
|
||||
formatter, and session event body. Each seam adapts the error into the
|
||||
shape it owns.
|
||||
|
||||
## Concrete Example: Provider Model Not Found
|
||||
|
||||
Before:
|
||||
|
||||
```ts
|
||||
export const ModelNotFoundError = NamedError.create("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
})
|
||||
```
|
||||
|
||||
Problems:
|
||||
|
||||
- Throwing it inside `Effect.fn` made it behave like a defect unless a
|
||||
compatibility bridge caught it.
|
||||
- HTTP middleware knew that this one domain error should be a `400`.
|
||||
- Callers read `.data.*`, which couples them to the legacy `{ name, data }`
|
||||
wire shape.
|
||||
|
||||
After:
|
||||
|
||||
```ts
|
||||
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
||||
}
|
||||
```
|
||||
|
||||
Boundary adapters:
|
||||
|
||||
```text
|
||||
CLI
|
||||
└─ FormatError sees _tag ProviderModelNotFoundError -> nice text
|
||||
|
||||
Session prompt
|
||||
└─ catch ModelNotFoundError -> publish Session.Event.Error as message/session wire shape
|
||||
|
||||
HTTP route
|
||||
└─ catch ModelNotFoundError -> declared BadRequest public API error when the endpoint needs it
|
||||
|
||||
HTTP middleware
|
||||
└─ no Provider.ModelNotFoundError knowledge
|
||||
```
|
||||
|
||||
## Refining Known Promise Failures
|
||||
|
||||
Use `EffectPromise.refineRejection(...)` when a Promise boundary can reject
|
||||
with many unknown values, but only one or two rejection classes are expected
|
||||
domain failures. Unknown rejections stay defects; the helper maps only known
|
||||
rejection shapes to typed errors.
|
||||
|
||||
```ts
|
||||
const language =
|
||||
yield *
|
||||
EffectPromise.refineRejection(
|
||||
async () => loadFromProvider(),
|
||||
(cause) => (cause instanceof NoSuchModelError ? new ModelNotFoundError({ providerID, modelID, cause }) : undefined),
|
||||
)
|
||||
```
|
||||
|
||||
Use this when the Promise can genuinely reject and most rejection values are
|
||||
still defects for the current module. Use `Effect.tryPromise({ try, catch })`
|
||||
when every rejection should become the same expected error type. Use
|
||||
`Effect.promise(...)` only when rejection means a defect and you do not need
|
||||
to refine known rejection classes.
|
||||
|
||||
## Helper Modules We Probably Want
|
||||
|
||||
Add helpers only when repeated call sites prove the seam is real.
|
||||
|
||||
### HTTP API Errors
|
||||
|
||||
Likely location: `src/server/routes/instance/httpapi/errors.ts`.
|
||||
|
||||
Purpose:
|
||||
|
||||
- construct public HTTP error bodies
|
||||
- preserve legacy `{ name, data }` where needed
|
||||
- attach `httpApiStatus`
|
||||
|
||||
Good helpers:
|
||||
|
||||
```ts
|
||||
notFound(message)
|
||||
badRequest(message)
|
||||
unknown()
|
||||
```
|
||||
|
||||
Avoid:
|
||||
|
||||
```ts
|
||||
mapAnyDomainError(error)
|
||||
```
|
||||
|
||||
That recreates the giant middleware mapper problem.
|
||||
|
||||
### Session / Message Error Wire Helpers
|
||||
|
||||
Likely location: near `src/session/message-error.ts` or a new narrow
|
||||
module such as `src/session/event-error.ts`.
|
||||
|
||||
Purpose:
|
||||
|
||||
- construct the `{ name, data }` shape used by `Session.Event.Error` and
|
||||
assistant message errors
|
||||
- replace `new NamedError.Unknown(...).toObject()` call sites
|
||||
- keep model-visible error bodies separate from service/domain errors
|
||||
|
||||
Good helpers:
|
||||
|
||||
```ts
|
||||
unknown(message)
|
||||
agentNotFound(agent, available)
|
||||
commandNotFound(command, available)
|
||||
modelNotFound(error: Provider.ModelNotFoundError)
|
||||
```
|
||||
|
||||
### CLI Formatters
|
||||
|
||||
Likely location: `src/cli/error.ts` until repetition demands domain-local
|
||||
format helpers.
|
||||
|
||||
Purpose:
|
||||
|
||||
- produce human-readable terminal messages from typed errors
|
||||
- support old `{ name, data }` shapes only while compatibility is needed
|
||||
|
||||
## Migration Queue
|
||||
|
||||
### Remove Domain Knowledge From HTTP Middleware
|
||||
|
||||
- [x] Storage not found no longer maps through defect fallback.
|
||||
- [x] Worktree expected errors moved to typed errors.
|
||||
- [x] Provider auth expected errors moved to typed errors.
|
||||
- [x] Provider model not found no longer needs an HTTP middleware status
|
||||
special case.
|
||||
- [ ] Convert `Session.BusyError` and map it at route boundaries.
|
||||
- [ ] Delete the broad `NamedError` middleware branch once no route relies
|
||||
on defect-wrapped legacy domain errors.
|
||||
- [ ] Keep one final unknown-defect fallback that logs `Cause.pretty(cause)`
|
||||
and returns a safe `500` body.
|
||||
|
||||
### Remaining `NamedError.create(...)` Service Errors
|
||||
|
||||
These should become `Schema.TaggedErrorClass` when touched:
|
||||
|
||||
- [ ] `src/provider/provider.ts` — `ProviderInitError`.
|
||||
- [ ] `src/storage/db.ts` — database `NotFoundError`.
|
||||
- [ ] `src/mcp/index.ts` — `MCPFailed`.
|
||||
- [ ] `src/skill/index.ts` — `SkillInvalidError`,
|
||||
`SkillNameMismatchError`.
|
||||
- [ ] `src/lsp/client.ts` — `LSPInitializeError`.
|
||||
- [ ] `src/ide/index.ts` — install errors.
|
||||
- [ ] `src/config/error.ts`, `src/config/config.ts`,
|
||||
`src/config/markdown.ts` — config errors. These already render well
|
||||
in the CLI, so migrate carefully and preserve diagnostics.
|
||||
|
||||
### Session / Message Wire Errors
|
||||
|
||||
These are not ordinary service errors. They mostly build `{ name, data }`
|
||||
objects for model-visible/session-visible output.
|
||||
|
||||
- [ ] Add a first-class session/message error wire helper.
|
||||
- [ ] Replace `new NamedError.Unknown(...).toObject()` in
|
||||
`src/session/prompt.ts`.
|
||||
- [ ] Replace `new NamedError.Unknown(...).toObject()` in config/skill/plugin
|
||||
session event publishing.
|
||||
- [ ] Move `src/session/message-error.ts` and `src/session/message-v2.ts`
|
||||
away from `NamedError.create(...)` once the wire helper exists.
|
||||
- [ ] Update retry/message tests to assert the wire schema/helper output,
|
||||
not `NamedError` instances.
|
||||
|
||||
### CLI Rendering
|
||||
|
||||
- [x] Tagged config errors render with useful diagnostics.
|
||||
- [x] Provider model not found renders from both old `{ name, data }` and
|
||||
new `_tag` shapes.
|
||||
- [ ] Add typed render cases as more `NamedError.create(...)` domains move
|
||||
to `Schema.TaggedErrorClass`.
|
||||
- [ ] Eventually remove old-shape compatibility branches when no callers can
|
||||
produce them.
|
||||
|
||||
## PR Checklist
|
||||
|
||||
For each migrated error:
|
||||
|
||||
- [ ] Domain error is `Schema.TaggedErrorClass`.
|
||||
- [ ] Service method exposes the typed error in its error channel.
|
||||
- [ ] No service error has `toObject()` just for compatibility.
|
||||
- [ ] CLI, HTTP, and session/message adapters each own their output shape.
|
||||
- [ ] HTTP middleware gets smaller or stays unchanged.
|
||||
- [ ] Focused tests cover the domain error and any public rendering/wire
|
||||
shape touched by the PR.
|
||||
207
packages/opencode/specs/effect/errors.md
Normal file
207
packages/opencode/specs/effect/errors.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Typed Error Migration
|
||||
|
||||
This note expands the `ERR`, `RENDER`, and `HTTP` tracks from
|
||||
[`todo.md`](./todo.md). It is the current reference for expected failures,
|
||||
typed service errors, and HTTP error boundaries.
|
||||
|
||||
For the migration architecture and queue, see
|
||||
[`error-boundaries-plan.md`](./error-boundaries-plan.md).
|
||||
|
||||
## Goal
|
||||
|
||||
- Expected service failures live on the Effect error channel.
|
||||
- Service interfaces expose those failures in their return types.
|
||||
- Domain errors are authored with `Schema.TaggedErrorClass`.
|
||||
- `Effect.die(...)` is reserved for defects: bugs, impossible states,
|
||||
violated invariants, and final unknown-boundary fallbacks.
|
||||
- HTTP status codes and public wire bodies are handled at HTTP route
|
||||
boundaries, not inside service modules.
|
||||
- User-facing boundaries render useful structured error details instead of
|
||||
opaque `Error: SomeName` strings.
|
||||
|
||||
## Service Error Shape
|
||||
|
||||
```ts
|
||||
export class SessionBusyError extends Schema.TaggedErrorClass<SessionBusyError>()("SessionBusyError", {
|
||||
sessionID: SessionID,
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export type Error = Storage.Error | SessionBusyError
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (id: SessionID) => Effect.Effect<Info, Error>
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Use `Schema.TaggedErrorClass` for expected domain failures.
|
||||
- Export a domain-level `Error` union from each service module.
|
||||
- Put expected errors in service method signatures.
|
||||
- Use `yield* new DomainError(...)` for direct early failures in
|
||||
`Effect.gen` / `Effect.fn`.
|
||||
- Use `Schema.Defect` for unknown cause fields when preserving the cause is
|
||||
useful for logs or callers.
|
||||
- Use `Effect.try(...)`, `Effect.tryPromise(...)`, `Effect.mapError`,
|
||||
`Effect.catchTag`, and `Effect.catchTags` to translate external
|
||||
failures into domain errors.
|
||||
- Do not use `throw`, `Effect.die(...)`, or `catchDefect` for expected
|
||||
user, IO, validation, missing-resource, auth, provider, worktree, or
|
||||
busy-state failures.
|
||||
|
||||
## HTTP Boundary Shape
|
||||
|
||||
Service modules stay transport-agnostic. They should not import HTTP
|
||||
status codes, `HttpApiError`, `HttpServerResponse`, or route-specific
|
||||
error schemas.
|
||||
|
||||
HTTP handlers translate service errors into public endpoint errors:
|
||||
|
||||
```ts
|
||||
const get = Effect.fn("SessionHttpApi.get")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||
return yield* session
|
||||
.get(ctx.params.sessionID)
|
||||
.pipe(Effect.catchTag("StorageNotFoundError", () => notFound("Session not found")))
|
||||
})
|
||||
```
|
||||
|
||||
Endpoint definitions declare which public errors can be emitted. Public
|
||||
HTTP error schemas carry their response status with `httpApiStatus` or the
|
||||
equivalent HttpApi schema annotation.
|
||||
|
||||
Effect's own HttpApi examples follow this pattern:
|
||||
|
||||
```ts
|
||||
export class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
|
||||
"Unauthorized",
|
||||
{ message: Schema.String },
|
||||
{ httpApiStatus: 401 },
|
||||
) {}
|
||||
|
||||
export class Authorization extends HttpApiMiddleware.Service<
|
||||
Authorization,
|
||||
{
|
||||
provides: CurrentUser
|
||||
}
|
||||
>()("app/Authorization", {
|
||||
security: { bearer: HttpApiSecurity.bearer },
|
||||
error: Unauthorized,
|
||||
}) {}
|
||||
```
|
||||
|
||||
Endpoint-level errors use the same idea:
|
||||
|
||||
```ts
|
||||
export class ConfigApiError extends Schema.ErrorClass<ConfigApiError>("ConfigApiError")(
|
||||
{
|
||||
name: Schema.Union(Schema.Literal("ConfigInvalidError"), Schema.Literal("ConfigJsonError")),
|
||||
data: Schema.Struct({ message: Schema.optional(Schema.String), path: Schema.String }),
|
||||
},
|
||||
{ httpApiStatus: 400 },
|
||||
) {}
|
||||
|
||||
HttpApiEndpoint.get("get", "/config", {
|
||||
success: Config.Info,
|
||||
error: ConfigApiError,
|
||||
})
|
||||
```
|
||||
|
||||
The service error and HTTP error may be the same class only when the wire
|
||||
shape is intentionally public. Use separate HTTP error schemas when the
|
||||
service error contains internals, low-level causes, retry hints, or data
|
||||
that should not be exposed to API clients.
|
||||
|
||||
Do not map every domain error into one universal HTTP error class. Prefer a
|
||||
small public error vocabulary by route group: shared shapes like
|
||||
`ApiNotFoundError`, route-specific shapes like `ConfigApiError`, and built-in
|
||||
empty `HttpApiError.*` only when an empty/no-content body is the intended SDK
|
||||
contract.
|
||||
|
||||
## Mapping Guidance
|
||||
|
||||
- Keep one-off translations inline in the handler.
|
||||
- Extract tiny shared helpers when the same translation repeats across a
|
||||
route group.
|
||||
- Do not create one giant `unknown -> status` mapper.
|
||||
- Do not grow generic HTTP middleware into a registry of domain errors.
|
||||
- Preserve existing public `{ name, data }` bodies until a deliberate
|
||||
breaking API change.
|
||||
- Use built-in `HttpApiError.*` only when its generated body and SDK
|
||||
surface are intentionally the public contract.
|
||||
- Prefer `Schema.ErrorClass` for public HTTP error bodies whose wire shape is
|
||||
not the same as the internal domain error shape.
|
||||
- Prefer `Schema.TaggedErrorClass` for service/domain errors and middleware
|
||||
errors that are naturally tagged by `_tag`.
|
||||
- If preserving a legacy `{ name, data }` body, model that shape explicitly in
|
||||
the public API error schema instead of relying on `NamedError.toObject()` in
|
||||
generic middleware.
|
||||
|
||||
## User-Facing Rendering
|
||||
|
||||
HTTP serialization and user rendering are separate boundaries. The server
|
||||
should send structured public errors; CLI and TUI code should format those
|
||||
structures through one shared formatter.
|
||||
|
||||
For SDK calls using `{ throwOnError: true }`, the generated client may wrap the
|
||||
decoded response body in an `Error`. The original body should remain available
|
||||
under `error.cause.body`; `FormatError` is the right place to unwrap and render
|
||||
that body. TUI aggregation helpers should call `FormatError` first, then fall
|
||||
back to generic `Error.message` / string rendering.
|
||||
|
||||
When several parallel startup requests fail from the same underlying issue,
|
||||
group identical rendered messages and list the affected request names once.
|
||||
For example:
|
||||
|
||||
```text
|
||||
Configuration is invalid at /path/to/opencode.json
|
||||
↳ Expected object, got "not-object" provider.bad.options
|
||||
Affected startup requests: config.providers, provider.list, app.agents, config.get
|
||||
```
|
||||
|
||||
## Middleware Guidance
|
||||
|
||||
HTTP middleware should be cross-cutting: auth, context, schema decode
|
||||
formatting, routing, and final unknown-defect fallback.
|
||||
|
||||
The current compatibility middleware still knows about some legacy domain
|
||||
errors. As route groups declare expected errors and handlers map them, that
|
||||
middleware should shrink. It should not gain new name checks.
|
||||
|
||||
Unknown `500` responses should log full details server-side with
|
||||
`Cause.pretty(cause)` and return a safe public body.
|
||||
|
||||
The config startup regression in #27056 is the failure mode this rule is meant
|
||||
to avoid: a user-authored invalid `opencode.json` crossed the HttpApi boundary
|
||||
as a defect, so middleware replaced a useful `ConfigInvalidError` with a safe
|
||||
generic `UnknownError`. The compatibility fix is to preserve config parse and
|
||||
validation errors as client-visible `400`s. The target architecture is better:
|
||||
config loading should fail on the typed error channel, config HTTP handlers
|
||||
should map those errors to declared `ConfigApiError` responses, and the generic
|
||||
middleware should never see them.
|
||||
|
||||
## Migration Order
|
||||
|
||||
Prefer small vertical slices:
|
||||
|
||||
1. Fix rendering at one user-visible boundary.
|
||||
2. Convert one service domain to `Schema.TaggedErrorClass` errors.
|
||||
3. Map those errors at the affected HTTP handlers.
|
||||
4. Remove the corresponding name-based middleware branch if possible.
|
||||
5. Add or update focused tests for both service error tags and HTTP wire
|
||||
bodies.
|
||||
|
||||
Good early domains are storage not-found, worktree errors, and provider
|
||||
auth validation errors because they currently drive HTTP behavior.
|
||||
|
||||
Config parse and validation errors are also a good early slice because they
|
||||
are startup-blocking and must be rendered clearly in both CLI and TUI flows.
|
||||
|
||||
## Checklist For A PR
|
||||
|
||||
- [ ] Expected failures are typed errors, not defects.
|
||||
- [ ] Service method signatures expose the expected error union.
|
||||
- [ ] HTTP handlers translate domain errors at the boundary.
|
||||
- [ ] Public HTTP error bodies preserve existing wire contracts.
|
||||
- [ ] Generic middleware gets smaller or stays unchanged.
|
||||
- [ ] Focused tests cover the service error and any public HTTP response.
|
||||
218
packages/opencode/specs/effect/facades.md
Normal file
218
packages/opencode/specs/effect/facades.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Facade removal checklist
|
||||
|
||||
Concrete inventory of the remaining `makeRuntime(...)`-backed facades in `packages/opencode`.
|
||||
|
||||
Current status on this branch:
|
||||
|
||||
- `src/` has 5 `makeRuntime(...)` call sites total.
|
||||
- 2 are intentionally excluded from this checklist: `src/bus/index.ts` and `src/effect/cross-spawn-spawner.ts`.
|
||||
- That leaves 2 live runtime-backed service facades still worth tracking here: `src/npm/index.ts` and `src/cli/cmd/tui/config/tui.ts`.
|
||||
|
||||
Recent progress:
|
||||
|
||||
- Wave 1 is merged: `Pty`, `Skill`, `Vcs`, `ToolRegistry`, `Auth`.
|
||||
- Wave 2 is merged: `Config`, `Provider`, `File`, `LSP`, `MCP`.
|
||||
|
||||
## Priority hotspots
|
||||
|
||||
- `src/cli/cmd/tui/config/tui.ts` still exports `makeRuntime(...)` plus async facade helpers for `get()` and `waitForDependencies()`.
|
||||
- `src/npm/index.ts` still exports `makeRuntime(...)` plus async facade helpers for `install()`, `add()`, `outdated()`, and `which()`.
|
||||
|
||||
## Completed Batches
|
||||
|
||||
Low-risk batch, all merged:
|
||||
|
||||
1. `src/pty/index.ts`
|
||||
2. `src/skill/index.ts`
|
||||
3. `src/project/vcs.ts`
|
||||
4. `src/tool/registry.ts`
|
||||
5. `src/auth/index.ts`
|
||||
|
||||
Caller-heavy batch, all merged:
|
||||
|
||||
1. `src/config/config.ts`
|
||||
2. `src/provider/provider.ts`
|
||||
3. `../core/src/filesystem.ts`
|
||||
4. `src/lsp/index.ts`
|
||||
5. `src/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 the `makeRuntime` import
|
||||
|
||||
## Done means
|
||||
|
||||
For each service in the low-risk batch, the work is complete only when all of these are true:
|
||||
|
||||
1. all production callers stop using `Namespace.method(...)` facade calls
|
||||
2. all direct test callers stop using the facade and instead yield the service from context
|
||||
3. the service file no longer has `makeRuntime(...)`
|
||||
4. the service file no longer exports runtime-backed facade helpers
|
||||
5. `grep` for 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.
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
await Effect.runPromise(FileSystem.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:
|
||||
|
||||
1. Tests should usually provide the specific service layer, not `AppRuntime`.
|
||||
2. If a test uses `provideTmpdirInstance(...)` and needs child processes, prefer `CrossSpawnSpawner.defaultLayer`.
|
||||
3. Location-scoped services may need both the service layer and the right location fixture. `FileSystem` tests, for example, provide `Location.Service` plus `FileSystem.locationLayer`.
|
||||
4. Do not wrap a single `Service.use(...)` call in `Effect.gen(...)` just to return it. Use the direct form.
|
||||
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
|
||||
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
|
||||
|
||||
## Remaining work
|
||||
|
||||
Most of the original facade-removal backlog is already done. The practical remaining work is narrower now:
|
||||
|
||||
1. remove the `Npm` runtime-backed facade from `src/npm/index.ts`
|
||||
2. remove the `TuiConfig` runtime-backed facade from `src/cli/cmd/tui/config/tui.ts`
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `src/npm/index.ts` (`Npm`) - still exports runtime-backed async facade helpers on top of `Npm.Service`
|
||||
- [ ] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - still exports runtime-backed async facade helpers on top of `TuiConfig.Service`
|
||||
- [x] `src/session/session.ts` / `src/session/prompt.ts` / `src/session/revert.ts` / `src/session/summary.ts` - service-local facades removed
|
||||
- [x] `src/agent/agent.ts` (`Agent`) - service-local facades removed
|
||||
- [x] `src/permission/index.ts` (`Permission`) - service-local facades removed
|
||||
- [x] `src/worktree/index.ts` (`Worktree`) - service-local facades removed
|
||||
- [x] `src/plugin/index.ts` (`Plugin`) - service-local facades removed
|
||||
- [x] `src/snapshot/index.ts` (`Snapshot`) - service-local facades removed
|
||||
- [x] `../core/src/filesystem.ts` (`FileSystem`) - legacy opencode service removed
|
||||
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged
|
||||
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged
|
||||
- [x] `src/config/config.ts` (`Config`) - facades removed and merged
|
||||
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged
|
||||
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged
|
||||
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged
|
||||
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged
|
||||
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged
|
||||
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged
|
||||
|
||||
## Excluded `makeRuntime(...)` sites
|
||||
|
||||
- `src/bus/index.ts` - core bus plumbing, not a normal facade-removal target.
|
||||
- `src/effect/cross-spawn-spawner.ts` - runtime helper for `ChildProcessSpawner`, not a service namespace facade.
|
||||
247
packages/opencode/specs/effect/guide.md
Normal file
247
packages/opencode/specs/effect/guide.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Effect Guide
|
||||
|
||||
How we write Effect code in `packages/opencode`. The companion roadmap is
|
||||
[`todo.md`](./todo.md).
|
||||
|
||||
This guide describes the preferred shape for new work and migrations. If a
|
||||
legacy file differs, migrate it only when it is already in scope.
|
||||
|
||||
## Service Shape
|
||||
|
||||
Use one module per service: flat top-level exports, traced Effect methods,
|
||||
explicit layers, and a self-reexport at the bottom.
|
||||
|
||||
```ts
|
||||
export interface Interface {
|
||||
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make<State>(Effect.fn("Foo.state")(() => Effect.succeed({})))
|
||||
|
||||
const get = Effect.fn("Foo.get")(function* (id: FooID) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
return yield* loadFoo(s, id)
|
||||
})
|
||||
|
||||
return Service.of({ get })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FooDep.defaultLayer))
|
||||
|
||||
export * as Foo from "./foo"
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not use `export namespace Foo { ... }`.
|
||||
- Use `Effect.fn("Foo.method")` for public service methods.
|
||||
- Use `Effect.fnUntraced` for small internal helpers that do not need a
|
||||
span.
|
||||
- Keep helpers as non-exported top-level declarations in the same file.
|
||||
- Self-reexport with `export * as Foo from "."` for `index.ts`, otherwise
|
||||
`export * as Foo from "./foo"`.
|
||||
- In `src/config`, keep the existing top-of-file self-export pattern.
|
||||
|
||||
## Runtime Boundaries
|
||||
|
||||
Most code should run through [`AppRuntime`](../../src/effect/app-runtime.ts).
|
||||
It hosts `AppLayer`, shares the global `memoMap`, and restores the current
|
||||
instance/workspace refs when crossing from non-Effect code.
|
||||
|
||||
Use `AppRuntime.runPromise(effect)` at app boundaries such as CLI commands,
|
||||
HTTP handlers, or plain async adapters.
|
||||
|
||||
`makeRuntime(...)` still exists for a few intentional service-local
|
||||
boundaries and migration leftovers. Do not add a new service-local runtime
|
||||
unless the service truly cannot live in `AppLayer`.
|
||||
|
||||
## Runtime Flags
|
||||
|
||||
Read opencode runtime flags through
|
||||
[`RuntimeFlags.Service`](../../src/effect/runtime-flags.ts), not through
|
||||
mutable `Flag` or late `process.env` reads.
|
||||
|
||||
Tests should vary behavior with explicit layer variants:
|
||||
|
||||
```ts
|
||||
const it = testEffect(MyService.defaultLayer.pipe(Layer.provide(RuntimeFlags.layer({ experimentalReferences: true }))))
|
||||
```
|
||||
|
||||
Do not mutate `process.env` or `Flag` after services/layers are built.
|
||||
|
||||
## Per-Instance State
|
||||
|
||||
Use [`InstanceState`](../../src/effect/instance-state.ts) when two open
|
||||
directories should not share one copy of a service's state. It is backed by
|
||||
a `ScopedCache`, keyed by directory, and disposed automatically when an
|
||||
instance is unloaded.
|
||||
|
||||
Put subscriptions, finalizers, and scoped background work inside the
|
||||
`InstanceState.make(...)` initializer:
|
||||
|
||||
```ts
|
||||
const cache =
|
||||
yield *
|
||||
InstanceState.make<State>(
|
||||
Effect.fn("Foo.state")(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
yield* bus.subscribeAll().pipe(
|
||||
Stream.runForEach((event) => handleEvent(event)),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
yield* Effect.acquireRelease(openResource, closeResource)
|
||||
|
||||
return yield* loadInitialState()
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Do not add separate `started` flags on top of `InstanceState`. Let
|
||||
`ScopedCache` handle run-once and deduplication.
|
||||
|
||||
To make `init()` non-blocking, fork at the caller/bootstrap boundary. Do
|
||||
not fork inside `InstanceState.make(...)` just to return early with
|
||||
partially initialized state.
|
||||
|
||||
## Errors
|
||||
|
||||
Expected domain failures belong on the Effect error channel. Defects are
|
||||
for bugs, impossible states, and final unknown-boundary fallbacks.
|
||||
|
||||
```ts
|
||||
export class SessionBusyError extends Schema.TaggedErrorClass<SessionBusyError>()("SessionBusyError", {
|
||||
sessionID: SessionID,
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export type Error = Storage.Error | SessionBusyError
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (id: SessionID) => Effect.Effect<Info, Error>
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Use `Schema.TaggedErrorClass` for new expected domain errors.
|
||||
- Export a domain-level `Error` union from service modules.
|
||||
- In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` for
|
||||
direct expected failures.
|
||||
- Use `Schema.Defect` for unknown cause fields.
|
||||
- Use `Effect.try(...)`, `Effect.tryPromise(...)`, `Effect.mapError`,
|
||||
`Effect.catchTag`, and `Effect.catchTags` to translate external
|
||||
failures into domain errors.
|
||||
- Do not use `Effect.die(...)` for user, IO, validation, missing-resource,
|
||||
auth, provider, or busy-state failures.
|
||||
|
||||
## HTTP Error Boundaries
|
||||
|
||||
Service modules stay HTTP-agnostic. They should not import HTTP status
|
||||
codes, `HttpApiError`, `HttpServerResponse`, or route-specific error
|
||||
schemas.
|
||||
|
||||
HTTP handlers translate service errors into endpoint-declared public error
|
||||
schemas. Keep mappings inline when they are one-off; extract tiny shared
|
||||
helpers only when the same translation repeats.
|
||||
|
||||
Do not turn generic middleware into a registry of domain errors. Middleware
|
||||
should handle cross-cutting concerns and the final unknown-defect fallback.
|
||||
|
||||
Preserve legacy public wire shapes, such as `{ name, data }`, until a
|
||||
deliberate breaking API change.
|
||||
|
||||
## Schemas
|
||||
|
||||
Use Effect Schema as the source of truth.
|
||||
|
||||
- Use `Schema.Class` for exported data objects with a clear identity.
|
||||
- Use `Schema.Struct` for local shapes and simple nested objects.
|
||||
- Use `Schema.brand` for single-value IDs.
|
||||
- Reuse named refinements instead of re-spelling constraints.
|
||||
- Prefer narrow boundary helpers over generic Schema-to-Zod bridges.
|
||||
|
||||
Intentional boundaries:
|
||||
|
||||
- Public plugin tools still expose Zod through `tool.schema = z`.
|
||||
- Tool parameter JSON Schema is generated through tool-specific helpers.
|
||||
- Public config and TUI schemas are generated through the schema script.
|
||||
|
||||
## Preferred Services
|
||||
|
||||
In effectified code, yield existing services instead of dropping to ad hoc
|
||||
platform APIs.
|
||||
|
||||
- Use `FSUtil.Service` instead of raw `fs/promises` for app file IO.
|
||||
- Use `AppProcess.Service` instead of direct `ChildProcessSpawner.spawn` or
|
||||
legacy process helpers.
|
||||
- Use `HttpClient.HttpClient` instead of raw `fetch` inside Effect code.
|
||||
- Use `Path.Path`, `Config`, `Clock`, and `DateTime` when already inside
|
||||
Effect.
|
||||
- Use `Effect.callback` for callback-based APIs.
|
||||
- Use `Effect.void` instead of `Effect.succeed(undefined)`.
|
||||
- Use `Effect.cached` when concurrent callers should share one in-flight
|
||||
computation.
|
||||
|
||||
For background loops, use `Effect.repeat` or `Effect.schedule` with
|
||||
`Effect.forkScoped` in the owning layer/state scope.
|
||||
|
||||
## Promise And ALS Bridges
|
||||
|
||||
[`EffectBridge`](../../src/effect/bridge.ts) is the sanctioned helper for
|
||||
Promise/callback interop that needs to preserve instance/workspace context.
|
||||
It preserves explicit `InstanceRef` / `WorkspaceRef` context for effects run
|
||||
through the bridge. Plain JS callbacks that need instance data should receive
|
||||
that data explicitly.
|
||||
|
||||
## Testing
|
||||
|
||||
Detailed test migration rules live in
|
||||
[`test/EFFECT_TEST_MIGRATION.md`](../../test/EFFECT_TEST_MIGRATION.md).
|
||||
|
||||
Core pattern:
|
||||
|
||||
```ts
|
||||
const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
|
||||
|
||||
describe("my service", () => {
|
||||
it.instance("does the thing", () =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* MyService.Service
|
||||
expect(yield* svc.run()).toEqual("ok")
|
||||
}),
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Use `it.effect(...)` for TestClock/TestConsole tests.
|
||||
- Use `it.live(...)` for real timers, filesystem mtimes, child processes,
|
||||
git, locks, or other live integration behavior.
|
||||
- Use `it.instance(...)` for service tests that need a scoped instance.
|
||||
- Prefer Effect-aware fixtures from `test/fixture/fixture.ts`.
|
||||
- Avoid sleeps; wait for real events or deterministic state transitions.
|
||||
- Avoid mutable `process.env`, `Flag`, or module-global changes after
|
||||
layers are built.
|
||||
- Use `Layer.mock` for partial service stubs.
|
||||
- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` test
|
||||
wrappers.
|
||||
|
||||
## Verification
|
||||
|
||||
From `packages/opencode`:
|
||||
|
||||
```bash
|
||||
bun run typecheck
|
||||
bun run test -- path/to/test.ts
|
||||
```
|
||||
|
||||
Do not run tests from the repo root; the repo has a guard for that.
|
||||
13
packages/opencode/specs/effect/instance-context.md
Normal file
13
packages/opencode/specs/effect/instance-context.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Instance Context
|
||||
|
||||
Instance selection is now Effect-provided context.
|
||||
|
||||
Use these APIs:
|
||||
|
||||
- `InstanceRef` for the current project context.
|
||||
- `WorkspaceRef` for the current workspace id.
|
||||
- `InstanceState.context` / `InstanceState.directory` inside Effect services that require an instance.
|
||||
- `InstanceStore` at entry boundaries that need to load, reload, or dispose project contexts.
|
||||
- `EffectBridge` for native, plugin, or plain JavaScript callback boundaries that need to re-enter Effect with captured refs.
|
||||
|
||||
Do not add new ambient instance globals. Promise and callback boundaries should either stay in Effect, use `EffectBridge`, or pass the required context explicitly.
|
||||
30
packages/opencode/specs/effect/loose-ends.md
Normal file
30
packages/opencode/specs/effect/loose-ends.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Effect loose ends
|
||||
|
||||
Small follow-ups that do not fit neatly into the main facade, route, tool, or schema migration checklists.
|
||||
|
||||
## Config / TUI
|
||||
|
||||
- [ ] `cli/cmd/tui/config/tui.ts` - finish the internal Effect migration.
|
||||
Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`.
|
||||
- [ ] `cli/cmd/tui/config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
|
||||
Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`.
|
||||
- [x] `env/index.ts` - already uses `InstanceState.make(...)`.
|
||||
|
||||
## ConfigPaths
|
||||
|
||||
- [ ] `config/paths.ts` - split pure helpers from effectful helpers.
|
||||
Keep `fileInDirectory(...)` as a plain function.
|
||||
- [ ] `config/paths.ts` - add a `ConfigPaths.Service` for the effectful operations so callers do not inherit `FSUtil.Service` directly.
|
||||
Initial service surface should cover:
|
||||
- `projectFiles(...)`
|
||||
- `directories(...)`
|
||||
- `readFile(...)`
|
||||
- `parseText(...)`
|
||||
- [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists.
|
||||
- [ ] `cli/cmd/tui/config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
|
||||
- [ ] `cli/cmd/tui/config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
|
||||
|
||||
## Notes
|
||||
|
||||
- Prefer small, semantics-preserving config migrations. Config precedence, legacy key migration, and plugin origin tracking are easy to break accidentally.
|
||||
- When changing config loading internals, rerun the config and TUI suites first before broad package sweeps.
|
||||
62
packages/opencode/specs/effect/migration.md
Normal file
62
packages/opencode/specs/effect/migration.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Effect Migration Patterns
|
||||
|
||||
This is the compact reference for moving code toward the current Effect
|
||||
shape. The high-level roadmap is [`todo.md`](./todo.md); examples and
|
||||
rules are in [`guide.md`](./guide.md).
|
||||
|
||||
## Default Shape
|
||||
|
||||
- Service methods return `Effect`.
|
||||
- Service methods are named with `Effect.fn("Domain.method")`.
|
||||
- Expected failures are typed errors on the error channel.
|
||||
- Dependencies are yielded once at layer construction and closed over by
|
||||
methods.
|
||||
- `defaultLayer` wires production dependencies; tests can use open layers
|
||||
when replacing dependencies.
|
||||
|
||||
## Instance State
|
||||
|
||||
Use `InstanceState` for per-directory state, subscriptions, scoped
|
||||
background work, and per-instance cleanup.
|
||||
|
||||
Do not add ad hoc `started` flags on top of `InstanceState`; the scoped
|
||||
cache handles run-once and concurrent deduplication.
|
||||
|
||||
## Runtime Boundaries
|
||||
|
||||
Prefer `AppRuntime` for crossing from non-Effect code into the shared app
|
||||
layer.
|
||||
|
||||
`makeRuntime(...)` exists for intentional service-local boundaries and
|
||||
legacy facades. Do not add new service-local runtimes unless the service is
|
||||
genuinely outside `AppLayer`.
|
||||
|
||||
## Platform Edges
|
||||
|
||||
- Use `FSUtil.Service` instead of raw filesystem APIs in
|
||||
effectified services.
|
||||
- Use `AppProcess.Service` instead of raw process wrappers.
|
||||
- Use `HttpClient.HttpClient` instead of raw `fetch` in Effect code.
|
||||
- Use `Effect.cached` for shared in-flight work.
|
||||
- Use `Effect.callback` for callback APIs.
|
||||
|
||||
## Tests During Migration
|
||||
|
||||
When migrating code, migrate touched tests toward
|
||||
[`test/EFFECT_TEST_MIGRATION.md`](../../test/EFFECT_TEST_MIGRATION.md):
|
||||
|
||||
- `testEffect(...)`
|
||||
- `it.effect`, `it.live`, or `it.instance`
|
||||
- explicit layers for behavior changes
|
||||
- deterministic waits instead of sleeps
|
||||
- no mutable env/global flags after layers are built
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [ ] The code has a single Effect body instead of Promise wrappers around
|
||||
service calls.
|
||||
- [ ] Expected failures are typed errors, not thrown exceptions or defects.
|
||||
- [ ] Layer requirements are explicit.
|
||||
- [ ] Tests use Effect-aware fixtures and focused layers.
|
||||
- [ ] Public behavior and wire shapes are preserved unless intentionally
|
||||
changed.
|
||||
61
packages/opencode/specs/effect/routes.md
Normal file
61
packages/opencode/specs/effect/routes.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# HTTP Route Patterns
|
||||
|
||||
Current guidance for `packages/opencode/src/server/routes/instance/httpapi`.
|
||||
|
||||
## Handler Shape
|
||||
|
||||
Use `HttpApiBuilder.group(...)` for normal JSON and streaming HTTP API
|
||||
endpoints. Yield stable services once while building the handler layer,
|
||||
then close over those services in endpoint implementations.
|
||||
|
||||
```ts
|
||||
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
|
||||
return handlers.handle("list", () => session.list())
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Use raw `HttpRouter` only for routes that do not fit the request/response
|
||||
HttpApi model, such as WebSocket upgrades or catch-all fallback routes.
|
||||
|
||||
Do not rebuild stable layers inside request handlers. Provide stable
|
||||
services at the route/layer boundary and use request-level provisioning
|
||||
only for request-derived context.
|
||||
|
||||
## Error Boundaries
|
||||
|
||||
Expected service errors should be mapped at the handler boundary to
|
||||
endpoint-declared public HTTP errors. Keep one-off mappings inline. Extract
|
||||
small helpers when the same mapping repeats.
|
||||
|
||||
Generic middleware should not become a domain-error mapper. It should
|
||||
handle cross-cutting concerns and final unknown-defect fallback.
|
||||
|
||||
Public JSON errors should be explicit schema contracts declared on each
|
||||
endpoint or group. Built-in `HttpApiError.*` is fine only when its generated
|
||||
body is intentionally the public wire shape.
|
||||
|
||||
Preserve existing `{ name, data }` error bodies until a deliberate breaking
|
||||
API change.
|
||||
|
||||
## OpenAPI Compatibility
|
||||
|
||||
`public.ts` still owns SDK/OpenAPI compatibility transforms. Shrink those
|
||||
transforms by tightening source schemas one workaround at a time.
|
||||
|
||||
When an OpenAPI-visible source schema changes:
|
||||
|
||||
- verify the generated SDK diff is intentional
|
||||
- preserve legacy compatibility unless the PR explicitly changes it
|
||||
- prefer source-schema fixes over new post-processing rules
|
||||
|
||||
## Checklist For Route PRs
|
||||
|
||||
- [ ] Stable services are yielded at handler-layer construction.
|
||||
- [ ] Expected domain errors are translated at the route boundary.
|
||||
- [ ] Endpoint/group error schemas describe the public body and status.
|
||||
- [ ] Middleware does not gain new domain-specific name checks.
|
||||
- [ ] Raw routes are used only when HttpApi is the wrong abstraction.
|
||||
88
packages/opencode/specs/effect/schema.md
Normal file
88
packages/opencode/specs/effect/schema.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Schema Migration
|
||||
|
||||
Use Effect Schema as the source of truth for domain models, DTOs, IDs,
|
||||
inputs, outputs, and typed errors.
|
||||
|
||||
This is guidance, not an inventory. Do not use this file to track which
|
||||
schema modules are complete; verify current state with `git grep` before
|
||||
starting a migration.
|
||||
|
||||
## Preferred Shapes
|
||||
|
||||
Use `Schema.Class` for exported data objects with a clear domain identity:
|
||||
|
||||
```ts
|
||||
export class Info extends Schema.Class<Info>("Foo.Info")({
|
||||
id: FooID,
|
||||
name: Schema.String,
|
||||
enabled: Schema.Boolean,
|
||||
}) {}
|
||||
```
|
||||
|
||||
Use `Schema.Struct` for local shapes and simple nested objects:
|
||||
|
||||
```ts
|
||||
const Payload = Schema.Struct({
|
||||
id: FooID,
|
||||
value: Schema.String,
|
||||
})
|
||||
```
|
||||
|
||||
Use `Schema.TaggedErrorClass` for expected domain errors:
|
||||
|
||||
```ts
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("FooNotFoundError", {
|
||||
id: FooID,
|
||||
}) {}
|
||||
```
|
||||
|
||||
Use branded schema-backed IDs for single-value domain identifiers.
|
||||
|
||||
## Boundary Rule
|
||||
|
||||
Effect Schema should own the type. Boundaries should consume Effect Schema
|
||||
directly or use narrow boundary-specific helpers. Avoid reintroducing a
|
||||
generic Effect Schema -> Zod bridge.
|
||||
|
||||
Current intentional boundaries:
|
||||
|
||||
- Public plugin tools still expose Zod through `tool.schema = z`.
|
||||
- Tool parameters use tool-specific JSON Schema helpers.
|
||||
- Public config and TUI schema generation goes through the schema script.
|
||||
- AI SDK object generation uses Standard Schema / JSON Schema helpers.
|
||||
|
||||
When Zod must stay temporarily, leave a short note explaining the boundary
|
||||
or compatibility reason.
|
||||
|
||||
## Refinements
|
||||
|
||||
Reuse named refinements instead of re-spelling constraints:
|
||||
|
||||
```ts
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
||||
```
|
||||
|
||||
Prefer domain-named leaf schemas when the name improves callers or error
|
||||
messages. Avoid adding brands purely for novelty.
|
||||
|
||||
## Migration Order
|
||||
|
||||
For a domain that still has mixed schemas:
|
||||
|
||||
1. Shared leaf models and branded IDs.
|
||||
2. Exported `Info`, `Input`, `Output`, and event payload types.
|
||||
3. Expected domain errors.
|
||||
4. Service-local internal models.
|
||||
5. HTTP/tool/AI boundary validators.
|
||||
|
||||
Keep public wire shapes stable unless the PR is explicitly a breaking API
|
||||
change.
|
||||
|
||||
## Checklist For A PR
|
||||
|
||||
- [ ] There is one schema source of truth for each migrated type.
|
||||
- [ ] Remaining Zod is an intentional boundary choice.
|
||||
- [ ] Public JSON/OpenAPI output is unchanged or intentionally updated.
|
||||
- [ ] Derived helpers are narrow and boundary-specific.
|
||||
- [ ] Tests assert behavior, not duplicated schema implementation details.
|
||||
58
packages/opencode/specs/effect/server-package.md
Normal file
58
packages/opencode/specs/effect/server-package.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Server Package Extraction
|
||||
|
||||
Practical reference for a future `packages/server` split after the opencode
|
||||
server moved to the Effect HttpApi backend.
|
||||
|
||||
## Current State
|
||||
|
||||
- The server still lives in `packages/opencode`.
|
||||
- The runtime and app layer are centralized in `src/effect/app-runtime.ts` and
|
||||
`src/effect/run-service.ts`.
|
||||
- The route tree lives under `src/server/routes/instance/httpapi` and is hosted
|
||||
from `src/server/server.ts`.
|
||||
- OpenAPI generation is based on the HttpApi contract plus compatibility
|
||||
translation in `src/server/routes/instance/httpapi/public.ts`.
|
||||
- There is no standalone `packages/server` workspace yet.
|
||||
|
||||
## Future State
|
||||
|
||||
Target package layout:
|
||||
|
||||
- `packages/core` - shared domain services and schemas
|
||||
- `packages/server` - HTTP contracts, handlers, OpenAPI generation, and an
|
||||
embeddable server API
|
||||
- `packages/cli` - TUI and CLI entrypoints
|
||||
- `packages/sdk` - generated from the server OpenAPI spec
|
||||
- `packages/plugin` - plugin authoring surface
|
||||
|
||||
## Extraction Rule
|
||||
|
||||
Do not create a package cycle.
|
||||
|
||||
Until enough shared service code lives outside `packages/opencode`, a future
|
||||
`packages/server` should either:
|
||||
|
||||
- own pure HttpApi contracts only, or
|
||||
- accept host-provided services/layers/callbacks from `packages/opencode`
|
||||
|
||||
It should not import `packages/opencode` services while `packages/opencode`
|
||||
imports it to host routes.
|
||||
|
||||
## Suggested PR Sequence
|
||||
|
||||
1. Keep shrinking OpenAPI compatibility shims in `httpapi/public.ts`.
|
||||
2. Move stable domain schemas into shared packages only when they no longer
|
||||
depend on opencode-local runtime modules.
|
||||
3. Extract pure HttpApi contract modules into `packages/server` once the contract
|
||||
can compile without importing `packages/opencode` implementation details.
|
||||
4. Extract handler factories after their service dependencies can be supplied by
|
||||
a host layer instead of imported directly.
|
||||
5. Move server hosting last, after package ownership is clear.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not revive the old dual-backend migration shape.
|
||||
- Do not split server hosting before service dependencies have a clean package
|
||||
boundary.
|
||||
- Do not switch SDK generation to a new package until generated output is known
|
||||
to remain compatible.
|
||||
241
packages/opencode/specs/effect/todo.md
Normal file
241
packages/opencode/specs/effect/todo.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Effect TODO
|
||||
|
||||
Short roadmap for Effect cleanup in `packages/opencode`.
|
||||
|
||||
Current patterns and examples live in [`guide.md`](./guide.md). Error
|
||||
boundary migration details live in
|
||||
[`error-boundaries-plan.md`](./error-boundaries-plan.md). Test migration rules live in
|
||||
[`test/EFFECT_TEST_MIGRATION.md`](../../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
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
- `ERR` Typed errors — replace legacy `NamedError.create(...)` and
|
||||
`Effect.die(...)` for expected service failures with
|
||||
`Schema.TaggedErrorClass` errors on the Effect error channel.
|
||||
Shrinks: [`NamedError`](../../../core/src/util/error.ts) usage.
|
||||
- `RENDER` User-visible error rendering — preserve structured typed-error
|
||||
details at CLI, HTTP, and tool boundaries.
|
||||
Shrinks: opaque `Error: Name` rendering.
|
||||
- `HTTP` HTTP route cleanup — make route errors explicit instead of
|
||||
relying on generic middleware to guess status/body from error names.
|
||||
Shrinks: [`middleware/error.ts`](../../src/server/routes/instance/httpapi/middleware/error.ts)
|
||||
and route-level compatibility shims.
|
||||
- `TEST` Effect test migration — use `testEffect`, `it.live`, and
|
||||
`it.instance` with explicit layers.
|
||||
Shrinks: Promise-style tests, sleeps, mutable global test flags.
|
||||
- `RF` RuntimeFlags / Flag deletion — move mutable
|
||||
[`Flag`](../../../core/src/flag/flag.ts) reads into typed runtime/config
|
||||
services.
|
||||
Shrinks: [`flag.ts`](../../../core/src/flag/flag.ts),
|
||||
[`test/fixture/flag.ts`](../../test/fixture/flag.ts).
|
||||
- `GLOBAL` Global paths / import side effects — make global path state
|
||||
explicit and testable instead of mutable module state.
|
||||
Shrinks: [`global.ts`](../../../core/src/global.ts) import-time side
|
||||
effects, mutable `Global.Path` overrides, and its `Flag` dependency.
|
||||
- `INST` Instance context — keep project context explicit through Effect refs
|
||||
and bridge boundaries.
|
||||
- `BRIDGE` Promise/callback interop — keep bridge helpers, but reduce
|
||||
legacy ALS coupling.
|
||||
Shrinks: ad hoc Promise/callback re-entry code.
|
||||
- `PROC` AppProcess migration — prefer `AppProcess.Service` over raw
|
||||
process wrappers.
|
||||
Shrinks: direct spawn callsites and legacy process helpers.
|
||||
- `FS` FSUtil migration — prefer `FSUtil.Service` over raw
|
||||
filesystem APIs.
|
||||
Shrinks: direct `fs` / `Bun.file` service callsites where inappropriate.
|
||||
- `RT` Runtime/facade cleanup — remove service-local `makeRuntime`
|
||||
facades when not intentional.
|
||||
Shrinks: async facade exports around services and
|
||||
[`run-service.ts`](../../src/effect/run-service.ts) usage.
|
||||
- `OA` OpenAPI compatibility — tighten source schemas instead of
|
||||
post-processing generated OpenAPI.
|
||||
Shrinks: schema workaround blocks in
|
||||
[`public.ts`](../../src/server/routes/instance/httpapi/public.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 to `Effect.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 `Error` union 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`, use `yield* 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
|
||||
|
||||
- [x] `RENDER-1` CLI tagged config error rendering (#27256, tests #27257).
|
||||
- [x] `ERR-1` [`storage/storage.ts`](../../src/storage/storage.ts) typed
|
||||
`NotFoundError` (#27265) and removal of the server defect fallback
|
||||
(#27287).
|
||||
- [x] `ERR-2` [`worktree/index.ts`](../../src/worktree/index.ts) typed
|
||||
errors (#27296).
|
||||
- [x] `ERR-3` [`provider/auth.ts`](../../src/provider/auth.ts) typed
|
||||
validation/oauth errors (#27301).
|
||||
- [x] `HTTP-1` Unknown-500 details no longer leaked (#27251); follow-up
|
||||
to stop exposing named defects (#27471).
|
||||
- [x] Session message reads typed and made effectful (#27269, #27275,
|
||||
#27280, #27291).
|
||||
- [x] Session HTTP error contracts tightened (#27308); busy-session
|
||||
mapping centralized (#27375, #27473).
|
||||
- [x] Provider init (#27484) and LSP init (#27494) errors typed.
|
||||
|
||||
### First PR Candidates
|
||||
|
||||
- [ ] `HTTP-2` Audit one route group for explicit error contracts and
|
||||
decide which mappings stay inline vs. shared helper.
|
||||
- [ ] `ERR-4` Sweep remaining `NamedError.create(...)` and
|
||||
`Effect.die(...)` callsites for expected failures — re-run `git
|
||||
grep` to build a current inventory.
|
||||
- [ ] `RENDER-2` Audit CLI and TUI surfaces for any remaining opaque
|
||||
`Error: Name` rendering of typed errors.
|
||||
|
||||
## P1: Tests
|
||||
|
||||
When touching tests, migrate them toward the ideal patterns in
|
||||
[`test/EFFECT_TEST_MIGRATION.md`](../../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.env` or mutable globals after layers are built.
|
||||
- Use explicit layer variants, such as `RuntimeFlags.layer(...)`, for
|
||||
behavior changes.
|
||||
|
||||
## P2: RuntimeFlags / Flag Deletion
|
||||
|
||||
Recently completed:
|
||||
|
||||
- [x] Plugin/pure-mode flags moved to RuntimeFlags.
|
||||
- [x] Tool visibility flags moved to RuntimeFlags.
|
||||
- [x] Built-in websearch provider selection uses the same runtime flags as
|
||||
tool visibility.
|
||||
- [x] Removed global default-plugin disabling from test preload.
|
||||
- [x] `RF-1` Reference reads routed through runtime flags (#27318).
|
||||
- [x] `RF-2` Plan-mode prompt read routed through runtime flags (#27320).
|
||||
- [x] `RF-3` Event-system reads routed through runtime flags (#27323).
|
||||
- [x] `RF-4` Workspaces reads routed through runtime flags for session
|
||||
(#27335), sync (#27336), and control-plane (#27337).
|
||||
- [x] LLM client (#27368) and installation client (#27369) routed
|
||||
through runtime flags.
|
||||
- [x] TUI plugin runtime flags simplified (#27506).
|
||||
- [x] 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 import [`flag.ts`](../../../core/src/flag/flag.ts).
|
||||
Decide per-callsite whether to route through RuntimeFlags, accept
|
||||
as legitimate env/config boundary, or migrate to typed `Config`.
|
||||
- [ ] Delete [`test/fixture/flag.ts`](../../test/fixture/flag.ts) once
|
||||
tests no longer mutate `Flag`.
|
||||
- [ ] Delete [`flag.ts`](../../../core/src/flag/flag.ts) once no packages
|
||||
import it.
|
||||
|
||||
## P3: Global Paths
|
||||
|
||||
[`global.ts`](../../../core/src/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.Path` by mutating exported module state.
|
||||
- Most callers use `Global.Path` directly instead of the Effect service.
|
||||
- `Global.make()` still reads mutable `Flag.OPENCODE_CONFIG_DIR`.
|
||||
|
||||
Next PR candidates:
|
||||
|
||||
- [ ] Replace mutable `Global.Path` test overrides with explicit test
|
||||
layers or scoped helpers.
|
||||
- [ ] Move directory creation and `Flock` setup behind an explicit init
|
||||
boundary where possible.
|
||||
- [ ] Remove the `Flag` dependency from global path resolution.
|
||||
|
||||
## P4: Instance And Bridge
|
||||
|
||||
Instance context migration is complete for the legacy sync shim. Promise and callback interop continues through [`effect/bridge.ts`](../../src/effect/bridge.ts).
|
||||
|
||||
Current rules:
|
||||
|
||||
- Effect services read instance data from `InstanceRef`, `WorkspaceRef`, `InstanceState`, or explicit arguments.
|
||||
- Plain JavaScript callback boundaries use `EffectBridge` or explicit context arguments.
|
||||
- Runtime entrypoints must provide refs explicitly when they are instance-scoped.
|
||||
|
||||
## Lower Priority Tracks
|
||||
|
||||
- `PROC` / `FS` — continue AppProcess and FSUtil migrations as
|
||||
focused PRs when touching relevant files.
|
||||
- `RT` — remove service-local runtime facades only when they are not an
|
||||
intentional boundary.
|
||||
- `OA` — shrink [`public.ts`](../../src/server/routes/instance/httpapi/public.ts)
|
||||
by 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: `webfetch` HTML extraction
|
||||
and `shell` raw stream/promise edges.
|
||||
88
packages/opencode/specs/effect/tools.md
Normal file
88
packages/opencode/specs/effect/tools.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Tool migration
|
||||
|
||||
Practical reference for the current tool-migration state in `packages/opencode`.
|
||||
|
||||
## Status
|
||||
|
||||
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch, and the built-in tool surface is now largely on the target shape.
|
||||
|
||||
The current exported tools in `src/tool` all use `Tool.define(...)` with Effect-based initialization, and nearly all of them already build their tool body with `Effect.gen(...)` and `Effect.fn(...)`.
|
||||
|
||||
So the remaining work is no longer "convert tools to Effect at all". The remaining work is mostly:
|
||||
|
||||
1. remove Promise and raw platform bridges inside individual tool bodies
|
||||
2. swap tool internals to Effect-native services like `FSUtil`, `HttpClient`, and `ChildProcessSpawner`
|
||||
3. keep tests and callers aligned with `yield* info.init()` and real service graphs
|
||||
|
||||
## Current shape
|
||||
|
||||
`Tool.define(...)` is already the Effect-native helper here.
|
||||
|
||||
- `init` is an `Effect`
|
||||
- `info.init()` returns an `Effect`
|
||||
- `execute(...)` returns an `Effect`
|
||||
|
||||
That means a tool does not need a separate `Tool.defineEffect(...)` helper to count as migrated. A tool is effectively migrated when its init and execute path stay Effect-native, even if some internals still bridge to Promise-based or raw APIs.
|
||||
|
||||
## Tests
|
||||
|
||||
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
|
||||
|
||||
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
|
||||
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
|
||||
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
|
||||
|
||||
This keeps tool tests aligned with the production service graph and makes follow-up cleanup mostly mechanical.
|
||||
|
||||
## Exported tools
|
||||
|
||||
These exported tool definitions currently use `Tool.define(...)` in `src/tool`:
|
||||
|
||||
- [x] `apply_patch.ts`
|
||||
- [x] `bash.ts`
|
||||
- [x] `edit.ts`
|
||||
- [x] `glob.ts`
|
||||
- [x] `grep.ts`
|
||||
- [x] `invalid.ts`
|
||||
- [x] `lsp.ts`
|
||||
- [x] `plan.ts`
|
||||
- [x] `question.ts`
|
||||
- [x] `read.ts`
|
||||
- [x] `skill.ts`
|
||||
- [x] `task.ts`
|
||||
- [x] `todo.ts`
|
||||
- [x] `webfetch.ts`
|
||||
- [x] `websearch.ts`
|
||||
- [x] `write.ts`
|
||||
|
||||
Notes:
|
||||
|
||||
- There is no current `ls.ts` tool file on this branch.
|
||||
- `truncate.ts` is an Effect service used by tools, not a tool definition itself.
|
||||
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
|
||||
|
||||
## Follow-up cleanup
|
||||
|
||||
Most exported tools are already on the intended Effect-native shape. The remaining cleanup is narrower than the old checklist implied.
|
||||
|
||||
Current spot cleanups worth tracking:
|
||||
|
||||
- [x] `read.ts` — streams through `FSUtil.Service.stream` with `Stream.splitLines`; the legacy Node stream / `readline` helper is gone
|
||||
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
|
||||
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
|
||||
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and file-search routes
|
||||
- [x] `patch/index.ts` — apply path now returns `Effect` over `FSUtil.Service`; the parser and chunk replacer stay pure
|
||||
|
||||
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
|
||||
|
||||
- `apply_patch.ts`
|
||||
- `grep.ts`
|
||||
- `write.ts`
|
||||
- `websearch.ts`
|
||||
- `edit.ts`
|
||||
|
||||
## Filesystem notes
|
||||
|
||||
Current raw fs users that still appear relevant here:
|
||||
|
||||
- `file/ripgrep.ts` — `fs/promises`
|
||||
Reference in New Issue
Block a user