opencode-effect-enforcer 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +15 -305
- package/guidance/progressive-disclosure-guidance.md +18 -24
- package/package.json +2 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +2 -2
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +4 -4
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-language-model/SKILL.md +50 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +50 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
- package/src/guidance.ts +0 -1
|
@@ -3,25 +3,27 @@ name: effect-workflow
|
|
|
3
3
|
description: Build durable workflows with Effect using Workflow, Activity, DurableClock, DurableDeferred, and DurableQueue for execution that survives restarts, supports compensation (saga pattern), and integrates with Effect Cluster for distribution.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are an Effect TypeScript expert specializing in durable workflow execution using the `effect/
|
|
6
|
+
You are an Effect TypeScript expert specializing in durable workflow execution using the `effect/workflow` module.
|
|
7
7
|
|
|
8
8
|
## Effect Source Reference
|
|
9
9
|
|
|
10
10
|
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
-
|
|
11
|
+
Inspect the `effect@4.0.0` tag for this skill; main may be newer. Keep `effect`
|
|
12
|
+
and all companion packages on the same release.
|
|
12
13
|
|
|
13
14
|
Key source files:
|
|
14
15
|
|
|
15
|
-
- `packages/effect/src/
|
|
16
|
-
- `packages/effect/src/
|
|
17
|
-
- `packages/effect/src/
|
|
18
|
-
- `packages/effect/src/
|
|
19
|
-
- `packages/effect/src/
|
|
20
|
-
- `packages/effect/src/
|
|
16
|
+
- `packages/effect/src/workflow/Workflow.ts` — Workflow definition, compensation, annotations
|
|
17
|
+
- `packages/effect/src/workflow/Activity.ts` — Activity definition, retry, idempotency
|
|
18
|
+
- `packages/effect/src/workflow/WorkflowEngine.ts` — Engine service, in-memory layer, encoded interface
|
|
19
|
+
- `packages/effect/src/workflow/DurableClock.ts` — Durable sleep/timers
|
|
20
|
+
- `packages/effect/src/workflow/DurableDeferred.ts` — Durable signal/wait, tokens, done/succeed/fail
|
|
21
|
+
- `packages/effect/src/workflow/DurableQueue.ts` — Durable queue handing work to persisted background workers
|
|
21
22
|
|
|
22
23
|
## IMPORTANT: Unstable API
|
|
23
24
|
|
|
24
|
-
The workflow module lives under `effect/
|
|
25
|
+
The workflow module lives under `effect/workflow` and remains `@stability unstable`:
|
|
26
|
+
its APIs may break in minor releases. All imports use this path:
|
|
25
27
|
|
|
26
28
|
```ts
|
|
27
29
|
import {
|
|
@@ -31,7 +33,7 @@ import {
|
|
|
31
33
|
DurableClock,
|
|
32
34
|
DurableDeferred,
|
|
33
35
|
DurableQueue
|
|
34
|
-
} from 'effect/
|
|
36
|
+
} from 'effect/workflow';
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
## Core Concepts
|
|
@@ -51,19 +53,18 @@ DurableQueue → hand work to a persisted background worker and await its resu
|
|
|
51
53
|
|
|
52
54
|
## Workflow Definition
|
|
53
55
|
|
|
54
|
-
Use `Workflow.make` to define a workflow. Every workflow has:
|
|
56
|
+
Use `Workflow.make(tag, options)` to define a workflow. Every workflow has:
|
|
55
57
|
|
|
56
|
-
- A unique `
|
|
58
|
+
- A unique tag (the first argument, exposed as `_tag`)
|
|
57
59
|
- A `payload` schema (struct fields or Schema)
|
|
58
60
|
- An `idempotencyKey` function that produces a deterministic execution ID from the payload
|
|
59
61
|
- Optional `success` and `error` schemas (default `Schema.Void` / `Schema.Never`)
|
|
60
62
|
|
|
61
63
|
```ts
|
|
62
|
-
import { Workflow } from 'effect/
|
|
64
|
+
import { Workflow } from 'effect/workflow';
|
|
63
65
|
import { Schema } from 'effect';
|
|
64
66
|
|
|
65
|
-
const SendEmail = Workflow.make({
|
|
66
|
-
name: 'SendEmail',
|
|
67
|
+
const SendEmail = Workflow.make('SendEmail', {
|
|
67
68
|
payload: {
|
|
68
69
|
to: Schema.String,
|
|
69
70
|
subject: Schema.String,
|
|
@@ -77,6 +78,21 @@ const SendEmail = Workflow.make({
|
|
|
77
78
|
|
|
78
79
|
### Registering a Workflow Handler
|
|
79
80
|
|
|
81
|
+
Class syntax is also supported for shared workflow definitions:
|
|
82
|
+
|
|
83
|
+
<!-- typecheck -->
|
|
84
|
+
```ts
|
|
85
|
+
import * as Schema from 'effect/Schema';
|
|
86
|
+
import { Workflow } from 'effect/workflow';
|
|
87
|
+
|
|
88
|
+
class SendReceipt extends Workflow.make('SendReceipt', {
|
|
89
|
+
payload: { orderId: Schema.String },
|
|
90
|
+
idempotencyKey: ({ orderId }) => orderId
|
|
91
|
+
}) {}
|
|
92
|
+
|
|
93
|
+
const receiptExecutionId = SendReceipt.executionId({ orderId: 'order-1' });
|
|
94
|
+
```
|
|
95
|
+
|
|
80
96
|
Use `workflow.toLayer(handler)` to register the execution logic. The handler receives the decoded payload and execution ID:
|
|
81
97
|
|
|
82
98
|
```ts
|
|
@@ -131,7 +147,17 @@ ID (`Schema.String`). Consumers can persist that ID to poll/resume the workflow.
|
|
|
131
147
|
Update generated client response types and tests that expected `void`; ordinary
|
|
132
148
|
RPC `discard: true` call options still discard the response and cannot return it.
|
|
133
149
|
|
|
134
|
-
The execution ID
|
|
150
|
+
The execution ID hashes the length-prefixed input
|
|
151
|
+
`"${name.length}:${name}:${idempotencyKey(payload)}"`. This avoids ambiguous
|
|
152
|
+
tag/key concatenations. The same workflow tag and idempotency key identify the
|
|
153
|
+
same execution, even if other payload fields differ; choose the key accordingly.
|
|
154
|
+
Use `workflow.executionId(payload)` instead of duplicating the hash algorithm.
|
|
155
|
+
|
|
156
|
+
This differs from rc.116's `"${name}-${key}"` hash: recomputing from an old payload
|
|
157
|
+
produces a different ID. Preserve stored execution IDs for polling, resuming,
|
|
158
|
+
interrupting, and deferred tokens for existing executions. Coordinate the
|
|
159
|
+
identity transition before resubmitting old payloads, which would otherwise
|
|
160
|
+
start distinct executions.
|
|
135
161
|
|
|
136
162
|
```ts
|
|
137
163
|
const id =
|
|
@@ -148,15 +174,15 @@ const id =
|
|
|
148
174
|
Activities are the **atomic units of work** inside a workflow. Their results are persisted by the engine, so on replay they return the cached result without re-executing.
|
|
149
175
|
|
|
150
176
|
```ts
|
|
151
|
-
import { Activity } from 'effect/
|
|
177
|
+
import { Activity } from 'effect/workflow';
|
|
152
178
|
|
|
153
179
|
const validateRecipient = Activity.make({
|
|
154
180
|
name: 'ValidateRecipient',
|
|
155
181
|
success: Schema.Struct({ valid: Schema.Boolean }),
|
|
156
182
|
error: Schema.String,
|
|
157
183
|
execute: Effect.gen(function* () {
|
|
158
|
-
//
|
|
159
|
-
//
|
|
184
|
+
// A persisted result is reused on replay. Interrupted/crashed work
|
|
185
|
+
// can rerun before its result is recorded: external writes need idempotency.
|
|
160
186
|
const result = yield* checkEmailService(payload.to);
|
|
161
187
|
return { valid: result.isValid };
|
|
162
188
|
})
|
|
@@ -180,7 +206,7 @@ const handler = SendEmail.toLayer((payload, executionId) =>
|
|
|
180
206
|
Use `Activity.retry` to retry an effect within an activity. The engine tracks the attempt count automatically and exposes it via `Activity.CurrentAttempt`:
|
|
181
207
|
|
|
182
208
|
```ts
|
|
183
|
-
import { Activity } from 'effect/
|
|
209
|
+
import { Activity } from 'effect/workflow';
|
|
184
210
|
|
|
185
211
|
const sendWithRetry = Activity.make({
|
|
186
212
|
name: 'SendWithRetry',
|
|
@@ -263,7 +289,7 @@ const result =
|
|
|
263
289
|
`DurableClock.sleep` creates a timer that survives process restarts. Short sleeps (<=60s by default) run in-memory as regular activities. Longer sleeps are scheduled through the engine.
|
|
264
290
|
|
|
265
291
|
```ts
|
|
266
|
-
import { DurableClock } from 'effect/
|
|
292
|
+
import { DurableClock } from 'effect/workflow';
|
|
267
293
|
|
|
268
294
|
// Inside a workflow handler:
|
|
269
295
|
yield*
|
|
@@ -290,7 +316,7 @@ Under the hood, a DurableClock creates a `DurableDeferred` and the engine schedu
|
|
|
290
316
|
### Creating and Awaiting
|
|
291
317
|
|
|
292
318
|
```ts
|
|
293
|
-
import { DurableDeferred } from 'effect/
|
|
319
|
+
import { DurableDeferred } from 'effect/workflow';
|
|
294
320
|
import { Schema, Exit } from 'effect';
|
|
295
321
|
|
|
296
322
|
// Define the deferred with typed schemas
|
|
@@ -305,6 +331,15 @@ const confirmation = yield* DurableDeferred.await(PaymentConfirmation);
|
|
|
305
331
|
|
|
306
332
|
The engine registers the awaited deferred before reading it. Completing that deferred while the workflow run is still active preempts a run parked on it, retains the pending result, and replays so the completion is observed. The in-memory engine follows the same behavior as `ClusterWorkflowEngine`; this closes the race where a live completion could otherwise be missed until a later retry.
|
|
307
333
|
|
|
334
|
+
The cluster engine retries failed run resets before acknowledging deferred
|
|
335
|
+
completion. It resets the suspended run conditionally against the reply it read,
|
|
336
|
+
so a stale concurrent resume cannot erase a newer completed reply. Custom
|
|
337
|
+
`MessageStorage.clearReplies(requestId, { expectedReplyId })` implementations
|
|
338
|
+
must atomically compare the latest reply ID before clearing; mismatch is a
|
|
339
|
+
successful no-op. `Sharding.reset` returning `false` means reset failed, not that
|
|
340
|
+
the expected-reply comparison mismatched. See `effect-rpc-cluster` for storage
|
|
341
|
+
claim-reset semantics.
|
|
342
|
+
|
|
308
343
|
### Completing from Outside
|
|
309
344
|
|
|
310
345
|
External code (e.g., a webhook handler) completes the deferred using a **token**:
|
|
@@ -395,8 +430,8 @@ const result =
|
|
|
395
430
|
`DurableQueue` lets a workflow delegate a unit of work to a **persisted background worker** and suspend until the worker records a result. The workflow calls `process` to enqueue an item and wait; a separate worker created with `worker` / `makeWorker` takes the item, runs the handler, and completes the waiting workflow through a `DurableDeferred` token.
|
|
396
431
|
|
|
397
432
|
```ts
|
|
398
|
-
import { DurableQueue, Workflow, WorkflowEngine } from 'effect/
|
|
399
|
-
import { PersistedQueue } from 'effect/
|
|
433
|
+
import { DurableQueue, Workflow, WorkflowEngine } from 'effect/workflow';
|
|
434
|
+
import { PersistedQueue } from 'effect/persistence';
|
|
400
435
|
import { Effect, Layer, Schema } from 'effect';
|
|
401
436
|
```
|
|
402
437
|
|
|
@@ -514,7 +549,7 @@ The compensation function receives:
|
|
|
514
549
|
Controls whether defects (unexpected errors) are captured in the workflow result. Default: `true`.
|
|
515
550
|
|
|
516
551
|
```ts
|
|
517
|
-
const MyWorkflow = Workflow.make({ ... }).annotate(Workflow.CaptureDefects, false)
|
|
552
|
+
const MyWorkflow = Workflow.make('MyWorkflow', { ... }).annotate(Workflow.CaptureDefects, false)
|
|
518
553
|
```
|
|
519
554
|
|
|
520
555
|
### SuspendOnFailure
|
|
@@ -522,7 +557,7 @@ const MyWorkflow = Workflow.make({ ... }).annotate(Workflow.CaptureDefects, fals
|
|
|
522
557
|
When `true`, the workflow suspends on any error instead of failing. You can then manually resume it:
|
|
523
558
|
|
|
524
559
|
```ts
|
|
525
|
-
const MyWorkflow = Workflow.make({ ... }).annotate(Workflow.SuspendOnFailure, true)
|
|
560
|
+
const MyWorkflow = Workflow.make('MyWorkflow', { ... }).annotate(Workflow.SuspendOnFailure, true)
|
|
526
561
|
|
|
527
562
|
// Later, after fixing the issue:
|
|
528
563
|
yield* MyWorkflow.resume(executionId)
|
|
@@ -557,7 +592,7 @@ The `WorkflowEngine` is a service that orchestrates workflow execution. It handl
|
|
|
557
592
|
For testing and local development, use the in-memory engine:
|
|
558
593
|
|
|
559
594
|
```ts
|
|
560
|
-
import { WorkflowEngine } from 'effect/
|
|
595
|
+
import { WorkflowEngine } from 'effect/workflow';
|
|
561
596
|
|
|
562
597
|
const TestLayer = Layer.mergeAll(
|
|
563
598
|
SendEmailLive
|
|
@@ -569,11 +604,11 @@ const TestLayer = Layer.mergeAll(
|
|
|
569
604
|
|
|
570
605
|
### Production Engine — `ClusterWorkflowEngine.layer`
|
|
571
606
|
|
|
572
|
-
For production, use `ClusterWorkflowEngine.layer` from `effect/
|
|
607
|
+
For production, use `ClusterWorkflowEngine.layer` from `effect/cluster`. It wires the workflow engine into `Sharding` + `MessageStorage` so executions, activities, and durable signals survive restarts and can be distributed across runners:
|
|
573
608
|
|
|
574
609
|
```ts
|
|
575
610
|
import { Layer } from 'effect';
|
|
576
|
-
import { ClusterWorkflowEngine } from 'effect/
|
|
611
|
+
import { ClusterWorkflowEngine } from 'effect/cluster';
|
|
577
612
|
|
|
578
613
|
const WorkflowsLayer = Layer.mergeAll(
|
|
579
614
|
SendEmailLive,
|
|
@@ -585,14 +620,18 @@ const WorkflowsLayer = Layer.mergeAll(
|
|
|
585
620
|
|
|
586
621
|
The `ClusterWorkflowEngine` requires `Sharding | MessageStorage` in context; both come from any of the cluster runtime bundles. See the `effect-rpc-cluster` skill for cluster setup.
|
|
587
622
|
|
|
623
|
+
Workflow tags are unique registration identities. Reusing a tag with a different
|
|
624
|
+
workflow definition logs a warning (including payload shapes) and retains the
|
|
625
|
+
existing definition; it is not a schema replacement mechanism.
|
|
626
|
+
|
|
588
627
|
#### Workflow shard-group routing
|
|
589
628
|
|
|
590
|
-
A workflow can be annotated with `ClusterSchema.ShardGroup` (from `effect/
|
|
629
|
+
A workflow can be annotated with `ClusterSchema.ShardGroup` (from `effect/cluster`), exactly like an entity:
|
|
591
630
|
|
|
592
631
|
```ts
|
|
593
|
-
import { ClusterSchema } from 'effect/
|
|
632
|
+
import { ClusterSchema } from 'effect/cluster';
|
|
594
633
|
|
|
595
|
-
const OrderWorkflow = Workflow.make({ /* ... */ })
|
|
634
|
+
const OrderWorkflow = Workflow.make('OrderWorkflow', { /* ... */ })
|
|
596
635
|
.annotate(ClusterSchema.ShardGroup, () => 'workflow');
|
|
597
636
|
```
|
|
598
637
|
|
|
@@ -633,8 +672,7 @@ The `Encoded` interface works with raw/encoded values (JSON-safe), while the `Wo
|
|
|
633
672
|
When a workflow suspends (waiting for an activity or deferred), the engine retries with `Schedule.min([Schedule.exponential("200 millis", 1.5), Schedule.spaced("30 seconds")])`: exponential backoff from 200ms, capped at 30s. (`Schedule.andThen` / `andThenResult` were renamed to `Schedule.concat` / `concatResult`, and the old `Schedule.either` cap pattern is now `Schedule.min`.) Override per-workflow:
|
|
634
673
|
|
|
635
674
|
```ts
|
|
636
|
-
const MyWorkflow = Workflow.make({
|
|
637
|
-
name: 'MyWorkflow',
|
|
675
|
+
const MyWorkflow = Workflow.make('MyWorkflow', {
|
|
638
676
|
payload: { id: Schema.String },
|
|
639
677
|
idempotencyKey: (p) => p.id,
|
|
640
678
|
suspendedRetrySchedule: Schedule.spaced('5 seconds')
|
|
@@ -651,7 +689,7 @@ import {
|
|
|
651
689
|
DurableDeferred,
|
|
652
690
|
Workflow,
|
|
653
691
|
WorkflowEngine
|
|
654
|
-
} from 'effect/
|
|
692
|
+
} from 'effect/workflow';
|
|
655
693
|
|
|
656
694
|
// --- Schemas ---
|
|
657
695
|
|
|
@@ -700,8 +738,7 @@ const shipOrder = Activity.make({
|
|
|
700
738
|
|
|
701
739
|
// --- Workflow ---
|
|
702
740
|
|
|
703
|
-
const ProcessOrder = Workflow.make({
|
|
704
|
-
name: 'ProcessOrder',
|
|
741
|
+
const ProcessOrder = Workflow.make('ProcessOrder', {
|
|
705
742
|
payload: {
|
|
706
743
|
orderId: Schema.String,
|
|
707
744
|
items: Schema.Array(Schema.String)
|
|
@@ -806,10 +843,10 @@ Compensation finalizers are only registered for top-level effects in the workflo
|
|
|
806
843
|
|
|
807
844
|
## Integration with Effect Cluster
|
|
808
845
|
|
|
809
|
-
For production durability and distribution, swap `WorkflowEngine.layerMemory` for `ClusterWorkflowEngine.layer` (from `effect/
|
|
846
|
+
For production durability and distribution, swap `WorkflowEngine.layerMemory` for `ClusterWorkflowEngine.layer` (from `effect/cluster`):
|
|
810
847
|
|
|
811
848
|
```ts
|
|
812
|
-
import { ClusterWorkflowEngine } from 'effect/
|
|
849
|
+
import { ClusterWorkflowEngine } from 'effect/cluster';
|
|
813
850
|
import { NodeClusterSocket } from '@effect/platform-node';
|
|
814
851
|
|
|
815
852
|
const MainLive = Layer.mergeAll(SendEmailLive, ProcessOrderLive).pipe(
|
package/src/guidance.ts
CHANGED
|
@@ -60,7 +60,6 @@ export const withPluginPolicy = (guidance: string): string =>
|
|
|
60
60
|
[
|
|
61
61
|
guidance,
|
|
62
62
|
"OpenCode Effect Enforcer policy:",
|
|
63
|
-
"- The bundled effect-* skills are available through OpenCode's skill tool. Load at least four relevant skills before planning or writing Effect code.",
|
|
64
63
|
"- Treat post-write pattern feedback as an immediate review request. Fix valid findings before continuing.",
|
|
65
64
|
"- Pattern findings are advisory: explain intentional exceptions or false positives instead of changing correct code."
|
|
66
65
|
].join("\n\n---\n\n")
|