From 628b04469eab5c6bfb01d6680fe3849c6dc6643a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 19:34:09 +0000 Subject: [PATCH 1/8] Add effect skill layered on Effect's own skills Effect v4 is at 4.0.0-rc.117 and npm `latest` is still 3.22.2. The v4 npm tarball ships AGENTS.md and ai-docs/, and the Effect team's effect-ts/skills repo is already in the pinned nix-skills index. Its `effect-ts` skill only points at that AGENTS.md. So this follows the ste100 layer-not-fork pattern: pin the upstream skills and write a thin local skill that holds only what upstream lacks. What the layer adds: - A gate on the installed major version. v3 code and v4 code do not compile against each other, and the v3 tarball ships no agent docs. Upstream effect-ts starts with `pnpm add effect@rc`, which in a v3 project (effect-playground is one) is an unrequested major upgrade. - The effect-playground Rust translation table, with a v4 column. - The v3/v4 traps found by typechecking effect-playground against rc.117: Schema.Date keeps its name with a different meaning (v3 string->Date is now DateFromString, and v4 Schema.Date rejects the string at runtime); Schedule.intersect -> Schedule.max takes an array and cannot be piped; Cause guards moved to cause.reasons; and the misleading TS2379 "add undefined" cascade under exactOptionalPropertyTypes. Each v4 entry was resolved against migration/v3-to-v4.md on Effect-TS/effect main. The entries marked "checked" were also typechecked and run against rc.117, including a v4 port of lesson 06's backoff (5 attempts transient, 1 permanent, same as v3). One name written from memory, Schema.TaggedErrorClass, turned out not to exist. That is why CLAUDE.md now says to verify version claims with a typecheck against the real package. Specific changes: - lib/skills-sh.nix: pin effect-ts and effect-v3-to-v4 (effect-ts/skills @ 2309e6f, which is also upstream HEAD) - home-manager/modules/tools/agent-skills.nix: deploy both, plus the repo skill `effect` - tools/agents/skills/effect/SKILL.md: the layer - CLAUDE.md: "look inside the package before writing a skill for a library" lesson Not verified here: this sandbox has no nix, so statix, deadnix and flake evaluation are left to CI. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- CLAUDE.md | 14 ++ home-manager/modules/tools/agent-skills.nix | 8 + lib/skills-sh.nix | 28 ++++ tools/agents/skills/effect/SKILL.md | 162 ++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 tools/agents/skills/effect/SKILL.md diff --git a/CLAUDE.md b/CLAUDE.md index 2d38dd4f..f7a66961 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -377,6 +377,19 @@ and consumed by `home-manager/modules/tools/agent-skills.nix`. locked nix-skills, then the file at that rev) or against `~/.agents/skills//` after a switch, and give the skill a fallback for anything that can lag +- **For a library, look inside the package before writing a skill for it.** + Effect v4 ships `AGENTS.md` and `ai-docs/` in its npm tarball, and the + Effect team's own `effect-ts` skill is only a pointer to them. A local API + reference would restate that content, and it would go stale at each + release candidate. The repo's `effect` skill therefore carries what the + package cannot: a gate on the *installed* major version (the v3 tarball + ships no agent docs, and `effect-ts` starts with `pnpm add effect@rc`, which + in a v3 project is an unrequested major upgrade), plus Alyssa's Rust + mapping. Get version-specific claims from a typecheck against the real + package, not from memory: typechecking effect-playground against + 4.0.0-rc.117 found `Schema.Date`, a name that v4 keeps with a different + meaning, and a v4 name written from memory (`Schema.TaggedErrorClass`) + that did not exist ### When nixpkgs lags: prefer the vendor's own Nix repo over NUR @@ -808,6 +821,7 @@ This document should evolve as patterns emerge. When you: **Add it here** and commit with a message explaining what prompted the addition. --- +*Last updated: 2026-09-22 - Added the `effect` skill as a layer over the Effect team's pinned `effect-ts` / `effect-v3-to-v4` skills, and recorded the lesson: check whether a library ships its own agent docs in the package (Effect v4 does, v3 does not), gate on the installed version, and verify v3/v4 claims by typechecking against the real package* *Last updated: 2026-09-21 - Ghost (#56): ghost.build is shutting down, so venari now runs a server for its OpenAPI contract from the alycda/ghost fork, with the CLI packaged from that fork and driven by an env-setting wrapper; recorded the two client-side assumptions (`tsdb` dbname, viper's empty-env handling) that the "no CLI patch needed" plan missed* *Last updated: 2026-09-21 - Added "prebuilt binaries in a persisted `$HOME` are image-scoped state" to Tools Nix Can't Fully Manage, after a rustup toolchain in the devhome volume survived an image rebuild and left `cargo` erroring ENOENT for a loader that no longer existed — with the corollary that an activation step guarded on "is it installed" can never repair state that went bad in place (#80)* *Last updated: 2026-09-16 - Verified inspect against a real binary and found its declared install route could never have worked: the ataraxy-labs/tap formula pins a checksum upstream invalidated by moving the v0.1.1 tag, so the brew fails and would abort activation. Replaced it with `lib/inspect.nix` (release binary, tart-style). Two lessons, both already in the tap write-up and both nearly repeated: a tap's risk is its maintenance, so check the formula's age and hash before declaring it, not after; and a prebuilt binary that runs on *this* machine proves little — this one linked Homebrew's openssl by absolute path, so `otool -L` is part of verifying any fetched macOS binary* diff --git a/home-manager/modules/tools/agent-skills.nix b/home-manager/modules/tools/agent-skills.nix index 9855cef0..9c804af4 100644 --- a/home-manager/modules/tools/agent-skills.nix +++ b/home-manager/modules/tools/agent-skills.nix @@ -36,6 +36,11 @@ # brag-doc - promo-packet impact entries from raw work notes # cf-now - private file sharing from Cloudflare R2 (pre-signed URLs) # commit-craft - commit-message craft + jj describe/push workflow +# effect - Effect (TypeScript) with a v3/v4 version gate and +# the effect-playground Rust mapping; carries no v4 +# API text, defers to the AGENTS.md the v4 package +# ships and to the external effect-ts / +# effect-v3-to-v4 skills # entity-level-git - Ataraxy Labs stack: sem (entity diff/blame/impact), # weave (semantic merge driver), inspect (review triage) # failure-doc - failures as deliberate-learning records @@ -90,6 +95,7 @@ let "brag-doc" "cf-now" "commit-craft" + "effect" "entity-level-git" "failure-doc" "hackmd-cli" @@ -102,6 +108,8 @@ let externalSkills = { asd-ste100-skill = pkgs.skills-sh.asd-ste100-skill; + effect-ts = pkgs.skills-sh.effect-ts; + effect-v3-to-v4 = pkgs.skills-sh.effect-v3-to-v4; here-now = pkgs.skills-sh.here-now; supabase-postgres-best-practices = pkgs.skills-sh.supabase-postgres-best-practices; }; diff --git a/lib/skills-sh.nix b/lib/skills-sh.nix index 0a34eccf..43c389cb 100644 --- a/lib/skills-sh.nix +++ b/lib/skills-sh.nix @@ -110,6 +110,34 @@ nix-skills: final: _: { name = "asd-ste100-skill"; }; + # https://www.skills.sh/effect-ts/skills + # The Effect team's own skills. Both are deliberately thin: `effect-ts` + # tells an agent to read node_modules/effect/AGENTS.md, which the v4 + # npm tarball ships alongside ai-docs/ (the v3 tarball ships neither), + # and `effect-v3-to-v4` drives a migration from the generated + # migration/v3-to-v4.md in Effect-TS/effect. The repo's `effect` skill + # (tools/agents/skills/effect) layers on both: a version gate, because + # `effect-ts` step 1 is `pnpm add effect@rc` and in a v3 project that + # is an unrequested major upgrade; and the Rust mapping from + # effect-playground. effect-v3-to-v4 sets disable-model-invocation, so + # it only runs when invoked by name. + # + # Written against the pinned rev 2309e6f (indexed 2026-08-28), which was + # also upstream HEAD on 2026-09-22. + effect-ts = mkSkill { + owner = "effect-ts"; + repo = "skills"; + path = "skills/effect-ts"; + name = "effect-ts"; + }; + + effect-v3-to-v4 = mkSkill { + owner = "effect-ts"; + repo = "skills"; + path = "skills/effect-v3-to-v4"; + name = "effect-v3-to-v4"; + }; + # https://www.skills.sh/supabase/agent-skills/supabase-postgres-best-practices # Postgres performance/schema/RLS guidance across 8 priority categories. supabase-postgres-best-practices = mkSkill { diff --git a/tools/agents/skills/effect/SKILL.md b/tools/agents/skills/effect/SKILL.md new file mode 100644 index 00000000..92f51b63 --- /dev/null +++ b/tools/agents/skills/effect/SKILL.md @@ -0,0 +1,162 @@ +--- +name: effect +description: > + Write, read, review, or explain TypeScript that uses the Effect library + (`effect` package: Effect.gen, Layer, Context, Schema, Scope, Fiber, + Schedule). Trigger when a file imports from "effect" or "effect/*", when + package.json lists `effect`, or on "effect-ts", "effectts", "Effect v4", + "typed errors in TypeScript", or "how does this map to Rust". Detects the + installed major version first, because v3 and v4 code do not compile + against each other. On v4 it defers to the guidance the package ships + (node_modules/effect/AGENTS.md); on both versions it adds a Rust + translation table from Alyssa's effect-playground. Carries no v4 API + reference of its own. Does not migrate: a v3-to-v4 upgrade is the pinned + effect-v3-to-v4 skill, run only when Alyssa asks for it. +--- + +# Effect + +This skill is a thin layer. The rules for writing v4 Effect code come from the +Effect team, not from here. The skill adds three things upstream does not +have: a version gate, the Rust mapping from +[effect-playground](https://tangled.org/alycda.tngl.sh/effect-playground), and +the v3/v4 traps found by typechecking that playground against v4. + +## Step 0: find the installed version + +Do this before you write a line of Effect code. Do not infer the version from +examples in your memory or on the web; most of both are v3. + +```sh +node -p "require('effect/package.json').version" # resolved version +grep '"effect"' package.json # declared range +``` + +| Result | Do this | +|---|---| +| `4.x` (including `-rc.N` / `-beta.N`) | Read `node_modules/effect/AGENTS.md` completely, then follow its links into `node_modules/effect/ai-docs/src/`. For an API it does not cover, search `node_modules/effect/src/`. Use the v4 column below. | +| `3.x` | The v3 package ships no `AGENTS.md`. Search `node_modules/effect/src/` for signatures. Use the v3 column below. Do not copy code from v4 docs, from `AGENTS.md` in another project, or from the pinned `effect-ts` skill; it will not compile. | +| not installed, new project | Ask v3 or v4. Give the tradeoff in one line: npm `latest` is v3 and matches the effect.website docs; v4 is a release candidate, is what the Effect team's own skill installs, and ships agent docs in the package. | +| not installed, existing code | Find out why before you install anything. | + +As of 2026-09-22: npm `latest` = `3.22.2`, `rc` = `4.0.0-rc.117`. Re-check with +`npm view effect dist-tags` before you state it. + +**Do not upgrade the major version unasked.** The pinned `effect-ts` skill +(`~/.agents/skills/effect-ts/`) starts with `pnpm add effect@rc`. In a v3 +project, that command is a v3-to-v4 migration. Use `effect-ts` only to set up a +v4 repo. For a migration, Alyssa runs `/effect-v3-to-v4`, which has +`disable-model-invocation: true` and so never starts automatically. + +If `~/.agents/skills/effect-ts/` does not exist, the v4 path still works: +`node_modules/effect/AGENTS.md` is the content that skill points at. + +## Rust translation + +Same table as the playground README, with a v4 column. Every v4 entry is either +unchanged or resolved against `migration/v3-to-v4.md` in Effect-TS/effect +(main, 4.0.0-rc.117). The four marked "checked" also typecheck and run on +rc.117. + +| Rust | v3 | v4 | Lesson | +|---|---|---|---| +| `Result` | `E` in `Effect` | same | 00, 01 | +| `?` | `yield*` in `Effect.gen` | same; `Effect.fn("name")` for reusable functions | 00 | +| `enum Error` + `thiserror` | `Data.TaggedError` | `Data.TaggedError` still exists; `AGENTS.md` prefers `Schema.TaggedError()("Tag", {...})` (checked) | 01 | +| `match` on the error enum | `Effect.catchTags` | same (checked) | 01 | +| catch-all `Err(_)` | `Effect.catchAll` | `Effect.catch` | 01 | +| `Result` as a value | `Effect.either` → `Either` | `Effect.result` → `Result` | 01, 06 | +| `panic!` | defect (`Effect.die`) | same; `Cause.isDie` → `Cause.hasDies` | 01 | +| `Drop` / RAII | `Scope` + `Effect.acquireRelease` | same | 02 | +| `std::thread::scope` | `Effect.scoped` | same | 02 | +| trait / `&dyn Trait` | `Context.Tag("id")()` | `Context.Service()("id")`, a structural rewrite and not a rename (checked) | 03 | +| impl at the call site | `Layer` + `Effect.provide` | same; build the value with `Service.of({...})` | 03 | +| `tokio::spawn` | `Effect.fork` | `Effect.forkChild` (checked); `forkDaemon` → `forkDetach` | 04 | +| `tokio::select!` | `Effect.race` | same | 04 | +| `Semaphore` over a task set | `{ concurrency: n }` | same | 04, 06 | +| `serde` derive | `Schema.Struct` | same | 05 | +| `#[serde(with = ...)]` | `Schema.Date` (string → `Date`) | **`Schema.DateFromString`**; see the traps below | 05 | +| `backoff` crate | `Schedule.intersect`, `whileInput` | `Schedule.max([...])`, `Schedule.while`; see the traps below | 06 | +| `Arc>` | `Ref` | same | — | +| `impl Iterator` | `Stream` | same | — | + +For a symbol that is not in this table, do not guess the v4 name. Search the +reference (the recipes are in `~/.agents/skills/effect-v3-to-v4/SKILL.md`) or +`node_modules/effect/src/`. This table was checked, and one guess still got +through: `Schema.TaggedErrorClass` does not exist. + +## v3 → v4 traps that a rename table hides + +These come from typechecking effect-playground (v3, strict tsconfig) against +`4.0.0-rc.117`. They matter when you read v3 code while you write v4 code, or +the other way round. + +- **Same name, different meaning: `Schema.Date`.** v3 `Schema.Date` decodes an + ISO string to a `Date`. v4 `Schema.Date` is the identity schema for a `Date`, + and it rejects the string (the decode returns `Failure`; checked). The v3 + meaning is now called `Schema.DateFromString`. Code that keeps the v3 name + still compiles and fails at runtime on every real payload. Lesson 05 exists + to teach this boundary. +- **Different shape: `Schedule.intersect` → `Schedule.max`.** v4 `max` takes an + array (`Schedule.max([a, b])`), so it cannot go in a `.pipe(...)` chain, and + it outputs a `Duration`, not a tuple. v3 `whileInput(pred)` becomes + `Schedule.while(({ input }) => pred(input))`. Lesson 06's backoff, ported + this way, still makes 5 attempts on a persistent transient error and 1 on a + permanent one (checked). +- **`Cause` is no longer a union you branch on.** `isFailType`, `isDie`, and + the other variant guards now apply to entries of `cause.reasons` + (`Cause.isFailReason`), or become cause-level predicates (`Cause.hasDies`). + `Cause.TimeoutException` → `Cause.TimeoutError`, and the `_tag` + discriminant changes with it. +- **Schema filters became checks.** `Schema.int()`, `between`, and `pattern` + become `isInt`, `isBetween`, and `isPattern`, applied with `Schema.check` or + a schema's `.check`. `Schema.annotations` → `Schema.annotate`. + `Schema.decodeUnknown` → `Schema.decodeUnknownEffect`. `ParseResult` is + gone. `Schema.Schema.Encoded` has no counterpart, so infer the type from the + constructor. +- **Misleading cascade under `exactOptionalPropertyTypes`.** One unresolved + symbol makes `R` infer as `unknown`, and `runPromise` then reports TS2379: + "`Effect` is not assignable to `Effect` ... Consider adding 'undefined'". Do not add `undefined`. Fix the + first TS2339/TS2551 in the file, and the TS2379 goes away. + +## Writing rules for both versions + +- An `Effect` is a description. Building one runs nothing. Retry, timeout, and + interruption are combinators because the value can run twice or never. +- Use `Effect.gen` for sequential logic. Use `.pipe(...)` to decorate one + effect (retry, timeout, catch). On v4, use `Effect.fn("name")` for a + reusable function, and pass decorators as extra arguments, not `.pipe` + (see `AGENTS.md`). +- To fail from a generator, write `return yield* new MyError(...)`. Without the + `return`, TypeScript thinks execution continues. +- Expected failures go in `E` as tagged errors. Broken invariants are defects + (`Effect.die`, `orDie`) and stay out of `E`. Name a payload field `reason`, + not `cause`: `Data.TaggedError` extends `Error`, whose `cause` conflicts + with a readonly redeclaration. +- Decode untrusted input with `Schema`. Do not cast. +- Keep the playground's tsconfig strictness: `strict`, + `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`. Without `strict`, + the error channel can degrade to `any`. +- Do not use `as` or `any` to silence an Effect type error. The error usually + means the shape is different, and a cast deletes that information. + +## Verify + +Done means the code typechecks and runs. Hover inference alone is not proof. + +```sh +npx tsc --noEmit # or the project's typecheck script +npx tsx path/to/file.ts # run it; Effect code that typechecks can still fail at runtime (Schema.Date) +``` + +In effect-playground itself, `npm run check && npm test` is the gate. Lesson 06 +calls a live API, so CI does not run it. + +## Hand-offs + +- Set up a new v4 repo: the pinned `effect-ts` skill. +- Migrate v3 → v4: `/effect-v3-to-v4`, and only on request. That skill forbids + compat shims and casts. This one agrees. +- Rust→WASM through Effect (playground lesson 07): the Rust side is ordinary + FFI work, so `power-of-ten` applies to any `unsafe` in it. From 070e2e4a5f366dc8b3a50dc198213aa95eb596a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 19:41:24 +0000 Subject: [PATCH 2/8] Add topcoat skill and pair it with the Effect stack Topcoat (tokio-rs/topcoat, 0.8.1, MSRV 1.98) is a server-rendered Rust web framework. create-epoch-app (Rhys Sullivan) is a full-stack Effect starter: Effect + Convex + Next.js + effect-atom. Put side by side, they are the Rust and Effect versions of one app, so the new `topcoat` skill carries the pairing table and `effect` hands off to it. Topcoat follows the same pattern as `effect`, with its docs in a different place. The guides are embedded in the published crates, so after a build they sit in ~/.cargo/registry/src/*/topcoat-/docs/. The AGENTS.md index is only in the repo, and main had already changed 13 guide files in the nine days after v0.8.1. So the skill reads the crate at the Cargo.lock version and the index at the matching tag. The pairing's main row was checked on both sides, not taken from the docs. Asking for a missing dependency: - Effect: the service stays in R, and runPromise gives TS2345 (rc.117). - Topcoat: app_context::(cx) compiles, then panics on each request that reaches it. The server catches the panic, returns 500 and keeps serving other routes (probe crate on crates.io 0.8.1, rustc 1.98.1). The two share a design rule (epoch-app's "yield* the dependency, don't pass it", Topcoat's "functions, not middlewares") but check it at different times. The skill states that as the tradeoff to name. create-epoch-app: I reviewed it and pinned nothing from it. It is a template with Cursor .mdc rules, on Effect ^3.19.8, and was last committed 2026-01-11. Its dependency rule went into `effect`. The skill records two rules not to import unchanged: - effect-primitives: prefers v3 names that v4 renames, and prefers Chunk/HashMap by default with no benchmark behind it. - never-use-as-any: bans `unknown`, which Schema decoding takes as input by design. Rhys Sullivan's effect-client-wrapper skill is pinned from rhyssullivan/skills. It is v3-shaped, so `effect` says to keep the pattern and translate the code on v4. Correction: the previous commit put a rule into `effect` that I had not verified: a Data.TaggedError payload field cannot be named `cause`. The rule came from a comment in effect-playground's lesson 06. It is false on 3.22.2 and rc.117. Renaming lesson 06's field to `readonly cause` typechecks under the playground's own tsconfig and passes its 6 tests. The skill now says so, and CLAUDE.md extends the lesson: a trusted source needs the same check as memory. Specific changes: - tools/agents/skills/topcoat/SKILL.md: new layer skill - tools/agents/skills/effect/SKILL.md: drop the false `cause` rule; add the yield* dependency rule; add hand-offs to effect-client-wrapper and topcoat; add a note on the reference projects - lib/skills-sh.nix: pin effect-client-wrapper (rhyssullivan/skills @ ab3a455, upstream HEAD) - home-manager/modules/tools/agent-skills.nix: deploy topcoat and effect-client-wrapper - CLAUDE.md: extend the "look inside the package" lesson Not verified here: nix is not installed in this sandbox, so statix, deadnix and flake evaluation are left to CI. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- CLAUDE.md | 11 +- home-manager/modules/tools/agent-skills.nix | 6 + lib/skills-sh.nix | 19 +++ tools/agents/skills/effect/SKILL.md | 38 +++++- tools/agents/skills/topcoat/SKILL.md | 131 ++++++++++++++++++++ 5 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 tools/agents/skills/topcoat/SKILL.md diff --git a/CLAUDE.md b/CLAUDE.md index f7a66961..33aee9ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -389,7 +389,15 @@ and consumed by `home-manager/modules/tools/agent-skills.nix`. package, not from memory: typechecking effect-playground against 4.0.0-rc.117 found `Schema.Date`, a name that v4 keeps with a different meaning, and a v4 name written from memory (`Schema.TaggedErrorClass`) - that did not exist + that did not exist. A source you trust needs the same check. The + playground's own comment (a `Data.TaggedError` field cannot be named + `cause`) went into the skill as a rule, and it was false: the renamed + field typechecks and passes the lesson's tests. Where the guides ship + also differs by project. Topcoat embeds its guides in the published crates + (`~/.cargo/registry/src/*/topcoat-/docs/`), but its `AGENTS.md` + index exists only in the repo, and `main` drifts from the release. The + `topcoat` skill reads the crate at the `Cargo.lock` version and the index + at the matching tag ### When nixpkgs lags: prefer the vendor's own Nix repo over NUR @@ -821,6 +829,7 @@ This document should evolve as patterns emerge. When you: **Add it here** and commit with a message explaining what prompted the addition. --- +*Last updated: 2026-09-22 - Added the `topcoat` skill (tokio-rs/topcoat, with the Effect <-> Topcoat pairing), pinned Rhys Sullivan's `effect-client-wrapper`, and reviewed create-epoch-app; corrected a `cause`-field rule the `effect` skill had copied unverified from effect-playground* *Last updated: 2026-09-22 - Added the `effect` skill as a layer over the Effect team's pinned `effect-ts` / `effect-v3-to-v4` skills, and recorded the lesson: check whether a library ships its own agent docs in the package (Effect v4 does, v3 does not), gate on the installed version, and verify v3/v4 claims by typechecking against the real package* *Last updated: 2026-09-21 - Ghost (#56): ghost.build is shutting down, so venari now runs a server for its OpenAPI contract from the alycda/ghost fork, with the CLI packaged from that fork and driven by an env-setting wrapper; recorded the two client-side assumptions (`tsdb` dbname, viper's empty-env handling) that the "no CLI patch needed" plan missed* *Last updated: 2026-09-21 - Added "prebuilt binaries in a persisted `$HOME` are image-scoped state" to Tools Nix Can't Fully Manage, after a rustup toolchain in the devhome volume survived an image rebuild and left `cargo` erroring ENOENT for a loader that no longer existed — with the corollary that an activation step guarded on "is it installed" can never repair state that went bad in place (#80)* diff --git a/home-manager/modules/tools/agent-skills.nix b/home-manager/modules/tools/agent-skills.nix index 9c804af4..e607bb2e 100644 --- a/home-manager/modules/tools/agent-skills.nix +++ b/home-manager/modules/tools/agent-skills.nix @@ -55,6 +55,10 @@ # surfaces; carries no rule text, delegates to the # external asd-ste100-skill and reads CONCEPTS.md as # the project's technical-name dictionary +# topcoat - tokio-rs/topcoat (server-rendered Rust web apps): +# reads the guides shipped in the crates at the +# locked version, the two contracts the compiler +# does not check, and the Effect <-> Topcoat pairing # # One of them is vendored rather than authored: # hackmd-cli - upstream's own skill, extracted from the hackmd-cli.skill zip @@ -104,10 +108,12 @@ let "jujutsu" "power-of-ten" "ste100" + "topcoat" ]; externalSkills = { asd-ste100-skill = pkgs.skills-sh.asd-ste100-skill; + effect-client-wrapper = pkgs.skills-sh.effect-client-wrapper; effect-ts = pkgs.skills-sh.effect-ts; effect-v3-to-v4 = pkgs.skills-sh.effect-v3-to-v4; here-now = pkgs.skills-sh.here-now; diff --git a/lib/skills-sh.nix b/lib/skills-sh.nix index 43c389cb..a8934795 100644 --- a/lib/skills-sh.nix +++ b/lib/skills-sh.nix @@ -138,6 +138,25 @@ nix-skills: final: _: { name = "effect-v3-to-v4"; }; + # https://www.skills.sh/rhyssullivan/skills/effect-client-wrapper + # Wrap a Promise-based SDK as an Effect service behind one `use(fn)` + # that maps rejections to a tagged error and opens a span. By the author + # of create-epoch-app. Its code is Effect v3 (Context.Tag, a static + # Default layer, Schedule.intersect); the repo's `effect` skill says to + # keep the pattern and translate the code on v4 rather than copy it. + # + # Taken from rhyssullivan/skills (indexed 2026-07-10, rev ab3a455, which + # was upstream HEAD on 2026-09-22), not from the older standalone + # rhyssullivan/effect-client-wrapper-skill repo the index also carries + # (indexed 2026-03-05). The rest of rhyssullivan/skills (quality-code, + # no-ui-flash, ...) is not wanted here. + effect-client-wrapper = mkSkill { + owner = "rhyssullivan"; + repo = "skills"; + path = "skills/effect-client-wrapper"; + name = "effect-client-wrapper"; + }; + # https://www.skills.sh/supabase/agent-skills/supabase-postgres-best-practices # Postgres performance/schema/RLS guidance across 8 priority categories. supabase-postgres-best-practices = mkSkill { diff --git a/tools/agents/skills/effect/SKILL.md b/tools/agents/skills/effect/SKILL.md index 92f51b63..707066a0 100644 --- a/tools/agents/skills/effect/SKILL.md +++ b/tools/agents/skills/effect/SKILL.md @@ -131,9 +131,17 @@ the other way round. - To fail from a generator, write `return yield* new MyError(...)`. Without the `return`, TypeScript thinks execution continues. - Expected failures go in `E` as tagged errors. Broken invariants are defects - (`Effect.die`, `orDie`) and stay out of `E`. Name a payload field `reason`, - not `cause`: `Data.TaggedError` extends `Error`, whose `cause` conflicts - with a readonly redeclaration. + (`Effect.die`, `orDie`) and stay out of `E`. A payload field named `cause` + is fine, including `readonly cause: unknown`, on v3 and v4 (checked: + 3.22.2 and rc.117). The playground's lesson 06 comment says it conflicts + with `Error.cause`. It does not: renaming that field to `cause` still + typechecks and passes 6/6 tests. Do not repeat that comment. +- Ask for a dependency with `yield* Service` inside the function. Do not pass + it in as an argument. The requirement then shows in `R`, and a missing + Layer is a compile error. Pass a service as an argument only at a non-Effect + boundary. (create-epoch-app's `effect-dependency-injection` rule; Topcoat's + "functions, not middlewares" is the same rule without the compile-time + check, see the `topcoat` skill.) - Decode untrusted input with `Schema`. Do not cast. - Keep the playground's tsconfig strictness: `strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`. Without `strict`, @@ -158,5 +166,29 @@ calls a live API, so CI does not run it. - Set up a new v4 repo: the pinned `effect-ts` skill. - Migrate v3 → v4: `/effect-v3-to-v4`, and only on request. That skill forbids compat shims and casts. This one agrees. +- Wrap a Promise-based SDK (Stripe, Resend, AWS) as a service: the pinned + `effect-client-wrapper` skill (Rhys Sullivan). Its code is v3: + `Context.Tag` and a hand-written `static Default` layer, and a + `Schedule.intersect` retry. On v4, keep the pattern (one `use(fn)` that + turns a rejected promise into a tagged error and a span) and convert the + code with the table above. +- The same app in Rust, or a comparison of a full-stack Effect design with + one: the `topcoat` skill, which holds the Effect ↔ Topcoat pairing. - Rust→WASM through Effect (playground lesson 07): the Rust side is ordinary FFI work, so `power-of-ten` applies to any `unsafe` in it. + +## Reference projects, and what not to take from them + +- **effect-playground** (Alyssa's): the Rust lens above. v3, strict tsconfig. +- **create-epoch-app** (github.com/RhysSullivan/create-epoch-app): a + full-stack starter (Effect + Convex + Next.js + effect-atom, bun). On + 2026-09-22 it was on Effect `^3.19.8`, and its last commit was + 2026-01-11. Its agent rules are Cursor `.mdc` files. The dependency rule + above comes from them. Do not import two of the others unchanged: + `effect-primitives.mdc` prefers `Either` and `catchAll`, which v4 renames, + and it prefers `Chunk`/`HashMap`/`List` over native collections by + default. That is a style choice: it costs a conversion at every interop + boundary, and the rule gives no benchmark for it (Alyssa's review, not an + upstream position). `never-use-as-any` + also bans `unknown`, but `Schema.decodeUnknown*` takes `unknown` input by + design. The ban on `as` and `any` is right. The ban on `unknown` is not. diff --git a/tools/agents/skills/topcoat/SKILL.md b/tools/agents/skills/topcoat/SKILL.md new file mode 100644 index 00000000..0c0fc53c --- /dev/null +++ b/tools/agents/skills/topcoat/SKILL.md @@ -0,0 +1,131 @@ +--- +name: topcoat +description: > + Write, read, or review a web app built on Topcoat (tokio-rs/topcoat), the + server-rendered Rust framework: `view!`, `#[component]`, `#[page]`, + `#[layout]`, `#[procedure]`, `#[shard]`, `live!`, `signal`, `app_context`, + `Cx`, `topcoat dev`, `topcoat ui`. Trigger when Cargo.toml depends on + `topcoat`, on "topcoat", or when Alyssa compares a full-stack Effect app + (create-epoch-app, Confect, effect-atom) with its Rust equivalent. Finds + the installed version first and reads the guides that ship inside the + crates at that version. Carries no API reference of its own. Adds the + Effect ↔ Topcoat pairing and two contracts the compiler does not check. + Not for work inside the Topcoat repository itself, which has its own + contributor skills in `.agents/skills/`. +--- + +# Topcoat + +Topcoat is pre-1.0; its README says "Early-stage and experimental. Expect +breaking changes." And the guides on `main` drift from the release (13 guide files +changed between v0.8.1 and `main` in the nine days after the tag). Read the +docs for the version in `Cargo.lock`, never `main`. + +This skill is a thin layer. The guides are upstream's. It adds a version gate, +two runtime contracts that the compiler does not check, and the pairing with +the Effect stack, which is where Alyssa is coming from. + +## Step 0: version and docs + +```sh +grep -A1 '^name = "topcoat"$' Cargo.lock # resolved version +rustc --version # 0.8.1 needs >= 1.98 +ls -d ~/.cargo/registry/src/*/topcoat-*/docs \ + ~/.cargo/registry/src/*/topcoat-*/macro/docs 2>/dev/null +``` + +The guides are embedded in the published crates (`include_str!` into the API +docs), so after a `cargo build` they are on disk at the resolved version: +`topcoat-/docs/` holds the facade guides (router, context, app_context, +runtime, session, ui, …), and `topcoat--macro-/docs/` holds the +macro references (`view.md`, `live.md`, `procedure.md`, `shard.md`, …). +`cargo doc --open -p topcoat` renders the same text. + +The guide *index* is `AGENTS.md` in the repository, and it does **not** ship in +the crate. Read it at the matching tag: +`https://github.com/tokio-rs/topcoat/blob/v/AGENTS.md`. Its "Project +structure" section and the `.agents/skills/` it references are for Topcoat +contributors; only the "Documentation" index applies to an app. + +Before you write code in an area, read its guide. Start with +`functions_not_middlewares.md`; it is the framework's design argument, and +the rest assumes it. + +## Contracts the compiler does not check + +**`app_context::(cx)` is a runtime lookup.** It is keyed by `TypeId`. A type +that was never registered with `.app_context(value)` compiles, then panics on +every request that reaches the call. Topcoat catches the panic and responds +500; other routes keep serving (checked on 0.8.1: the page returned +`internal server error [500]` and the log named the missing type). So: + +- Register every app-context value in the one `Router::builder()` chain, and + treat that chain as the list of what the app depends on. +- Use `try_app_context::(cx)` when a value is optional; it returns `None`. +- A green `cargo build` says nothing about registration. Smoke-test by + requesting each route once. This is the same lesson as "passing activation + is not proof a tool works" in this repo's CLAUDE.md. +- One value per type. Registering a type twice panics at startup; wrap in + newtypes (`struct PrimaryDb(Database)`). + +**Procedures and shards are public HTTP endpoints.** Their arguments come from +the browser and "must not be trusted" (upstream's words, both guides). Do the +auth check inside the body with a `cx` function (`require_auth(cx).await?`), +not by assuming the page that renders the button was guarded. A procedure's +`Err` becomes an error response that the calling expression cannot observe; +if the browser must react to a failure, return it as data +(`Result>`). + +## Coming from Effect: the pairing + +create-epoch-app (Rhys Sullivan: Effect + Convex + Next.js + effect-atom, on +Effect 3.19, last commit 2026-01-11) is the full-stack Effect shape; Topcoat is +the nearest Rust shape. Rows marked "checked" were run; the rest come from the +0.8.1 guides and the epoch-app source. + +| Concern | create-epoch-app / Effect | Topcoat 0.8.1 | +|---|---|---| +| Ask for a dependency | `const db = yield* Database`; epoch-app's rule is "prefer `yield*` over passing deps as arguments" | `app_context::(cx)` inside a small `fn(cx: &Cx)`; "functions, not middlewares" is the same rule | +| Missing dependency | **Compile error**: the service stays in `R`, and `runPromise` rejects `Effect` (checked, TS2345) | **Runtime panic → 500** on that request only (checked) | +| Wire implementations | `Layer` + `Effect.provide`; swap a Layer for tests | `Router::builder().app_context(value)`; swap the value in a test router | +| Request-scoped dedupe | no one-to-one; `RequestResolver` dedupes and batches | `#[memoize]` on a `cx` function, per request | +| Error type | typed `E` channel, tagged errors, `catchTags` | `topcoat::Error` wraps `anyhow::Error` (type-erased); router error types map to status codes, anything else is 500. Keep `thiserror` enums in the domain and convert at the handler | +| Auth guard | Better Auth on Convex; a service you `yield*` | `topcoat-session` (bring your own storage) plus `require_auth(cx)` functions called where the data is used | +| Server call from UI | Confect RPC (`factory.query/mutation`), typed errors cross the wire via Schema | `#[procedure]` async fn called in a runtime expression; `Err` is opaque to the client | +| Client state | effect-atom (`useAtom`, `useAtomValue`) | `signal(cx, init)` + runtime expressions (`@click`, `:value`) | +| Re-render on input change | atom-derived React component | `#[shard]`: the browser tracks the signals, the server re-renders, the DOM swaps | +| Slow data | React Suspense | `live!` / `emit!` stream regions into the same response; `suspense`/`error_boundary` components | +| Push after load | Convex live queries | no DB subscription; `Sse` response (`sse` feature), or Datastar over SSE | +| Validate input | `Schema.decodeUnknown` | `FromRequest` extractors (`content.md`) and serde | +| UI kit | `@packages/ui` (shadcn / Radix) + Tailwind via Node | `topcoat ui` copies component source from a registry (the shadcn model) + standalone Tailwind CLI, no Node | +| Client build | Next.js bundle, bun, turbo | none: no wasm, no JS build step | +| Tracing | `Effect.withSpan`, `@effect/opentelemetry` | a router layer (tower), which the guide reserves for transport concerns like tracing | + +The row that decides most design arguments is the second. Effect moves the +"did you wire it" check into the type system; Topcoat keeps it a startup and +request-time invariant, and buys a smaller, plainer API with it. Say which +side of that trade a design relies on. + +The "Server call from UI" row is where the two differ most on errors: a +Confect endpoint declares an error schema, so the client gets a typed +failure; a Topcoat procedure cannot deliver one unless the error is part of +the `Ok` value. + +## Tooling + +- `topcoat dev` (from `cargo install topcoat-cli`): build, watch, restart. + Pages that include `topcoat::dev::script()` reload themselves. +- `topcoat fmt`: formats macro bodies (`view!` and others) alongside + `rustfmt`. Run it before you commit view code. +- `HOST` / `PORT` set the bind address; the default is `127.0.0.1:3000`. + +## Unsafe + +Topcoat's own workspace sets `unsafe_code = "deny"`. Suggestion, not a +requirement: an app on it rarely needs `unsafe` either, so the same lint in +`[lints.rust]` costs nothing. Where FFI does need it, `power-of-ten` applies. + +## Hand-offs + +- The Effect side of any comparison: the `effect` skill (version gate first). +- A third-party SDK wrapped as an Effect service: `effect-client-wrapper`. From 62548663daa0b7af8229a9ff7de2e52c11e95bdd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 19:47:54 +0000 Subject: [PATCH 3/8] topcoat skill: Ghost is the test database, not Convex A question about whether the self-hosted Ghost server replaces Convex showed the pairing table left that open. It doesn't. Ghost provisions disposable Postgres databases; Convex is an application backend with server functions, auth and live queries. Ghost does match create-epoch-app's convex-test: a cheap TEMPLATE fork per test run. Added that as its own row, with the three reasons (one shared role, unverified TLS, disposable by design) that keep Ghost out of the persistent-store role. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/agents/skills/topcoat/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/agents/skills/topcoat/SKILL.md b/tools/agents/skills/topcoat/SKILL.md index 0c0fc53c..b4f5bc62 100644 --- a/tools/agents/skills/topcoat/SKILL.md +++ b/tools/agents/skills/topcoat/SKILL.md @@ -96,6 +96,7 @@ the nearest Rust shape. Rows marked "checked" were run; the rest come from the | Re-render on input change | atom-derived React component | `#[shard]`: the browser tracks the signals, the server re-renders, the DOM swaps | | Slow data | React Suspense | `live!` / `emit!` stream regions into the same response; `suspense`/`error_boundary` components | | Push after load | Convex live queries | no DB subscription; `Sse` response (`sse` feature), or Datastar over SSE | +| Test database | `convex-test` (in-memory Convex) | a Ghost fork per test run or agent task (`ghost fork seeded run-42`) on venari's Ghost server. Ghost is the *test* database, not the Convex replacement: its databases share one role, use unverified TLS, and are disposable by design (`tools/venari/README.md`, "Ghost") | | Validate input | `Schema.decodeUnknown` | `FromRequest` extractors (`content.md`) and serde | | UI kit | `@packages/ui` (shadcn / Radix) + Tailwind via Node | `topcoat ui` copies component source from a registry (the shadcn model) + standalone Tailwind CLI, no Node | | Client build | Next.js bundle, bun, turbo | none: no wasm, no JS build step | From e940340b60d9d1c6c9b1b0d8ce69638420f84ac7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 07:50:01 +0000 Subject: [PATCH 4/8] Pin effect-ts/skills to 155c50c, ahead of the index effect 4.0.0 went out on 2026-10-01; npm `latest` is now 4.0.5, and the `rc` tag has stayed at 4.0.0-rc.118 since 2026-09-28. The locked index still has effect-ts/skills at 2309e6f, written during the RC. Both of its skills now give wrong instructions: - effect-ts says `pnpm add effect@rc`, which installs rc.118, older than every stable 4.0.x. - effect-v3-to-v4 says to use `effect/unstable/*` imports where the reference maps to them. GA removed those paths with no compatibility exports. Upstream 155c50c ("docs: align Effect skills with v4 GA", 2026-10-09) fixes both. It is not in the index yet, so `nix flake update nix-skills` cannot be relied on to pick it up. This pins it by hand, the way main pins supabase-postgres-best-practices. Asked for: effect-ts. Also done: effect-v3-to-v4. Same repo, one fetch, and leaving the migration skill at 2309e6f would keep the removed paths in it. The hash was computed without nix, which this sandbox lacks: a NAR serialization of `git archive 155c50c`, hashed with sha256. The same method reproduced the index's hash for 2309e6f and main's supabase pin exactly, so the method is checked against two known values; the result itself still needs a real fetch (`nix flake prefetch github:effect-ts/skills/155c50c`) or CI to confirm. Neither skill has symlinks, and each SKILL.md `name` matches its deploy directory, so plain subdirectories of the source work, with no builder. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- lib/skills-sh.nix | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/skills-sh.nix b/lib/skills-sh.nix index cfe53e41..0f34b85d 100644 --- a/lib/skills-sh.nix +++ b/lib/skills-sh.nix @@ -26,6 +26,14 @@ nix-skills: final: prev: let github = (nix-skills.overlays.default final prev).agent-skills.github; + + # effect-ts/skills, pinned ahead of the index; see effect-ts below. + effect-skills = final.fetchFromGitHub { + owner = "effect-ts"; + repo = "skills"; + rev = "155c50c911f9c99c294ef8a4461981c5bbe6453e"; # 2026-10-09 + hash = "sha256-GSmGLOQSp4taxckrg7vfMTD/mbkfBYjv7oBsvDJdyck="; + }; in { skills-sh = { @@ -70,12 +78,30 @@ in # `effect-v3-to-v4` drives a migration from the generated # migration/v3-to-v4.md in Effect-TS/effect. The repo's `effect` skill # (tools/agents/skills/effect) layers on both: a version gate, because - # `effect-ts` step 1 is `pnpm add effect@rc`, and in a v3 project that is - # an unrequested major upgrade; and the Rust mapping from + # `effect-ts` step 1 is `pnpm add effect@latest`, and in a v3 project that + # is an unrequested major upgrade; and the Rust mapping from # effect-playground. effect-v3-to-v4 sets disable-model-invocation, so it - # only runs when invoked by name. Indexed at 2309e6f (agents.nix 0b4d80e). - effect-ts = github.effect-ts.skills.effect-ts; - effect-v3-to-v4 = github.effect-ts.skills.effect-v3-to-v4; + # only runs when invoked by name. + # + # Pinned here by hand, ahead of the index. agents.nix 0b4d80e still has + # 2309e6f, written while v4 was a release candidate: its `effect-ts` says + # `pnpm add effect@rc`, which now installs 4.0.0-rc.118, older than the + # stable 4.0.x on `latest`; and its `effect-v3-to-v4` sends imports to + # `effect/unstable/*` paths that v4 GA removed with no compatibility + # exports. 155c50c ("align Effect skills with v4 GA", 2026-10-09) fixes + # both. One fetch serves both skills, so they cannot drift apart. When + # the index reaches 155c50c or later, switch back to + # github.effect-ts.skills. and delete the pin. Until then + # `nix flake update nix-skills` does not move it; bump rev + hash by hand. + # The hash was computed without nix (a NAR serialization of `git archive + # `), a method that reproduced the index's hash for 2309e6f and the + # supabase pin below exactly; `nix flake prefetch + # github:effect-ts/skills/` is the normal way. + # + # Subdirectories, no builder: neither skill has symlinks, and each + # SKILL.md name already matches its deploy directory. + effect-ts = "${effect-skills}/skills/effect-ts"; + effect-v3-to-v4 = "${effect-skills}/skills/effect-v3-to-v4"; # https://www.skills.sh/rhyssullivan/skills/effect-client-wrapper # Wrap a Promise-based SDK as an Effect service behind one `use(fn)` that From 19200b5302764fc5c31a8547cebf5ae53dd223bf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 07:50:14 +0000 Subject: [PATCH 5/8] effect skill: v4 is stable now Effect 4.0.0 went out on 2026-10-01 (npm `latest` is 4.0.5, `prev` is 3.22.3, `rc` has stayed at rc.118). The skill was written while v4 was a release candidate, and three of its statements turned around. - The new-project row said npm `latest` is v3 and v4 is a release candidate. Now v4 is the default and v3 needs a stated reason. A resolved -rc/-beta version gets its own row: it is a pre-GA build with the old import paths. - The version gate named `effect@rc` as the accidental major upgrade. Now it is `effect@latest`. Before writing that, I tested which commands cross the major in a project pinned to ^3.22.2 (npm 10 and pnpm 10): a bare `npm install effect` or `pnpm add effect` keeps the ^3 range; only `effect@latest` rewrites it to ^4. The skill says exactly that rather than "never install effect". - A new trap: GA moved `effect/unstable/*` modules to plain paths (`effect/unstable/http` -> `effect/http`) with no compatibility exports. On 4.0.5 the old path fails with TS2307. The note that stability is now a JSDoc tag was checked against the 4.0.5 source: 6894 `@stability stable`, 5595 `unstable`, 271 `experimental`. Re-checked against 4.0.5 (item 5), using the same probes as rc.117: - Every v4 name in the Rust table and the trap list typechecks. That covers more than the four entries marked "checked" before. - Schema.Date still rejects an ISO string at runtime, and DateFromString decodes it. - The lesson-06 backoff port still makes 5 attempts (transient) and 1 (permanent). - A `readonly cause` payload field still compiles. - `ParseResult` and `Schema.Schema.Encoded` are still gone. - effect-playground still gives the same 71 typecheck errors as on rc.117. - topcoat's missing-service row now names both codes it produces, on 4.0.5 as on rc.117: TS2345, or TS2379 under exactOptionalPropertyTypes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/agents/skills/effect/SKILL.md | 44 ++++++++++++++++++++-------- tools/agents/skills/topcoat/SKILL.md | 2 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/tools/agents/skills/effect/SKILL.md b/tools/agents/skills/effect/SKILL.md index 707066a0..12b7f30f 100644 --- a/tools/agents/skills/effect/SKILL.md +++ b/tools/agents/skills/effect/SKILL.md @@ -34,18 +34,25 @@ grep '"effect"' package.json # declared range | Result | Do this | |---|---| -| `4.x` (including `-rc.N` / `-beta.N`) | Read `node_modules/effect/AGENTS.md` completely, then follow its links into `node_modules/effect/ai-docs/src/`. For an API it does not cover, search `node_modules/effect/src/`. Use the v4 column below. | +| `4.x` | Read `node_modules/effect/AGENTS.md` completely, then follow its links into `node_modules/effect/ai-docs/src/`. For an API it does not cover, search `node_modules/effect/src/`. Use the v4 column below. | +| `4.0.0-rc.N` / `-beta.N` | A pre-GA build. Treat it as v4, and also apply the import-path trap below: a pre-GA install has the `effect/unstable/*` paths that GA removed. Say that it is a pre-release, and do not upgrade it unasked. | | `3.x` | The v3 package ships no `AGENTS.md`. Search `node_modules/effect/src/` for signatures. Use the v3 column below. Do not copy code from v4 docs, from `AGENTS.md` in another project, or from the pinned `effect-ts` skill; it will not compile. | -| not installed, new project | Ask v3 or v4. Give the tradeoff in one line: npm `latest` is v3 and matches the effect.website docs; v4 is a release candidate, is what the Effect team's own skill installs, and ships agent docs in the package. | +| not installed, new project | v4: npm `latest` is 4.x, the Effect team's skill installs it, and it ships agent docs in the package. v3 needs a stated reason, for example code that must match an existing v3 codebase. Say which one you chose in one line. | | not installed, existing code | Find out why before you install anything. | -As of 2026-09-22: npm `latest` = `3.22.2`, `rc` = `4.0.0-rc.117`. Re-check with -`npm view effect dist-tags` before you state it. +As of 2026-10-16: npm `latest` = `4.0.5` (4.0.0 went out 2026-10-01), `prev` = +`3.22.3` (v3 still gets releases), and `rc` has stayed at `4.0.0-rc.118` since +2026-09-28, behind the stable releases. Re-check with `npm view effect +dist-tags` before you state it. **Do not upgrade the major version unasked.** The pinned `effect-ts` skill -(`~/.agents/skills/effect-ts/`) starts with `pnpm add effect@rc`. In a v3 -project, that command is a v3-to-v4 migration. Use `effect-ts` only to set up a -v4 repo. For a migration, Alyssa runs `/effect-v3-to-v4`, which has +(`~/.agents/skills/effect-ts/`) starts with `pnpm add effect@latest`. Since +`latest` became 4.x, that command is a v3-to-v4 migration in a v3 project. +Use `effect-ts` only to set up a v4 repo. In a v3 project, add or update with +a range, not a tag. `npm install effect` and `pnpm add effect` with no version +keep the `^3` range already in `package.json`; `effect@latest` rewrites it to +`^4` (checked with npm 10 and pnpm 10 on 2026-10-16). Use `effect@3` when you +need to name it. For a migration, Alyssa runs `/effect-v3-to-v4`, which has `disable-model-invocation: true` and so never starts automatically. If `~/.agents/skills/effect-ts/` does not exist, the v4 path still works: @@ -54,9 +61,10 @@ If `~/.agents/skills/effect-ts/` does not exist, the v4 path still works: ## Rust translation Same table as the playground README, with a v4 column. Every v4 entry is either -unchanged or resolved against `migration/v3-to-v4.md` in Effect-TS/effect -(main, 4.0.0-rc.117). The four marked "checked" also typecheck and run on -rc.117. +unchanged or resolved against `migration/v3-to-v4.md` in Effect-TS/effect. On +2026-10-16, every v4 name in this table and in the trap list below was +re-typechecked against the stable `4.0.5`. The entries marked "checked" were +also run, first on `4.0.0-rc.117` and again on `4.0.5`, with the same results. | Rust | v3 | v4 | Lesson | |---|---|---|---| @@ -88,8 +96,18 @@ through: `Schema.TaggedErrorClass` does not exist. ## v3 → v4 traps that a rename table hides These come from typechecking effect-playground (v3, strict tsconfig) against -`4.0.0-rc.117`. They matter when you read v3 code while you write v4 code, or -the other way round. +v4: first `4.0.0-rc.117`, then `4.0.5`, which produced the same 71 errors. +They matter when you read v3 code while you write v4 code, or the other way +round. + +- **Pre-GA import paths: `effect/unstable/*` is gone.** Before 4.0.0, modules + such as http, rpc and ai lived under `effect/unstable/*`. At GA they moved to + plain paths (`effect/unstable/http` → `effect/http`), with no compatibility + exports: on `4.0.5` the old path fails with TS2307 (checked). Code, examples + and answers written against a v4 beta or RC use the old paths, so drop the + `unstable` segment. Moving a module did not make its API stable. Stability is + now a JSDoc tag: `@stability unstable` APIs can break in a minor release, and + `@stability experimental` APIs in a patch release. - **Same name, different meaning: `Schema.Date`.** v3 `Schema.Date` decodes an ISO string to a `Date`. v4 `Schema.Date` is the identity schema for a `Date`, @@ -133,7 +151,7 @@ the other way round. - Expected failures go in `E` as tagged errors. Broken invariants are defects (`Effect.die`, `orDie`) and stay out of `E`. A payload field named `cause` is fine, including `readonly cause: unknown`, on v3 and v4 (checked: - 3.22.2 and rc.117). The playground's lesson 06 comment says it conflicts + 3.22.2, rc.117 and 4.0.5). The playground's lesson 06 comment says it conflicts with `Error.cause`. It does not: renaming that field to `cause` still typechecks and passes 6/6 tests. Do not repeat that comment. - Ask for a dependency with `yield* Service` inside the function. Do not pass diff --git a/tools/agents/skills/topcoat/SKILL.md b/tools/agents/skills/topcoat/SKILL.md index b4f5bc62..6a9d8335 100644 --- a/tools/agents/skills/topcoat/SKILL.md +++ b/tools/agents/skills/topcoat/SKILL.md @@ -86,7 +86,7 @@ the nearest Rust shape. Rows marked "checked" were run; the rest come from the | Concern | create-epoch-app / Effect | Topcoat 0.8.1 | |---|---|---| | Ask for a dependency | `const db = yield* Database`; epoch-app's rule is "prefer `yield*` over passing deps as arguments" | `app_context::(cx)` inside a small `fn(cx: &Cx)`; "functions, not middlewares" is the same rule | -| Missing dependency | **Compile error**: the service stays in `R`, and `runPromise` rejects `Effect` (checked, TS2345) | **Runtime panic → 500** on that request only (checked) | +| Missing dependency | **Compile error**: the service stays in `R`, and `runPromise` rejects `Effect` (checked on 4.0.5: TS2345, or TS2379 under `exactOptionalPropertyTypes`) | **Runtime panic → 500** on that request only (checked) | | Wire implementations | `Layer` + `Effect.provide`; swap a Layer for tests | `Router::builder().app_context(value)`; swap the value in a test router | | Request-scoped dedupe | no one-to-one; `RequestResolver` dedupes and batches | `#[memoize]` on a `cx` function, per request | | Error type | typed `E` channel, tagged errors, `catchTags` | `topcoat::Error` wraps `anyhow::Error` (type-erased); router error types map to status codes, anything else is 500. Keep `thiserror` enums in the domain and convert at the handler | From a7a6c5fa2de2484240117b1e91543905192e7cac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 07:58:37 +0000 Subject: [PATCH 6/8] CLAUDE.md: effect-ts installs effect@latest, not @rc The lesson on library skills still quoted effect-ts's first step as `pnpm add effect@rc`. That was true at 2309e6f; the pin is now 155c50c, which says `pnpm add effect@latest`, and skills-sh.nix and the effect skill were updated in 19200b5 while this file was not. CLAUDE.md is always loaded, so the stale line told every agent the external skill installs the RC, when in a v3 project it installs v4 - the exact upgrade the version gate exists to stop. Also: "stale at each release candidate" -> "at each release", since v4 is GA. Found by /code-review (finding 1). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- CLAUDE.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 50aead9f..a30c899d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -399,10 +399,11 @@ enough, so the next breakage is a prompt to check. Effect v4 ships `AGENTS.md` and `ai-docs/` in its npm tarball, and the Effect team's own `effect-ts` skill is only a pointer to them. A local API reference would restate that content, and it would go stale at each - release candidate. The repo's `effect` skill therefore carries what the - package cannot: a gate on the *installed* major version (the v3 tarball - ships no agent docs, and `effect-ts` starts with `pnpm add effect@rc`, which - in a v3 project is an unrequested major upgrade), plus Alyssa's Rust + release. The repo's `effect` skill therefore carries what the package + cannot: a gate on the *installed* major version (the v3 tarball ships no + agent docs, and `effect-ts` starts with `pnpm add effect@latest`, which + since v4 GA on 2026-10-01 is an unrequested major upgrade in a v3 project; + during the RC the same step said `effect@rc`), plus Alyssa's Rust mapping. Get version-specific claims from a typecheck against the real package, not from memory: typechecking effect-playground against 4.0.0-rc.117 found `Schema.Date`, a name that v4 keeps with a different From 15d6a0c77bc006a377e57ba8db8558a8ef534f20 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 07:58:54 +0000 Subject: [PATCH 7/8] topcoat skill: list the docs where the registry puts them Step 0's ls had a second glob, topcoat-*/macro/docs, which is the repository layout (crates//macro/docs). In the cargo registry each macro crate is its own package, topcoat--macro-/docs, which the first glob already matches. So the second glob never matched, 2>/dev/null hid that, and the skill described two layouts for one set of files. Checked against the registry after building topcoat 0.8.1: the one glob finds 8 docs directories, including topcoat-router-/docs, which the prose had not mentioned at all; it does now. Found by /code-review (finding 8). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/agents/skills/topcoat/SKILL.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/agents/skills/topcoat/SKILL.md b/tools/agents/skills/topcoat/SKILL.md index 6a9d8335..913a18bc 100644 --- a/tools/agents/skills/topcoat/SKILL.md +++ b/tools/agents/skills/topcoat/SKILL.md @@ -30,15 +30,18 @@ the Effect stack, which is where Alyssa is coming from. ```sh grep -A1 '^name = "topcoat"$' Cargo.lock # resolved version rustc --version # 0.8.1 needs >= 1.98 -ls -d ~/.cargo/registry/src/*/topcoat-*/docs \ - ~/.cargo/registry/src/*/topcoat-*/macro/docs 2>/dev/null +ls -d ~/.cargo/registry/src/*/topcoat-*/docs # every crate that has guides ``` The guides are embedded in the published crates (`include_str!` into the API -docs), so after a `cargo build` they are on disk at the resolved version: +docs), so after a `cargo build` they are on disk at the resolved version, in +three kinds of crate (the `ls` above lists all of them): `topcoat-/docs/` holds the facade guides (router, context, app_context, -runtime, session, ui, …), and `topcoat--macro-/docs/` holds the -macro references (`view.md`, `live.md`, `procedure.md`, `shard.md`, …). +runtime, session, ui, …); `topcoat-router-/docs/` holds the router's own +(content types, errors, module_router, tower); and +`topcoat--macro-/docs/` holds the macro references (`view.md`, +`live.md`, `procedure.md`, `shard.md`, …). The repository keeps macro docs at +`crates//macro/docs/`; that path does not exist in the registry. `cargo doc --open -p topcoat` renders the same text. The guide *index* is `AGENTS.md` in the repository, and it does **not** ship in From 4c6c12d3dce0a5ad954e6a32b2666e94e96edc10 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 07:59:18 +0000 Subject: [PATCH 8/8] effect skill: make the migration-reference lookup work alone The skill told agents to search the migration reference using the recipes in effect-v3-to-v4. Those recipes grep .repos/effect/migration/v3-to-v4.md, a clone that only the migration skill's setup step creates. In an ordinary v4 project an agent following the pointer gets "No such file" from rg, and the cheapest next move is the guess the paragraph forbids. Now the paragraph says to search node_modules/effect/src first. Then it gives the reference's own fetch: a sparse, blobless clone of just migration/ into the same .repos/effect path, so upstream's recipes also work afterwards. The package does not ship the reference (checked in 4.0.5). The commands were run as written: 6.5 MB on disk, and the rg recipe returns `Effect.catchAll` -> `Effect.catch`. Found by /code-review (finding 9). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tw8tuRUyKtkcjmN4upeawF --- tools/agents/skills/effect/SKILL.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tools/agents/skills/effect/SKILL.md b/tools/agents/skills/effect/SKILL.md index 12b7f30f..6d69448c 100644 --- a/tools/agents/skills/effect/SKILL.md +++ b/tools/agents/skills/effect/SKILL.md @@ -88,10 +88,22 @@ also run, first on `4.0.0-rc.117` and again on `4.0.5`, with the same results. | `Arc>` | `Ref` | same | — | | `impl Iterator` | `Stream` | same | — | -For a symbol that is not in this table, do not guess the v4 name. Search the -reference (the recipes are in `~/.agents/skills/effect-v3-to-v4/SKILL.md`) or -`node_modules/effect/src/`. This table was checked, and one guess still got -through: `Schema.TaggedErrorClass` does not exist. +For a symbol that is not in this table, do not guess the v4 name. Search +`node_modules/effect/src/` first. For a v3 name, look it up in the generated +migration reference. The package does not ship that reference, so if +`.repos/effect` does not exist, fetch only the reference (6.5 MB) with: + +```sh +git clone --depth 1 --filter=blob:none --sparse https://github.com/Effect-TS/effect .repos/effect +git -C .repos/effect sparse-checkout set migration +rg -n '^- `Effect\.catchAll`' .repos/effect/migration/v3-to-v4.md # one symbol at a time +``` + +Never read `v3-to-v4.md` whole; it is about 17,000 lines. The other search +recipes are in `~/.agents/skills/effect-v3-to-v4/SKILL.md`; they expect this +same `.repos/effect` path, so they work after the clone above. This table was +checked, and one guess still got through: `Schema.TaggedErrorClass` does not +exist. ## v3 → v4 traps that a rename table hides