--- name: effect-v4-source-lookup description: Use when you need to confirm an Effect v4 API before relying on it — does this symbol exist, what is its signature, what does it actually do at runtime. Gives the evidence ladder (migration notes settle renames, vendored source settles existence and signature, only a probe settles semantics) and the probe preconditions that keep a probe from silently false-passing against Effect v3. --- # Looking up the truth about Effect v4 Never write, review, or rely on a v4 API you have not confirmed. Memory is confidently wrong, and so is any `effect@3` a workspace root happens to resolve. This skill tells you where to look and how far to go. (If the question is merely "which module does X" — start at `effect-v4-module-index`, then verify here.) **Every claim this skill settles is written about current behaviour, never a version.** Cite a source finding by module and symbol, with the line resolved against the vendored tree, and never name a pinned Effect version as history — the reader takes their own Effect through their own pin, and a number in a claim goes stale on the next advance without teaching anything the current tree does not already show. ## The evidence ladder Three rungs, ordered by cost. Each settles a strictly different class of question. Climb to the rung your claim needs — no further, no less. | Rung | Where | Settles | | --- | --- | --- | | 1 | `$SRC/migration/*.md`, `$SRC/ai-docs/`, `$SRC/LLMS.md` | **Renames** | | 2 | `$SRC/packages/*/src`, or `$EFFECT_SRC` | **Existence and signature** | | 3 | A probe compiled and run from inside a package | **Semantics** | **Rung 2 also tells you how far to trust an API.** A TSDoc `@stability unstable` tag on a module or symbol means it may break in a minor release; an untagged API follows strict semver. Grep the source for the tag (`grep -n "@stability" $SRC/packages/effect/src/.ts`) before building on an API, and treat an unstable one as a moving target: pin your usage behind one seam and re-verify it when the installed `effect` moves. ### Resolving `$SRC` and `$EFFECT_SRC` Two source roots, and they do not settle the same rungs. **Always use the absolute form** — the probe protocol below has you `cd` into a package, and every relative path breaks the moment you do. - **`$SRC`** — a vendored Effect checkout, a `git submodule` under `.repos/` pinned to the exact `effect` release tag the workspace compiles against. Serves rungs **1 and 2**. Present when this plugin runs from the `effected` monorepo (after `savvy repos sync` on a fresh clone or worktree); absent elsewhere. - **`$EFFECT_SRC`** — `node_modules/effect/src`. Serves rung **2 only**, and does so at full fidelity: `effect` publishes `src/**/*.ts` in its `files` array, so every consumer already has the complete v4 TypeScript source, `internal/` implementations included. The lockfile pins it to exactly the version you compile against — so on the existence-and-signature question it is at least as trustworthy as the vendored tree, which can drift from the catalog between re-pins. Every `@effect/*` package ships the same way (`@effect/platform-node`, `@effect/vitest`, …), so resolve whichever one your claim is about — the vendored tree's `packages/*/src` has a `node_modules` counterpart in each case. **The converse is false**: the vendored checkout is sparse and carries only `packages/effect` and `packages/vitest`, so the platform *implementations* are not there. The Node `FileSystem` — the one `@effected/memfs` has to mirror, e.g. `seek` failing `BadArgument` before the start of the file — lives in `@effect/platform-node-shared`; under pnpm read `node_modules/.pnpm/@effect+platform-node-shared@_effect@/node_modules/@effect/platform-node-shared/src/NodeFileSystem.ts` (the store directory carries the peer suffix; `find node_modules/.pnpm -path '*platform-node-shared@*' -name NodeFileSystem.ts` locates it). When a semantic has to be *copied* from the platform layer, that file is rung 2 for it; the vendored tree is silent, not authoritative. **At the same version, `$EFFECT_SRC` and `$SRC` still disagree on line numbers**: npm's published `effect` carries publish-time TSDoc the vendored tag does not, so a declaration sits at a different line in each tree even when both resolve the identical release. A `Module.ts:line` anchor in this plugin always names the **vendored tag** (`$SRC`); a consumer reading `node_modules` finds the same declaration by symbol name instead of by line — `grep -n "export const layerStdio" "$EFFECT_SRC/ai/McpServer.ts"`. Resolve both before you trust any lookup. The bottom of the ladder is a hard failure, never a fallback to memory — a wrong answer from v3 memory is indistinguishable from a right one. #### When they disagree, the installed source wins **Do not assume the vendored tree matches what you compile against.** The submodule sits at the exact tag it was last pinned to, and a lockfile resolution that moves without its matching `savvy repos pin` leaves the two silently disagreeing — exactly this drift happened under the old subtree pattern, a vendored tree sitting one release behind an already-bumped install — and version-checking is still the reader's job, not the tooling's. So a rung-2 answer from `$SRC` can be a *stale* answer, delivered with total confidence and no error. The rule: > **`$EFFECT_SRC` is the authority on existence and signature. `$SRC` is a convenience > and the only home of rung 1.** When they disagree, `node_modules` wins — it is what > your code links against; the vendored tree is what someone pinned last. Two agents were saved by this during the store work. Check the drift in one line before you trust the tree — the versions match, or they do not (the snippet uses the `$SRC` shell variable that the resolution block *below* defines — set it first): ```bash diff <(node -p 'require("'"$SRC"'/packages/effect/package.json").version') \ <(node -p 'require("effect/package.json").version') \ || echo "VENDORED TREE IS STALE — settle rung 2 against \$EFFECT_SRC, not \$SRC." ``` **The version-string `diff` above cannot see a second, quieter drift**: even when the two trees report the identical version, npm's publish step adds TSDoc the vendored tag never carries, so a symbol's line number can still differ between them. A version match is not a line-number match — check that separately, against a symbol, never a line count alone: ```bash diff <(grep -n "export const resource" "$SRC/packages/effect/src/ai/McpServer.ts") \ <(grep -n "export const resource" "$EFFECT_SRC/ai/McpServer.ts") \ || echo "ANCHORS ARE VENDORED-TREE LINES — search installed source by symbol." ``` ```bash # Rungs 1+2 — the vendored tree, if this project has one. SRC="${EFFECT_SMOL_SRC:-${CLAUDE_PROJECT_DIR}/.repos/effect}" test -d "$SRC/packages/effect/src" || SRC="" # Rung 2 — the installed source. Resolve it, then GATE ON THE VERSION. EFFECT_SRC="$(node -p 'const p=require("node:path");p.join(p.dirname(require.resolve("effect/package.json")),"src")' 2>/dev/null)" test -d "$EFFECT_SRC" || EFFECT_SRC="" if [ -n "$EFFECT_SRC" ]; then V="$(node -p 'require("effect/package.json").version' 2>/dev/null)" case "$V" in 4.*) ;; *) echo "REFUSING \$EFFECT_SRC: resolved effect@$V, not v4. cd into a package that depends on effect@4." >&2 EFFECT_SRC="" ;; esac fi if [ -z "$SRC" ] && [ -z "$EFFECT_SRC" ]; then echo "FATAL: no Effect v4 source. Set EFFECT_SMOL_SRC, or cd into a package that depends on effect@4." >&2 exit 1 fi [ -z "$SRC" ] && echo "NOTE: no vendored tree — rung 1 unavailable. Settle renames at rung 2." >&2 ``` **The version gate is the load-bearing line, and it must refuse rather than report.** `effect@3` also publishes `src/`, so wherever a v3 is installed, `require.resolve` finds `node_modules/.pnpm/effect@3.x/.../src` — a complete, confident, *wrong* rung-2 source. A block that merely *prints* the resolved version reports v3 source in passing, and a reader skims past it. Refuse, and the trap cannot spring. **That does not mean "the root gives you v3".** In *this* repo the workspace root resolves **nothing**: a bare `effect` import there dies with `ERR_MODULE_NOT_FOUND`. The lockfile carries **two `effect` entries by intent** — the toolchain's own pin (`@savvy-web/tsdown-plugins`, `rolldown-pnpm-config` and `@vitest-agent/*` declare `effect` and their `@effected/*` inputs as regular dependencies, so the published kit binds to the toolchain's copy) and the kit's own current pin. Neither copy is v3, and neither resolves from the root; which one a probe links against is decided by where the probe file lives — which is the point of the rule. `okf/conventions/one-resolved-effect-copy.md` has the full shape of the two-copies-by-intent arrangement. Which failure you get depends on what a given repo has installed, so the gate must key on the *resolved version*, never on a remembered answer for a particular directory. **Read the LOCKFILE, and read it carefully — neither the pnpm store nor a raw grep count is the check.** `ls node_modules/.pnpm | grep '^effect@'` lists store directories, which outlive the lockfile: during a catalog advance the previous release's directory lingers there, orphaned. And a grep of the lockfile itself can show a second version that is not a second copy — while a toolchain `overrides` bridge is up, `pnpm-lock.yaml` carries a redirect line (`effect@: `) whose *only* hit is the mapping. One resolved version, two spellings. (A `packageExtensions` bridge, the other shape, really does keep two copies — read `okf/conventions/one-resolved-effect-copy.md` for which shape stands.) What voids a probe is the version it **resolves**, printed from inside itself. **Rung 1 has no fallback.** The npm package ships no `migration/`, no `ai-docs/`, no `LLMS.md` — they are not in its `files` array. That degrades honestly rather than silently: rung 1 is prescriptive, not authoritative, and (as below) a removal is never settled by it anyway. When `$SRC` is absent, say so and climb to rung 2. The vendored tree is **read-only**. Never point a writing tool at it: the repo's markdownlint config sets `"fix": true`, and running it over the tree silently rewrites the migration notes. A corrupted rung-1 source cannot be detected by reading it — only by `git status "$SRC"`. ### Rung 1 is prescriptive, not exhaustive The migration notes document the *recommended path*, not the surface. They are excellent for renames and silent about everything else. Verified gaps, each of which cost real work: - `migration/services.md` migrates `Context.Tag` → `Context.Service` and **never mentions `Context.Key`** — which is the primitive you need for a type-only key, and whose `out Shape` covariance means a `Context.Key` parameter will *not* give you the compile error you expect. - `migration/forking.md` names `Effect.fork` → `Effect.forkChild`, and never mentions that `Effect.makeSemaphore` is gone and `Semaphore` is a top-level module (`Semaphore.make` / `makeUnsafe`, then `withPermits(1)(effect)`). - `migration/cause.md` gives `Cause.isFailure` → `Cause.hasFails`, and never mentions `Exit.causeOption` → `Exit.getCause`, which returns `Option>`. So: **a removal is never settled by rung 1.** If the docs are silent on a symbol, that is not evidence the symbol is fine. Go to rung 2. ### Rung 1 also asserts things source refutes Silence is the *gentler* failure. The migration notes also make positive claims that the tree contradicts, in both directions — and a confident wrong answer costs more than an absent one. Both of these were found in one audit and both still hold: - **A method that does not exist.** `migration/yieldable.md` documents the `Yieldable` trait as `asEffect(): Effect` and states the runtime calls `.asEffect()` internally. **`asEffect` has zero occurrences in the entire source tree.** A design built on it fails at the first call. - **A removal that did not happen.** `migration/fiberref.md` lists `Differ` as removed alongside `FiberRef` / `FiberRefs` / `FiberRefsPatch`. Those three are genuinely gone; **`Differ` is alive** (`index.ts:153`), and `migration/v3-to-v4.md` even maps `effect/Differ` → `effect/Differ` and documents the surviving interface. The notes contradict themselves. **Rung 1 settles renames and nothing else.** Not existence, not removal, not trait mechanics — those are rung 2, and behaviour is rung 3. Treat a positive claim in the notes about *what a symbol is or does* exactly as you treat their silence: unsettled until you have read the source. ### `SCHEMA.md` is a version-exact oracle that still loses to source The vendored tree ships `packages/effect/SCHEMA.md` **at the pin** — 7,400 lines of upstream Schema documentation, versioned with the source rather than floating like a website. That makes it far stronger than `migration/`: it describes the surface you actually have, so it is an excellent **diff oracle** for checking a claim quickly, and when it disagrees with a skill it is usually the skill that is wrong. It is still a document, and it does not outrank `src`. An audit of the Schema references found roughly thirty disagreements where `SCHEMA.md` was right — and **eight where it was itself wrong**, on `Getter`/`Parser`, the `effect/data` import path, `Schema.brand()`, `UnknownFromJsonString`, `cause.failures`, `Array$`, `toJsonSchema` and `ValidDate`. Those corrections went beyond the pinned upstream doc on source authority. So: reach for it early because it is cheap and version-exact, and treat a disagreement with it as a strong signal worth chasing — then settle the answer in `packages/effect/src`. Call it rung 1.5: better than the migration notes, never a substitute for reading the declaration. ### A bare existence-grep false-negatives on built-in names Rung 2 is usually one grep: `grep -n 'export const Foo\|export function Foo'`. That grep **lies about names that collide with a TS/JS built-in**, and it lies in the dangerous direction — it reports *absent* for a symbol that exists, which reads as "removed in v4" and sends you off to invent a replacement. The mechanism is that a module cannot declare `const Array` beside its own uses of the global `Array` type, so core defines the symbol under a private name and renames it in an `export {}` block: ```text // Schema.ts:4502 — the real definition, under a name you did not grep for const ArraySchema = Struct_.lambda((schema) => …) // Schema.ts:4505 — the export, in a block your grep pattern never matches export { /* …tsdoc… */ ArraySchema as Array } // the rename lands at :4522 ``` `Schema.Array` is real, and `grep 'export const Array' Schema.ts` returns nothing. The confirmed occurrences of this pattern (vendored-tree lines) — `Schema.ts:4522`, `Equivalence.ts:620`, `Order.ts:580` — are all `Array`, but treat the *class* of names as suspect, not just that one: `Array`, `Record`, `Map`, `Set`, `Error`, `Date`, `Number`, `String`, `Object`, `Symbol`, `Function`, `Boolean`. (Some of them do grep normally — `Schema.Record` is a plain `export function Record` at `Schema.ts:3867`, and `Config.Array` a plain `export function Array` at `Config.ts:1111` — which is exactly why the inconsistency catches people.) **When a built-in-colliding name greps as absent, do not conclude it was removed.** Settle it one of these ways instead: - **Read the module's export list**, `export {}` blocks included — not just `export const` / `export function` lines. - **Grep for the use site, not the declaration**: `grep -rn 'Schema\.Array('` across this kit's `packages/*/src` proves the call compiles today against the installed `effect`. - **Ask the runtime**, from inside a package on the v4 catalog: `node --input-type=module -e "import * as S from 'effect/Schema'; console.log(typeof S.Array)"`. The same caution applies to the aliased-import direction: this kit writes `import { Function as Fn } from "effect"`, so `grep 'Function.dual'` misses every real call site, which all read `Fn.dual`. ### The ladder is not Effect-specific: third-party `.d.ts` earns the same climb Every rule above is written about `effect` because that is where the cost lands most often, but the *reason* — a confident summary of an API nobody read — has nothing to do with which package it is. A dependency's shipped `.d.ts` (or `src`, where it ships one) is a rung-2 source in exactly the same sense, and it settles the same class of question. The concrete case: an evidence pack recommended octokit's `RestEndpointMethodTypes` for typing a REST surface, on a plausible reading of its docs. **Reading octokit's own types overturned it** — the already-typed `request` surface in core was the right seam, and the recommendation would have bought a large generated type map for nothing. Nobody had read the `.d.ts`; the summary was reasonable and wrong. > **Rule: before designing against a third-party type surface, read the surface.** > Reach for `node_modules//dist/**/*.d.ts` (or its `src`) the same way you > reach for `$EFFECT_SRC`. "The docs say" is rung 1 for someone else's package > too — prescriptive, not exhaustive, and silent about removals. ### The registry rung: published artifacts and installed consumers The ladder settles claims about a source tree. Two claim classes need a different authority — the registry and the installed artifacts — because no tree you are standing in can answer them: - **Closed upstream ≠ released.** An issue's or PR's closed state proves nothing about any published artifact. What actually shipped is settled by `npm view @` and by reading the INSTALLED `.d.ts`/`.js` under `node_modules` — never by the tracker's state. - **A repo-local grep structurally cannot see downstream consumers.** Before calling an API change "breaking in-package only", read the installed artifacts of the known consumers — the `node_modules` of a consuming repo. That check catches a shape change that would land as a runtime defect in an installed consumer, where a repo-local grep confidently reports zero consumers. ### When two reads of one file disagree, settle it against the committed blob A second-order failure, and it is a *reasoning* failure rather than a lookup one: you read a file early in a session, read it again later, and the two reads do not match. The temptation is to pick the read that supports the conclusion you have already drawn — and it is always available, because one of them does. Neither read is evidence. A working tree changes under you: another agent edits it, an earlier step in your own session wrote to it, a formatter ran. **Settle it against the committed blob** — `git show HEAD:path/to/file` — and then, if the working tree differs, treat that as its own finding to explain rather than noise to discard. The rule generalizes past files: **when two observations of the same thing disagree, the tie-break must be a third source, never the one that suits the conclusion.** Picking is how a stale read gets laundered into a verified fact. ### Worked example: the three rungs disagree `Context.Key` (`Context.ts:64`): - **Rung 1** — `migration/services.md` never mentions it. Reading harder produces nothing. - **A runtime check** says it does not exist: `typeof Context.Key` is `undefined` and `"Key" in Context` is `false`, because it is type-only. - **Rung 2** — `$SRC/packages/effect/src/Context.ts:64` settles it: ```text export interface Key extends Effect ``` It exists, it is type-only, and `Shape` is **covariant** — so a `Context.Key` parameter accepts a wider shape than declared, and a design that expected a compile error there will not get one. `$EFFECT_SRC/Context.ts:64` is the same declaration, and — for this one anchor — the same line: `Context.ts` overall carries far more publish-time TSDoc in the installed copy than the vendored tag, but the extra lines land after this declaration, not before it. Do not generalize that coincidence: confirm any other anchor by symbol, per the drift check above, rather than assuming a line number survives the same way. Three answers, one truth, and the cheap rungs are the ones that lie. ### Rung 3 is the only one that settles behaviour Existence and signature do not tell you what a function *does*. Real examples where the signature was innocent and the behaviour was not: - `Effect.cached` memoizes the `Exit` **including interrupts** — an interrupted first caller permanently bricks the cached effect with a cause outside its declared error channel. - `it.effect` **always** installs a virtual `TestClock`, so `Effect.sleep` / `delay` / `timeout` hang silently to the vitest timeout. - `Effect.catchCause` swallows interrupts. A probe built on it reports success exactly where real code hangs. When the exposure is a defect, use `catchDefect`. ## Probe protocol A probe that cannot fail is worse than no probe. Every precondition below exists because it was violated. **Venue: a repo with a `scratchpad/` workspace sends probes there first.** > **"Scratchpad" names two unrelated places — do not confuse them.** The agent > harness hands most sessions a private scratch DIRECTORY for temporary files > (an absolute path under `/tmp` or similar, named in the system prompt). That > is **not** the repo's `scratchpad/` workspace, it has no `node_modules`, and a > probe written there dies with > `ERR_MODULE_NOT_FOUND: Cannot find package 'effect'` — precondition 3's > failure, reached by a route that feels like following this rule rather than > breaking it. The venue below means a `scratchpad/` DIRECTORY INSIDE THE REPO, > resolved relative to the repo root: a probe written to the harness scratchpad > fails to resolve `effect`, and the identical file copied into `packages/npm/` > runs. Some kit repos (the effected monorepo among them) ship a private `scratchpad/` workspace member with every kit package at `workspace:*` and `effect` at the catalog's resolution. There, a probe is TYPED: write free-form probes as `scratchpad/probes/.ts` and run `pnpm scratchpad:probe probes/.ts` from the repo root (`pnpm --filter scratchpad probe` under the hood — pnpm's `--filter` executes the underlying script with its cwd inside `scratchpad/`, so precondition 1's never-from-the-repo-root resolution hazard does not arise), or write test-shaped probes as `scratchpad/__test__/.test.ts` and run `pnpm exec vitest run --project scratchpad --coverage.enabled=false`. Type-level probes run `pnpm scratchpad:check` (`tsc --noEmit` over the scratchpad program) — tsx and vitest strip types without checking, so only `scratchpad:check` proves a does-this-compile question. Both areas are gitignored and disposable (`pnpm scratchpad:reset` reseeds them), so the placement and delete-by-absolute-path rules below — which exist to keep probes safe in repos WITHOUT a scratchpad — do not apply there. Every OTHER precondition (print the resolved version, run the control first, exercise a non-first member) applies unchanged in either venue. No `scratchpad/` workspace in the repo? The protocol below is the venue. **"Scratchpad first" is NOT "scratchpad always" — the subject decides.** Read what the probe is *about* before picking: | the probe's subject | venue | | --- | --- | | `effect` itself, or a **published/built** kit package | the scratchpad. Bare `@effected/` imports resolve to that package's `dist/dev` build (verified: `scratchpad/node_modules/@effected/yaml` → `packages/yaml/dist/dev/pkg`), which is the honest artifact-shaped answer. | | the package's **OWN uncommitted `src/`** | **the package root**, per precondition 3 — a probe file at `packages//probe.ts` importing `./src/…` relatively. | The second row is the one agents get wrong, and the failure is silent: the scratchpad resolves `dist/dev`, so a probe there measures the **last build**, not your edit, and reports the old behavior with total confidence — the exact "resolves to recollection" failure the ladder exists to prevent. You can force the scratchpad to see the edit with `pnpm build --filter @effected/` first, and that is the right move when you also want to know the *built artifact* behaves that way; it is the wrong move when you are iterating, because every edit costs a build and a forgotten one is undetectable. Two riders on the package-root form, both learned by leaving mess behind: - **A `__probe__/` (or any) subdirectory is wrong twice.** It is precondition 3's silent false-pass — the tsconfig `include` is `${configDir}/*.ts` and does not match subdirectories, so the file leaves the compilation program and its control never fires — and a directory is easier to forget than a file. Three `__probe__/` files were left in `packages/schemastore/` during one review this way. - **Delete by absolute path, and `git status --porcelain packages/` before you report.** The scratchpad's disposability does not transfer: the package root is committed ground. 1. **Run from inside the package, never the repo root.** A workspace root that has a v3 installed resolves it and will describe the v3 surface with total confidence; a root that has none — this repo today — fails with `ERR_MODULE_NOT_FOUND` instead. Both are the same rule: only `packages//` is guaranteed to resolve the pinned v4. 2. **Print the resolved version inside every probe, and compare it to the installed `effect` and the vendored tag — never to a prerelease channel word.** `catalog:effect` is a caret range, so it cannot be the exact match: the exact version is the **lockfile's resolution**, the `version` in `node_modules/effect/package.json` (and the vendored tree's tag, `.repos/effect`'s `effect@`). The probe prints what *it* resolved and that must equal both; a mismatch means the probe linked a different copy or the vendored tree is stale, and what it says about semantics is unreliable until reconciled. Resolving **v3** (`3.x`) voids a probe outright. 3. **In a repo without a scratchpad workspace: probe files live at the package root** — *inside* `packages//`, written there, not merely run from there. Two distinct failures, and they bite at different moments: - **Outside the package, it will not even load.** Node resolves bare imports relative to the **script's own path, not the cwd**, walking up from the file for a `node_modules`. A probe parked in a scratch/temp directory therefore dies with `ERR_MODULE_NOT_FOUND: Cannot find package 'effect'` no matter how carefully you `cd packages/` first. Write the file into the package; `cd` alone buys you nothing. - **In a *subdirectory* of the package, it silently false-passes.** The tsconfig `include` is `${configDir}/*.ts` and does **not** match subdirectories, so a probe one level down drops out of the compilation program and its control error never fires. The safe spelling is `packages//probe.ts` — package root, top level, deleted by absolute path afterwards. **Probing a package's OWN engine/internal modules: use `npx tsx` — still from a file inside the package.** `npx tsx packages//probe.ts` resolves the house `.js`-extension TypeScript imports correctly and runs without a test project. The file placement rule is NOT relaxed: a probe in `/tmp` dies with `ERR_MODULE_NOT_FOUND` the moment anything in its import graph names `effect` bare (the entry's own imports resolve from `/tmp`, which has no `node_modules`). The safe universal spelling is one probe file at the package root, run via `npx tsx`, **never named `*.test.ts`**: one scratch test file with a load-time error fails the package's whole run (`✗ test suite failed to load`, exit 1 — see `effect-v4-testing`). Delete the probe by absolute path afterwards, same as rule 6. 4. **Run the control first.** Write a line you *know* must fail. Watch it fail. Only then write the real assertion. For a **behavioural** probe, "must fail" is the wrong control — invert it and prove the probe can *observe the effect at all*. A probe asking "does a defect roll the transaction back?" reads success as *zero rows*, and zero rows is also what a broken harness prints; the control that rescues it is a **committing** transaction that must leave its row behind. Ask what a silently-dead probe would print, and make the control the thing that distinguishes it. 5. **A probe of any multi-value API must exercise a NON-first member.** A probe that constructs with the first literal of a union, the first element of a list, or the first overload succeeds under both the correct reading and a silently-degraded one — it cannot fail, so it settles nothing. The `@effected/glob` planning probe for `Schema.Literal("a", "b", "c")` passed precisely because it constructed with `"a"`; only a `"b"` construction exposed that v4's runtime keeps the first literal and drops the rest. 6. **Delete the probe by absolute path** when done. Example (no-scratchpad venue — see precondition 3): ```bash cd packages/ node -e 'console.log("resolved effect:", require("effect/package.json").version)' # write packages//probe.ts (NOT a temp dir — see precondition 3), then either: pnpm exec tsc # type-level probe node probe.ts # behavioural probe; Node runs TS directly rm -f "$PWD/probe.ts" ``` **Run `tsc` BARE — never pass the probe file as a CLI argument.** Under TypeScript 7 a found tsconfig plus CLI file args is a hard error (`TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline`). The bare form compiles the package's own program, which includes a root-level probe (precondition 3's whole point). If a file argument is genuinely unavoidable, `--ignoreConfig` proceeds — but it abandons the package's tsconfig, so the bare form stays canonical. A type-level control that works — shown as `text` because it exists to fail the typecheck: ```text import { Effect } from "effect"; const control = Effect.catchAll; // a name that does not exist; must fail // probe.ts(2,24): error TS2339: Property 'catchAll' does not exist on type 'typeof Effect' ``` Inside a probe **file**, print the version with an import, not `require` — a `require` alongside a top-level `await` makes the module format ambiguous and Node refuses to run it (`ERR_AMBIGUOUS_MODULE_SYNTAX`). The `require` one-liner above is fine because `-e` has no `await`. ```ts import pkg from "effect/package.json" with { type: "json" }; console.log("resolved effect:", pkg.version); // must equal the installed (lockfile) effect and the vendored tag ``` ## Portability `${CLAUDE_PROJECT_DIR}` resolves to the **consuming project's** root, not the plugin's repo. Today those coincide, because the plugin is dogfooded only from the `effected` monorepo — so `$SRC` finds the vendored tree. Published into someone else's project, it will not, and the resolver above is what keeps that from mattering. What a consumer without the vendored tree actually loses is **rung 1 alone**. Rung 2 arrives intact via `$EFFECT_SRC`, and rung 3 never needed a tree — a probe compiles against the package the consumer already installed. So the ladder degrades by exactly one rung, and by its weakest. **Never trade the loud failure for a quiet fallback.** Each step above resolves to a *version-exact source tree* or it does not resolve. There is no step that resolves to recollection. Silent degradation into memory is the precise failure this plugin exists to prevent, and it is indistinguishable from success. This is the **only file in the plugin that names the path** — the agents reference this skill, never the directory. Keep it that way: ```bash test "$(grep -rl '\.repos/effect' plugin/ | wc -l)" -eq 1 ```