# Effect 4 reference projects (studied 2026-09-18)

Source study of high-quality Effect v4 codebases, used to pressure-test the rat-stack floor. Clones inspected locally under `~/.repo-autopsy/`; versions read from each repo's lockfile or catalog on the day of study.

## Projects

| Repo | Stars | Effect | Runtime | Why it matters to us |
| --- | --- | --- | --- | --- |
| [pingdotgg/t3code](https://github.com/pingdotgg/t3code) | 23k | `4.0.0-rc.115` (catalog) | Node 24, pnpm 11, TS 7.0.2 | Same Effect, TS, and pnpm pins as rat-stack. Closest sibling. Heaviest `@effect/vitest` user (479 files). |
| [anomalyco/opencode](https://github.com/anomalyco/opencode) | 208k | `4.0.0-beta.83` (patched) | Bun | Architecture reference only. Older beta: still has `ServiceMap.Service` remnants and `Schema.TaggedErrorClass`. Do not copy API names from it. |
| [tim-smart/lalph](https://github.com/tim-smart/lalph) | 130 | `4.0.0-beta.94` | Node, pnpm, tsgo | Effect core maintainer's CLI. Uses `effect/unstable/cli`, `Context.Service` + `static layer`, `@effect/language-service`. Closest shape to our CLI. |
| [kitlangton/ghui](https://github.com/kitlangton/ghui) | 1.1k | `4.0.0-beta.90` | Bun | Kit's TUI. `Context.Service`, `Schema.TaggedError`, `@effect/language-service`, oxlint + oxfmt. |
| [alchemy-run/alchemy](https://github.com/alchemy-run/alchemy) | 1.3k | peer `>=4.0.0-rc.115` | Node | Our infra layer. Peer-depends on `@effect/language-service ^0.87`. |
| [cevr/effect-machine](https://github.com/cevr/effect-machine) | 72 | peer `>=4.0.0-rc.112 <5` | any | Schema-first state machines native to Effect. The Effect-world answer to XState. |

Other Effect 4 adopters found via GitHub code search for `effect/unstable` imports: `alchemy-run/distilled`, `MapleTechLabs/maple`, `tim-smart/receipts`, `tim-smart/dfx`, `kitlangton/motel`, `kitlangton/stack`, `leonitousconforti/the-moby-effect`, `crosshatch/crosshatch`, `anomalyco/effect-http-recorder`, `Effectful-Tech/clanka`.

## Patterns every quality codebase shares

- **Service tags are `Context.Service` classes** with a namespaced string id (`"t3/persistence/Services/ProjectionState"`, `"lalph/Editor"`, `"@opencode/ServerAuthConfig"`). `Effect.Service` is effectively gone (t3code: 4 files, opencode: 0). `ServiceMap` was a beta-only name; rc.115 exports `Context.Service`, `Context.Reference`, `Context.Key`.
- **`static layer = Layer.effect(this, this.make).pipe(Layer.provide(...))`** co-located on the service class (lalph, ghui, Kit's effect-solutions). t3code splits `Services/` (tag + shape interface) from `Layers/` (implementation) per module.
- **Errors are `Schema.TaggedError`** with `override get message()` and `cause: Schema.Defect()`. t3code keeps wire-crossing errors in `packages/contracts` so they serialize. Same shape rat-stack already uses in `packages/core/src/stats.ts`.
- **`Effect.fn("name")` for traced functions, `Effect.fnUntraced` for hot paths.** Straight from `node_modules/effect/AGENTS.md`.
- **Tests use `@effect/vitest`**: `it.effect`, `it.layer(TestLayer)`, `Layer.mock(Tag)({...})`. t3code enforces this with a custom oxlint rule `no-manual-effect-runtime-in-tests` that bans `Effect.runPromise` and `ManagedRuntime.make` in test files.
- **`@effect/language-service` is on in every Node project** (lalph, ghui, t3code, alchemy peer). t3code turns ~25 diagnostics to `error`: `leakingRequirements`, `missingEffectServiceDependency`, `importFromBarrel`, `preferSchemaOverJson`, `schemaSyncInEffect`, `nodeBuiltinImport`, `globalDate/Console/Fetch/Timers/Random(InEffect)`, `unknownInEffectCatch`. With TS 7 this runs through `@effect/tsgo` and `effect-tsgo patch` in `prepare`, so diagnostics appear in `tsc --noEmit`, not only in the editor.
- **CLI entry is `effect/unstable/cli` `Command.runWith` + `NodeRuntime.runMain`.** lalph and t3code scripts do exactly what `apps/cli/src/cli.ts` does. opencode still uses yargs.
- **Vendored reference source is pinned to the installed version.** t3code's `scripts/sync-reference-repos.ts` derives the git tag from the pnpm catalog entry and mirrors into `.repos/`. Its AGENTS.md says to read `.repos/effect-smol/LLMS.md` before writing Effect.
- **Config via Effect `Config`.** opencode's `ConfigService.Service(id, { field: Config.string("ENV") })` generates a service with `.layer` (reads env) and `.configLayer(values)` (tests). Small and worth stealing.

## XState 6 and `@xstate/effect` (verified 2026-09-18)

None of the reference apps use XState. t3code models lifecycle as decider, projector, and reactor. opencode uses a `LayerNode` graph. `cevr/effect-machine` is the community Effect-native machine. But XState itself now ships an Effect 4 bridge for v6, and it is the one that matters for this stack.

- **Package:** `@xstate/effect` at `packages/xstate-effect` on the `next` branch of `statelyai/xstate`. PR [#5687](https://github.com/statelyai/xstate/pull/5687) merged 2026-09-15; [#5691](https://github.com/statelyai/xstate/pull/5691) added Effect Schema support. Author David Khourshid.
- **Pins:** peer `effect ^4.0.0-rc.115` and `xstate` workspace (6.0.0-alpha.58). Both match rat-stack exactly.
- **On npm since 2026-09-19 17:00 UTC.** Changesets versioned it to `0.1.0-alpha.2` on 2026-09-17; that day's Publish run failed on `@xstate/effect` with `E404 PUT https://registry.npmjs.org/@xstate%2feffect`, a first-publish scope problem. The registry tarball published two days later is byte-identical to the one built from the tag (same 20 files, same `package.json`, same `dist`). rat-stack swapped `packages/core` to the registry version on 2026-09-20 (commit noted in `vendor/README.md`); the version-scoped `minimumReleaseAgeExclude` entry stays, like every other prerelease pin. Issue #5725 has no maintainer reply yet.
- **Shape:** `createEffectActor(machine)` returns `Effect<EffectActor, never, R | Scope>` and drives XState 6's pure `transition` through `xstate/durable` on an Effect fiber. Mailbox is a `Queue`, timers are `Effect.sleep` on the Effect `Clock` so `TestClock` drives `after`, declared Effect actions fork in the actor scope, and `R` is collected from declared actions and actors via `RequirementsFrom`. `fromEffect`, `fromEffectStream`, `fromEffectEventStream` turn Effects and Streams into actor logic with typed `E` and `R`. `setupEffect` accepts Effect schemas and Effect-returning actions. `send`, `waitFor`, `join`, `snapshots`, `inspect`, `deadLetters` are the Effect-native observation API. `taggedState` gives `Match.exhaustive` over states. An actor as a service is `Layer.effect(Tag, createEffectActor(machine))`.
- **Fence:** the repo ships `scripts/oxlint-plugin-xstate-effect.mjs` (172 lines) with rule `xstate-effect/no-inline-effect`, catching an Effect returned from an inline `enq` callback (created and discarded, since XState awaits only promises) and inline Effect logic passed to `enq.spawn`. Lift it.
- **Gotchas from the docs:** Effect-backed logic must start under `createEffectActor`, never `createActor`. Only declared actions and actors contribute to `R`; inline Effects infer `never` and fail at runtime. Restoring a persisted snapshot into a new interpreter is not supported yet; the durable loop is the stated path. Requirements collection stops at 10 levels of machine nesting.

### Upstream contribution filed (2026-09-18)

Issue https://github.com/statelyai/xstate/issues/5725 (Joel's account, labelled agent-generated, no PR by decision). Patch gist https://gist.github.com/joelhooks/99d193172a03609e67b4ef5849e8bbb1. Watch the issue for maintainer response before touching the lint plugin again.

Local branch `xstate-effect-typed-lint-plugin` in `~/.repo-autopsy/statelyai/xstate`, one commit on top of `next` at 0748e1b, exported to `/tmp/0001-chore-xstate-effect-typed-oxlint-plugin-and-a-note-o.patch`. It ports the oxlint plugin to TypeScript on `@oxlint/plugins@^1.76.0` (same version as their pinned oxlint), repoints `.oxlintrc.json`, the lint fixture config, `lint.test.ts`, and two docs mentions, and corrects the README and `docs/testing-and-errors.md` claim that `join` fails with "the machine's typed error": a machine's `ErrorFrom` is `unknown`, and `docs/observing-actors.md` now says so and recommends final states plus `orDie` or `catchAll`. Verified in their tree: `manypkg check`, frozen install with `postinstall`, `lint.test.ts` 2/2, `pnpm lint`, `oxfmt --check`. Lock diff is 9 lines, spliced by hand because a non-frozen `pnpm install` here strips every `tarball:` field their lock keeps. Draft PR body at `/tmp/xstate-effect-pr-body.md`; draft publish-failure issue at `/tmp/xstate-effect-publish-issue.md`.

## Split, not consensus

- **Barrel vs subpath imports** is a split, not a consensus. t3code uses `import * as Effect from "effect/Effect"` with `importFromBarrel: error` for client bundle size. lalph, ghui, opencode use `import { Effect } from "effect"`. For a Node CLI the barrel is fine.

## Applied 2026-09-18 (second slice, all remaining gaps)

- **Language service in the fence.** `@effect/tsgo@0.45.0` (supports TS 7.0.2) patches the tsgo binary in `prepare`; `tsconfig.base.json` carries the diagnostic set at `error`. Verified: a probe with `console.log`, `Date.now()`, and `fetch` inside `Effect.gen` fails `tsc` with `globalConsoleInEffect`, `globalDateInEffect`, `globalFetchInEffect`; the earlier core test file failed on `nodeBuiltinImport` and `asyncFunction` until converted. Two things a probe did not catch, correctly: `Effect.tryPromise` without a catch already types its error as `UnknownError`, and returning a service from an exported Effect is not what `leakingRequirements` checks (it checks service method signatures). Escape hatch is `// @effect-diagnostics-next-line <rule>:off`, used exactly twice: `classSelfMismatch` on the generic config-service factory, and `anyUnknownInErrorContext` at the two `join` sites (machine `ErrorFrom` is `unknown`, issue #5725). The e2e test keeps `node:` imports with a file-level `nodeBuiltinImport:off` because it spawns the built binary as a black box.
- **Reference service.** `packages/core/src/file-inspector.ts`: `Context.Service` with `make` capturing `FileSystem`, methods with `R = never`, `static layer`. The machine's actor uses `FileInspector.use(...)`, so `RequirementsFrom` is `FileInspector` and `cli.ts` is the one composition root: `Layer.provideMerge(FileInspector.layer, NodeServices.layer)`.
- **Config service.** opencode's pattern on rc.115 in `packages/core/src/config-service.ts` (`Config.all` needs one cast for a generic record; `Layer.succeed(this, input)` replaces `this.of`). `AppConfig` mirrors `.env.schema`. Tests use `ConfigProvider.layer(ConfigProvider.fromUnknown({...}))`.
- **`@effect/vitest`.** All Effect tests are `it.effect` or `it.layer(TestLayer)(name, (test) => ...)`. Temp files come from `FileSystem.makeTempDirectoryScoped`, no `node:fs`. Rule `effect-tests/no-manual-effect-runtime-in-tests` (t3code port, no debt ceiling) bans `Effect.run*` and `ManagedRuntime.make` in test files. `it` is renamed `test` inside `it.layer` callbacks because `no-shadow` is on.
- **Mirrors.** `vendor-agent-sources.sh` derives refs from the package pins and links `.agent-sources/effect` (pi-effect's path) to the pinned mirror; the beta.99 copy that the pi-effect hydrate could not fix is parked at `/tmp/agent-sources-effect-beta99`. AGENTS.md now points at `node_modules/effect/AGENTS.md` first.
- **Gotchas hit.** `classSelfMismatch` fires on any generic service factory. `no-barrel-file` counts `export * as` re-exports against Effect's whole module graph, so namespaces are exported as a const object. pnpm 11's `verify-deps-before-run` re-serializes a lockfile it did not write (drops `tarball:` fields), which matters when preparing patches for repos whose lock keeps them.

## Sweep 2026-09-20

Joel asked for a library sweep. Bumped to the newest tag on each pinned line: `effect`, `@effect/platform-node`, `@effect/vitest` rc.115 to rc.116; `alchemy` beta.78 to beta.79; `turbo` 2.10.13 to 2.11.2; `varlock` 1.19.0 to 1.20.0; `oxlint-tsgolint` 7.0.2001 to 7.0.2002; `@earendil-works/pi-coding-agent` 0.85.1 to 0.86.1; `@types/node` 24.13.5 to 24.13.6 (staying on the Node 24 line; 26.x exists). Already latest: TypeScript 7.0.2, vitest 5.0.1, oxlint and `@oxlint/plugins` 1.83.0, ultracite 7.12.0, oxfmt 0.68.0, lefthook 2.1.14, xstate 6.0.0-alpha.58, `@xstate/effect` 0.1.0-alpha.2, `@effect/tsgo` 0.45.0. Gate green with no source change for rc.116. pnpm wrote the six `@earendil-works/*@0.86.1` entries into `minimumReleaseAgeExclude` itself ("loose mode"); the dead beta.78 entries were removed by hand. `capnp-es` still wants TypeScript 5 or 6 under alchemy; pre-existing, ignored.

Two fence findings on the way:

- **`turbo run check` never linted.** Each package's `check` script is just `typecheck`, so `pnpm turbo run check test build`, the documented gate and what CI runs, skipped `ultracite check` and `oxfmt --check`. Only the root `pnpm check`, which lefthook runs on commit, linted. Every lint failure this week was caught by the hook, never by CI. Fixed by a root `lint` script and a `//#lint` turbo task that `check` depends on; the gate now runs 15 tasks.
- **`oxlint-tsgolint` 7.0.2002 reports a multi-line `x as unknown as T` on the line holding the second `as`,** out of reach of a `disable-next-line` above the statement. `to-toolkit.ts` now binds the value as `unknown` on its own line and casts on the next. Pattern for the other cast sites if the rule moves again.

The vendor script's xstate pin still pointed at `apps/cli/package.json` after the machine moved to `packages/core`; it resolved `xstate@undefined`. Fixed, mirrors refreshed to rc.116 and beta.79.

## Gaps in rat-stack against this bar (as found 2026-09-18, all now closed)

1. **No `@effect/language-service`.** This is the single largest missing fence for "agents write good Effect". Every reference project has it.
2. **No service or layer in the example.** `readFileStats` yields `FileSystem` directly. Agents most often get `Context.Service` + `Layer` + `Layer.mock` wrong in v4, and the template shows none of it.
3. **Plain vitest, not `@effect/vitest`.** `@effect/vitest@4.0.0-rc.115` is already in the pnpm store via alchemy but not declared. No `it.effect` or `it.layer` example exists.
4. **Closed 2026-09-18.** `@xstate/effect@0.1.0-alpha.2` is vendored as `vendor/xstate-effect-0.1.0-alpha.2.tgz` (built from tag `xstate@6.0.0-alpha.58`, see `vendor/README.md` for the npm swap rule). `apps/cli/src/inspect-machine.ts` is the reference machine: `setupEffect` with a declared `fromEffect` actor, `reading` to `inspected` or `unreadable`, `join` plus `Effect.orDie` lifting the outcome back into the Effect error channel. Its oxlint rule is ported to `scripts/oxlint-plugin-xstate-effect.ts` on `@oxlint/plugins@1.83.0` and wired as `xstate-effect/no-inline-effect: error`. Two things learned on the way: the machine context must be declared via `types<Ctx>()` or `schemas.context`, otherwise `createMachine`'s function-form `context` falls back to `MachineContext` and the output type collapses to `any`; and `ErrorFrom` of a machine is `unknown`, so `join` needs `orDie` (or an explicit catch) to keep a typed error channel. Also fixed on the way: `Flag.Boolean` in `effect/unstable/cli` rc.115 is required unless `Flag.withDefault(false)` is applied; the template's `--json` flag was silently mandatory.
5. **Two Effect mirrors, one stale.** `.agent_sources/github.com/Effect-TS/effect` is at rc.115 (correct). `.agent-sources/effect` (the `pi-effect` tool's mirror) is at beta.99. `.agent_sources/README.md` still says rc.110. Effect's own AGENTS.md warns against unrelated copies.
6. **`node_modules/effect/AGENTS.md` is not referenced.** It ships in the package (409 lines), is always version-matched, and is what other Effect 4 migrations point agents at first.
7. **Vendor script pins by hand.** t3code derives the ref from the catalog. Ours can derive it from `packages/core/package.json`.

## Sources

- t3code: `tsconfig.base.json`, `oxlint-plugin-t3code/rules/*.ts`, `apps/server/src/persistence/{Services,Layers}/ProjectionState.ts`, `scripts/sync-reference-repos.ts`, `AGENTS.md`
- opencode: `packages/opencode/src/effect/config-service.ts`, `packages/core/src/effect/{layer-node,runtime}.ts`, `AGENTS.md`
- lalph: `src/Editor.ts`, `package.json`, `tsconfig.json`
- Effect rc.115: `node_modules/.pnpm/effect@4.0.0-rc.115/node_modules/effect/{AGENTS.md,src/Context.ts,src/Schema.ts}`
