You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: packages/opencode/AGENTS.md
+30-46Lines changed: 30 additions & 46 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,71 +9,55 @@
9
9
-**Output**: creates `migration/<timestamp>_<slug>/migration.sql` and `snapshot.json`.
10
10
-**Tests**: migration tests should read the per-folder layout (no `_journal.json`).
11
11
12
-
# opencode Effect guide
12
+
# opencode Effect rules
13
13
14
-
Instructions to follow when writing Effect.
14
+
Use these rules when writing or migrating Effect code.
15
15
16
-
## Schemas
16
+
See `specs/effect-migration.md` for the compact pattern reference and examples.
17
17
18
-
- Use `Schema.Class` for data types with multiple fields.
19
-
- Use branded schemas (`Schema.brand`) for single-value types.
20
-
21
-
## Services
22
-
23
-
- Services use `ServiceMap.Service<ServiceName, ServiceName.Service>()("@console/<Name>")`.
24
-
- In `Layer.effect`, always return service implementations with `ServiceName.of({ ... })`, never a plain object.
25
-
26
-
## Errors
27
-
28
-
- Use `Schema.TaggedErrorClass` for typed errors.
29
-
- For defect-like causes, use `Schema.Defect` instead of `unknown`.
30
-
- In `Effect.gen`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches.
31
-
32
-
## Effects
18
+
## Core
33
19
34
20
- Use `Effect.gen(function* () { ... })` for composition.
35
-
- Use `Effect.fn("ServiceName.method")` for named/traced effects and `Effect.fnUntraced` for internal helpers.
36
-
-`Effect.fn` / `Effect.fnUntraced` accept pipeable operators as extra arguments, so avoid unnecessary `flow` or outer `.pipe()` wrappers.
37
-
-**`Effect.callback`** (not `Effect.async`) for callback-based APIs. The classic `Effect.async` was renamed to `Effect.callback` in effect-smol/v4.
38
-
39
-
## Time
40
-
21
+
- Use `Effect.fn("Domain.method")` for named/traced effects and `Effect.fnUntraced` for internal helpers.
22
+
-`Effect.fn` / `Effect.fnUntraced` accept pipeable operators as extra arguments, so avoid unnecessary outer `.pipe()` wrappers.
23
+
- Use `Effect.callback` for callback-based APIs.
41
24
- Prefer `DateTime.nowAsDate` over `new Date(yield* Clock.currentTimeMillis)` when you need a `Date`.
42
25
43
-
## Errors
26
+
## Schemas and errors
27
+
28
+
- Use `Schema.Class` for multi-field data.
29
+
- Use branded schemas (`Schema.brand`) for single-value types.
30
+
- Use `Schema.TaggedErrorClass` for typed errors.
31
+
- Use `Schema.Defect` instead of `unknown` for defect-like causes.
32
+
- In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches.
44
33
45
-
- In `Effect.gen/fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches.
34
+
## Runtime vs Instances
46
35
47
-
## Instance-scoped Effect services
36
+
- Use the shared runtime for process-wide services with one lifecycle for the whole app.
37
+
- Use `src/effect/instances.ts` for per-directory or per-project services that need `InstanceContext`, per-instance state, or per-instance cleanup.
38
+
- If two open directories should not share one copy of the service, it belongs in `Instances`.
39
+
- Instance-scoped services should read context from `InstanceContext`, not `Instance.*` globals.
48
40
49
-
Services that need per-directory lifecycle (created/destroyed per instance) go through the `Instances` LayerMap:
41
+
## Preferred Effect services
50
42
51
-
1. Define a `ServiceMap.Service` with a `static readonly layer` (see `FileWatcherService`, `QuestionService`, `PermissionService`, `ProviderAuthService`).
52
-
2. Add it to `InstanceServices` union and `Layer.mergeAll(...)` in `src/effect/instances.ts`.
53
-
3. Use `InstanceContext` inside the layer to read `directory` and `project` instead of `Instance.*` globals.
54
-
4. Call from legacy code via `runPromiseInstance(MyService.use((s) => s.method()))`.
43
+
- In effectified services, prefer yielding existing Effect services over dropping down to ad hoc platform APIs.
44
+
- Prefer `FileSystem.FileSystem` instead of raw `fs/promises` for effectful file I/O.
45
+
- Prefer `ChildProcessSpawner.ChildProcessSpawner` with `ChildProcess.make(...)` instead of custom process wrappers.
46
+
- Prefer `HttpClient.HttpClient` instead of raw `fetch`.
47
+
- Prefer `Path.Path`, `Config`, `Clock`, and `DateTime` when those concerns are already inside Effect code.
48
+
- For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition.
55
49
56
-
###Instance.bind — ALS context for native callbacks
50
+
## Instance.bind — ALS for native callbacks
57
51
58
-
`Instance.bind(fn)` captures the current Instance AsyncLocalStorage context and returns a wrapper that restores it synchronously when called.
52
+
`Instance.bind(fn)` captures the current Instance AsyncLocalStorage context and restores it synchronously when called.
59
53
60
-
**Use it** when passing callbacks to native C/C++ addons (`@parcel/watcher`, `node-pty`, native `fs.watch`, etc.) that need to call `Bus.publish`, `Instance.state()`, or anything that reads `Instance.directory`.
54
+
Use it for native addon callbacks (`@parcel/watcher`, `node-pty`, native `fs.watch`, etc.) that need to call `Bus.publish`, `Instance.state()`, or anything that reads `Instance.directory`.
61
55
62
-
**Don't need it** for `setTimeout`, `Promise.then`, `EventEmitter.on`, or Effect fibers — Node.js ALS propagates through those automatically.
56
+
You do not need it for `setTimeout`, `Promise.then`, `EventEmitter.on`, or Effect fibers.
63
57
64
58
```typescript
65
-
// Native addon callback — needs Instance.bind
66
59
const cb =Instance.bind((err, evts) => {
67
60
Bus.publish(MyEvent, { ... })
68
61
})
69
62
nativeAddon.subscribe(dir, cb)
70
63
```
71
-
72
-
## Flag → Effect.Config migration
73
-
74
-
Flags in `src/flag/flag.ts` are being migrated from static `truthy(...)` reads to `Config.boolean(...).pipe(Config.withDefault(false))` as their consumers get effectified.
75
-
76
-
- Effectful flags return `Config<boolean>` and are read with `yield*` inside `Effect.gen`.
77
-
- The default `ConfigProvider` reads from `process.env`, so env vars keep working.
78
-
- Tests can override via `ConfigProvider.layer(ConfigProvider.fromUnknown({ ... }))`.
79
-
- Keep all flags in `flag.ts` as the single registry — just change the implementation from `truthy()` to `Config.boolean()` when the consumer moves to Effect.
- Keep `Interface`, `Service`, `layer`, and `defaultLayer` on the owning namespace
46
+
- Export `defaultLayer` only when wiring dependencies is useful
47
+
- Use the direct namespace form once the module is fully migrated
48
+
49
+
## Temporary mixed-mode pattern
50
+
51
+
Prefer a single namespace whenever possible.
52
+
53
+
Use a `*Effect` namespace only when there is a real mixed-mode split, usually because a legacy boundary facade still exists or because merging everything immediately would create awkward cycles.
0 commit comments